123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346 |
- 'use strict';
- const path = require('path');
- const fs = require('fs');
- const EventEmitter = require('events');
- const cp = require('child_process');
- const assert = require('assert');
- const debug = require('debug')('coffee');
- const spawn = require('cross-spawn');
- const show = require('./show');
- const Rule = require('./rule');
- const ErrorRule = require('./rule_error');
- const KEYS = {
- UP: '\u001b[A',
- DOWN: '\u001b[B',
- LEFT: '\u001b[D',
- RIGHT: '\u001b[C',
- ENTER: '\n',
- SPACE: ' ',
- };
- class Coffee extends EventEmitter {
- constructor(options = {}) {
- super();
- const { method, cmd, args, opt = {} } = options;
- assert(method && cmd, 'should specify method and cmd');
- assert(!opt.cwd || fs.existsSync(opt.cwd), `opt.cwd(${opt.cwd}) not exists`);
- this.method = method;
- this.cmd = cmd;
- this.args = args;
- this.opt = opt;
- // Only accept these type below for assertion
- this.RuleMapping = {
- stdout: Rule,
- stderr: Rule,
- code: Rule,
- error: ErrorRule,
- };
- this.restore();
- this._hookEvent();
- if (process.env.COFFEE_DEBUG) {
- this.debug(process.env.COFFEE_DEBUG);
- }
- process.nextTick(this._run.bind(this));
- }
- _hookEvent() {
- this.on('stdout_data', buf => {
- debug('output stdout `%s`', show(buf));
- this._debug_stdout && process.stdout.write(buf);
- this.stdout += buf;
- this.emit('stdout', buf.toString(), this);
- });
- this.on('stderr_data', buf => {
- debug('output stderr `%s`', show(buf));
- this._debug_stderr && process.stderr.write(buf);
- this.stderr += buf;
- this.emit('stderr', buf.toString(), this);
- });
- this.on('error', err => {
- this.error = err;
- });
- this.once('close', code => {
- debug('output code `%s`', show(code));
- this.code = code;
- this.complete = true;
- try {
- for (const rule of this._waitAssert) {
- rule.validate();
- }
- // suc
- const result = {
- stdout: this.stdout,
- stderr: this.stderr,
- code: this.code,
- error: this.error,
- proc: this.proc,
- };
- this.emit('complete_success', result);
- this.cb && this.cb(undefined, result);
- } catch (err) {
- err.proc = this.proc;
- this.emit('complete_error', err);
- return this.cb && this.cb(err);
- }
- });
- }
- coverage() {
- // it has not been impelmented
- // if (enable === false) {
- // process.env.NYC_NO_INSTRUMENT = true;
- // }
- return this;
- }
- debug(level) {
- this._debug_stderr = false;
- // 0 (default) -> stdout + stderr
- // 1 -> stdout
- // 2 -> stderr
- switch (String(level)) {
- case '1':
- this._debug_stdout = true;
- break;
- case '2':
- this._debug_stderr = true;
- break;
- case 'false':
- this._debug_stdout = false;
- this._debug_stderr = false;
- break;
- default:
- this._debug_stdout = true;
- this._debug_stderr = true;
- }
- return this;
- }
- /**
- * Assert type with expected value
- *
- * @param {String} type - assertion rule type, can be `code`,`stdout`,`stderr`,`error`.
- * @param {Array} args - spread args, the first item used to be a test value `{Number|String|RegExp|Array} expected`
- * @return {Coffee} return self for chain
- */
- expect(type, ...args) {
- this._addAssertion({
- type,
- args,
- });
- return this;
- }
- /**
- * Assert type with not expected value, opposite assertion of `expect`.
- *
- * @param {String} type - assertion rule type, can be `code`,`stdout`,`stderr`,`error`.
- * @param {Array} args - spread args, the first item used to be a test value `{Number|String|RegExp|Array} expected`
- * @return {Coffee} return self for chain
- */
- notExpect(type, ...args) {
- this._addAssertion({
- type,
- args,
- isOpposite: true,
- });
- return this;
- }
- _addAssertion({ type, args, isOpposite }) {
- const RuleClz = this.RuleMapping[type];
- assert(RuleClz, `unknown rule type: ${type}`);
- const rule = new RuleClz({
- ctx: this,
- type,
- expected: args[0],
- args,
- isOpposite,
- });
- if (this.complete) {
- rule.validate();
- } else {
- this._waitAssert.push(rule);
- }
- }
- /**
- * allow user to custom rule
- * @param {String} type - rule type
- * @param {Rule} RuleClz - custom rule class
- * @protected
- */
- setRule(type, RuleClz) {
- this.RuleMapping[type] = RuleClz;
- }
- /**
- * Write data to stdin of the command
- * @param {String} input - input text
- * @return {Coffee} return self for chain
- */
- write(input) {
- assert(!this._isEndCalled, 'can\'t call write after end');
- this.stdin.push(input);
- return this;
- }
- /**
- * Write special key sequence to stdin of the command, if key name not found then write origin key.
- * @example `.writeKey('2', 'ENTER', '3')`
- * @param {...String} args - input key names, will join as one key
- * @return {Coffee} return self for chain
- */
- writeKey(...args) {
- const input = args.map(x => KEYS[x] || x);
- return this.write(input.join(''));
- }
- /**
- * whether set as prompt mode
- *
- * mark as `prompt`, all stdin call by `write` will wait for `prompt` event then output
- * @param {Boolean} [enable] - default to true
- * @return {Coffee} return self for chain
- */
- waitForPrompt(enable) {
- this._isWaitForPrompt = enable !== false;
- return this;
- }
- /**
- * get `end` hook
- *
- * @param {Function} [cb] - callback, recommended to left undefind and use promise
- * @return {Promise} - end promise
- */
- end(cb) {
- this.cb = cb;
- if (!cb) {
- return new Promise((resolve, reject) => {
- this.on('complete_success', resolve);
- this.on('complete_error', reject);
- });
- }
- }
- /**
- * inject script file for mock purpose
- *
- * @param {String} scriptFile - script file full path
- * @return {Coffee} return self for chain
- */
- beforeScript(scriptFile) {
- assert(this.method === 'fork', `can't set beforeScript on ${this.method} process`);
- assert(path.isAbsolute(this.cmd), `can't set beforeScript, ${this.cmd} must be absolute path`);
- this._beforeScriptFile = scriptFile;
- return this;
- }
- _run() {
- this._isEndCalled = true;
- if (this._beforeScriptFile) {
- const execArgv = this.opt.execArgv ? this.opt.execArgv : [].concat(process.execArgv);
- execArgv.push('-r', this._beforeScriptFile);
- this.opt.execArgv = execArgv;
- }
- const cmd = this.proc = run(this.method, this.cmd, this.args, this.opt);
- cmd.stdout && cmd.stdout.on('data', this.emit.bind(this, 'stdout_data'));
- cmd.stderr && cmd.stderr.on('data', this.emit.bind(this, 'stderr_data'));
- cmd.once('error', this.emit.bind(this, 'error'));
- cmd.once('close', this.emit.bind(this, 'close'));
- process.once('exit', code => {
- debug(`coffee exit with ${code}`);
- cmd.exitCode = code;
- cmd.kill();
- });
- if (this.stdin.length) {
- if (this._isWaitForPrompt) {
- // wait for message then write to stdin
- cmd.on('message', msg => {
- if (msg.type !== 'prompt' || this.stdin.length === 0) return;
- const buf = this.stdin.shift();
- debug('prompt stdin `%s`', show(buf));
- cmd.stdin.write(buf);
- if (this.stdin.length === 0) cmd.stdin.end();
- });
- } else {
- // write immediately
- this.stdin.forEach(function(buf) {
- debug('input stdin `%s`', show(buf));
- cmd.stdin.write(buf);
- });
- cmd.stdin.end();
- }
- }
- return this;
- }
- restore() {
- // cache input for command
- this.stdin = [];
- // cache output for command
- this.stdout = '';
- this.stderr = '';
- this.code = null;
- this.error = null;
- // cache expected output
- this._waitAssert = [];
- this.complete = false;
- this._isEndCalled = false;
- this._isWaitForPrompt = false;
- this._debug_stdout = false;
- this._debug_stderr = false;
- this._isCoverage = true;
- return this;
- }
- }
- module.exports = Coffee;
- function run(method, cmd, args, opt) {
- if (!opt && args && typeof args === 'object' && !Array.isArray(args)) {
- // run(method, cmd, opt)
- opt = args;
- args = null;
- }
- args = args || [];
- opt = opt || {};
- // Force pipe to parent
- if (method === 'fork') {
- // Boolean If true, stdin, stdout, and stderr of the child will be piped to the parent,
- // otherwise they will be inherited from the parent
- opt.silent = true;
- }
- debug('child_process.%s("%s", [%s], %j)', method, cmd, args, opt);
- let handler = cp[method];
- /* istanbul ignore next */
- if (process.platform === 'win32' && method === 'spawn') handler = spawn;
- return handler(cmd, args, opt);
- }
|