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

Improve returned error.message #180

Merged
merged 13 commits into from Mar 6, 2019
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 @@ -455,7 +466,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