node/test/parallel/test-repl-history-navigation.js
Ruben Bridgewater 9da57a86ee
test: fix flaky parallel/test-repl-history-navigation test
Two scenarios should be tested:

1. The completion is triggered and the result is printed before the
   next invocation.
2. The completion is triggered multiple times right after each other
   without waiting for the result. In that case only the last result
   should be printed.

The first scenario did not need a timeout while the latter did not
need a timeout for the second invocation.

PR-URL: https://github.com/nodejs/node/pull/31708
Fixes: https://github.com/nodejs/node/issues/31094
Reviewed-By: Denys Otrishko <shishugi@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
2020-02-13 21:18:30 +01:00

567 lines
16 KiB
JavaScript

'use strict';
// Flags: --expose-internals
const common = require('../common');
const stream = require('stream');
const REPL = require('internal/repl');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const { inspect } = require('util');
const tmpdir = require('../common/tmpdir');
tmpdir.refresh();
const defaultHistoryPath = path.join(tmpdir.path, '.node_repl_history');
// Create an input stream specialized for testing an array of actions
class ActionStream extends stream.Stream {
run(data) {
const _iter = data[Symbol.iterator]();
const doAction = () => {
const next = _iter.next();
if (next.done) {
// Close the repl. Note that it must have a clean prompt to do so.
this.emit('keypress', '', { ctrl: true, name: 'd' });
return;
}
const action = next.value;
if (typeof action === 'object') {
this.emit('keypress', '', action);
} else {
this.emit('data', `${action}`);
}
setImmediate(doAction);
};
doAction();
}
resume() {}
pause() {}
}
ActionStream.prototype.readable = true;
// Mock keys
const ENTER = { name: 'enter' };
const UP = { name: 'up' };
const DOWN = { name: 'down' };
const LEFT = { name: 'left' };
const RIGHT = { name: 'right' };
const DELETE = { name: 'delete' };
const BACKSPACE = { name: 'backspace' };
const WORD_LEFT = { name: 'left', ctrl: true };
const WORD_RIGHT = { name: 'right', ctrl: true };
const GO_TO_END = { name: 'end' };
const DELETE_WORD_LEFT = { name: 'backspace', ctrl: true };
const SIGINT = { name: 'c', ctrl: true };
const prompt = '> ';
const WAIT = '€';
const prev = process.features.inspector;
let completions = 0;
const tests = [
{ // Creates few history to navigate for
env: { NODE_REPL_HISTORY: defaultHistoryPath },
test: [ 'let ab = 45', ENTER,
'555 + 909', ENTER,
'{key : {key2 :[] }}', ENTER,
'Array(100).fill(1).map((e, i) => i ** i)', LEFT, LEFT, DELETE,
'2', ENTER],
expected: [],
clean: false
},
{
env: { NODE_REPL_HISTORY: defaultHistoryPath },
test: [UP, UP, UP, UP, UP, DOWN, DOWN, DOWN, DOWN, DOWN],
expected: [prompt,
`${prompt}Array(100).fill(1).map((e, i) => i ** 2)`,
prev && '\n// [ 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, ' +
'144, 169, 196, 225, 256, 289, 324, 361, 400, 441, 484, 529,' +
' 576, 625, 676, 729, 784, 841, 900, 961, 1024, 1089, 1156, ' +
'1225, 1296, 1369, 1444, 1521, 1600, 1681, 1764, 1849, 1936,' +
' 2025, 2116, 2209,...',
`${prompt}{key : {key2 :[] }}`,
prev && '\n// { key: { key2: [] } }',
`${prompt}555 + 909`,
prev && '\n// 1464',
`${prompt}let ab = 45`,
prompt,
`${prompt}let ab = 45`,
`${prompt}555 + 909`,
prev && '\n// 1464',
`${prompt}{key : {key2 :[] }}`,
prev && '\n// { key: { key2: [] } }',
`${prompt}Array(100).fill(1).map((e, i) => i ** 2)`,
prev && '\n// [ 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, ' +
'144, 169, 196, 225, 256, 289, 324, 361, 400, 441, 484, 529,' +
' 576, 625, 676, 729, 784, 841, 900, 961, 1024, 1089, 1156, ' +
'1225, 1296, 1369, 1444, 1521, 1600, 1681, 1764, 1849, 1936,' +
' 2025, 2116, 2209,...',
prompt].filter((e) => typeof e === 'string'),
clean: false
},
{ // Creates more history entries to navigate through.
env: { NODE_REPL_HISTORY: defaultHistoryPath },
test: [
'555 + 909', ENTER, // Add a duplicate to the history set.
'const foo = true', ENTER,
'555n + 111n', ENTER,
'5 + 5', ENTER,
'55 - 13 === 42', ENTER
],
expected: [],
clean: false
},
{
env: { NODE_REPL_HISTORY: defaultHistoryPath },
checkTotal: true,
preview: false,
showEscapeCodes: true,
test: [
'55', UP, UP, UP, UP, UP, UP, ENTER
],
expected: [
'\x1B[1G', '\x1B[0J', prompt, '\x1B[3G',
// '55'
'5', '5',
// UP
'\x1B[1G', '\x1B[0J',
'> 55 - 13 === 42', '\x1B[17G',
// UP - skipping 5 + 5
'\x1B[1G', '\x1B[0J',
'> 555n + 111n', '\x1B[14G',
// UP - skipping const foo = true
'\x1B[1G', '\x1B[0J',
'> 555 + 909', '\x1B[12G',
// UP, UP
// UPs at the end of the history reset the line to the original input.
'\x1B[1G', '\x1B[0J',
'> 55', '\x1B[5G',
// ENTER
'\r\n', '55\n',
'\x1B[1G', '\x1B[0J',
'> ', '\x1B[3G',
'\r\n'
],
clean: true
},
{
env: { NODE_REPL_HISTORY: defaultHistoryPath },
skip: !process.features.inspector,
test: [
// あ is a full width character with a length of one.
// 🐕 is a full width character with a length of two.
// 𐐷 is a half width character with the length of two.
// '\u0301', '0x200D', '\u200E' are zero width characters.
`const x1 = '${'あ'.repeat(124)}'`, ENTER, // Fully visible
ENTER,
`const y1 = '${'あ'.repeat(125)}'`, ENTER, // Cut off
ENTER,
`const x2 = '${'🐕'.repeat(124)}'`, ENTER, // Fully visible
ENTER,
`const y2 = '${'🐕'.repeat(125)}'`, ENTER, // Cut off
ENTER,
`const x3 = '${'𐐷'.repeat(248)}'`, ENTER, // Fully visible
ENTER,
`const y3 = '${'𐐷'.repeat(249)}'`, ENTER, // Cut off
ENTER,
`const x4 = 'a${'\u0301'.repeat(1000)}'`, ENTER, // á
ENTER,
`const ${'veryLongName'.repeat(30)} = 'I should be previewed'`,
ENTER,
'const e = new RangeError("visible\\ninvisible")',
ENTER,
'e',
ENTER,
'veryLongName'.repeat(30),
ENTER,
`${'\x1B[90m \x1B[39m'.repeat(235)} fun`,
ENTER,
`${' '.repeat(236)} fun`,
ENTER
],
expected: [],
clean: false
},
{
env: { NODE_REPL_HISTORY: defaultHistoryPath },
columns: 250,
checkTotal: true,
showEscapeCodes: true,
skip: !process.features.inspector,
test: [
UP,
UP,
UP,
WORD_LEFT,
UP,
BACKSPACE,
'x1',
BACKSPACE,
'2',
BACKSPACE,
'3',
BACKSPACE,
'4',
DELETE_WORD_LEFT,
'y1',
BACKSPACE,
'2',
BACKSPACE,
'3',
SIGINT
],
// A = Cursor n up
// B = Cursor n down
// C = Cursor n forward
// D = Cursor n back
// G = Cursor to column n
// J = Erase in screen; 0 = right; 1 = left; 2 = total
// K = Erase in line; 0 = right; 1 = left; 2 = total
expected: [
// 0. Start
'\x1B[1G', '\x1B[0J',
prompt, '\x1B[3G',
// 1. UP
// This exceeds the maximum columns (250):
// Whitespace + prompt + ' // '.length + 'function'.length
// 236 + 2 + 4 + 8
'\x1B[1G', '\x1B[0J',
`${prompt}${' '.repeat(236)} fun`, '\x1B[243G',
' // ction', '\x1B[243G',
' // ction', '\x1B[243G',
'\x1B[0K',
// 2. UP
'\x1B[1G', '\x1B[0J',
`${prompt}${' '.repeat(235)} fun`, '\x1B[242G',
// TODO(BridgeAR): Investigate why the preview is generated twice.
' // ction', '\x1B[242G',
' // ction', '\x1B[242G',
// Preview cleanup
'\x1B[0K',
// 3. UP
'\x1B[1G', '\x1B[0J',
// 'veryLongName'.repeat(30).length === 360
// prompt.length === 2
// 360 % 250 + 2 === 112 (+1)
`${prompt}${'veryLongName'.repeat(30)}`, '\x1B[113G',
// "// 'I should be previewed'".length + 86 === 112 (+1)
"\n// 'I should be previewed'", '\x1B[113G', '\x1B[1A',
// Preview cleanup
'\x1B[1B', '\x1B[2K', '\x1B[1A',
// 4. WORD LEFT
// Almost identical as above. Just one extra line.
// Math.floor(360 / 250) === 1
'\x1B[1A',
'\x1B[1G', '\x1B[0J',
`${prompt}${'veryLongName'.repeat(30)}`, '\x1B[3G', '\x1B[1A',
'\x1B[1B', "\n// 'I should be previewed'", '\x1B[3G', '\x1B[2A',
// Preview cleanup
'\x1B[2B', '\x1B[2K', '\x1B[2A',
// 5. UP
'\x1B[1G', '\x1B[0J',
`${prompt}e`, '\x1B[4G',
// '// RangeError: visible'.length - 19 === 3 (+1)
'\n// RangeError: visible', '\x1B[4G', '\x1B[1A',
// Preview cleanup
'\x1B[1B', '\x1B[2K', '\x1B[1A',
// 6. Backspace
'\x1B[1G', '\x1B[0J',
'> ', '\x1B[3G', 'x', '1',
`\n// '${'あ'.repeat(124)}'`,
'\x1B[5G', '\x1B[1A',
'\x1B[1B', '\x1B[2K', '\x1B[1A',
'\x1B[1G', '\x1B[0J',
'> x', '\x1B[4G', '2',
`\n// '${'🐕'.repeat(124)}'`,
'\x1B[5G', '\x1B[1A',
'\x1B[1B', '\x1B[2K', '\x1B[1A',
'\x1B[1G', '\x1B[0J',
'> x', '\x1B[4G', '3',
`\n// '${'𐐷'.repeat(248)}'`,
'\x1B[5G', '\x1B[1A',
'\x1B[1B', '\x1B[2K', '\x1B[1A',
'\x1B[1G', '\x1B[0J',
'> x', '\x1B[4G', '4',
`\n// 'a${'\u0301'.repeat(1000)}'`,
'\x1B[5G', '\x1B[1A',
'\x1B[1B', '\x1B[2K', '\x1B[1A',
'\x1B[1G', '\x1B[0J',
'> ', '\x1B[3G', 'y', '1',
`\n// '${'あ'.repeat(121)}...`,
'\x1B[5G', '\x1B[1A',
'\x1B[1B', '\x1B[2K', '\x1B[1A',
'\x1B[1G', '\x1B[0J',
'> y', '\x1B[4G', '2',
`\n// '${'🐕'.repeat(121)}...`,
'\x1B[5G', '\x1B[1A',
'\x1B[1B', '\x1B[2K', '\x1B[1A',
'\x1B[1G', '\x1B[0J',
'> y', '\x1B[4G', '3',
`\n// '${'𐐷'.repeat(242)}...`,
'\x1B[5G', '\x1B[1A',
'\x1B[1B', '\x1B[2K', '\x1B[1A',
'\r\n',
'\x1B[1G', '\x1B[0J',
'> ', '\x1B[3G',
'\r\n'
],
clean: true
},
{
env: { NODE_REPL_HISTORY: defaultHistoryPath },
showEscapeCodes: true,
skip: !process.features.inspector,
test: [
'fu',
'n',
RIGHT,
BACKSPACE,
LEFT,
LEFT,
'A',
BACKSPACE,
GO_TO_END,
BACKSPACE,
WORD_LEFT,
WORD_RIGHT,
ENTER
],
// C = Cursor n forward
// D = Cursor n back
// G = Cursor to column n
// J = Erase in screen; 0 = right; 1 = left; 2 = total
// K = Erase in line; 0 = right; 1 = left; 2 = total
expected: [
// 0.
// 'f'
'\x1B[1G', '\x1B[0J', prompt, '\x1B[3G', 'f',
// 'u'
'u', ' // nction', '\x1B[5G',
// 'n' - Cleanup
'\x1B[0K',
'n', ' // ction', '\x1B[6G',
// 1. Right. Cleanup
'\x1B[0K',
'ction',
// 2. Backspace. Refresh
'\x1B[1G', '\x1B[0J', `${prompt}functio`, '\x1B[10G',
// Autocomplete and refresh?
' // n', '\x1B[10G', ' // n', '\x1B[10G',
// 3. Left. Cleanup
'\x1B[0K',
'\x1B[1D', '\x1B[10G', ' // n', '\x1B[9G',
// 4. Left. Cleanup
'\x1B[10G', '\x1B[0K', '\x1B[9G',
'\x1B[1D', '\x1B[10G', ' // n', '\x1B[8G',
// 5. 'A' - Cleanup
'\x1B[10G', '\x1B[0K', '\x1B[8G',
// Refresh
'\x1B[1G', '\x1B[0J', `${prompt}functAio`, '\x1B[9G',
// 6. Backspace. Refresh
'\x1B[1G', '\x1B[0J', `${prompt}functio`, '\x1B[8G', '\x1B[10G', ' // n',
'\x1B[8G', '\x1B[10G', ' // n',
'\x1B[8G', '\x1B[10G',
// 7. Go to end. Cleanup
'\x1B[0K', '\x1B[8G', '\x1B[2C',
'n',
// 8. Backspace. Refresh
'\x1B[1G', '\x1B[0J', `${prompt}functio`, '\x1B[10G',
// Autocomplete
' // n', '\x1B[10G', ' // n', '\x1B[10G',
// 9. Word left. Cleanup
'\x1B[0K', '\x1B[7D', '\x1B[10G', ' // n', '\x1B[3G', '\x1B[10G',
// 10. Word right. Cleanup
'\x1B[0K', '\x1B[3G', '\x1B[7C', ' // n', '\x1B[10G',
'\x1B[0K',
// 11. ENTER
'\r\n',
'Uncaught ReferenceError: functio is not defined\n',
'\x1B[1G', '\x1B[0J',
prompt, '\x1B[3G', '\r\n'
],
clean: true
},
{
// Check changed inspection defaults.
env: { NODE_REPL_HISTORY: defaultHistoryPath },
skip: !process.features.inspector,
test: [
'util.inspect.replDefaults.showHidden',
ENTER
],
expected: [],
clean: false
},
{
env: { NODE_REPL_HISTORY: defaultHistoryPath },
skip: !process.features.inspector,
checkTotal: true,
test: [
'[ ]',
WORD_LEFT,
WORD_LEFT,
UP,
' = true',
ENTER,
'[ ]',
ENTER
],
expected: [
prompt,
'[', ' ', ']',
'\n// []', '\n// []', '\n// []',
'> util.inspect.replDefaults.showHidden',
'\n// false',
' ', '=', ' ', 't', 'r', 'u', 'e',
'true\n',
'> ', '[', ' ', ']',
'\n// [ [length]: 0 ]',
'[ [length]: 0 ]\n',
'> ',
],
clean: true
},
{
// Check that the completer ignores completions that are outdated.
env: { NODE_REPL_HISTORY: defaultHistoryPath },
completer(line, callback) {
if (line.endsWith(WAIT)) {
if (completions++ === 0) {
callback(null, [[`${WAIT}WOW`], line]);
} else {
setTimeout(callback, 1000, null, [[`${WAIT}WOW`], line]).unref();
}
} else {
callback(null, [[' Always visible'], line]);
}
},
skip: !process.features.inspector,
test: [
WAIT, // The first call is awaited before new input is triggered!
BACKSPACE,
's',
BACKSPACE,
WAIT, // The second call is not awaited. It won't trigger the preview.
BACKSPACE,
's',
BACKSPACE
],
expected: [
prompt,
WAIT,
' // WOW',
prompt,
's',
' // Always visible',
prompt,
WAIT,
prompt,
's',
' // Always visible',
prompt,
],
clean: true
}
];
const numtests = tests.length;
const runTestWrap = common.mustCall(runTest, numtests);
function cleanupTmpFile() {
try {
// Write over the file, clearing any history
fs.writeFileSync(defaultHistoryPath, '');
} catch (err) {
if (err.code === 'ENOENT') return true;
throw err;
}
return true;
}
function runTest() {
const opts = tests.shift();
if (!opts) return; // All done
const { expected, skip } = opts;
// Test unsupported on platform.
if (skip) {
setImmediate(runTestWrap, true);
return;
}
const lastChunks = [];
let i = 0;
REPL.createInternalRepl(opts.env, {
input: new ActionStream(),
output: new stream.Writable({
write(chunk, _, next) {
const output = chunk.toString();
if (!opts.showEscapeCodes &&
(output[0] === '\x1B' || /^[\r\n]+$/.test(output))) {
return next();
}
lastChunks.push(output);
if (expected.length && !opts.checkTotal) {
try {
assert.strictEqual(output, expected[i]);
} catch (e) {
console.error(`Failed test # ${numtests - tests.length}`);
console.error('Last outputs: ' + inspect(lastChunks, {
breakLength: 5, colors: true
}));
throw e;
}
// TODO(BridgeAR): Auto close on last chunk!
i++;
}
next();
}
}),
completer: opts.completer,
prompt,
useColors: false,
preview: opts.preview,
terminal: true
}, function(err, repl) {
if (err) {
console.error(`Failed test # ${numtests - tests.length}`);
throw err;
}
repl.once('close', () => {
if (opts.clean)
cleanupTmpFile();
if (opts.checkTotal) {
assert.deepStrictEqual(lastChunks, expected);
} else if (expected.length !== i) {
console.error(tests[numtests - tests.length - 1]);
throw new Error(`Failed test # ${numtests - tests.length}`);
}
setImmediate(runTestWrap, true);
});
if (opts.columns) {
Object.defineProperty(repl, 'columns', {
value: opts.columns,
enumerable: true
});
}
repl.inputStream.run(opts.test);
});
}
// run the tests
runTest();