Skip to content

Commit

Permalink
Add command-{name|index} and !command-{name|index} condition to --suc…
Browse files Browse the repository at this point in the history
…cess (#318)

Co-authored-by: Pascal Jufer <pascal-jufer@bluewin.ch>
  • Loading branch information
gustavohenke and paescuj committed May 15, 2022
1 parent c5231ea commit 706c4d6
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 24 deletions.
18 changes: 12 additions & 6 deletions README.md
Expand Up @@ -146,14 +146,20 @@ General
--name-separator The character to split <names> on. Example usage:
concurrently -n "styles|scripts|server"
--name-separator "|" [default: ","]
-s, --success Return exit code of zero or one based on the
success or failure of the "first" child to
terminate, the "last child", or succeed only if
"all" child processes succeed.
[choices: "first", "last", "all"] [default: "all"]
-s, --success Which command(s) must exit with code 0 in order
for concurrently exit with code 0 too. Options
are:
- "first" for the first command to exit;
- "last" for the last command to exit;
- "all" for all commands;
- "command-{name}"/"command-{index}" for the
commands with that name or index;
- "!command-{name}"/"!command-{index}" for all
commands but the ones with that name or index.
[default: "all"]
-r, --raw Output only raw output of processes, disables
prettifying and concurrently coloring. [boolean]
--no-color Disables colors from logging. [boolean]
--no-color Disables colors from logging [boolean]
--hide Comma-separated list of processes to hide the
output.
The processes can be identified by their name or
Expand Down
13 changes: 9 additions & 4 deletions bin/concurrently.ts
Expand Up @@ -48,10 +48,15 @@ const args = yargs(argsBeforeSep)
'success': {
alias: 's',
describe:
'Return exit code of zero or one based on the success or failure ' +
'of the "first" child to terminate, the "last child", or succeed ' +
'only if "all" child processes succeed.',
choices: ['first', 'last', 'all'] as const,
'Which command(s) must exit with code 0 in order for concurrently exit with ' +
'code 0 too. Options are:\n' +
'- "first" for the first command to exit;\n' +
'- "last" for the last command to exit;\n' +
'- "all" for all commands;\n' +
// Note: not a typo. Multiple commands can have the same name.
'- "command-{name}"/"command-{index}" for the commands with that name or index;\n' +
'- "!command-{name}"/"!command-{index}" for all commands but the ones with that ' +
'name or index.\n',
default: defaults.success,
},
'raw': {
Expand Down
124 changes: 123 additions & 1 deletion src/completion-listener.spec.ts
@@ -1,11 +1,16 @@
import { TestScheduler } from 'rxjs/testing';
import { CloseEvent } from './command';
import { CompletionListener, SuccessCondition } from './completion-listener';
import { createFakeCloseEvent, FakeCommand } from './fixtures/fake-command';

let commands: FakeCommand[];
let scheduler: TestScheduler;
beforeEach(() => {
commands = [new FakeCommand('foo'), new FakeCommand('bar')];
commands = [
new FakeCommand('foo', 'echo', 0),
new FakeCommand('bar', 'echo', 1),
new FakeCommand('baz', 'echo', 2),
];
scheduler = new TestScheduler(() => true);
});

Expand All @@ -15,12 +20,18 @@ const createController = (successCondition?: SuccessCondition) =>
scheduler,
});

const emitFakeCloseEvent = (
command: FakeCommand,
event?: Partial<CloseEvent>,
) => command.close.next(createFakeCloseEvent({ ...event, command, index: command.index }));

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();

Expand All @@ -32,6 +43,7 @@ describe('with default success condition set', () => {

commands[0].close.next(createFakeCloseEvent({ exitCode: 0 }));
commands[1].close.next(createFakeCloseEvent({ exitCode: 1 }));
commands[2].close.next(createFakeCloseEvent({ exitCode: 0 }));

scheduler.flush();

Expand All @@ -45,6 +57,7 @@ describe('with success condition set to first', () => {

commands[1].close.next(createFakeCloseEvent({ exitCode: 0 }));
commands[0].close.next(createFakeCloseEvent({ exitCode: 1 }));
commands[2].close.next(createFakeCloseEvent({ exitCode: 1 }));

scheduler.flush();

Expand All @@ -56,6 +69,7 @@ describe('with success condition set to first', () => {

commands[1].close.next(createFakeCloseEvent({ exitCode: 1 }));
commands[0].close.next(createFakeCloseEvent({ exitCode: 0 }));
commands[2].close.next(createFakeCloseEvent({ exitCode: 0 }));

scheduler.flush();

Expand All @@ -69,6 +83,7 @@ describe('with success condition set to last', () => {

commands[1].close.next(createFakeCloseEvent({ exitCode: 1 }));
commands[0].close.next(createFakeCloseEvent({ exitCode: 0 }));
commands[2].close.next(createFakeCloseEvent({ exitCode: 0 }));

scheduler.flush();

Expand All @@ -80,10 +95,117 @@ describe('with success condition set to last', () => {

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' as const, 'bar'],
['command-1' as const, 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(`fails if some commands ${nameOrIndex} exit with non-0 code`, () => {
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: 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' as const, 'bar'],
['!command-1' as const, 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());
});
});
43 changes: 30 additions & 13 deletions src/completion-listener.ts
Expand Up @@ -8,8 +8,10 @@ import { CloseEvent, Command } from './command';
* - `first`: only the first specified command;
* - `last`: only the last specified command;
* - `all`: all commands.
* - `command-{name|index}`: only the commands with the specified names or index.
* - `!command-{name|index}`: all commands but the ones with the specified names or index.
*/
export type SuccessCondition = 'first' | 'last' | 'all';
export type SuccessCondition = 'first' | 'last' | 'all' | `command-${string|number}` | `!command-${string|number}`;

/**
* Provides logic to determine whether lists of commands ran successfully.
Expand All @@ -36,19 +38,34 @@ export class CompletionListener {
this.scheduler = scheduler;
}

private isSuccess(exitCodes: (string | number)[]) {
switch (this.successCondition) {
/* eslint-disable indent */
case 'first':
return exitCodes[0] === 0;

case 'last':
return exitCodes[exitCodes.length - 1] === 0;
private isSuccess(events: CloseEvent[]) {
if (this.successCondition === 'first') {
return events[0].exitCode === 0;
} else if (this.successCondition === 'last') {
return events[events.length - 1].exitCode === 0;
} else if (!/^!?command-.+$/.test(this.successCondition)) {
// If not a `command-` syntax, then it's an 'all' condition or it's treated as such.
return events.every(({ exitCode }) => exitCode === 0);
}

default:
return exitCodes.every(exitCode => exitCode === 0);
/* eslint-enable indent */
// 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] = this.successCondition.split('-');
const targetCommandsEvents = events.filter(({ command, index }) => (
command.name === nameOrIndex
|| 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
));
}
// Only the specified commands must exit succesfully
return targetCommandsEvents.length > 0
&& targetCommandsEvents.every(event => event.exitCode === 0);
}

/**
Expand All @@ -62,7 +79,7 @@ export class CompletionListener {
.pipe(
bufferCount(closeStreams.length),
switchMap(exitInfos =>
this.isSuccess(exitInfos.map(({ exitCode }) => exitCode))
this.isSuccess(exitInfos)
? Rx.of(exitInfos, this.scheduler)
: Rx.throwError(exitInfos, this.scheduler),
),
Expand Down

0 comments on commit 706c4d6

Please sign in to comment.