• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2const spawn = require('child_process').spawn;
3
4// This allows us to keep the helper inside of `test/` without tap warning
5// about "pending" test files.
6const tap = require('tap');
7tap.test('startCLI', (t) => t.end());
8
9const CLI =
10  process.env.USE_EMBEDDED_NODE_INSPECT === '1' ?
11    'inspect' :
12    require.resolve('../../cli.js');
13
14const BREAK_MESSAGE = new RegExp('(?:' + [
15  'assert', 'break', 'break on start', 'debugCommand',
16  'exception', 'other', 'promiseRejection',
17].join('|') + ') in', 'i');
18
19function isPreBreak(output) {
20  return /Break on start/.test(output) && /1 \(function \(exports/.test(output);
21}
22
23function startCLI(args, flags = [], spawnOpts = {}) {
24  const child = spawn(process.execPath, [...flags, CLI, ...args], spawnOpts);
25  let isFirstStdoutChunk = true;
26
27  const outputBuffer = [];
28  function bufferOutput(chunk) {
29    if (isFirstStdoutChunk) {
30      isFirstStdoutChunk = false;
31      outputBuffer.push(chunk.replace(/^debug>\s*/, ''));
32    } else {
33      outputBuffer.push(chunk);
34    }
35  }
36
37  function getOutput() {
38    return outputBuffer.join('').toString()
39      .replace(/^[^\n]*?[\b]/mg, '');
40  }
41
42  child.stdout.setEncoding('utf8');
43  child.stdout.on('data', bufferOutput);
44  child.stderr.setEncoding('utf8');
45  child.stderr.on('data', bufferOutput);
46
47  if (process.env.VERBOSE === '1') {
48    child.stdout.pipe(process.stderr);
49    child.stderr.pipe(process.stderr);
50  }
51
52  return {
53    flushOutput() {
54      const output = this.output;
55      outputBuffer.length = 0;
56      return output;
57    },
58
59    waitFor(pattern, timeout = 2000) {
60      function checkPattern(str) {
61        if (Array.isArray(pattern)) {
62          return pattern.every((p) => p.test(str));
63        }
64        return pattern.test(str);
65      }
66
67      return new Promise((resolve, reject) => {
68        function checkOutput() {
69          if (checkPattern(getOutput())) {
70            tearDown(); // eslint-disable-line no-use-before-define
71            resolve();
72          }
73        }
74
75        function onChildExit() {
76          tearDown(); // eslint-disable-line no-use-before-define
77          reject(new Error(
78            `Child quit while waiting for ${pattern}; found: ${this.output}`));
79        }
80
81        const timer = setTimeout(() => {
82          tearDown(); // eslint-disable-line no-use-before-define
83          reject(new Error([
84            `Timeout (${timeout}) while waiting for ${pattern}`,
85            `found: ${this.output}`,
86          ].join('; ')));
87        }, timeout);
88
89        function tearDown() {
90          clearTimeout(timer);
91          child.stdout.removeListener('data', checkOutput);
92          child.removeListener('exit', onChildExit);
93        }
94
95        child.on('exit', onChildExit);
96        child.stdout.on('data', checkOutput);
97        checkOutput();
98      });
99    },
100
101    waitForPrompt(timeout = 2000) {
102      return this.waitFor(/>\s+$/, timeout);
103    },
104
105    waitForInitialBreak(timeout = 2000) {
106      return this.waitFor(/break (?:on start )?in/i, timeout)
107        .then(() => {
108          if (isPreBreak(this.output)) {
109            return this.command('next', false)
110              .then(() => this.waitFor(/break in/, timeout));
111          }
112        });
113    },
114
115    get breakInfo() {
116      const output = this.output;
117      const breakMatch =
118        output.match(/break (?:on start )?in ([^\n]+):(\d+)\n/i);
119
120      if (breakMatch === null) {
121        throw new Error(
122          `Could not find breakpoint info in ${JSON.stringify(output)}`);
123      }
124      return { filename: breakMatch[1], line: +breakMatch[2] };
125    },
126
127    ctrlC() {
128      return this.command('.interrupt');
129    },
130
131    get output() {
132      return getOutput();
133    },
134
135    get rawOutput() {
136      return outputBuffer.join('').toString();
137    },
138
139    parseSourceLines() {
140      return getOutput().split('\n')
141        .map((line) => line.match(/(?:\*|>)?\s*(\d+)/))
142        .filter((match) => match !== null)
143        .map((match) => +match[1]);
144    },
145
146    writeLine(input, flush = true) {
147      if (flush) {
148        this.flushOutput();
149      }
150      if (process.env.VERBOSE === '1') {
151        process.stderr.write(`< ${input}\n`);
152      }
153      child.stdin.write(input);
154      child.stdin.write('\n');
155    },
156
157    command(input, flush = true) {
158      this.writeLine(input, flush);
159      return this.waitForPrompt();
160    },
161
162    stepCommand(input) {
163      this.writeLine(input, true);
164      return this
165        .waitFor(BREAK_MESSAGE)
166        .then(() => this.waitForPrompt());
167    },
168
169    quit() {
170      return new Promise((resolve) => {
171        child.stdin.end();
172        child.on('exit', resolve);
173      });
174    },
175  };
176}
177module.exports = startCLI;
178