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