concurrently.spec.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. const readline = require('readline');
  2. const _ = require('lodash');
  3. const Rx = require('rxjs');
  4. const { buffer, map } = require('rxjs/operators');
  5. const spawn = require('spawn-command');
  6. const isWindows = process.platform === 'win32';
  7. const createKillMessage = prefix => new RegExp(
  8. _.escapeRegExp(prefix) +
  9. ' exited with code ' +
  10. (isWindows ? 1 : '(SIGTERM|143)')
  11. );
  12. const run = args => {
  13. const child = spawn('node ./concurrently.js ' + args, {
  14. cwd: __dirname,
  15. env: Object.assign({}, process.env, {
  16. // When upgrading from jest 23 -> 24, colors started printing in the test output.
  17. // They are forcibly disabled here
  18. FORCE_COLOR: 0
  19. }),
  20. });
  21. const stdout = readline.createInterface({
  22. input: child.stdout,
  23. output: null
  24. });
  25. const stderr = readline.createInterface({
  26. input: child.stderr,
  27. output: null
  28. });
  29. const close = Rx.fromEvent(child, 'close');
  30. const log = Rx.merge(
  31. Rx.fromEvent(stdout, 'line'),
  32. Rx.fromEvent(stderr, 'line')
  33. ).pipe(map(data => data.toString()));
  34. return {
  35. close,
  36. log,
  37. stdin: child.stdin,
  38. pid: child.pid
  39. };
  40. };
  41. it('has help command', done => {
  42. run('--help').close.subscribe(event => {
  43. expect(event[0]).toBe(0);
  44. done();
  45. }, done);
  46. });
  47. it('has version command', done => {
  48. Rx.combineLatest(
  49. run('--version').close,
  50. run('-V').close,
  51. run('-v').close
  52. ).subscribe(events => {
  53. expect(events[0][0]).toBe(0);
  54. expect(events[1][0]).toBe(0);
  55. expect(events[2][0]).toBe(0);
  56. done();
  57. }, done);
  58. });
  59. describe('exiting conditions', () => {
  60. it('is of success by default when running successful commands', done => {
  61. run('"echo foo" "echo bar"')
  62. .close
  63. .subscribe(exit => {
  64. expect(exit[0]).toBe(0);
  65. done();
  66. }, done);
  67. });
  68. it('is of failure by default when one of the command fails', done => {
  69. run('"echo foo" "exit 1"')
  70. .close
  71. .subscribe(exit => {
  72. expect(exit[0]).toBeGreaterThan(0);
  73. done();
  74. }, done);
  75. });
  76. it('is of success when --success=first and first command to exit succeeds', done => {
  77. run('--success=first "echo foo" "sleep 0.5 && exit 1"')
  78. .close
  79. .subscribe(exit => {
  80. expect(exit[0]).toBe(0);
  81. done();
  82. }, done);
  83. });
  84. it('is of failure when --success=first and first command to exit fails', done => {
  85. run('--success=first "exit 1" "sleep 0.5 && echo foo"')
  86. .close
  87. .subscribe(exit => {
  88. expect(exit[0]).toBeGreaterThan(0);
  89. done();
  90. }, done);
  91. });
  92. it('is of success when --success=last and last command to exit succeeds', done => {
  93. run('--success=last "exit 1" "sleep 0.5 && echo foo"')
  94. .close
  95. .subscribe(exit => {
  96. expect(exit[0]).toBe(0);
  97. done();
  98. }, done);
  99. });
  100. it('is of failure when --success=last and last command to exit fails', done => {
  101. run('--success=last "echo foo" "sleep 0.5 && exit 1"')
  102. .close
  103. .subscribe(exit => {
  104. expect(exit[0]).toBeGreaterThan(0);
  105. done();
  106. }, done);
  107. });
  108. it.skip('is of success when a SIGINT is sent', done => {
  109. const child = run('"node fixtures/read-echo.js"');
  110. child.close.subscribe(exit => {
  111. // TODO This is null within Node, but should be 0 outside (eg from real terminal)
  112. expect(exit[0]).toBe(0);
  113. done();
  114. }, done);
  115. process.kill(child.pid, 'SIGINT');
  116. });
  117. it('is aliased to -s', done => {
  118. run('-s last "exit 1" "sleep 0.5 && echo foo"')
  119. .close
  120. .subscribe(exit => {
  121. expect(exit[0]).toBe(0);
  122. done();
  123. }, done);
  124. });
  125. });
  126. describe('--raw', () => {
  127. it('is aliased to -r', done => {
  128. const child = run('-r "echo foo" "echo bar"');
  129. child.log.pipe(buffer(child.close)).subscribe(lines => {
  130. expect(lines).toHaveLength(2);
  131. expect(lines).toContainEqual(expect.stringContaining('foo'));
  132. expect(lines).toContainEqual(expect.stringContaining('bar'));
  133. done();
  134. }, done);
  135. });
  136. it('does not log any extra output', done => {
  137. const child = run('--raw "echo foo" "echo bar"');
  138. child.log.pipe(buffer(child.close)).subscribe(lines => {
  139. expect(lines).toHaveLength(2);
  140. expect(lines).toContainEqual(expect.stringContaining('foo'));
  141. expect(lines).toContainEqual(expect.stringContaining('bar'));
  142. done();
  143. }, done);
  144. });
  145. });
  146. describe('--hide', () => {
  147. it('hides the output of a process by its index', done => {
  148. const child = run('--hide 1 "echo foo" "echo bar"');
  149. child.log.pipe(buffer(child.close)).subscribe(lines => {
  150. expect(lines).toContainEqual(expect.stringContaining('foo'));
  151. expect(lines).not.toContainEqual(expect.stringContaining('bar'));
  152. done();
  153. }, done);
  154. });
  155. it('hides the output of a process by its name', done => {
  156. const child = run('-n foo,bar --hide bar "echo foo" "echo bar"');
  157. child.log.pipe(buffer(child.close)).subscribe(lines => {
  158. expect(lines).toContainEqual(expect.stringContaining('foo'));
  159. expect(lines).not.toContainEqual(expect.stringContaining('bar'));
  160. done();
  161. }, done);
  162. });
  163. });
  164. describe('--names', () => {
  165. it('is aliased to -n', done => {
  166. const child = run('-n foo,bar "echo foo" "echo bar"');
  167. child.log.pipe(buffer(child.close)).subscribe(lines => {
  168. expect(lines).toContainEqual(expect.stringContaining('[foo] foo'));
  169. expect(lines).toContainEqual(expect.stringContaining('[bar] bar'));
  170. done();
  171. }, done);
  172. });
  173. it('prefixes with names', done => {
  174. const child = run('--names foo,bar "echo foo" "echo bar"');
  175. child.log.pipe(buffer(child.close)).subscribe(lines => {
  176. expect(lines).toContainEqual(expect.stringContaining('[foo] foo'));
  177. expect(lines).toContainEqual(expect.stringContaining('[bar] bar'));
  178. done();
  179. }, done);
  180. });
  181. it('is split using --name-separator arg', done => {
  182. const child = run('--names "foo|bar" --name-separator "|" "echo foo" "echo bar"');
  183. child.log.pipe(buffer(child.close)).subscribe(lines => {
  184. expect(lines).toContainEqual(expect.stringContaining('[foo] foo'));
  185. expect(lines).toContainEqual(expect.stringContaining('[bar] bar'));
  186. done();
  187. }, done);
  188. });
  189. });
  190. describe('--prefix', () => {
  191. it('is aliased to -p', done => {
  192. const child = run('-p command "echo foo" "echo bar"');
  193. child.log.pipe(buffer(child.close)).subscribe(lines => {
  194. expect(lines).toContainEqual(expect.stringContaining('[echo foo] foo'));
  195. expect(lines).toContainEqual(expect.stringContaining('[echo bar] bar'));
  196. done();
  197. }, done);
  198. });
  199. it('specifies custom prefix', done => {
  200. const child = run('--prefix command "echo foo" "echo bar"');
  201. child.log.pipe(buffer(child.close)).subscribe(lines => {
  202. expect(lines).toContainEqual(expect.stringContaining('[echo foo] foo'));
  203. expect(lines).toContainEqual(expect.stringContaining('[echo bar] bar'));
  204. done();
  205. }, done);
  206. });
  207. });
  208. describe('--prefix-length', () => {
  209. it('is aliased to -l', done => {
  210. const child = run('-p command -l 5 "echo foo" "echo bar"');
  211. child.log.pipe(buffer(child.close)).subscribe(lines => {
  212. expect(lines).toContainEqual(expect.stringContaining('[ec..o] foo'));
  213. expect(lines).toContainEqual(expect.stringContaining('[ec..r] bar'));
  214. done();
  215. }, done);
  216. });
  217. it('specifies custom prefix length', done => {
  218. const child = run('--prefix command --prefix-length 5 "echo foo" "echo bar"');
  219. child.log.pipe(buffer(child.close)).subscribe(lines => {
  220. expect(lines).toContainEqual(expect.stringContaining('[ec..o] foo'));
  221. expect(lines).toContainEqual(expect.stringContaining('[ec..r] bar'));
  222. done();
  223. }, done);
  224. });
  225. });
  226. describe('--restart-tries', () => {
  227. it('changes how many times a command will restart', done => {
  228. const child = run('--restart-tries 1 "exit 1"');
  229. child.log.pipe(buffer(child.close)).subscribe(lines => {
  230. expect(lines).toEqual([
  231. expect.stringContaining('[0] exit 1 exited with code 1'),
  232. expect.stringContaining('[0] exit 1 restarted'),
  233. expect.stringContaining('[0] exit 1 exited with code 1'),
  234. ]);
  235. done();
  236. }, done);
  237. });
  238. });
  239. describe('--kill-others', () => {
  240. it('is aliased to -k', done => {
  241. const child = run('-k "sleep 10" "exit 0"');
  242. child.log.pipe(buffer(child.close)).subscribe(lines => {
  243. expect(lines).toContainEqual(expect.stringContaining('[1] exit 0 exited with code 0'));
  244. expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes'));
  245. expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] sleep 10')));
  246. done();
  247. }, done);
  248. });
  249. it('kills on success', done => {
  250. const child = run('--kill-others "sleep 10" "exit 0"');
  251. child.log.pipe(buffer(child.close)).subscribe(lines => {
  252. expect(lines).toContainEqual(expect.stringContaining('[1] exit 0 exited with code 0'));
  253. expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes'));
  254. expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] sleep 10')));
  255. done();
  256. }, done);
  257. });
  258. it('kills on failure', done => {
  259. const child = run('--kill-others "sleep 10" "exit 1"');
  260. child.log.pipe(buffer(child.close)).subscribe(lines => {
  261. expect(lines).toContainEqual(expect.stringContaining('[1] exit 1 exited with code 1'));
  262. expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes'));
  263. expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] sleep 10')));
  264. done();
  265. }, done);
  266. });
  267. });
  268. describe('--kill-others-on-fail', () => {
  269. it('does not kill on success', done => {
  270. const child = run('--kill-others-on-fail "sleep 0.5" "exit 0"');
  271. child.log.pipe(buffer(child.close)).subscribe(lines => {
  272. expect(lines).toContainEqual(expect.stringContaining('[1] exit 0 exited with code 0'));
  273. expect(lines).toContainEqual(expect.stringContaining('[0] sleep 0.5 exited with code 0'));
  274. done();
  275. }, done);
  276. });
  277. it('kills on failure', done => {
  278. const child = run('--kill-others-on-fail "sleep 10" "exit 1"');
  279. child.log.pipe(buffer(child.close)).subscribe(lines => {
  280. expect(lines).toContainEqual(expect.stringContaining('[1] exit 1 exited with code 1'));
  281. expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes'));
  282. expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] sleep 10')));
  283. done();
  284. }, done);
  285. });
  286. });
  287. describe('--handle-input', () => {
  288. it('is aliased to -i', done => {
  289. const child = run('-i "node fixtures/read-echo.js"');
  290. child.log.subscribe(line => {
  291. if (/READING/.test(line)) {
  292. child.stdin.write('stop\n');
  293. }
  294. if (/\[0\] stop/.test(line)) {
  295. done();
  296. }
  297. }, done);
  298. });
  299. it('forwards input to first process by default', done => {
  300. const child = run('--handle-input "node fixtures/read-echo.js"');
  301. child.log.subscribe(line => {
  302. if (/READING/.test(line)) {
  303. child.stdin.write('stop\n');
  304. }
  305. if (/\[0\] stop/.test(line)) {
  306. done();
  307. }
  308. }, done);
  309. });
  310. it('forwards input to process --default-input-target', done => {
  311. const lines = [];
  312. const child = run('-ki --default-input-target 1 "node fixtures/read-echo.js" "node fixtures/read-echo.js"');
  313. child.log.subscribe(line => {
  314. lines.push(line);
  315. if (/\[1\] READING/.test(line)) {
  316. child.stdin.write('stop\n');
  317. }
  318. }, done);
  319. child.close.subscribe(exit => {
  320. expect(exit[0]).toBeGreaterThan(0);
  321. expect(lines).toContainEqual(expect.stringContaining('[1] stop'));
  322. expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] node fixtures/read-echo.js')));
  323. done();
  324. }, done);
  325. });
  326. it('forwards input to specified process', done => {
  327. const lines = [];
  328. const child = run('-ki "node fixtures/read-echo.js" "node fixtures/read-echo.js"');
  329. child.log.subscribe(line => {
  330. lines.push(line);
  331. if (/\[1\] READING/.test(line)) {
  332. child.stdin.write('1:stop\n');
  333. }
  334. }, done);
  335. child.close.subscribe(exit => {
  336. expect(exit[0]).toBeGreaterThan(0);
  337. expect(lines).toContainEqual(expect.stringContaining('[1] stop'));
  338. expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] node fixtures/read-echo.js')));
  339. done();
  340. }, done);
  341. });
  342. });
  343. describe('--timings', () => {
  344. const defaultTimestampFormatRegex = /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}/;
  345. const processStartedMessageRegex = (index, command) => {
  346. return new RegExp( `^\\[${ index }] ${ command } started at ${ defaultTimestampFormatRegex.source }$` );
  347. };
  348. const processStoppedMessageRegex = (index, command) => {
  349. return new RegExp( `^\\[${ index }] ${ command } stopped at ${ defaultTimestampFormatRegex.source } after (\\d|,)+ms$` );
  350. };
  351. const expectLinesForProcessStartAndStop = (lines, index, command) => {
  352. const escapedCommand = _.escapeRegExp(command);
  353. expect(lines).toContainEqual(expect.stringMatching(processStartedMessageRegex(index, escapedCommand)));
  354. expect(lines).toContainEqual(expect.stringMatching(processStoppedMessageRegex(index, escapedCommand)));
  355. };
  356. const expectLinesForTimingsTable = (lines) => {
  357. const tableTopBorderRegex = /┌[─┬]+┐/g;
  358. expect(lines).toContainEqual(expect.stringMatching(tableTopBorderRegex));
  359. const tableHeaderRowRegex = /(\W+(name|duration|exit code|killed|command)\W+){5}/g;
  360. expect(lines).toContainEqual(expect.stringMatching(tableHeaderRowRegex));
  361. const tableBottomBorderRegex = /└[─┴]+┘/g;
  362. expect(lines).toContainEqual(expect.stringMatching(tableBottomBorderRegex));
  363. };
  364. it('shows timings on success', done => {
  365. const child = run('--timings "sleep 0.5" "exit 0"');
  366. child.log.pipe(buffer(child.close)).subscribe(lines => {
  367. expectLinesForProcessStartAndStop(lines, 0, 'sleep 0.5');
  368. expectLinesForProcessStartAndStop(lines, 1, 'exit 0');
  369. expectLinesForTimingsTable(lines);
  370. done();
  371. }, done);
  372. });
  373. it('shows timings on failure', done => {
  374. const child = run('--timings "sleep 0.75" "exit 1"');
  375. child.log.pipe(buffer(child.close)).subscribe(lines => {
  376. expectLinesForProcessStartAndStop(lines, 0, 'sleep 0.75');
  377. expectLinesForProcessStartAndStop(lines, 1, 'exit 1');
  378. expectLinesForTimingsTable(lines);
  379. done();
  380. }, done);
  381. });
  382. });