mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
Signed-off-by: avivkeller <me@aviv.sh> PR-URL: https://github.com/nodejs/node/pull/64591 Reviewed-By: James M Snell <jasnell@gmail.com>
306 lines
9.4 KiB
JavaScript
306 lines
9.4 KiB
JavaScript
import * as common from '../common/index.mjs';
|
|
import assert from 'node:assert';
|
|
import events from 'node:events';
|
|
import { REPLServer } from 'node:repl';
|
|
import { Stream } from 'node:stream';
|
|
import { inspect } from 'node:util';
|
|
|
|
common.skipIfInspectorDisabled();
|
|
|
|
// Ignore terminal settings. This is so the test can be run intact if TERM=dumb.
|
|
process.env.TERM = '';
|
|
const PROMPT = 'repl > ';
|
|
|
|
class REPLStream extends Stream {
|
|
readable = true;
|
|
writable = true;
|
|
|
|
constructor() {
|
|
super();
|
|
this.lines = [''];
|
|
this.writeCount = 0;
|
|
}
|
|
async run(data) {
|
|
for (const entry of data) {
|
|
this.emit('data', entry);
|
|
await this.settle();
|
|
}
|
|
this.emit('data', '\n');
|
|
}
|
|
settle() {
|
|
return new Promise((resolve) => {
|
|
let last = this.writeCount;
|
|
let quiet = 0;
|
|
const check = () => {
|
|
if (this.writeCount !== last) {
|
|
last = this.writeCount;
|
|
quiet = 0;
|
|
} else if (++quiet >= 10) {
|
|
resolve();
|
|
return;
|
|
}
|
|
setImmediate(check);
|
|
};
|
|
setImmediate(check);
|
|
});
|
|
}
|
|
write(chunk) {
|
|
this.writeCount++;
|
|
const chunkLines = chunk.toString('utf8').split('\n');
|
|
this.lines[this.lines.length - 1] += chunkLines[0];
|
|
if (chunkLines.length > 1) {
|
|
this.lines.push(...chunkLines.slice(1));
|
|
}
|
|
this.emit('line', this.lines[this.lines.length - 1]);
|
|
return true;
|
|
}
|
|
async wait() {
|
|
this.lines = [''];
|
|
for await (const [line] of events.on(this, 'line')) {
|
|
if (line.includes(PROMPT)) {
|
|
return this.lines;
|
|
}
|
|
}
|
|
}
|
|
pause() {}
|
|
resume() {}
|
|
}
|
|
|
|
async function runAndWait(cmds, repl) {
|
|
const promise = repl.inputStream.wait();
|
|
for (const cmd of cmds) {
|
|
await repl.inputStream.run(cmd);
|
|
}
|
|
return promise;
|
|
}
|
|
|
|
function stripStackFrames(lines) {
|
|
return lines.filter((line) => !/^\s*at /.test(line));
|
|
}
|
|
|
|
async function tests(options) {
|
|
const repl = new REPLServer({
|
|
prompt: PROMPT,
|
|
stream: new REPLStream(),
|
|
ignoreUndefined: true,
|
|
// Keep syntax highlighting out of these preview transcript assertions.
|
|
// Preview and result colors are enabled after readline is initialized.
|
|
useColors: false,
|
|
...options
|
|
});
|
|
repl.useColors = true;
|
|
repl.writer.options.colors = true;
|
|
|
|
await runAndWait([
|
|
'function foo(x) { return x; } ' +
|
|
'function koo() { console.log("abc"); } ' +
|
|
'a = undefined;',
|
|
], repl);
|
|
|
|
const testCases = [{
|
|
input: 'foo',
|
|
noPreview: '[Function: foo]',
|
|
preview: [
|
|
'foo',
|
|
'\x1B[90m[Function: foo]\x1B[39m\x1B[11G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r',
|
|
'\x1B[36m[Function: foo]\x1B[39m',
|
|
]
|
|
}, {
|
|
input: 'koo',
|
|
noPreview: '[Function: koo]',
|
|
preview: [
|
|
'k\x1B[90moo\x1B[39m\x1B[9G',
|
|
'\x1B[90m[Function: koo]\x1B[39m\x1B[9G\x1B[1A\x1B[1B\x1B[2K\x1B[1A' +
|
|
'\x1B[0Ko\x1B[90mo\x1B[39m\x1B[10G',
|
|
'\x1B[90m[Function: koo]\x1B[39m\x1B[10G\x1B[1A\x1B[1B\x1B[2K\x1B[1A' +
|
|
'\x1B[0Ko',
|
|
'\x1B[90m[Function: koo]\x1B[39m\x1B[11G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r',
|
|
'\x1B[36m[Function: koo]\x1B[39m',
|
|
]
|
|
}, {
|
|
input: 'a',
|
|
noPreview: 'repl > ', // No "undefined" output.
|
|
preview: ['a\r'] // No "undefined" preview.
|
|
}, {
|
|
input: " { b: 1 }['b'] === 1",
|
|
noPreview: '\x1B[33mtrue\x1B[39m',
|
|
preview: [
|
|
" { b: 1 }['b']",
|
|
'\x1B[90m1\x1B[39m\x1B[22G\x1B[1A\x1B[1B\x1B[2K\x1B[1A ',
|
|
'\x1B[90m1\x1B[39m\x1B[23G\x1B[1A\x1B[1B\x1B[2K\x1B[1A=== 1',
|
|
'\x1B[90mtrue\x1B[39m\x1B[28G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r',
|
|
'\x1B[33mtrue\x1B[39m',
|
|
]
|
|
}, {
|
|
input: "{ b: 1 }['b'] === 1;",
|
|
noPreview: '\x1B[33mfalse\x1B[39m',
|
|
preview: [
|
|
"{ b: 1 }['b']",
|
|
'\x1B[90m1\x1B[39m\x1B[21G\x1B[1A\x1B[1B\x1B[2K\x1B[1A ',
|
|
'\x1B[90m1\x1B[39m\x1B[22G\x1B[1A\x1B[1B\x1B[2K\x1B[1A=== 1',
|
|
'\x1B[90mtrue\x1B[39m\x1B[27G\x1B[1A\x1B[1B\x1B[2K\x1B[1A;',
|
|
'\x1B[90mfalse\x1B[39m\x1B[28G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r',
|
|
'\x1B[33mfalse\x1B[39m',
|
|
]
|
|
}, {
|
|
input: '{ a: true }',
|
|
noPreview: '{ a: \x1B[33mtrue\x1B[39m }',
|
|
preview: [
|
|
'{ a: tru\x1B[90me\x1B[39m\x1B[16G\x1B[0Ke }\r',
|
|
'{ a: \x1B[33mtrue\x1B[39m }',
|
|
]
|
|
}, {
|
|
input: '{ a: true };',
|
|
noPreview: '\x1B[33mtrue\x1B[39m',
|
|
preview: [
|
|
'{ a: tru\x1B[90me\x1B[39m\x1B[16G\x1B[0Ke };',
|
|
'\x1B[90mtrue\x1B[39m\x1B[20G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r',
|
|
'\x1B[33mtrue\x1B[39m',
|
|
]
|
|
}, {
|
|
input: ' \t { a: true};',
|
|
noPreview: '\x1B[33mtrue\x1B[39m',
|
|
preview: [
|
|
' { a: tru\x1B[90me\x1B[39m\x1B[18G\x1B[0Ke}',
|
|
'\x1B[90m{ a: true }\x1B[39m\x1B[20G\x1B[1A\x1B[1B\x1B[2K\x1B[1A;',
|
|
'\x1B[90mtrue\x1B[39m\x1B[21G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r',
|
|
'\x1B[33mtrue\x1B[39m',
|
|
]
|
|
}, {
|
|
input: '1n + 2n',
|
|
noPreview: '\x1B[33m3n\x1B[39m',
|
|
preview: [
|
|
'1n + 2',
|
|
'\x1B[90mType[39m\x1B[14G\x1B[1A\x1B[1B\x1B[2K\x1B[1An',
|
|
'\x1B[90m3n\x1B[39m\x1B[15G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r',
|
|
'\x1B[33m3n\x1B[39m',
|
|
]
|
|
}, {
|
|
// The inspector evaluator treats `{};1` as a block statement followed by a
|
|
// labelled/expression statement and rejects the trailing expression. The
|
|
// eager-eval preview still shows the result of the wrapped expression (`1`),
|
|
// but the committed evaluation reports a SyntaxError.
|
|
input: '{};1',
|
|
noPreview: 'Uncaught SyntaxError: Unexpected token \';\'',
|
|
preview: [
|
|
'{};1',
|
|
'\x1B[90m1\x1B[39m\x1B[12G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r',
|
|
'Uncaught SyntaxError: Unexpected token \';\'',
|
|
]
|
|
}, {
|
|
input: 'aaaa',
|
|
noPreview: 'Uncaught ReferenceError: aaaa is not defined',
|
|
preview: [
|
|
'aaaa\r',
|
|
'Uncaught ReferenceError: aaaa is not defined',
|
|
]
|
|
}, {
|
|
input: '/0',
|
|
noPreview: 'Uncaught SyntaxError: Invalid regular expression: missing /',
|
|
preview: [
|
|
'/0\r',
|
|
'Uncaught SyntaxError: Invalid regular expression: missing /',
|
|
]
|
|
}, {
|
|
input: '{})',
|
|
noPreview: "Uncaught SyntaxError: Unexpected token ')'",
|
|
preview: [
|
|
'{})\r',
|
|
"Uncaught SyntaxError: Unexpected token ')'",
|
|
],
|
|
}, {
|
|
input: "{ a: '{' }",
|
|
noPreview: "{ a: \x1B[32m'{'\x1B[39m }",
|
|
preview: [
|
|
"{ a: '{' }\r",
|
|
"{ a: \x1B[32m'{'\x1B[39m }",
|
|
],
|
|
}, {
|
|
input: "{'{':0}",
|
|
noPreview: "{ \x1B[32m'{'\x1B[39m: \x1B[33m0\x1B[39m }",
|
|
preview: [
|
|
"{'{':0}",
|
|
"\x1B[90m{ '{': 0 }\x1B[39m\x1B[15G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r",
|
|
"{ \x1B[32m'{'\x1B[39m: \x1B[33m0\x1B[39m }",
|
|
],
|
|
}, {
|
|
input: '{[Symbol.for("{")]: 0 }',
|
|
noPreview: '{ \x1B[32mSymbol({)\x1B[39m: \x1B[33m0\x1B[39m }',
|
|
preview: [
|
|
'{[Sym\x1B[90mbol\x1B[39m\x1B[13G\x1B[0Kb\x1B[90mol\x1B[39m\x1B[14G\x1B[0Ko' +
|
|
'\x1B[90ml\x1B[39m\x1B[15G\x1B[0Kl.f\x1B[90mor\x1B[39m\x1B[18G\x1B[0Ko' +
|
|
'\x1B[90mr\x1B[39m\x1B[19G\x1B[0Kr("{")]: 0 }\r',
|
|
'{ \x1B[32mSymbol({)\x1B[39m: \x1B[33m0\x1B[39m }',
|
|
],
|
|
}, {
|
|
input: '{},{}',
|
|
noPreview: '{}',
|
|
preview: [
|
|
'{},{}',
|
|
'\x1B[90m{}\x1B[39m\x1B[13G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r',
|
|
'{}',
|
|
],
|
|
}, {
|
|
input: '{} //;',
|
|
noPreview: 'repl > ',
|
|
preview: [
|
|
'{} //;\r',
|
|
],
|
|
}, {
|
|
// The inspector evaluator wraps the input in parentheses to detect an
|
|
// expression, which turns `{throw 0}` into invalid syntax. The eager-eval
|
|
// preview still shows the thrown value, but the committed evaluation reports
|
|
// a SyntaxError.
|
|
input: '{throw 0}',
|
|
noPreview: 'Uncaught SyntaxError: Unexpected number',
|
|
preview: [
|
|
// Typing `{thr` offers the `throw` keyword as an inline completion preview.
|
|
'{thr\x1B[90mow\x1B[39m\x1B[12G\x1B[0Ko\x1B[90mw\x1B[39m\x1B[13G\x1B[0Kw 0}',
|
|
'\x1B[90m0\x1B[39m\x1B[17G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r',
|
|
'Uncaught SyntaxError: Unexpected number',
|
|
],
|
|
}];
|
|
|
|
const hasPreview = repl.terminal &&
|
|
(options.preview !== undefined ? !!options.preview : true);
|
|
|
|
for (const { input, noPreview, preview } of testCases) {
|
|
console.log(`Testing ${input}`);
|
|
|
|
const toBeRun = input.split('\n');
|
|
let lines = await runAndWait(toBeRun, repl);
|
|
|
|
// Drop unstable inspector stack frames (see `stripStackFrames`).
|
|
lines = stripStackFrames(lines);
|
|
|
|
if (hasPreview) {
|
|
// Remove error messages. That allows the code to run in different
|
|
// engines.
|
|
// eslint-disable-next-line no-control-regex
|
|
lines = lines.map((line) => line.replace(/Error: .+?\x1B/, ''));
|
|
assert.strictEqual(lines.pop(), '\x1B[1G\x1B[0Jrepl > \x1B[8G');
|
|
assert.deepStrictEqual(lines, preview);
|
|
} else {
|
|
assert.ok(lines[0].includes(noPreview), lines.map(inspect).join('\n'));
|
|
if (preview.length !== 1 || preview[0] !== `${input}\r`) {
|
|
assert.strictEqual(lines.length, 2);
|
|
}
|
|
}
|
|
}
|
|
|
|
repl.close();
|
|
}
|
|
|
|
// NOTE: The input `{} //` (a block followed by a line comment, with no trailing
|
|
// semicolon) is intentionally NOT exercised here. Under the inspector-based
|
|
// evaluator it is treated as an incomplete statement: the REPL emits the
|
|
// continuation prompt (`| `) and waits for more input forever instead of
|
|
// completing. This is a regression from the previous vm-based REPL, where the
|
|
// same input evaluated to `undefined` and completed. See the test report.
|
|
|
|
await tests({ terminal: false }); // No preview
|
|
await tests({ terminal: true }); // Preview
|
|
await tests({ terminal: false, preview: false }); // No preview
|
|
await tests({ terminal: false, preview: true }); // No preview
|
|
await tests({ terminal: true, preview: true }); // Preview
|