1'use strict'; 2 3const common = require('../common'); 4const fixtures = require('../common/fixtures'); 5const stream = require('stream'); 6const REPL = require('repl'); 7const assert = require('assert'); 8const fs = require('fs'); 9const path = require('path'); 10const os = require('os'); 11const util = require('util'); 12 13common.skipIfDumbTerminal(); 14 15const tmpdir = require('../common/tmpdir'); 16tmpdir.refresh(); 17 18// Mock os.homedir() 19os.homedir = function() { 20 return tmpdir.path; 21}; 22 23// Create an input stream specialized for testing an array of actions 24class ActionStream extends stream.Stream { 25 run(data) { 26 const _iter = data[Symbol.iterator](); 27 const doAction = () => { 28 const next = _iter.next(); 29 if (next.done) { 30 // Close the repl. Note that it must have a clean prompt to do so. 31 setImmediate(() => { 32 this.emit('keypress', '', { ctrl: true, name: 'd' }); 33 }); 34 return; 35 } 36 const action = next.value; 37 38 if (typeof action === 'object') { 39 this.emit('keypress', '', action); 40 } else { 41 this.emit('data', `${action}\n`); 42 } 43 setImmediate(doAction); 44 }; 45 setImmediate(doAction); 46 } 47 resume() {} 48 pause() {} 49} 50ActionStream.prototype.readable = true; 51 52 53// Mock keys 54const UP = { name: 'up' }; 55const DOWN = { name: 'down' }; 56const ENTER = { name: 'enter' }; 57const CLEAR = { ctrl: true, name: 'u' }; 58 59// File paths 60const historyFixturePath = fixtures.path('.node_repl_history'); 61const historyPath = path.join(tmpdir.path, '.fixture_copy_repl_history'); 62const historyPathFail = fixtures.path('nonexistent_folder', 'filename'); 63const defaultHistoryPath = path.join(tmpdir.path, '.node_repl_history'); 64const emptyHiddenHistoryPath = fixtures.path('.empty-hidden-repl-history-file'); 65const devNullHistoryPath = path.join(tmpdir.path, 66 '.dev-null-repl-history-file'); 67// Common message bits 68const prompt = '> '; 69const replDisabled = '\nPersistent history support disabled. Set the ' + 70 'NODE_REPL_HISTORY environment\nvariable to a valid, ' + 71 'user-writable path to enable.\n'; 72const homedirErr = '\nError: Could not get the home directory.\n' + 73 'REPL session history will not be persisted.\n'; 74const replFailedRead = '\nError: Could not open history file.\n' + 75 'REPL session history will not be persisted.\n'; 76 77const tests = [ 78 { 79 env: { NODE_REPL_HISTORY: '' }, 80 test: [UP], 81 expected: [prompt, replDisabled, prompt] 82 }, 83 { 84 env: { NODE_REPL_HISTORY: ' ' }, 85 test: [UP], 86 expected: [prompt, replDisabled, prompt] 87 }, 88 { 89 env: { NODE_REPL_HISTORY: historyPath }, 90 test: [UP, CLEAR], 91 expected: [prompt, `${prompt}'you look fabulous today'`, prompt] 92 }, 93 { 94 env: {}, 95 test: [UP, '21', ENTER, "'42'", ENTER], 96 expected: [ 97 prompt, 98 // TODO(BridgeAR): The line is refreshed too many times. The double prompt 99 // is redundant and can be optimized away. 100 '2', '1', '21\n', prompt, prompt, 101 "'", '4', '2', "'", "'42'\n", prompt, prompt, 102 ], 103 clean: false 104 }, 105 { // Requires the above test case 106 env: {}, 107 test: [UP, UP, UP, DOWN, ENTER], 108 expected: [ 109 prompt, 110 `${prompt}'42'`, 111 `${prompt}21`, 112 prompt, 113 `${prompt}21`, 114 '21\n', 115 prompt, 116 ] 117 }, 118 { 119 env: { NODE_REPL_HISTORY: historyPath, 120 NODE_REPL_HISTORY_SIZE: 1 }, 121 test: [UP, UP, DOWN, CLEAR], 122 expected: [ 123 prompt, 124 `${prompt}'you look fabulous today'`, 125 prompt, 126 `${prompt}'you look fabulous today'`, 127 prompt, 128 ] 129 }, 130 { 131 env: { NODE_REPL_HISTORY: historyPathFail, 132 NODE_REPL_HISTORY_SIZE: 1 }, 133 test: [UP], 134 expected: [prompt, replFailedRead, prompt, replDisabled, prompt] 135 }, 136 { 137 before: function before() { 138 if (common.isWindows) { 139 const execSync = require('child_process').execSync; 140 execSync(`ATTRIB +H "${emptyHiddenHistoryPath}"`, (err) => { 141 assert.ifError(err); 142 }); 143 } 144 }, 145 env: { NODE_REPL_HISTORY: emptyHiddenHistoryPath }, 146 test: [UP], 147 expected: [prompt] 148 }, 149 { 150 before: function before() { 151 if (!common.isWindows) 152 fs.symlinkSync('/dev/null', devNullHistoryPath); 153 }, 154 env: { NODE_REPL_HISTORY: devNullHistoryPath }, 155 test: [UP], 156 expected: [prompt] 157 }, 158 { // Make sure this is always the last test, since we change os.homedir() 159 before: function before() { 160 // Mock os.homedir() failure 161 os.homedir = function() { 162 throw new Error('os.homedir() failure'); 163 }; 164 }, 165 env: {}, 166 test: [UP], 167 expected: [prompt, homedirErr, prompt, replDisabled, prompt] 168 }, 169]; 170const numtests = tests.length; 171 172 173function cleanupTmpFile() { 174 try { 175 // Write over the file, clearing any history 176 fs.writeFileSync(defaultHistoryPath, ''); 177 } catch (err) { 178 if (err.code === 'ENOENT') return true; 179 throw err; 180 } 181 return true; 182} 183 184// Copy our fixture to the tmp directory 185fs.createReadStream(historyFixturePath) 186 .pipe(fs.createWriteStream(historyPath)).on('unpipe', () => runTest()); 187 188const runTestWrap = common.mustCall(runTest, numtests); 189 190function runTest(assertCleaned) { 191 const opts = tests.shift(); 192 if (!opts) return; // All done 193 194 console.log('NEW'); 195 196 if (assertCleaned) { 197 try { 198 assert.strictEqual(fs.readFileSync(defaultHistoryPath, 'utf8'), ''); 199 } catch (e) { 200 if (e.code !== 'ENOENT') { 201 console.error(`Failed test # ${numtests - tests.length}`); 202 throw e; 203 } 204 } 205 } 206 207 const test = opts.test; 208 const expected = opts.expected; 209 const clean = opts.clean; 210 const before = opts.before; 211 const historySize = opts.env.NODE_REPL_HISTORY_SIZE; 212 const historyFile = opts.env.NODE_REPL_HISTORY; 213 214 if (before) before(); 215 216 const repl = REPL.start({ 217 input: new ActionStream(), 218 output: new stream.Writable({ 219 write(chunk, _, next) { 220 const output = chunk.toString(); 221 console.log('INPUT', util.inspect(output)); 222 223 // Ignore escapes and blank lines 224 if (output.charCodeAt(0) === 27 || /^[\r\n]+$/.test(output)) 225 return next(); 226 227 try { 228 assert.strictEqual(output, expected.shift()); 229 } catch (err) { 230 console.error(`Failed test # ${numtests - tests.length}`); 231 throw err; 232 } 233 next(); 234 } 235 }), 236 prompt: prompt, 237 useColors: false, 238 terminal: true, 239 historySize: historySize 240 }); 241 242 repl.setupHistory(historyFile, function(err, repl) { 243 if (err) { 244 console.error(`Failed test # ${numtests - tests.length}`); 245 throw err; 246 } 247 248 repl.once('close', () => { 249 if (repl._flushing) { 250 repl.once('flushHistory', onClose); 251 return; 252 } 253 254 onClose(); 255 }); 256 257 function onClose() { 258 const cleaned = clean === false ? false : cleanupTmpFile(); 259 260 try { 261 // Ensure everything that we expected was output 262 assert.strictEqual(expected.length, 0); 263 setImmediate(runTestWrap, cleaned); 264 } catch (err) { 265 console.error(`Failed test # ${numtests - tests.length}`); 266 throw err; 267 } 268 } 269 270 repl.inputStream.run(test); 271 }); 272} 273