Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
test: refactor repl save-load tests
refactor the test/parallel/test-repl-save-load.js file by:
  - making the tests in the file self-contained
    (instead of all of them sharing the same REPL instance and
     constantly calling `.clear` on it)
  - clearly separating and commenting the various tests to make
    clearer what is being tested
  • Loading branch information
dario-piotrowicz committed Jun 17, 2025
commit 97e90bf83c77dd449738d131b364e4352da13d03
253 changes: 140 additions & 113 deletions test/parallel/test-repl-save-load.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,162 +20,189 @@
// USE OR OTHER DEALINGS IN THE SOFTWARE.

'use strict';

const common = require('../common');
const ArrayStream = require('../common/arraystream');
const assert = require('assert');
const fs = require('fs');

const assert = require('node:assert');
const fs = require('node:fs');
const repl = require('node:repl');
const path = require('node:path');

const tmpdir = require('../common/tmpdir');
tmpdir.refresh();

// TODO: the following async IIFE and the completePromise function are necessary because
// the reply tests are all run against the same repl instance (testMe) and thus coordination
// needs to be in place for the tests not to interfere with each other, this is really
// not ideal, the tests in this file should be refactored so that each use its own isolated
// repl instance, making sure that no special coordination needs to be in place for them
// and also allowing the tests to all be run in parallel
(async () => {
const repl = require('repl');

const works = [['inner.one'], 'inner.o'];

const putIn = new ArrayStream();
const testMe = repl.start('', putIn);

// Some errors might be passed to the domain.
testMe._domain.on('error', function(reason) {
const err = new Error('Test failed');
err.reason = reason;
throw err;
function prepareREPL(replOpts = {}) {
const input = new ArrayStream();
const output = new ArrayStream();

const replServer = repl.start({
prompt: '',
input,
output,
allowBlockingCompletions: true,
...replOpts,
});

async function completePromise(query, callback) {
return new Promise((resolve) => {
testMe.complete(query, (...args) => {
callback(...args);
resolve();
});
});
}
// Some errors are passed to the domain, but do not callback
replServer._domain.on('error', assert.ifError);

return { replServer, input, output };
}

const testFile = [
// The tests in this file test the REPL saving and loading session data to/from a file


// The REPL can save a session's data to a file and load it back
Comment thread
dario-piotrowicz marked this conversation as resolved.
Outdated
{
const { replServer, input } = prepareREPL();
tmpdir.refresh();

const filePath = path.resolve(tmpdir.path, 'test.save.js');

const testFileContents = [
'let inner = (function() {',
' return {one:1};',
'})()',
];
const saveFileName = tmpdir.resolve('test.save.js');

// Add some data.
putIn.run(testFile);
input.run(testFileContents);
input.run([`.save ${filePath}`]);

// Save it to a file.
putIn.run([`.save ${saveFileName}`]);
assert.strictEqual(fs.readFileSync(filePath, 'utf8'),
testFileContents.join('\n'));

// The file should have what I wrote.
assert.strictEqual(fs.readFileSync(saveFileName, 'utf8'),
testFile.join('\n'));
const innerOCompletions = [['inner.one'], 'inner.o'];

// Make sure that the REPL data is "correct".
await completePromise('inner.o', common.mustSucceed((data) => {
assert.deepStrictEqual(data, works);
// Double check that the data is still present in the repl after the save
replServer.completer('inner.o', common.mustSucceed((data) => {
assert.deepStrictEqual(data, innerOCompletions);
}));

// Clear the REPL.
putIn.run(['.clear']);
// Clear the repl context
input.run(['.clear']);

// Double check that the data is no longer present in the repl
replServer.completer('inner.o', common.mustSucceed((data) => {
assert.deepStrictEqual(data, [[], 'inner.o']);
}));

testMe._sawKeyPress = true;
// Load the file back in.
putIn.run([`.load ${saveFileName}`]);
input.run([`.load ${filePath}`]);

// Make sure loading doesn't insert extra indentation
// https://github.com/nodejs/node/issues/47673
assert.strictEqual(testMe.line, '');
assert.strictEqual(replServer.line, '');

// Make sure that the REPL data is "correct".
await completePromise('inner.o', common.mustSucceed((data) => {
assert.deepStrictEqual(data, works);
// Make sure that the loaded data is present
replServer.complete('inner.o', common.mustSucceed((data) => {
assert.deepStrictEqual(data, innerOCompletions);
}));

// Clear the REPL.
putIn.run(['.clear']);
replServer.close();
}

let loadFile = tmpdir.resolve('file.does.not.exist');
// An appropriate error is displayed if .load is called without a filename
{
const { replServer, input, output } = prepareREPL();

// Should not break.
putIn.write = common.mustCall(function(data) {
// Make sure I get a failed to load message and not some crazy error.
assert.strictEqual(data, `Failed to load: ${loadFile}\n`);
// Eat me to avoid work.
putIn.write = () => {};
output.write = common.mustCall(function(data) {
assert.strictEqual(data, 'The "file" argument must be specified\n');
output.write = () => {};
});
putIn.run([`.load ${loadFile}`]);

// Throw error on loading directory.
loadFile = tmpdir.path;
putIn.write = common.mustCall(function(data) {
assert.strictEqual(data, `Failed to load: ${loadFile} is not a valid file\n`);
putIn.write = () => {};
input.run(['.load']);

replServer.close();
}

// An appropriate error is displayed if .save is called without a filename
{
const { replServer, input, output } = prepareREPL();

output.write = common.mustCall(function(data) {
assert.strictEqual(data, 'The "file" argument must be specified\n');
output.write = () => {};
});
putIn.run([`.load ${loadFile}`]);

// Clear the REPL.
putIn.run(['.clear']);
input.run(['.save']);

replServer.close();
}

// The case in which the user tries to load a non existing file is appropriately handled
{
const { replServer, input, output } = prepareREPL();

const filePath = tmpdir.resolve('file.does.not.exist');

output.write = common.mustCall(function(data) {
assert.strictEqual(data, `Failed to load: ${filePath}\n`);
output.write = () => {};
});

input.run([`.load ${filePath}`]);

replServer.close();
}

// The case in which the user tries to load a directory instead of a file is appropriately handled
{
const { replServer, input, output } = prepareREPL();

const dirPath = tmpdir.path;

output.write = common.mustCall(function(data) {
assert.strictEqual(data, `Failed to load: ${dirPath} is not a valid file\n`);
output.write = () => {};
});

input.run([`.load ${dirPath}`]);

replServer.close();
}

// The case in which a file save fails is appropriately handled
{
const { replServer, input, output } = prepareREPL();

// NUL (\0) is disallowed in filenames in UNIX-like operating systems and
// Windows so we can use that to test failed saves.
const invalidFileName = tmpdir.resolve('\0\0\0\0\0');

// Should not break.
putIn.write = common.mustCall(function(data) {
// Make sure I get a failed to save message and not some other error.
assert.strictEqual(data, `Failed to save: ${invalidFileName}\n`);
// Reset to no-op.
putIn.write = () => {};
const invalidFilePath = tmpdir.resolve('\0\0\0\0\0');

output.write = common.mustCall(function(data) {
assert.strictEqual(data, `Failed to save: ${invalidFilePath}\n`);
output.write = () => {};
});

// Save it to a file.
putIn.run([`.save ${invalidFileName}`]);
input.run([`.save ${invalidFilePath}`]);

{
// Save .editor mode code.
const cmds = [
'function testSave() {',
'return "saved";',
'}',
];
const putIn = new ArrayStream();
const replServer = repl.start({ terminal: true, stream: putIn });
replServer.close();
}

putIn.run(['.editor']);
putIn.run(cmds);
replServer.write('', { ctrl: true, name: 'd' });
// Saving in editor mode works
{
const { replServer, input } = prepareREPL({ terminal: true });
tmpdir.refresh();

putIn.run([`.save ${saveFileName}`]);
replServer.close();
assert.strictEqual(fs.readFileSync(saveFileName, 'utf8'),
`${cmds.join('\n')}\n`);
}
input.run(['.editor']);

// Check if the file is present when using save
const commands = [
'function testSave() {',
'return "saved";',
'}',
];

// Clear the REPL.
putIn.run(['.clear']);
input.run(commands);

// Error message when using save without a file
putIn.write = common.mustCall(function(data) {
assert.strictEqual(data, 'The "file" argument must be specified\n');
putIn.write = () => {};
});
putIn.run(['.save']);
replServer.write('', { ctrl: true, name: 'd' });

// Check if the file is present when using load
const filePath = path.resolve(tmpdir.path, 'test.save.js');

// Clear the REPL.
putIn.run(['.clear']);
input.run([`.save ${filePath}`]);

// Error message when using load without a file
putIn.write = common.mustCall(function(data) {
assert.strictEqual(data, 'The "file" argument must be specified\n');
putIn.write = () => {};
});
putIn.run(['.load']);
})().then(common.mustCall());
assert.strictEqual(fs.readFileSync(filePath, 'utf8'),
`${commands.join('\n')}\n`);

replServer.close();
}
Loading