coffee.js 8.7 KB

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