Skip to content

Commit

Permalink
Improve returned error.message (#180)
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky authored and sindresorhus committed Mar 6, 2019
1 parent eac23b0 commit 1fd9db4
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 39 deletions.
71 changes: 48 additions & 23 deletions index.js
@@ -1,5 +1,6 @@
'use strict';
const path = require('path');
const os = require('os');
const childProcess = require('child_process');
const crossSpawn = require('cross-spawn');
const stripFinalNewline = require('strip-final-newline');
Expand Down Expand Up @@ -120,43 +121,67 @@ function getStream(process, stream, {encoding, buffer, maxBuffer}) {
}

function makeError(result, options) {
const {stdout, stderr} = result;

const {stdout, stderr, code, signal} = result;
let {error} = result;
const {code, signal} = result;

const {parsed, joinedCommand} = options;
const timedOut = options.timedOut || false;
const {joinedCommand, timedOut, parsed: {options: {timeout}}} = options;

if (!error) {
let output = '';

if (Array.isArray(parsed.options.stdio)) {
if (parsed.options.stdio[2] !== 'inherit') {
output += output.length > 0 ? stderr : `\n${stderr}`;
}

if (parsed.options.stdio[1] !== 'inherit') {
output += `\n${stdout}`;
}
} else if (parsed.options.stdio !== 'inherit') {
output = `\n${stderr}${stdout}`;
}
const [codeString, codeNumber] = getCode(result, code);

error = new Error(`Command failed: ${joinedCommand}${output}`);
error.code = code < 0 ? errname(code) : code;
if (!(error instanceof Error)) {
const message = [joinedCommand, stderr, stdout].filter(Boolean).join('\n');
error = new Error(message);
}

const prefix = getErrorPrefix({timedOut, timeout, signal, codeString, codeNumber});
error.message = `Command ${prefix}: ${error.message}`;

error.code = codeNumber || codeString;
error.stdout = stdout;
error.stderr = stderr;
error.failed = true;
error.signal = signal || null;
error.cmd = joinedCommand;
error.timedOut = timedOut;
error.timedOut = Boolean(timedOut);

return error;
}

function getCode({error = {}}, code) {
if (error.code) {
return [error.code, os.constants.errno[error.code]];
}

if (Number.isInteger(code)) {
return [errname(-Math.abs(code)), Math.abs(code)];
}

return [];
}

function getErrorPrefix({timedOut, timeout, signal, codeString, codeNumber}) {
if (timedOut) {
return `timed out after ${timeout} milliseconds`;
}

if (signal) {
return `was killed with ${signal}`;
}

if (codeString !== undefined && codeNumber !== undefined) {
return `failed with exit code ${codeNumber} (${codeString})`;
}

if (codeString !== undefined) {
return `failed with exit code ${codeString}`;
}

if (codeNumber !== undefined) {
return `failed with exit code ${codeNumber}`;
}

return 'failed';
}

function joinCommand(command, args) {
let joinedCommand = command;

Expand Down
43 changes: 27 additions & 16 deletions test.js
Expand Up @@ -14,6 +14,11 @@ process.env.FOO = 'foo';

const NO_NEWLINES_REGEXP = /^[^\n]*$/;
const STDERR_STDOUT_REGEXP = /stderr[^]*stdout/;
const TIMEOUT_REGEXP = /timed out after/;

function getExitRegExp(exitMessage) {
return new RegExp(`failed with exit code ${exitMessage}`);
}

test('execa()', async t => {
const {stdout} = await m('noop', ['foo']);
Expand Down Expand Up @@ -45,7 +50,7 @@ test('execa.stderr()', async t => {
});

test('stdout/stderr available on errors', async t => {
const err = await t.throwsAsync(m('exit', ['2']));
const err = await t.throwsAsync(m('exit', ['2']), {message: getExitRegExp('2')});
t.is(typeof err.stdout, 'string');
t.is(typeof err.stderr, 'string');
});
Expand Down Expand Up @@ -99,7 +104,7 @@ test('execa.sync()', t => {
});

test('execa.sync() throws error if written to stderr', t => {
t.throws(() => m.sync('foo'), process.platform === 'win32' ? /'foo' is not recognized as an internal or external command/ : 'spawnSync foo ENOENT');
t.throws(() => m.sync('foo'), process.platform === 'win32' ? /'foo' is not recognized as an internal or external command/ : /spawnSync foo ENOENT/);
});

test('execa.sync() includes stdout and stderr in errors for improved debugging', t => {
Expand Down Expand Up @@ -256,9 +261,13 @@ test('skip throwing when using reject option', async t => {
t.is(typeof error.stderr, 'string');
});

test('allow unknown exit code', async t => {
await t.throwsAsync(m('exit', ['255']), {message: /exit code 255 \(Unknown system error -255\)/});
});

test('execa() returns code and failed properties', async t => {
const {code, failed} = await m('noop', ['foo']);
const error = await t.throwsAsync(m('exit', ['2']), {code: 2});
const error = await t.throwsAsync(m('exit', ['2']), {code: 2, message: getExitRegExp('2')});
t.is(code, 0);
t.false(failed);
t.true(error.failed);
Expand All @@ -284,7 +293,7 @@ test('error.killed is true if process was killed directly', async t => {
cp.kill();
}, 100);

const error = await t.throwsAsync(cp);
const error = await t.throwsAsync(cp, {message: /was killed with SIGTERM/});
t.true(error.killed);
});

Expand All @@ -296,7 +305,9 @@ test('error.killed is false if process was killed indirectly', async t => {
process.kill(cp.pid, 'SIGINT');
}, 100);

const error = await t.throwsAsync(cp);
// `process.kill()` is emulated by Node.js on Windows
const message = process.platform === 'win32' ? /failed with exit code 1/ : /was killed with SIGINT/;
const error = await t.throwsAsync(cp, {message});
t.false(error.killed);
});

Expand All @@ -322,7 +333,7 @@ if (process.platform !== 'win32') {
process.kill(cp.pid, 'SIGINT');
}, 100);

const error = await t.throwsAsync(cp);
const error = await t.throwsAsync(cp, {message: /was killed with SIGINT/});
t.is(error.signal, 'SIGINT');
});

Expand All @@ -333,12 +344,12 @@ if (process.platform !== 'win32') {
process.kill(cp.pid, 'SIGTERM');
}, 100);

const error = await t.throwsAsync(cp);
const error = await t.throwsAsync(cp, {message: /was killed with SIGTERM/});
t.is(error.signal, 'SIGTERM');
});

