UNPKG

8.92 kBJavaScriptView Raw
1'use strict';
2
3const path = require('path');
4const fs = require('fs');
5const EventEmitter = require('events');
6const cp = require('child_process');
7const assert = require('assert');
8const debug = require('debug')('coffee');
9const spawn = require('cross-spawn');
10const show = require('./show');
11const Rule = require('./rule');
12const ErrorRule = require('./rule_error');
13
14const KEYS = {
15 UP: '\u001b[A',
16 DOWN: '\u001b[B',
17 LEFT: '\u001b[D',
18 RIGHT: '\u001b[C',
19 ENTER: '\n',
20 SPACE: ' ',
21};
22
23class Coffee extends EventEmitter {
24
25 constructor(options = {}) {
26 super();
27 const { method, cmd, args, opt = {} } = options;
28
29 assert(method && cmd, 'should specify method and cmd');
30 assert(!opt.cwd || fs.existsSync(opt.cwd), `opt.cwd(${opt.cwd}) not exists`);
31
32 this.method = method;
33 this.cmd = cmd;
34 this.args = args;
35 this.opt = opt;
36
37 // Only accept these type below for assertion
38 this.RuleMapping = {
39 stdout: Rule,
40 stderr: Rule,
41 code: Rule,
42 error: ErrorRule,
43 };
44
45 this.restore();
46
47 this._hookEvent();
48
49 if (process.env.COFFEE_DEBUG) {
50 this.debug(process.env.COFFEE_DEBUG);
51 }
52
53 process.nextTick(this._run.bind(this));
54 }
55
56 _hookEvent() {
57 this.on('stdout_data', buf => {
58 debug('output stdout `%s`', show(buf));
59 this._debug_stdout && process.stdout.write(buf);
60 this.stdout += buf;
61 this.emit('stdout', buf.toString());
62 });
63 this.on('stderr_data', buf => {
64 debug('output stderr `%s`', show(buf));
65 this._debug_stderr && process.stderr.write(buf);
66 this.stderr += buf;
67 this.emit('stderr', buf.toString());
68 });
69 this.on('error', err => {
70 this.error = err;
71 });
72 this.once('close', code => {
73 debug('output code `%s`', show(code));
74 this.code = code;
75 this.complete = true;
76 try {
77 for (const rule of this._waitAssert) {
78 rule.validate();
79 }
80 // suc
81 const result = {
82 stdout: this.stdout,
83 stderr: this.stderr,
84 code: this.code,
85 error: this.error,
86 proc: this.proc,
87 };
88 this.emit('complete_success', result);
89 this.cb && this.cb(undefined, result);
90 } catch (err) {
91 err.proc = this.proc;
92 this.emit('complete_error', err);
93 return this.cb && this.cb(err);
94 }
95 });
96 }
97
98 coverage() {
99 // it has not been impelmented
100 // if (enable === false) {
101 // process.env.NYC_NO_INSTRUMENT = true;
102 // }
103 return this;
104 }
105
106 debug(level) {
107 this._debug_stderr = false;
108
109 // 0 (default) -> stdout + stderr
110 // 1 -> stdout
111 // 2 -> stderr
112 switch (String(level)) {
113 case '1':
114 this._debug_stdout = true;
115 break;
116 case '2':
117 this._debug_stderr = true;
118 break;
119 case 'false':
120 this._debug_stdout = false;
121 this._debug_stderr = false;
122 break;
123 default:
124 this._debug_stdout = true;
125 this._debug_stderr = true;
126 }
127
128 return this;
129 }
130
131 /**
132 * Assert type with expected value
133 *
134 * @param {String} type - assertion rule type, can be `code`,`stdout`,`stderr`,`error`.
135 * @param {Array} args - spread args, the first item used to be a test value `{Number|String|RegExp|Array} expected`
136 * @return {Coffee} return self for chain
137 */
138 expect(type, ...args) {
139 this._addAssertion({
140 type,
141 args,
142 });
143 return this;
144 }
145
146 /**
147 * Assert type with not expected value, opposite assertion of `expect`.
148 *
149 * @param {String} type - assertion rule type, can be `code`,`stdout`,`stderr`,`error`.
150 * @param {Array} args - spread args, the first item used to be a test value `{Number|String|RegExp|Array} expected`
151 * @return {Coffee} return self for chain
152 */
153 notExpect(type, ...args) {
154 this._addAssertion({
155 type,
156 args,
157 isOpposite: true,
158 });
159 return this;
160 }
161
162 _addAssertion({ type, args, isOpposite }) {
163 const RuleClz = this.RuleMapping[type];
164 assert(RuleClz, `unknown rule type: ${type}`);
165
166 const rule = new RuleClz({
167 ctx: this,
168 type,
169 expected: args[0],
170 args,
171 isOpposite,
172 });
173
174 if (this.complete) {
175 rule.validate();
176 } else {
177 this._waitAssert.push(rule);
178 }
179 }
180
181 /**
182 * allow user to custom rule
183 * @param {String} type - rule type
184 * @param {Rule} RuleClz - custom rule class
185 * @protected
186 */
187 setRule(type, RuleClz) {
188 this.RuleMapping[type] = RuleClz;
189 }
190
191 /**
192 * Write data to stdin of the command
193 * @param {String} input - input text
194 * @return {Coffee} return self for chain
195 */
196 write(input) {
197 assert(!this._isEndCalled, 'can\'t call write after end');
198 this.stdin.push(input);
199 return this;
200 }
201
202 /**
203 * Write special key sequence to stdin of the command, if key name not found then write origin key.
204 * @example `.writeKey('2', 'ENTER', '3')`
205 * @param {...String} args - input key names, will join as one key
206 * @return {Coffee} return self for chain
207 */
208 writeKey(...args) {
209 const input = args.map(x => KEYS[x] || x);
210 return this.write(input.join(''));
211 }
212
213 /**
214 * whether set as prompt mode
215 *
216 * mark as `prompt`, all stdin call by `write` will wait for `prompt` event then output
217 * @param {Boolean} [enable] - default to true
218 * @return {Coffee} return self for chain
219 */
220 waitForPrompt(enable) {
221 this._isWaitForPrompt = enable !== false;
222 return this;
223 }
224
225 /**
226 * get `end` hook
227 *
228 * @param {Function} [cb] - callback, recommended to left undefind and use promise
229 * @return {Promise} - end promise
230 */
231 end(cb) {
232 this.cb = cb;
233 if (!cb) {
234 return new Promise((resolve, reject) => {
235 this.on('complete_success', resolve);
236 this.on('complete_error', reject);
237 });
238 }
239 }
240
241 /**
242 * inject script file for mock purpose
243 *
244 * @param {String} scriptFile - script file full path
245 * @return {Coffee} return self for chain
246 */
247 beforeScript(scriptFile) {
248 assert(this.method === 'fork', `can't set beforeScript on ${this.method} process`);
249 assert(path.isAbsolute(this.cmd), `can't set beforeScript, ${this.cmd} must be absolute path`);
250 this._beforeScriptFile = scriptFile;
251
252 return this;
253 }
254
255 _run() {
256 this._isEndCalled = true;
257
258 if (this._beforeScriptFile) {
259 const execArgv = this.opt.execArgv ? this.opt.execArgv : [].concat(process.execArgv);
260 execArgv.push('-r', this._beforeScriptFile);
261 this.opt.execArgv = execArgv;
262 }
263
264 const cmd = this.proc = run(this.method, this.cmd, this.args, this.opt);
265
266 cmd.stdout && cmd.stdout.on('data', this.emit.bind(this, 'stdout_data'));
267 cmd.stderr && cmd.stderr.on('data', this.emit.bind(this, 'stderr_data'));
268 cmd.once('error', this.emit.bind(this, 'error'));
269 cmd.once('close', this.emit.bind(this, 'close'));
270
271 process.once('exit', code => {
272 debug(`coffee exit with ${code}`);
273 cmd.exitCode = code;
274 cmd.kill();
275 });
276
277 if (this.stdin.length) {
278 if (this._isWaitForPrompt) {
279 // wait for message then write to stdin
280 cmd.on('message', msg => {
281 if (msg.type !== 'prompt' || this.stdin.length === 0) return;
282 const buf = this.stdin.shift();
283 debug('prompt stdin `%s`', show(buf));
284 cmd.stdin.write(buf);
285 if (this.stdin.length === 0) cmd.stdin.end();
286 });
287 } else {
288 // write immediately
289 this.stdin.forEach(function(buf) {
290 debug('input stdin `%s`', show(buf));
291 cmd.stdin.write(buf);
292 });
293 cmd.stdin.end();
294 }
295 } else {
296 // end stdin anyway
297 cmd.stdin.end();
298 }
299
300 return this;
301 }
302
303 restore() {
304 // cache input for command
305 this.stdin = [];
306
307 // cache output for command
308 this.stdout = '';
309 this.stderr = '';
310 this.code = null;
311 this.error = null;
312
313 // cache expected output
314 this._waitAssert = [];
315 this.complete = false;
316 this._isEndCalled = false;
317 this._isWaitForPrompt = false;
318 this._debug_stdout = false;
319 this._debug_stderr = false;
320 this._isCoverage = true;
321 return this;
322 }
323}
324
325module.exports = Coffee;
326
327function run(method, cmd, args, opt) {
328 if (!opt && args && typeof args === 'object' && !Array.isArray(args)) {
329 // run(method, cmd, opt)
330 opt = args;
331 args = null;
332 }
333
334 args = args || [];
335 opt = opt || {};
336
337 // Force pipe to parent
338 if (method === 'fork') {
339 // Boolean If true, stdin, stdout, and stderr of the child will be piped to the parent,
340 // otherwise they will be inherited from the parent
341 opt.silent = true;
342 }
343
344 debug('child_process.%s("%s", [%s], %j)', method, cmd, args, opt);
345 let handler = cp[method];
346 /* istanbul ignore next */
347 if (process.platform === 'win32' && method === 'spawn') handler = spawn;
348 return handler(cmd, args, opt);
349}