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