Skip to content

feat: throw on non-zero exit #34

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ The options object can have the following properties:
- `persist` - if `true`, the process will continue after the host exits
- `stdin` - another `Result` can be used as the input to this process
- `nodeOptions` - any valid options to node's underlying `spawn` function
- `throwOnError` - if true, non-zero exit codes will throw an error

### Piping to another process

Expand Down
20 changes: 20 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import {computeEnv} from './env.js';
import {combineStreams} from './stream.js';
import readline from 'node:readline';
import {_parse} from 'cross-spawn';
import {NonZeroExitError} from './non-zero-exit-error.js';

export {NonZeroExitError};

export interface Output {
stderr: string;
Expand Down Expand Up @@ -40,6 +43,7 @@ export interface Options {
timeout: number;
persist: boolean;
stdin: ExecProcess;
throwOnError: boolean;
}

export interface TinyExec {
Expand Down Expand Up @@ -188,6 +192,14 @@ export class ExecProcess implements Result {
if (this._thrownError) {
throw this._thrownError;
}

if (
this._options?.throwOnError &&
this.exitCode !== 0 &&
this.exitCode !== undefined
) {
throw new NonZeroExitError(this);
}
}

protected async _waitForOutput(): Promise<Output> {
Expand Down Expand Up @@ -229,6 +241,14 @@ export class ExecProcess implements Result {
stdout
};

if (
this._options.throwOnError &&
this.exitCode !== 0 &&
this.exitCode !== undefined
) {
throw new NonZeroExitError(this, result);
}

return result;
}

Expand Down
20 changes: 20 additions & 0 deletions src/non-zero-exit-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type {Result, Output} from './main.js';

export class NonZeroExitError extends Error {
public readonly result: Result;
public readonly output?: Output;

public get exitCode(): number | undefined {
if (this.result.exitCode !== null) {
return this.result.exitCode;
}
return undefined;
}

public constructor(result: Result, output?: Output) {
super(`Process exited with non-zero status (${result.exitCode})`);

this.result = result;
this.output = output;
}
}
31 changes: 29 additions & 2 deletions src/test/main_test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {x} from '../main.js';
import {x, NonZeroExitError} from '../main.js';
import * as assert from 'node:assert/strict';
import {test} from 'node:test';
import os from 'node:os';
Expand All @@ -19,6 +19,14 @@ test('exec', async (t) => {
assert.equal(proc.exitCode, 0);
});

await t.test('non-zero exitCode throws when throwOnError=true', async () => {
const proc = x('node', ['-e', 'process.exit(1);'], {throwOnError: true});
await assert.rejects(async () => {
await proc;
}, NonZeroExitError);
assert.equal(proc.exitCode, 1);
});

await t.test('async iterator gets correct output', async () => {
const proc = x('node', ['-e', "console.log('foo'); console.log('bar');"]);
const lines = [];
Expand Down Expand Up @@ -64,6 +72,25 @@ if (isWindows) {
assert.equal(result.stdout, '');
});

await t.test('throws spawn errors when throwOnError=true', async () => {
const proc = x('definitelyNonExistent', [], {throwOnError: true});
await assert.rejects(
async () => {
await proc;
},
(err) => {
assert.ok(err instanceof NonZeroExitError);
assert.equal(
err.output?.stderr,
"'definitelyNonExistent' is not recognized as an internal" +
' or external command,\r\noperable program or batch file.\r\n'
);
assert.equal(err.output?.stdout, '');
return true;
}
);
});

await t.test('kill terminates the process', async () => {
// Somewhat filthy way of waiting for 2 seconds across cmd/ps
const proc = x('ping', ['127.0.0.1', '-n', '2']);
Expand Down Expand Up @@ -100,7 +127,7 @@ if (isWindows) {

await t.test('async iterator receives errors as lines', async () => {
const proc = x('nonexistentforsure');
const lines = [];
const lines: string[] = [];
for await (const line of proc) {
lines.push(line);
}
Expand Down