1 | 'use strict';
|
2 |
|
3 | const path = require('path');
|
4 | const fs = require('fs');
|
5 | const EventEmitter = require('events');
|
6 | const cp = require('child_process');
|
7 | const assert = require('assert');
|
8 | const debug = require('debug')('coffee');
|
9 | const spawn = require('cross-spawn');
|
10 | const show = require('./show');
|
11 | const Rule = require('./rule');
|
12 | const ErrorRule = require('./rule_error');
|
13 |
|
14 | const KEYS = {
|
15 | UP: '\u001b[A',
|
16 | DOWN: '\u001b[B',
|
17 | LEFT: '\u001b[D',
|
18 | RIGHT: '\u001b[C',
|
19 | ENTER: '\n',
|
20 | SPACE: ' ',
|
21 | };
|
22 |
|
23 | class 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 |
|
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 |
|
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 |
|
100 |
|
101 |
|
102 |
|
103 | return this;
|
104 | }
|
105 |
|
106 | debug(level) {
|
107 | this._debug_stderr = false;
|
108 |
|
109 |
|
110 |
|
111 |
|
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 |
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 | expect(type, ...args) {
|
139 | this._addAssertion({
|
140 | type,
|
141 | args,
|
142 | });
|
143 | return this;
|
144 | }
|
145 |
|
146 | |
147 |
|
148 |
|
149 |
|
150 |
|
151 |
|
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 |
|
183 |
|
184 |
|
185 |
|
186 |
|
187 | setRule(type, RuleClz) {
|
188 | this.RuleMapping[type] = RuleClz;
|
189 | }
|
190 |
|
191 | |
192 |
|
193 |
|
194 |
|
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 |
|
204 |
|
205 |
|
206 |
|
207 |
|
208 | writeKey(...args) {
|
209 | const input = args.map(x => KEYS[x] || x);
|
210 | return this.write(input.join(''));
|
211 | }
|
212 |
|
213 | |
214 |
|
215 |
|
216 |
|
217 |
|
218 |
|
219 |
|
220 | waitForPrompt(enable) {
|
221 | this._isWaitForPrompt = enable !== false;
|
222 | return this;
|
223 | }
|
224 |
|
225 | |
226 |
|
227 |
|
228 |
|
229 |
|
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 |
|
243 |
|
244 |
|
245 |
|
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 |
|
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 |
|
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 |
|
297 | cmd.stdin.end();
|
298 | }
|
299 |
|
300 | return this;
|
301 | }
|
302 |
|
303 | restore() {
|
304 |
|
305 | this.stdin = [];
|
306 |
|
307 |
|
308 | this.stdout = '';
|
309 | this.stderr = '';
|
310 | this.code = null;
|
311 | this.error = null;
|
312 |
|
313 |
|
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 |
|
325 | module.exports = Coffee;
|
326 |
|
327 | function run(method, cmd, args, opt) {
|
328 | if (!opt && args && typeof args === 'object' && !Array.isArray(args)) {
|
329 |
|
330 | opt = args;
|
331 | args = null;
|
332 | }
|
333 |
|
334 | args = args || [];
|
335 | opt = opt || {};
|
336 |
|
337 |
|
338 | if (method === 'fork') {
|
339 |
|
340 |
|
341 | opt.silent = true;
|
342 | }
|
343 |
|
344 | debug('child_process.%s("%s", [%s], %j)', method, cmd, args, opt);
|
345 | let handler = cp[method];
|
346 |
|
347 | if (process.platform === 'win32' && method === 'spawn') handler = spawn;
|
348 | return handler(cmd, args, opt);
|
349 | }
|