• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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