| 'use strict'; |
| |
| const common = require('../common'); |
| const fixtures = require('../common/fixtures'); |
| const stream = require('stream'); |
| const REPL = require('repl'); |
| const assert = require('assert'); |
| const fs = require('fs'); |
| const path = require('path'); |
| const os = require('os'); |
| |
| const tmpdir = require('../common/tmpdir'); |
| tmpdir.refresh(); |
| |
| // Mock os.homedir() |
| os.homedir = function() { |
| return tmpdir.path; |
| }; |
| |
| // 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. |
| setImmediate(() => { |
| 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}\n`); |
| } |
| setImmediate(doAction); |
| }; |
| setImmediate(doAction); |
| } |
| resume() {} |
| pause() {} |
| } |
| ActionStream.prototype.readable = true; |
| |
| |
| // Mock keys |
| const UP = { name: 'up' }; |
| const ENTER = { name: 'enter' }; |
| const CLEAR = { ctrl: true, name: 'u' }; |
| |
| // File paths |
| const historyFixturePath = fixtures.path('.node_repl_history'); |
| const historyPath = path.join(tmpdir.path, '.fixture_copy_repl_history'); |
| const historyPathFail = fixtures.path('nonexistent_folder', 'filename'); |
| const defaultHistoryPath = path.join(tmpdir.path, '.node_repl_history'); |
| const emptyHiddenHistoryPath = fixtures.path('.empty-hidden-repl-history-file'); |
| const devNullHistoryPath = path.join(tmpdir.path, |
| '.dev-null-repl-history-file'); |
| // Common message bits |
| const prompt = '> '; |
| const replDisabled = '\nPersistent history support disabled. Set the ' + |
| 'NODE_REPL_HISTORY environment\nvariable to a valid, ' + |
| 'user-writable path to enable.\n'; |
| const homedirErr = '\nError: Could not get the home directory.\n' + |
| 'REPL session history will not be persisted.\n'; |
| const replFailedRead = '\nError: Could not open history file.\n' + |
| 'REPL session history will not be persisted.\n'; |
| |
| const tests = [ |
| { |
| env: { NODE_REPL_HISTORY: '' }, |
| test: [UP], |
| expected: [prompt, replDisabled, prompt] |
| }, |
| { |
| env: { NODE_REPL_HISTORY: ' ' }, |
| test: [UP], |
| expected: [prompt, replDisabled, prompt] |
| }, |
| { |
| env: { NODE_REPL_HISTORY: historyPath }, |
| test: [UP, CLEAR], |
| expected: [prompt, `${prompt}'you look fabulous today'`, prompt] |
| }, |
| { |
| env: {}, |
| test: [UP, '\'42\'', ENTER], |
| expected: [prompt, '\'', '4', '2', '\'', '\'42\'\n', prompt, prompt], |
| clean: false |
| }, |
| { // Requires the above test case |
| env: {}, |
| test: [UP, UP, ENTER], |
| expected: [prompt, `${prompt}'42'`, '\'42\'\n', prompt] |
| }, |
| { |
| env: { NODE_REPL_HISTORY: historyPath, |
| NODE_REPL_HISTORY_SIZE: 1 }, |
| test: [UP, UP, CLEAR], |
| expected: [prompt, `${prompt}'you look fabulous today'`, prompt] |
| }, |
| { |
| env: { NODE_REPL_HISTORY: historyPathFail, |
| NODE_REPL_HISTORY_SIZE: 1 }, |
| test: [UP], |
| expected: [prompt, replFailedRead, prompt, replDisabled, prompt] |
| }, |
| { |
| before: function before() { |
| if (common.isWindows) { |
| const execSync = require('child_process').execSync; |
| execSync(`ATTRIB +H "${emptyHiddenHistoryPath}"`, (err) => { |
| assert.ifError(err); |
| }); |
| } |
| }, |
| env: { NODE_REPL_HISTORY: emptyHiddenHistoryPath }, |
| test: [UP], |
| expected: [prompt] |
| }, |
| { |
| before: function before() { |
| if (!common.isWindows) |
| fs.symlinkSync('/dev/null', devNullHistoryPath); |
| }, |
| env: { NODE_REPL_HISTORY: devNullHistoryPath }, |
| test: [UP], |
| expected: [prompt] |
| }, |
| { // Make sure this is always the last test, since we change os.homedir() |
| before: function before() { |
| // Mock os.homedir() failure |
| os.homedir = function() { |
| throw new Error('os.homedir() failure'); |
| }; |
| }, |
| env: {}, |
| test: [UP], |
| expected: [prompt, homedirErr, prompt, replDisabled, prompt] |
| } |
| ]; |
| const numtests = tests.length; |
| |
| |
| 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; |
| } |
| |
| // Copy our fixture to the tmp directory |
| fs.createReadStream(historyFixturePath) |
| .pipe(fs.createWriteStream(historyPath)).on('unpipe', () => runTest()); |
| |
| const runTestWrap = common.mustCall(runTest, numtests); |
| |
| function runTest(assertCleaned) { |
| const opts = tests.shift(); |
| if (!opts) return; // All done |
| |
| if (assertCleaned) { |
| try { |
| assert.strictEqual(fs.readFileSync(defaultHistoryPath, 'utf8'), ''); |
| } catch (e) { |
| if (e.code !== 'ENOENT') { |
| console.error(`Failed test # ${numtests - tests.length}`); |
| throw e; |
| } |
| } |
| } |
| |
| const test = opts.test; |
| const expected = opts.expected; |
| const clean = opts.clean; |
| const before = opts.before; |
| const historySize = opts.env.NODE_REPL_HISTORY_SIZE; |
| const historyFile = opts.env.NODE_REPL_HISTORY; |
| |
| if (before) before(); |
| |
| const repl = REPL.start({ |
| input: new ActionStream(), |
| output: new stream.Writable({ |
| write(chunk, _, next) { |
| const output = chunk.toString(); |
| |
| // Ignore escapes and blank lines |
| if (output.charCodeAt(0) === 27 || /^[\r\n]+$/.test(output)) |
| return next(); |
| |
| try { |
| assert.strictEqual(output, expected.shift()); |
| } catch (err) { |
| console.error(`Failed test # ${numtests - tests.length}`); |
| throw err; |
| } |
| next(); |
| } |
| }), |
| prompt: prompt, |
| useColors: false, |
| terminal: true, |
| historySize: historySize |
| }); |
| |
| repl.setupHistory(historyFile, function(err, repl) { |
| if (err) { |
| console.error(`Failed test # ${numtests - tests.length}`); |
| throw err; |
| } |
| |
| repl.once('close', () => { |
| if (repl._flushing) { |
| repl.once('flushHistory', onClose); |
| return; |
| } |
| |
| onClose(); |
| }); |
| |
| function onClose() { |
| const cleaned = clean === false ? false : cleanupTmpFile(); |
| |
| try { |
| // Ensure everything that we expected was output |
| assert.strictEqual(expected.length, 0); |
| setImmediate(runTestWrap, cleaned); |
| } catch (err) { |
| console.error(`Failed test # ${numtests - tests.length}`); |
| throw err; |
| } |
| } |
| |
| repl.inputStream.run(test); |
| }); |
| } |