Skip to content

Commit 9bd4725

Browse files
test: add tests for REPL custom evals
this commit reintroduces the REPL custom eval tests that have been introduced in #57691 but reverted in #57793 the tests turned out problematic before because `getReplOutput`, the function used to return the repl output wasn't taking into account that input processing and output emitting are asynchronous operation can resolve with a small delay the new implementation here replaces `getReplOutput` with `getReplRunOutput` that resolves repl inputs by running them and using the repl prompt as an indicator to when the input processing has completed
1 parent 58e1cba commit 9bd4725

File tree

4 files changed

+220
-34
lines changed

4 files changed

+220
-34
lines changed

lib/repl.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,6 @@ function REPLServer(prompt,
310310
options.useColors = shouldColorize(options.output);
311311
}
312312

313-
// TODO(devsnek): Add a test case for custom eval functions.
314313
const preview = options.terminal &&
315314
(options.preview !== undefined ? !!options.preview : !eval_);
316315

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
'use strict';
2+
3+
require('../common');
4+
const ArrayStream = require('../common/arraystream');
5+
const assert = require('assert');
6+
const { describe, it } = require('node:test');
7+
8+
const repl = require('repl');
9+
10+
const testingReplPrompt = '_REPL_TESTING_PROMPT_>';
11+
12+
// Processes some input in a REPL instance and returns a promise that
13+
// resolves to the produced output (as a string).
14+
function getReplRunOutput(input, replOptions) {
15+
return new Promise((resolve) => {
16+
const inputStream = new ArrayStream();
17+
const outputStream = new ArrayStream();
18+
19+
const replServer = repl.start({
20+
input: inputStream,
21+
output: outputStream,
22+
prompt: testingReplPrompt,
23+
...replOptions,
24+
});
25+
26+
let output = '';
27+
28+
outputStream.write = (chunk) => {
29+
output += chunk;
30+
// The prompt appears after the input has been processed
31+
if (output.includes(testingReplPrompt)) {
32+
replServer.close();
33+
resolve(output);
34+
}
35+
};
36+
37+
inputStream.emit('data', input);
38+
39+
inputStream.run(['']);
40+
});
41+
}
42+
43+
describe('with previews', () => {
44+
it("doesn't show previews by default", async () => {
45+
const input = "'Hello custom' + ' eval World!'";
46+
const output = await getReplRunOutput(
47+
input,
48+
{
49+
terminal: true,
50+
eval: (code, _ctx, _replRes, cb) => cb(null, eval(code)),
51+
},
52+
);
53+
const lines = getSingleCommandLines(output);
54+
assert.match(lines.command, /^'Hello custom' \+ ' eval World!'/);
55+
assert.match(lines.prompt, new RegExp(`${testingReplPrompt}$`));
56+
assert.strictEqual(lines.result, "'Hello custom eval World!'");
57+
assert.strictEqual(lines.preview, undefined);
58+
});
59+
60+
it('does show previews if `preview` is set to `true`', async () => {
61+
const input = "'Hello custom' + ' eval World!'";
62+
const output = await getReplRunOutput(
63+
input,
64+
{
65+
terminal: true,
66+
eval: (code, _ctx, _replRes, cb) => cb(null, eval(code)),
67+
preview: true,
68+
},
69+
);
70+
const lines = getSingleCommandLines(output);
71+
assert.match(lines.command, /^'Hello custom' \+ ' eval World!'/);
72+
assert.match(lines.prompt, new RegExp(`${testingReplPrompt}$`));
73+
assert.strictEqual(lines.result, "'Hello custom eval World!'");
74+
assert.match(lines.preview, /'Hello custom eval World!'/);
75+
});
76+
});
77+
78+
function getSingleCommandLines(output) {
79+
const outputLines = output.split('\n');
80+
81+
// The first line contains the command being run
82+
const command = outputLines.shift();
83+
84+
// The last line contains the prompt (asking for some new input)
85+
const prompt = outputLines.pop();
86+
87+
// The line before the last one contains the result of the command
88+
const result = outputLines.pop();
89+
90+
// The line before that contains the preview of the command
91+
const preview = outputLines.shift();
92+
93+
return {
94+
command,
95+
prompt,
96+
result,
97+
preview,
98+
};
99+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
'use strict';
2+
3+
require('../common');
4+
const ArrayStream = require('../common/arraystream');
5+
const assert = require('assert');
6+
const { describe, it } = require('node:test');
7+
8+
const repl = require('repl');
9+
10+
// Processes some input in a REPL instance and returns a promise that
11+
// resolves to the produced output (as a string).
12+
function getReplRunOutput(input, replOptions) {
13+
return new Promise((resolve) => {
14+
const inputStream = new ArrayStream();
15+
const outputStream = new ArrayStream();
16+
17+
const testingReplPrompt = '_REPL_TESTING_PROMPT_>';
18+
19+
const replServer = repl.start({
20+
input: inputStream,
21+
output: outputStream,
22+
prompt: testingReplPrompt,
23+
...replOptions,
24+
});
25+
26+
let output = '';
27+
28+
outputStream.write = (chunk) => {
29+
output += chunk;
30+
// The prompt appears after the input has been processed
31+
if (output.includes(testingReplPrompt)) {
32+
replServer.close();
33+
resolve(output);
34+
}
35+
};
36+
37+
inputStream.emit('data', input);
38+
39+
inputStream.run(['']);
40+
});
41+
}
42+
43+
describe('repl with custom eval', { concurrency: true }, () => {
44+
it('uses the custom eval function as expected', async () => {
45+
const output = await getReplRunOutput('Convert this to upper case', {
46+
terminal: true,
47+
eval: (code, _ctx, _replRes, cb) => cb(null, code.toUpperCase()),
48+
});
49+
assert.match(
50+
output,
51+
/Convert this to upper case\r\n'CONVERT THIS TO UPPER CASE\\n'/
52+
);
53+
});
54+
55+
it('surfaces errors as expected', async () => {
56+
const output = await getReplRunOutput('Convert this to upper case', {
57+
terminal: true,
58+
eval: (_code, _ctx, _replRes, cb) => cb(new Error('Testing Error')),
59+
});
60+
assert.match(output, /Uncaught Error: Testing Error\n/);
61+
});
62+
63+
it('provides a repl context to the eval callback', async () => {
64+
const context = await new Promise((resolve) => {
65+
const r = repl.start({
66+
eval: (_cmd, context) => resolve(context),
67+
});
68+
r.context = { foo: 'bar' };
69+
r.write('\n.exit\n');
70+
});
71+
assert.strictEqual(context.foo, 'bar');
72+
});
73+
74+
it('provides the global context to the eval callback', async () => {
75+
const context = await new Promise((resolve) => {
76+
const r = repl.start({
77+
useGlobal: true,
78+
eval: (_cmd, context) => resolve(context),
79+
});
80+
global.foo = 'global_foo';
81+
r.write('\n.exit\n');
82+
});
83+
84+
assert.strictEqual(context.foo, 'global_foo');
85+
delete global.foo;
86+
});
87+
88+
it('inherits variables from the global context but does not use it afterwords if `useGlobal` is false', async () => {
89+
global.bar = 'global_bar';
90+
const context = await new Promise((resolve) => {
91+
const r = repl.start({
92+
useGlobal: false,
93+
eval: (_cmd, context) => resolve(context),
94+
});
95+
global.baz = 'global_baz';
96+
r.write('\n.exit\n');
97+
});
98+
99+
assert.strictEqual(context.bar, 'global_bar');
100+
assert.notStrictEqual(context.baz, 'global_baz');
101+
delete global.bar;
102+
delete global.baz;
103+
});
104+
105+
/**
106+
* Default preprocessor transforms
107+
* function f() {} to
108+
* var f = function f() {}
109+
* This test ensures that original input is preserved.
110+
* Reference: https://github.com/nodejs/node/issues/9743
111+
*/
112+
it('preserves the original input', async () => {
113+
const cmd = await new Promise((resolve) => {
114+
const r = repl.start({
115+
eval: (cmd) => resolve(cmd),
116+
});
117+
r.write('function f() {}\n.exit\n');
118+
});
119+
assert.strictEqual(cmd, 'function f() {}\n');
120+
});
121+
});

test/parallel/test-repl-eval.js

Lines changed: 0 additions & 33 deletions
This file was deleted.

0 commit comments

Comments
 (0)