const readline = require('readline'); const _ = require('lodash'); const Rx = require('rxjs'); const { buffer, map } = require('rxjs/operators'); const spawn = require('spawn-command'); const isWindows = process.platform === 'win32'; const createKillMessage = prefix => new RegExp( _.escapeRegExp(prefix) + ' exited with code ' + (isWindows ? 1 : '(SIGTERM|143)') ); const run = args => { const child = spawn('node ./concurrently.js ' + args, { cwd: __dirname, env: Object.assign({}, process.env, { // When upgrading from jest 23 -> 24, colors started printing in the test output. // They are forcibly disabled here FORCE_COLOR: 0 }), }); const stdout = readline.createInterface({ input: child.stdout, output: null }); const stderr = readline.createInterface({ input: child.stderr, output: null }); const close = Rx.fromEvent(child, 'close'); const log = Rx.merge( Rx.fromEvent(stdout, 'line'), Rx.fromEvent(stderr, 'line') ).pipe(map(data => data.toString())); return { close, log, stdin: child.stdin, pid: child.pid }; }; it('has help command', done => { run('--help').close.subscribe(event => { expect(event[0]).toBe(0); done(); }, done); }); it('has version command', done => { Rx.combineLatest( run('--version').close, run('-V').close, run('-v').close ).subscribe(events => { expect(events[0][0]).toBe(0); expect(events[1][0]).toBe(0); expect(events[2][0]).toBe(0); done(); }, done); }); describe('exiting conditions', () => { it('is of success by default when running successful commands', done => { run('"echo foo" "echo bar"') .close .subscribe(exit => { expect(exit[0]).toBe(0); done(); }, done); }); it('is of failure by default when one of the command fails', done => { run('"echo foo" "exit 1"') .close .subscribe(exit => { expect(exit[0]).toBeGreaterThan(0); done(); }, done); }); it('is of success when --success=first and first command to exit succeeds', done => { run('--success=first "echo foo" "sleep 0.5 && exit 1"') .close .subscribe(exit => { expect(exit[0]).toBe(0); done(); }, done); }); it('is of failure when --success=first and first command to exit fails', done => { run('--success=first "exit 1" "sleep 0.5 && echo foo"') .close .subscribe(exit => { expect(exit[0]).toBeGreaterThan(0); done(); }, done); }); it('is of success when --success=last and last command to exit succeeds', done => { run('--success=last "exit 1" "sleep 0.5 && echo foo"') .close .subscribe(exit => { expect(exit[0]).toBe(0); done(); }, done); }); it('is of failure when --success=last and last command to exit fails', done => { run('--success=last "echo foo" "sleep 0.5 && exit 1"') .close .subscribe(exit => { expect(exit[0]).toBeGreaterThan(0); done(); }, done); }); it.skip('is of success when a SIGINT is sent', done => { const child = run('"node fixtures/read-echo.js"'); child.close.subscribe(exit => { // TODO This is null within Node, but should be 0 outside (eg from real terminal) expect(exit[0]).toBe(0); done(); }, done); process.kill(child.pid, 'SIGINT'); }); it('is aliased to -s', done => { run('-s last "exit 1" "sleep 0.5 && echo foo"') .close .subscribe(exit => { expect(exit[0]).toBe(0); done(); }, done); }); }); describe('--raw', () => { it('is aliased to -r', done => { const child = run('-r "echo foo" "echo bar"'); child.log.pipe(buffer(child.close)).subscribe(lines => { expect(lines).toHaveLength(2); expect(lines).toContainEqual(expect.stringContaining('foo')); expect(lines).toContainEqual(expect.stringContaining('bar')); done(); }, done); }); it('does not log any extra output', done => { const child = run('--raw "echo foo" "echo bar"'); child.log.pipe(buffer(child.close)).subscribe(lines => { expect(lines).toHaveLength(2); expect(lines).toContainEqual(expect.stringContaining('foo')); expect(lines).toContainEqual(expect.stringContaining('bar')); done(); }, done); }); }); describe('--names', () => { it('is aliased to -n', done => { const child = run('-n foo,bar "echo foo" "echo bar"'); child.log.pipe(buffer(child.close)).subscribe(lines => { expect(lines).toContainEqual(expect.stringContaining('[foo] foo')); expect(lines).toContainEqual(expect.stringContaining('[bar] bar')); done(); }, done); }); it('prefixes with names', done => { const child = run('--names foo,bar "echo foo" "echo bar"'); child.log.pipe(buffer(child.close)).subscribe(lines => { expect(lines).toContainEqual(expect.stringContaining('[foo] foo')); expect(lines).toContainEqual(expect.stringContaining('[bar] bar')); done(); }, done); }); it('is split using --name-separator arg', done => { const child = run('--names "foo|bar" --name-separator "|" "echo foo" "echo bar"'); child.log.pipe(buffer(child.close)).subscribe(lines => { expect(lines).toContainEqual(expect.stringContaining('[foo] foo')); expect(lines).toContainEqual(expect.stringContaining('[bar] bar')); done(); }, done); }); }); describe('--prefix', () => { it('is alised to -p', done => { const child = run('-p command "echo foo" "echo bar"'); child.log.pipe(buffer(child.close)).subscribe(lines => { expect(lines).toContainEqual(expect.stringContaining('[echo foo] foo')); expect(lines).toContainEqual(expect.stringContaining('[echo bar] bar')); done(); }, done); }); it('specifies custom prefix', done => { const child = run('--prefix command "echo foo" "echo bar"'); child.log.pipe(buffer(child.close)).subscribe(lines => { expect(lines).toContainEqual(expect.stringContaining('[echo foo] foo')); expect(lines).toContainEqual(expect.stringContaining('[echo bar] bar')); done(); }, done); }); }); describe('--prefix-length', () => { it('is alised to -l', done => { const child = run('-p command -l 5 "echo foo" "echo bar"'); child.log.pipe(buffer(child.close)).subscribe(lines => { expect(lines).toContainEqual(expect.stringContaining('[ec..o] foo')); expect(lines).toContainEqual(expect.stringContaining('[ec..r] bar')); done(); }, done); }); it('specifies custom prefix length', done => { const child = run('--prefix command --prefix-length 5 "echo foo" "echo bar"'); child.log.pipe(buffer(child.close)).subscribe(lines => { expect(lines).toContainEqual(expect.stringContaining('[ec..o] foo')); expect(lines).toContainEqual(expect.stringContaining('[ec..r] bar')); done(); }, done); }); }); describe('--restart-tries', () => { it('changes how many times a command will restart', done => { const child = run('--restart-tries 1 "exit 1"'); child.log.pipe(buffer(child.close)).subscribe(lines => { expect(lines).toEqual([ expect.stringContaining('[0] exit 1 exited with code 1'), expect.stringContaining('[0] exit 1 restarted'), expect.stringContaining('[0] exit 1 exited with code 1'), ]); done(); }, done); }); }); describe('--kill-others', () => { it('is alised to -k', done => { const child = run('-k "sleep 10" "exit 0"'); child.log.pipe(buffer(child.close)).subscribe(lines => { expect(lines).toContainEqual(expect.stringContaining('[1] exit 0 exited with code 0')); expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes')); expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] sleep 10'))); done(); }, done); }); it('kills on success', done => { const child = run('--kill-others "sleep 10" "exit 0"'); child.log.pipe(buffer(child.close)).subscribe(lines => { expect(lines).toContainEqual(expect.stringContaining('[1] exit 0 exited with code 0')); expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes')); expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] sleep 10'))); done(); }, done); }); it('kills on failure', done => { const child = run('--kill-others "sleep 10" "exit 1"'); child.log.pipe(buffer(child.close)).subscribe(lines => { expect(lines).toContainEqual(expect.stringContaining('[1] exit 1 exited with code 1')); expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes')); expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] sleep 10'))); done(); }, done); }); }); describe('--kill-others-on-fail', () => { it('does not kill on success', done => { const child = run('--kill-others-on-fail "sleep 0.5" "exit 0"'); child.log.pipe(buffer(child.close)).subscribe(lines => { expect(lines).toContainEqual(expect.stringContaining('[1] exit 0 exited with code 0')); expect(lines).toContainEqual(expect.stringContaining('[0] sleep 0.5 exited with code 0')); done(); }, done); }); it('kills on failure', done => { const child = run('--kill-others-on-fail "sleep 10" "exit 1"'); child.log.pipe(buffer(child.close)).subscribe(lines => { expect(lines).toContainEqual(expect.stringContaining('[1] exit 1 exited with code 1')); expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes')); expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] sleep 10'))); done(); }, done); }); }); describe('--handle-input', () => { it('is aliased to -i', done => { const child = run('-i "node fixtures/read-echo.js"'); child.log.subscribe(line => { if (/READING/.test(line)) { child.stdin.write('stop\n'); } if (/\[0\] stop/.test(line)) { done(); } }, done); }); it('forwards input to first process by default', done => { const child = run('--handle-input "node fixtures/read-echo.js"'); child.log.subscribe(line => { if (/READING/.test(line)) { child.stdin.write('stop\n'); } if (/\[0\] stop/.test(line)) { done(); } }, done); }); it('forwards input to process --default-input-target', done => { const lines = []; const child = run('-ki --default-input-target 1 "node fixtures/read-echo.js" "node fixtures/read-echo.js"'); child.log.subscribe(line => { lines.push(line); if (/\[1\] READING/.test(line)) { child.stdin.write('stop\n'); } }, done); child.close.subscribe(exit => { expect(exit[0]).toBeGreaterThan(0); expect(lines).toContainEqual(expect.stringContaining('[1] stop')); expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] node fixtures/read-echo.js'))); done(); }, done); }); it('forwards input to specified process', done => { const lines = []; const child = run('-ki "node fixtures/read-echo.js" "node fixtures/read-echo.js"'); child.log.subscribe(line => { lines.push(line); if (/\[1\] READING/.test(line)) { child.stdin.write('1:stop\n'); } }, done); child.close.subscribe(exit => { expect(exit[0]).toBeGreaterThan(0); expect(lines).toContainEqual(expect.stringContaining('[1] stop')); expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] node fixtures/read-echo.js'))); done(); }, done); }); });