Skip to content

Commit

Permalink
Merge 28cec90 into c5231ea
Browse files Browse the repository at this point in the history
  • Loading branch information
gustavohenke committed May 13, 2022
2 parents c5231ea + 28cec90 commit 2cbfda2
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 24 deletions.
16 changes: 11 additions & 5 deletions README.md
Expand Up @@ -146,11 +146,17 @@ 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
command with that name or index;
- "!command-{name}"/"!command-{index}" for all
commands but the one 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]
Expand Down
12 changes: 8 additions & 4 deletions bin/concurrently.ts
Expand Up @@ -48,10 +48,14 @@ 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' +
'- "command-{name}"/"command-{index}" for the command with that name or index;\n' +
'- "!command-{name}"/"!command-{index}" for all commands but the one with that ' +
'name or index.\n',
default: defaults.success,
},
'raw': {
Expand Down
98 changes: 97 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,91 @@ 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(`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 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());
});
});
38 changes: 24 additions & 14 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 command with the specified name or index.
* - `!command-{name|index}`: all commands but the one with the specified name 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,27 @@ 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;

default:
return exitCodes.every(exitCode => exitCode === 0);
/* eslint-enable indent */
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);
}

// Check `command-` syntax condition
const [, nameOrIndex] = this.successCondition.split('-');
const targetCommandEvent = events.find(({ command, index }) => (
command.name === nameOrIndex
|| index === Number(nameOrIndex)
));
return this.successCondition.startsWith('!')
// All commands except the specified one must exit succesfully
? events.every((event) => event === targetCommandEvent || event.exitCode === 0)
// Only the specified command must exit succesfully
: targetCommandEvent && targetCommandEvent.exitCode === 0;
}

/**
Expand All @@ -62,7 +72,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 2cbfda2

Please sign in to comment.