Skip to content

Commit

Permalink
Interpret aborted commands as success
Browse files Browse the repository at this point in the history
  • Loading branch information
gustavohenke committed Jan 10, 2024
1 parent 74485c6 commit 07553f8
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 19 deletions.
25 changes: 25 additions & 0 deletions src/completion-listener.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,31 @@ const createController = (successCondition?: SuccessCondition) =>
const emitFakeCloseEvent = (command: FakeCommand, event?: Partial<CloseEvent>) =>
command.close.next(createFakeCloseEvent({ ...event, command, index: command.index }));

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 = createFakeCloseEvent({ exitCode: 0 });
commands[0].close.next(event);
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();
const result = createController('all').listen([new FakeCommand()], abortCtrl.signal);

abortCtrl.abort();
scheduler.flush();

await expect(result).resolves.toHaveLength(0);
});

describe('with default success condition set', () => {
it('succeeds if all processes exited with code 0', () => {
const result = createController().listen(commands);
Expand Down
58 changes: 40 additions & 18 deletions src/completion-listener.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as Rx from 'rxjs';
import { bufferCount, switchMap, take } from 'rxjs/operators';
import { bufferCount, delay, filter, map, switchMap, take } from 'rxjs/operators';

import { CloseEvent, Command } from './command';

Expand Down Expand Up @@ -47,54 +47,72 @@ export class CompletionListener {
this.scheduler = scheduler;
}

private isSuccess(events: CloseEvent[]) {
private isSuccess(events: (CloseEvent | undefined)[]) {
if (this.successCondition === 'first') {
return events[0].exitCode === 0;
return isSuccess(events[0]);
} else if (this.successCondition === 'last') {
return events[events.length - 1].exitCode === 0;
return isSuccess(events[events.length - 1]);
}

const commandSyntaxMatch = this.successCondition.match(/^!?command-(.+)$/);
if (commandSyntaxMatch == null) {
// If not a `command-` syntax, then it's an 'all' condition or it's treated as such.
return events.every(({ exitCode }) => exitCode === 0);
return events.every(isSuccess);
}

// Check `command-` syntax condition.
// Note that a command's `name` is not necessarily unique,
// in which case all of them must meet the success condition.
const nameOrIndex = commandSyntaxMatch[1];
const targetCommandsEvents = events.filter(
({ command, index }) => command.name === nameOrIndex || index === Number(nameOrIndex),
(event) => event?.command.name === nameOrIndex || event?.index === Number(nameOrIndex),
);
if (this.successCondition.startsWith('!')) {
// All commands except the specified ones must exit succesfully
return events.every(
(event) => targetCommandsEvents.includes(event) || event.exitCode === 0,
(event) => targetCommandsEvents.includes(event) || isSuccess(event),
);
}
// Only the specified commands must exit succesfully
return (
targetCommandsEvents.length > 0 &&
targetCommandsEvents.every((event) => event.exitCode === 0)
);
return targetCommandsEvents.length > 0 && targetCommandsEvents.every(isSuccess);
}

/**
* Given a list of commands, wait for all of them to exit and then evaluate their exit codes.
*
* @returns A Promise that resolves if the success condition is met, or rejects otherwise.
* In either case, the value is a list of close events for commands that spawned.
* Commands that didn't spawn are filtered out.
*/
listen(commands: Command[]): Promise<CloseEvent[]> {
const closeStreams = commands.map((command) => command.close);
listen(commands: Command[], abortSignal?: AbortSignal): Promise<CloseEvent[]> {
const abort =
abortSignal &&
Rx.fromEvent(abortSignal, 'abort', { once: true }).pipe(
// The abort signal must happen before commands are killed, otherwise new commands
// might spawn. Because of this, it's not be possible to capture the close events
// without an immediate delay
delay(0, this.scheduler),
map(() => undefined),
);

const closeStreams = commands.map((command) =>
abort
? // Commands that have been started must close.
Rx.race(command.close, abort.pipe(filter(() => command.state === 'stopped')))
: command.close,
);
return Rx.lastValueFrom(
Rx.merge(...closeStreams).pipe(
bufferCount(closeStreams.length),
switchMap((exitInfos) =>
this.isSuccess(exitInfos)
? this.emitWithScheduler(Rx.of(exitInfos))
: this.emitWithScheduler(Rx.throwError(() => exitInfos)),
),
switchMap((events) => {
const success = this.isSuccess(events);
const filteredEvents = events.filter(
(event): event is CloseEvent => event != null,
);
return success
? this.emitWithScheduler(Rx.of(filteredEvents))
: this.emitWithScheduler(Rx.throwError(() => filteredEvents));
}),
take(1),
),
);
Expand All @@ -104,3 +122,7 @@ export class CompletionListener {
return this.scheduler ? input.pipe(Rx.observeOn(this.scheduler)) : input;
}
}

function isSuccess(event: CloseEvent | undefined) {
return event == null || event.exitCode === 0;
}
3 changes: 2 additions & 1 deletion src/concurrently.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ export type ConcurrentlyResult = {
* A promise that resolves when concurrently ran successfully according to the specified
* success condition, or reject otherwise.
*
* Both the resolved and rejected value is the list of all command's close events.
* Both the resolved and rejected value is a list of all the close events for commands that
* spawned; commands that didn't spawn are filtered out.
*/
result: Promise<CloseEvent[]>;
};
Expand Down

0 comments on commit 07553f8

Please sign in to comment.