Skip to content

Commit

Permalink
Enhance error object in sync methods - fixes #98 (#107)
Browse files Browse the repository at this point in the history
  • Loading branch information
SamVerschueren authored and sindresorhus committed Oct 17, 2017
1 parent dc7e21b commit d31cc91
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 43 deletions.
124 changes: 81 additions & 43 deletions index.js
Expand Up @@ -128,16 +128,62 @@ function getStream(process, stream, encoding, maxBuffer) {
});
}

module.exports = (cmd, args, opts) => {
function makeError(result, options) {
const stdout = result.stdout;
const stderr = result.stderr;

let err = result.error;
const code = result.code;
const signal = result.signal;

const parsed = options.parsed;
const joinedCmd = options.joinedCmd;
const timedOut = options.timedOut || false;

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

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

if (parsed.opts.stdio[1] !== 'inherit') {
output += `\n${stdout}`;
}
} else if (parsed.opts.stdio !== 'inherit') {
output = `\n${stderr}${stdout}`;
}

err = new Error(`Command failed: ${joinedCmd}${output}`);
err.code = code < 0 ? errname(code) : code;
}

err.stdout = stdout;
err.stderr = stderr;
err.failed = true;
err.signal = signal || null;
err.cmd = joinedCmd;
err.timedOut = timedOut;

return err;
}

function joinCmd(cmd, args) {
let joinedCmd = cmd;

if (Array.isArray(args) && args.length > 0) {
joinedCmd += ' ' + args.join(' ');
}

return joinedCmd;
}

module.exports = (cmd, args, opts) => {
const parsed = handleArgs(cmd, args, opts);
const encoding = parsed.opts.encoding;
const maxBuffer = parsed.opts.maxBuffer;
const joinedCmd = joinCmd(cmd, args);

let spawned;
try {
Expand Down Expand Up @@ -179,13 +225,13 @@ module.exports = (cmd, args, opts) => {

spawned.on('error', err => {
cleanupTimeout();
resolve({err});
resolve({error: err});
});

if (spawned.stdin) {
spawned.stdin.on('error', err => {
cleanupTimeout();
resolve({err});
resolve({error: err});
});
}
});
Expand All @@ -206,49 +252,25 @@ module.exports = (cmd, args, opts) => {
getStream(spawned, 'stderr', encoding, maxBuffer)
]).then(arr => {
const result = arr[0];
const stdout = arr[1];
const stderr = arr[2];

let err = result.err;
const code = result.code;
const signal = result.signal;
result.stdout = arr[1];
result.stderr = arr[2];

if (removeExitHandler) {
removeExitHandler();
}

if (err || code !== 0 || signal !== null) {
if (!err) {
let output = '';

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

if (parsed.opts.stdio[1] !== 'inherit') {
output += `\n${stdout}`;
}
} else if (parsed.opts.stdio !== 'inherit') {
output = `\n${stderr}${stdout}`;
}

err = new Error(`Command failed: ${joinedCmd}${output}`);
err.code = code < 0 ? errname(code) : code;
}
if (result.error || result.code !== 0 || result.signal !== null) {
const err = makeError(result, {
joinedCmd,
parsed,
timedOut
});

// TODO: missing some timeout logic for killed
// https://github.com/nodejs/node/blob/master/lib/child_process.js#L203
// err.killed = spawned.killed || killed;
err.killed = err.killed || spawned.killed;

err.stdout = stdout;
err.stderr = stderr;
err.failed = true;
err.signal = signal || null;
err.cmd = joinedCmd;
err.timedOut = timedOut;

if (!parsed.opts.reject) {
return err;
}
Expand All @@ -257,8 +279,8 @@ module.exports = (cmd, args, opts) => {
}

return {
stdout: handleOutput(parsed.opts, stdout),
stderr: handleOutput(parsed.opts, stderr),
stdout: handleOutput(parsed.opts, result.stdout),
stderr: handleOutput(parsed.opts, result.stderr),
code: 0,
failed: false,
killed: false,
Expand Down Expand Up @@ -292,21 +314,37 @@ module.exports.shell = (cmd, opts) => handleShell(module.exports, cmd, opts);

module.exports.sync = (cmd, args, opts) => {
const parsed = handleArgs(cmd, args, opts);
const joinedCmd = joinCmd(cmd, args);

if (isStream(parsed.opts.input)) {
throw new TypeError('The `input` option cannot be a stream in sync mode');
}

const result = childProcess.spawnSync(parsed.cmd, parsed.args, parsed.opts);
result.code = result.status;

if (result.error || result.status !== 0) {
throw (result.error || new Error(result.stderr === '' ? result.stdout : result.stderr));
}
if (result.error || result.status !== 0 || result.signal !== null) {
const err = makeError(result, {
joinedCmd,
parsed
});

if (!parsed.opts.reject) {
return err;
}

result.stdout = handleOutput(parsed.opts, result.stdout);
result.stderr = handleOutput(parsed.opts, result.stderr);
throw err;
}

return result;
return {
stdout: handleOutput(parsed.opts, result.stdout),
stderr: handleOutput(parsed.opts, result.stderr),
code: 0,
failed: false,
signal: null,
cmd: joinedCmd,
timedOut: false
};
};

module.exports.shellSync = (cmd, opts) => handleShell(module.exports.sync, cmd, opts);
Expand Down
18 changes: 18 additions & 0 deletions readme.md
Expand Up @@ -55,6 +55,24 @@ execa.shell('exit 3').catch(error => {
}
*/
});

// example of catching an error with a sync method
try {
execa.shellSync('exit 3');
} catch (err) {
console.log(err);
/*
{
message: 'Command failed: /bin/sh -c exit 3'
code: 3,
signal: null,
cmd: '/bin/sh -c exit 3',
stdout: '',
stderr: '',
timedOut: false
}
*/
}
```


Expand Down
26 changes: 26 additions & 0 deletions test.js
Expand Up @@ -104,11 +104,37 @@ 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');
});

test('execa.sync() includes stdout and stderr in errors for improved debugging', t => {
const err = t.throws(() => m.sync('node', ['fixtures/error-message.js']));
t.regex(err.message, /stdout/);
t.regex(err.message, /stderr/);
t.is(err.code, 1);
});

test('skip throwing when using reject option in execa.sync()', t => {
const err = m.sync('node', ['fixtures/error-message.js'], {reject: false});
t.is(typeof err.stdout, 'string');
t.is(typeof err.stderr, 'string');
});

test('execa.shellSync()', t => {
const {stdout} = m.shellSync('node fixtures/noop foo');
t.is(stdout, 'foo');
});

test('execa.shellSync() includes stdout and stderr in errors for improved debugging', t => {
const err = t.throws(() => m.shellSync('node fixtures/error-message.js'));
t.regex(err.message, /stdout/);
t.regex(err.message, /stderr/);
t.is(err.code, 1);
});

test('skip throwing when using reject option in execa.shellSync()', t => {
const err = m.shellSync('node fixtures/error-message.js', {reject: false});
t.is(typeof err.stdout, 'string');
t.is(typeof err.stderr, 'string');
});

test('stripEof option', async t => {
const {stdout} = await m('noop', ['foo'], {stripEof: false});
t.is(stdout, 'foo\n');
Expand Down

0 comments on commit d31cc91

Please sign in to comment.