Skip to content
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

Add SubprocessError class #83

Merged
merged 1 commit into from
Oct 27, 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
2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,8 @@ Subprocesses fail either when their [exit code](#subprocesserrorexitcode) is not

Subprocess errors have the same shape as [successful results](#result), with the following additional properties.

This error class is exported, so you can use `if (error instanceof SubprocessError) { ... }`.

##### subprocessError.exitCode

_Type_: `number | undefined`
Expand Down
11 changes: 9 additions & 2 deletions source/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,14 @@ When the subprocess fails, its promise is rejected with this error.

Subprocesses fail either when their exit code is not `0` or when terminated by a signal. Other failure reasons include misspelling the command name or using the [`timeout`](https://nodejs.org/api/child_process.html#child_processspawncommand-args-options) option.
*/
export type SubprocessError = Error & Result & {
export class SubprocessError extends Error implements Result {
stdout: Result['stdout'];
stderr: Result['stderr'];
output: Result['output'];
command: Result['command'];
durationMs: Result['durationMs'];
pipedFrom?: Result['pipedFrom'];

/**
The numeric [exit code](https://en.wikipedia.org/wiki/Exit_status) of the subprocess that was run.

Expand All @@ -150,7 +157,7 @@ export type SubprocessError = Error & Result & {
If a signal terminated the subprocess, this property is defined and included in the [error message](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/message). Otherwise it is `undefined`.
*/
signalName?: string;
};
}

/**
Subprocess started by `spawn()`.
Expand Down
2 changes: 2 additions & 0 deletions source/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {getResult} from './result.js';
import {handlePipe} from './pipe.js';
import {lineIterator, combineAsyncIterators} from './iterable.js';

export {SubprocessError} from './result.js';

export default function spawn(file, second, third, previous) {
const [commandArguments = [], options = {}] = Array.isArray(second) ? [second, third] : [[], second];
const context = getContext([file, ...commandArguments]);
Expand Down
29 changes: 15 additions & 14 deletions source/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import {
expectError,
} from 'tsd';
import spawn, {
SubprocessError,
type Options,
type Result,
type SubprocessError,
type Subprocess,
} from './index.js';

Expand All @@ -28,19 +28,20 @@ try {
expectError(result.signalName);
expectError(result.other);
} catch (error) {
const subprocessError = error as SubprocessError;
expectType<string>(subprocessError.stdout);
expectType<string>(subprocessError.stderr);
expectType<string>(subprocessError.output);
expectType<string>(subprocessError.command);
expectType<number>(subprocessError.durationMs);
expectType<Result | SubprocessError | undefined>(subprocessError.pipedFrom);
expectType<Result | SubprocessError | undefined>(subprocessError.pipedFrom?.pipedFrom);
expectType<number | undefined>(subprocessError.pipedFrom?.durationMs);
expectAssignable<Error>(subprocessError);
expectType<number | undefined>(subprocessError.exitCode);
expectType<string | undefined>(subprocessError.signalName);
expectError(subprocessError.other);
if (error instanceof SubprocessError) {
expectType<string>(error.stdout);
expectType<string>(error.stderr);
expectType<string>(error.output);
expectType<string>(error.command);
expectType<number>(error.durationMs);
expectType<Result | SubprocessError | undefined>(error.pipedFrom);
expectType<Result | SubprocessError | undefined>(error.pipedFrom?.pipedFrom);
expectType<number | undefined>(error.pipedFrom?.durationMs);
expectAssignable<Error>(error);
expectType<number | undefined>(error.exitCode);
expectType<string | undefined>(error.signalName);
expectError(error.other);
}
}

expectAssignable<Options>({} as const);
Expand Down
12 changes: 8 additions & 4 deletions source/result.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ const onStreamError = async stream => {

const checkFailure = ({command}, {exitCode, signalName}) => {
if (signalName !== undefined) {
throw new Error(`Command was terminated with ${signalName}: ${command}`);
throw new SubprocessError(`Command was terminated with ${signalName}: ${command}`);
}

if (exitCode !== undefined) {
throw new Error(`Command failed with exit code ${exitCode}: ${command}`);
throw new SubprocessError(`Command failed with exit code ${exitCode}: ${command}`);
}
};

Expand All @@ -47,9 +47,13 @@ export const getResultError = (error, instance, context) => Object.assign(
getOutputs(context),
);

const getErrorInstance = (error, {command}) => error?.message.startsWith('Command ')
const getErrorInstance = (error, {command}) => error instanceof SubprocessError
? error
: new Error(`Command failed: ${command}`, {cause: error});
: new SubprocessError(`Command failed: ${command}`, {cause: error});

export class SubprocessError extends Error {
name = 'SubprocessError';
}

const getErrorOutput = ({exitCode, signalCode}) => ({
// `exitCode` can be a negative number (`errno`) when the `error` event is emitted on the `instance`
Expand Down
31 changes: 22 additions & 9 deletions test/helpers/assert.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import {nonExistentCommand, nodeHangingCommand, nodeEvalCommandStart} from './commands.js';

export const assertSubprocessErrorName = (t, name) => {
t.is(name, 'SubprocessError');
};

export const assertDurationMs = (t, durationMs) => {
t.true(Number.isFinite(durationMs));
t.true(durationMs >= 0);
};

export const assertNonExistent = (t, {exitCode, signalName, command, message, stderr, cause, durationMs}, commandStart = nonExistentCommand, expectedCommand = commandStart) => {
export const assertNonExistent = (t, {name, exitCode, signalName, command, message, stderr, cause, durationMs}, commandStart = nonExistentCommand, expectedCommand = commandStart) => {
assertSubprocessErrorName(t, name);
t.is(exitCode, undefined);
t.is(signalName, undefined);
t.is(command, expectedCommand);
Expand All @@ -18,7 +23,8 @@ export const assertNonExistent = (t, {exitCode, signalName, command, message, st
assertDurationMs(t, durationMs);
};

export const assertWindowsNonExistent = (t, {exitCode, signalName, command, message, stderr, cause, durationMs}, expectedCommand = nonExistentCommand) => {
export const assertWindowsNonExistent = (t, {name, exitCode, signalName, command, message, stderr, cause, durationMs}, expectedCommand = nonExistentCommand) => {
assertSubprocessErrorName(t, name);
t.is(exitCode, 1);
t.is(signalName, undefined);
t.is(command, expectedCommand);
Expand All @@ -28,7 +34,8 @@ export const assertWindowsNonExistent = (t, {exitCode, signalName, command, mess
assertDurationMs(t, durationMs);
};

export const assertUnixNonExistentShell = (t, {exitCode, signalName, command, message, stderr, cause, durationMs}, expectedCommand = nonExistentCommand) => {
export const assertUnixNonExistentShell = (t, {name, exitCode, signalName, command, message, stderr, cause, durationMs}, expectedCommand = nonExistentCommand) => {
assertSubprocessErrorName(t, name);
t.is(exitCode, 127);
t.is(signalName, undefined);
t.is(command, expectedCommand);
Expand All @@ -38,7 +45,8 @@ export const assertUnixNonExistentShell = (t, {exitCode, signalName, command, me
assertDurationMs(t, durationMs);
};

export const assertUnixNotFound = (t, {exitCode, signalName, command, message, stderr, cause, durationMs}, expectedCommand = nonExistentCommand) => {
export const assertUnixNotFound = (t, {name, exitCode, signalName, command, message, stderr, cause, durationMs}, expectedCommand = nonExistentCommand) => {
assertSubprocessErrorName(t, name);
t.is(exitCode, 127);
t.is(signalName, undefined);
t.is(command, expectedCommand);
Expand All @@ -48,7 +56,8 @@ export const assertUnixNotFound = (t, {exitCode, signalName, command, message, s
assertDurationMs(t, durationMs);
};

export const assertFail = (t, {exitCode, signalName, command, message, cause, durationMs}, commandStart = nodeEvalCommandStart) => {
export const assertFail = (t, {name, exitCode, signalName, command, message, cause, durationMs}, commandStart = nodeEvalCommandStart) => {
assertSubprocessErrorName(t, name);
t.is(exitCode, 2);
t.is(signalName, undefined);
t.true(command.startsWith(commandStart));
Expand All @@ -57,7 +66,8 @@ export const assertFail = (t, {exitCode, signalName, command, message, cause, du
assertDurationMs(t, durationMs);
};

export const assertSigterm = (t, {exitCode, signalName, command, message, stderr, cause, durationMs}, expectedCommand = nodeHangingCommand) => {
export const assertSigterm = (t, {name, exitCode, signalName, command, message, stderr, cause, durationMs}, expectedCommand = nodeHangingCommand) => {
assertSubprocessErrorName(t, name);
t.is(exitCode, undefined);
t.is(signalName, 'SIGTERM');
t.is(command, expectedCommand);
Expand All @@ -67,7 +77,8 @@ export const assertSigterm = (t, {exitCode, signalName, command, message, stderr
assertDurationMs(t, durationMs);
};

export const assertEarlyError = (t, {exitCode, signalName, command, message, stderr, cause, durationMs}, commandStart = nodeEvalCommandStart) => {
export const assertEarlyError = (t, {name, exitCode, signalName, command, message, stderr, cause, durationMs}, commandStart = nodeEvalCommandStart) => {
assertSubprocessErrorName(t, name);
t.is(exitCode, undefined);
t.is(signalName, undefined);
t.true(command.startsWith(commandStart));
Expand All @@ -78,7 +89,8 @@ export const assertEarlyError = (t, {exitCode, signalName, command, message, std
assertDurationMs(t, durationMs);
};

export const assertAbortError = (t, {exitCode, signalName, command, stderr, message, cause, durationMs}, expectedCause, expectedCommand = nodeHangingCommand) => {
export const assertAbortError = (t, {name, exitCode, signalName, command, stderr, message, cause, durationMs}, expectedCause, expectedCommand = nodeHangingCommand) => {
assertSubprocessErrorName(t, name);
t.is(exitCode, undefined);
t.is(signalName, undefined);
t.is(command, expectedCommand);
Expand All @@ -89,7 +101,8 @@ export const assertAbortError = (t, {exitCode, signalName, command, stderr, mess
assertDurationMs(t, durationMs);
};

export const assertErrorEvent = (t, {exitCode, signalName, command, message, stderr, cause, durationMs}, expectedCause, commandStart = nodeEvalCommandStart) => {
export const assertErrorEvent = (t, {name, exitCode, signalName, command, message, stderr, cause, durationMs}, expectedCause, commandStart = nodeEvalCommandStart) => {
assertSubprocessErrorName(t, name);
t.is(exitCode, undefined);
t.is(signalName, undefined);
t.true(command.startsWith(commandStart));
Expand Down
9 changes: 8 additions & 1 deletion test/result.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import test from 'ava';
import spawn from '../source/index.js';
import spawn, {SubprocessError} from '../source/index.js';
import {
isWindows,
isLinux,
Expand All @@ -9,6 +9,7 @@ import {
} from './helpers/main.js';
import {testString, secondTestString} from './helpers/arguments.js';
import {
assertSubprocessErrorName,
assertFail,
assertSigterm,
assertEarlyError,
Expand All @@ -33,6 +34,12 @@ test('result.exitCode|signalName on success', async t => {
t.is(signalName, undefined);
});

test('Error is an instance of SubprocessError', async t => {
const error = await t.throwsAsync(spawn(...nodeEval('process.exit(2)')));
t.true(error instanceof SubprocessError);
assertSubprocessErrorName(t, error.name);
});

test('Error on non-0 exit code', async t => {
const error = await t.throwsAsync(spawn(...nodeEval('process.exit(2)')));
assertFail(t, error);
Expand Down