concurrently.spec.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  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('--names', () => {
  147. it('is aliased to -n', done => {
  148. const child = run('-n foo,bar "echo foo" "echo bar"');
  149. child.log.pipe(buffer(child.close)).subscribe(lines => {
  150. expect(lines).toContainEqual(expect.stringContaining('[foo] foo'));
  151. expect(lines).toContainEqual(expect.stringContaining('[bar] bar'));
  152. done();
  153. }, done);
  154. });
  155. it('prefixes with names', done => {
  156. const child = run('--names foo,bar "echo foo" "echo bar"');
  157. child.log.pipe(buffer(child.close)).subscribe(lines => {
  158. expect(lines).toContainEqual(expect.stringContaining('[foo] foo'));
  159. expect(lines).toContainEqual(expect.stringContaining('[bar] bar'));
  160. done();
  161. }, done);
  162. });
  163. it('is split using --name-separator arg', done => {
  164. const child = run('--names "foo|bar" --name-separator "|" "echo foo" "echo bar"');
  165. child.log.pipe(buffer(child.close)).subscribe(lines => {
  166. expect(lines).toContainEqual(expect.stringContaining('[foo] foo'));
  167. expect(lines).toContainEqual(expect.stringContaining('[bar] bar'));
  168. done();
  169. }, done);
  170. });
  171. });
  172. describe('--prefix', () => {
  173. it('is alised to -p', done => {
  174. const child = run('-p command "echo foo" "echo bar"');
  175. child.log.pipe(buffer(child.close)).subscribe(lines => {
  176. expect(lines).toContainEqual(expect.stringContaining('[echo foo] foo'));
  177. expect(lines).toContainEqual(expect.stringContaining('[echo bar] bar'));
  178. done();
  179. }, done);
  180. });
  181. it('specifies custom prefix', done => {
  182. const child = run('--prefix command "echo foo" "echo bar"');
  183. child.log.pipe(buffer(child.close)).subscribe(lines => {
  184. expect(lines).toContainEqual(expect.stringContaining('[echo foo] foo'));
  185. expect(lines).toContainEqual(expect.stringContaining('[echo bar] bar'));
  186. done();
  187. }, done);
  188. });
  189. });
  190. describe('--prefix-length', () => {
  191. it('is alised to -l', done => {
  192. const child = run('-p command -l 5 "echo foo" "echo bar"');
  193. child.log.pipe(buffer(child.close)).subscribe(lines => {
  194. expect(lines).toContainEqual(expect.stringContaining('[ec..o] foo'));
  195. expect(lines).toContainEqual(expect.stringContaining('[ec..r] bar'));
  196. done();
  197. }, done);
  198. });
  199. it('specifies custom prefix length', done => {
  200. const child = run('--prefix command --prefix-length 5 "echo foo" "echo bar"');
  201. child.log.pipe(buffer(child.close)).subscribe(lines => {
  202. expect(lines).toContainEqual(expect.stringContaining('[ec..o] foo'));
  203. expect(lines).toContainEqual(expect.stringContaining('[ec..r] bar'));
  204. done();
  205. }, done);
  206. });
  207. });
  208. describe('--restart-tries', () => {
  209. it('changes how many times a command will restart', done => {
  210. const child = run('--restart-tries 1 "exit 1"');
  211. child.log.pipe(buffer(child.close)).subscribe(lines => {
  212. expect(lines).toEqual([
  213. expect.stringContaining('[0] exit 1 exited with code 1'),
  214. expect.stringContaining('[0] exit 1 restarted'),
  215. expect.stringContaining('[0] exit 1 exited with code 1'),
  216. ]);
  217. done();
  218. }, done);
  219. });
  220. });
  221. describe('--kill-others', () => {
  222. it('is alised to -k', done => {
  223. const child = run('-k "sleep 10" "exit 0"');
  224. child.log.pipe(buffer(child.close)).subscribe(lines => {
  225. expect(lines).toContainEqual(expect.stringContaining('[1] exit 0 exited with code 0'));
  226. expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes'));
  227. expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] sleep 10')));
  228. done();
  229. }, done);
  230. });
  231. it('kills on success', done => {
  232. const child = run('--kill-others "sleep 10" "exit 0"');
  233. child.log.pipe(buffer(child.close)).subscribe(lines => {
  234. expect(lines).toContainEqual(expect.stringContaining('[1] exit 0 exited with code 0'));
  235. expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes'));
  236. expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] sleep 10')));
  237. done();
  238. }, done);
  239. });
  240. it('kills on failure', done => {
  241. const child = run('--kill-others "sleep 10" "exit 1"');
  242. child.log.pipe(buffer(child.close)).subscribe(lines => {
  243. expect(lines).toContainEqual(expect.stringContaining('[1] exit 1 exited with code 1'));
  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. });
  250. describe('--kill-others-on-fail', () => {
  251. it('does not kill on success', done => {
  252. const child = run('--kill-others-on-fail "sleep 0.5" "exit 0"');
  253. child.log.pipe(buffer(child.close)).subscribe(lines => {
  254. expect(lines).toContainEqual(expect.stringContaining('[1] exit 0 exited with code 0'));
  255. expect(lines).toContainEqual(expect.stringContaining('[0] sleep 0.5 exited with code 0'));
  256. done();
  257. }, done);
  258. });
  259. it('kills on failure', done => {
  260. const child = run('--kill-others-on-fail "sleep 10" "exit 1"');
  261. child.log.pipe(buffer(child.close)).subscribe(lines => {
  262. expect(lines).toContainEqual(expect.stringContaining('[1] exit 1 exited with code 1'));
  263. expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes'));
  264. expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] sleep 10')));
  265. done();
  266. }, done);
  267. });
  268. });
  269. describe('--handle-input', () => {
  270. it('is aliased to -i', done => {
  271. const child = run('-i "node fixtures/read-echo.js"');
  272. child.log.subscribe(line => {
  273. if (/READING/.test(line)) {
  274. child.stdin.write('stop\n');
  275. }
  276. if (/\[0\] stop/.test(line)) {
  277. done();
  278. }
  279. }, done);
  280. });
  281. it('forwards input to first process by default', done => {
  282. const child = run('--handle-input "node fixtures/read-echo.js"');
  283. child.log.subscribe(line => {
  284. if (/READING/.test(line)) {
  285. child.stdin.write('stop\n');
  286. }
  287. if (/\[0\] stop/.test(line)) {
  288. done();
  289. }
  290. }, done);
  291. });
  292. it('forwards input to process --default-input-target', done => {
  293. const lines = [];
  294. const child = run('-ki --default-input-target 1 "node fixtures/read-echo.js" "node fixtures/read-echo.js"');
  295. child.log.subscribe(line => {
  296. lines.push(line);
  297. if (/\[1\] READING/.test(line)) {
  298. child.stdin.write('stop\n');
  299. }
  300. }, done);
  301. child.close.subscribe(exit => {
  302. expect(exit[0]).toBeGreaterThan(0);
  303. expect(lines).toContainEqual(expect.stringContaining('[1] stop'));
  304. expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] node fixtures/read-echo.js')));
  305. done();
  306. }, done);
  307. });
  308. it('forwards input to specified process', done => {
  309. const lines = [];
  310. const child = run('-ki "node fixtures/read-echo.js" "node fixtures/read-echo.js"');
  311. child.log.subscribe(line => {
  312. lines.push(line);
  313. if (/\[1\] READING/.test(line)) {
  314. child.stdin.write('1:stop\n');
  315. }
  316. }, done);
  317. child.close.subscribe(exit => {
  318. expect(exit[0]).toBeGreaterThan(0);
  319. expect(lines).toContainEqual(expect.stringContaining('[1] stop'));
  320. expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] node fixtures/read-echo.js')));
  321. done();
  322. }, done);
  323. });
  324. });