UNPKG

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