test('custom error.signal', async t => {
const error = await t.throwsAsync(m('delay', ['3000', '0'], {killSignal: 'SIGHUP', timeout: 1500}));
const error = await t.throwsAsync(m('delay', ['3000', '0'], {killSignal: 'SIGHUP', timeout: 1500, message: TIMEOUT_REGEXP}));
t.is(error.signal, 'SIGHUP');
});
}
Expand All @@ -348,27 +359,27 @@ test('result.signal is null for successful execution', async t => {
});

test('result.signal is null if process failed, but was not killed', async t => {
const error = await t.throwsAsync(m('exit', [2]));
const error = await t.throwsAsync(m('exit', [2]), {message: getExitRegExp('2')});
t.is(error.signal, null);
});

async function code(t, num) {
await t.throwsAsync(m('exit', [`${num}`]), {code: num});
await t.throwsAsync(m('exit', [`${num}`]), {code: num, message: getExitRegExp(num)});
}

test('error.code is 2', code, 2);
test('error.code is 3', code, 3);
test('error.code is 4', code, 4);

test('timeout will kill the process early', async t => {
const error = await t.throwsAsync(m('delay', ['60000', '0'], {timeout: 1500}));
const error = await t.throwsAsync(m('delay', ['60000', '0'], {timeout: 1500, message: TIMEOUT_REGEXP}));

t.true(error.timedOut);
t.not(error.code, 22);
});

test('timeout will not kill the process early', async t => {
const error = await t.throwsAsync(m('delay', ['3000', '22'], {timeout: 30000}), {code: 22});
const error = await t.throwsAsync(m('delay', ['3000', '22'], {timeout: 30000}), {code: 22, message: getExitRegExp('22')});
t.false(error.timedOut);
});

Expand All @@ -378,7 +389,7 @@ test('timedOut will be false if no timeout was set and zero exit code', async t
});

test('timedOut will be false if no timeout was set and non-zero exit code', async t => {
const error = await t.throwsAsync(m('delay', ['1000', '3']));
const error = await t.throwsAsync(m('delay', ['1000', '3']), {message: getExitRegExp('3')});
t.false(error.timedOut);
});

Expand All @@ -388,8 +399,8 @@ async function errorMessage(t, expected, ...args) {

errorMessage.title = (message, expected) => `error.message matches: ${expected}`;

test(errorMessage, /Command failed: exit 2 foo bar/, 2, 'foo', 'bar');
test(errorMessage, /Command failed: exit 3 baz quz/, 3, 'baz', 'quz');
test(errorMessage, /Command failed with exit code 2.*: exit 2 foo bar/, 2, 'foo', 'bar');
test(errorMessage, /Command failed with exit code 3.*: exit 3 baz quz/, 3, 'baz', 'quz');

async function cmd(t, expected, ...args) {
const error = await t.throwsAsync(m('fail', args));
Expand Down Expand Up @@ -453,7 +464,7 @@ if (process.platform !== 'win32') {
await m(`fast-exit-${process.platform}`, [], {input: 'data'});
t.pass();
} catch (error) {
t.is(error.code, 'EPIPE');
t.is(error.code, 32);
}
});
}
Expand Down

0 comments on commit 1fd9db4

Please sign in to comment.