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