UNPKG

concurrently

Version:
230 lines (229 loc) 11.8 kB
import { getEventListeners } from 'node:events'; import { TestScheduler } from 'rxjs/testing'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { createFakeCloseEvent, FakeCommand } from './__fixtures__/fake-command.js'; import { CompletionListener } from './completion-listener.js'; let commands; let scheduler; beforeEach(() => { commands = [ new FakeCommand('foo', 'echo', 0), new FakeCommand('bar', 'echo', 1), new FakeCommand('baz', 'echo', 2), ]; scheduler = new TestScheduler(() => true); }); const createController = (successCondition) => new CompletionListener({ successCondition, scheduler, }); const emitFakeCloseEvent = (command, event) => { const fakeEvent = createFakeCloseEvent({ ...event, command, index: command.index }); command.state = 'exited'; command.close.next(fakeEvent); return fakeEvent; }; const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0)); describe('listen', () => { it('resolves when there are no commands', async () => { const result = createController().listen([]); await expect(result).resolves.toHaveLength(0); }); it('completes only when commands emit a close event, returns close event', async () => { const abortCtrl = new AbortController(); const result = createController('all').listen(commands, abortCtrl.signal); commands[0].state = 'started'; abortCtrl.abort(); const event = emitFakeCloseEvent(commands[0]); scheduler.flush(); await expect(result).resolves.toHaveLength(1); await expect(result).resolves.toEqual([event]); }); it('completes when abort signal is received and command is stopped, returns nothing', async () => { const abortCtrl = new AbortController(); // Use success condition = first to test index access when there are no close events const result = createController('first').listen([new FakeCommand()], abortCtrl.signal); abortCtrl.abort(); scheduler.flush(); await expect(result).resolves.toHaveLength(0); }); it('does not leak memory when listening for abort signals', () => { const abortCtrl = new AbortController(); createController().listen(Array.from({ length: 10 }, () => new FakeCommand()), abortCtrl.signal); expect(getEventListeners(abortCtrl.signal, 'abort')).toHaveLength(1); }); it('check for success once all commands have emitted at least a single close event', async () => { const finallyCallback = vi.fn(); const result = createController().listen(commands).finally(finallyCallback); // Emitting multiple close events to mimic calling command `kill/start` APIs. emitFakeCloseEvent(commands[0]); emitFakeCloseEvent(commands[0]); emitFakeCloseEvent(commands[0]); scheduler.flush(); // A broken implementation will have called finallyCallback only after flushing promises await flushPromises(); expect(finallyCallback).not.toHaveBeenCalled(); emitFakeCloseEvent(commands[1]); emitFakeCloseEvent(commands[2]); scheduler.flush(); await expect(result).resolves.toEqual(expect.anything()); expect(finallyCallback).toHaveBeenCalled(); }); it('takes last event emitted from each command', async () => { const result = createController().listen(commands); emitFakeCloseEvent(commands[0], { exitCode: 0 }); emitFakeCloseEvent(commands[0], { exitCode: 1 }); emitFakeCloseEvent(commands[1], { exitCode: 0 }); emitFakeCloseEvent(commands[2], { exitCode: 0 }); scheduler.flush(); await expect(result).rejects.toEqual(expect.anything()); }); it('waits for manually restarted events to close', async () => { const finallyCallback = vi.fn(); const result = createController().listen(commands).finally(finallyCallback); emitFakeCloseEvent(commands[0]); commands[0].state = 'started'; emitFakeCloseEvent(commands[1]); emitFakeCloseEvent(commands[2]); scheduler.flush(); // A broken implementation will have called finallyCallback only after flushing promises await flushPromises(); expect(finallyCallback).not.toHaveBeenCalled(); commands[0].state = 'exited'; emitFakeCloseEvent(commands[0]); scheduler.flush(); await expect(result).resolves.toEqual(expect.anything()); expect(finallyCallback).toHaveBeenCalled(); }); }); describe('detect commands exit conditions', () => { describe('with default success condition set', () => { it('succeeds if all processes exited with code 0', () => { const result = createController().listen(commands); commands[0].close.next(createFakeCloseEvent({ exitCode: 0 })); commands[1].close.next(createFakeCloseEvent({ exitCode: 0 })); commands[2].close.next(createFakeCloseEvent({ exitCode: 0 })); scheduler.flush(); return expect(result).resolves.toEqual(expect.anything()); }); it('fails if one of the processes exited with non-0 code', () => { const result = createController().listen(commands); commands[0].close.next(createFakeCloseEvent({ exitCode: 0 })); commands[1].close.next(createFakeCloseEvent({ exitCode: 1 })); commands[2].close.next(createFakeCloseEvent({ exitCode: 0 })); scheduler.flush(); return expect(result).rejects.toEqual(expect.anything()); }); }); describe('with success condition set to first', () => { it('succeeds if first process to exit has code 0', () => { const result = createController('first').listen(commands); commands[1].close.next(createFakeCloseEvent({ exitCode: 0 })); commands[0].close.next(createFakeCloseEvent({ exitCode: 1 })); commands[2].close.next(createFakeCloseEvent({ exitCode: 1 })); scheduler.flush(); return expect(result).resolves.toEqual(expect.anything()); }); it('fails if first process to exit has non-0 code', () => { const result = createController('first').listen(commands); commands[1].close.next(createFakeCloseEvent({ exitCode: 1 })); commands[0].close.next(createFakeCloseEvent({ exitCode: 0 })); commands[2].close.next(createFakeCloseEvent({ exitCode: 0 })); scheduler.flush(); return expect(result).rejects.toEqual(expect.anything()); }); }); describe('with success condition set to last', () => { it('succeeds if last process to exit has code 0', () => { const result = createController('last').listen(commands); commands[1].close.next(createFakeCloseEvent({ exitCode: 1 })); commands[0].close.next(createFakeCloseEvent({ exitCode: 0 })); commands[2].close.next(createFakeCloseEvent({ exitCode: 0 })); scheduler.flush(); return expect(result).resolves.toEqual(expect.anything()); }); it('fails if last process to exit has non-0 code', () => { const result = createController('last').listen(commands); commands[1].close.next(createFakeCloseEvent({ exitCode: 0 })); commands[0].close.next(createFakeCloseEvent({ exitCode: 1 })); commands[2].close.next(createFakeCloseEvent({ exitCode: 1 })); scheduler.flush(); return expect(result).rejects.toEqual(expect.anything()); }); }); describe.each([ // Use the middle command for both cases to make it more difficult to make a mess up // in the implementation cause false passes. ['command-bar', 'bar'], ['command-1', 1], ])('with success condition set to %s', (condition, nameOrIndex) => { it(`succeeds if command ${nameOrIndex} exits with code 0`, () => { const result = createController(condition).listen(commands); emitFakeCloseEvent(commands[0], { exitCode: 1 }); emitFakeCloseEvent(commands[1], { exitCode: 0 }); emitFakeCloseEvent(commands[2], { exitCode: 1 }); scheduler.flush(); return expect(result).resolves.toEqual(expect.anything()); }); it(`succeeds if all commands ${nameOrIndex} exit with code 0`, () => { commands = [commands[0], commands[1], commands[1]]; const result = createController(condition).listen(commands); emitFakeCloseEvent(commands[0], { exitCode: 1 }); emitFakeCloseEvent(commands[1], { exitCode: 0 }); emitFakeCloseEvent(commands[2], { exitCode: 0 }); scheduler.flush(); return expect(result).resolves.toEqual(expect.anything()); }); it(`fails if command ${nameOrIndex} exits with non-0 code`, () => { const result = createController(condition).listen(commands); emitFakeCloseEvent(commands[0], { exitCode: 0 }); emitFakeCloseEvent(commands[1], { exitCode: 1 }); emitFakeCloseEvent(commands[2], { exitCode: 0 }); scheduler.flush(); return expect(result).rejects.toEqual(expect.anything()); }); it(`succeeds if command ${nameOrIndex} exits with code 0 even when others fail`, () => { const result = createController(condition).listen(commands); emitFakeCloseEvent(commands[0], { exitCode: 1 }); emitFakeCloseEvent(commands[1], { exitCode: 0 }); emitFakeCloseEvent(commands[2], { exitCode: 1 }); scheduler.flush(); return expect(result).resolves.toEqual(expect.anything()); }); it(`fails if command ${nameOrIndex} doesn't exist`, () => { const result = createController(condition).listen([commands[0]]); emitFakeCloseEvent(commands[0], { exitCode: 0 }); scheduler.flush(); return expect(result).rejects.toEqual(expect.anything()); }); }); describe.each([ // Use the middle command for both cases to make it more difficult to make a mess up // in the implementation cause false passes. ['!command-bar', 'bar'], ['!command-1', 1], ])('with success condition set to %s', (condition, nameOrIndex) => { it(`succeeds if all commands but ${nameOrIndex} exit with code 0`, () => { const result = createController(condition).listen(commands); emitFakeCloseEvent(commands[0], { exitCode: 0 }); emitFakeCloseEvent(commands[1], { exitCode: 1 }); emitFakeCloseEvent(commands[2], { exitCode: 0 }); scheduler.flush(); return expect(result).resolves.toEqual(expect.anything()); }); it(`fails if any commands but ${nameOrIndex} exit with non-0 code`, () => { const result = createController(condition).listen(commands); emitFakeCloseEvent(commands[0], { exitCode: 1 }); emitFakeCloseEvent(commands[1], { exitCode: 1 }); emitFakeCloseEvent(commands[2], { exitCode: 0 }); scheduler.flush(); return expect(result).rejects.toEqual(expect.anything()); }); it(`succeeds if command ${nameOrIndex} doesn't exist`, () => { const result = createController(condition).listen([commands[0]]); emitFakeCloseEvent(commands[0], { exitCode: 0 }); scheduler.flush(); return expect(result).resolves.toEqual(expect.anything()); }); }); });