From 6c46865b8f4113095ff336a83a45ef5b4fca5b4a Mon Sep 17 00:00:00 2001 From: Tommaso Sotte Date: Sun, 20 Jan 2019 10:42:08 +0100 Subject: [PATCH 01/17] Added result.all intermixed stdout/stderr (concat for sync) Added test cases for result.all intermixed output. Added fixtures/noop-132 which should output the sequence '123' but due to the unpredictable nature of the stdout/stderr it outputs '132' as the most common cases I've tried, as listed on noop-132, a different number of technique to make it predictable. On execa.sync, as it's using spawnSync internally, I couldn't get the stream and merge them. As such the output of result.all is just a concatenation of stdout + stderr. This makes the result.all in different use case (ex. async vs. sync) unreliable for string comparison, but at least useful for logging or checking on the output. --- fixtures/noop-132 | 14 ++++++++++++++ index.js | 35 ++++++++++++++++++++++++++++++++++- package.json | 1 + test.js | 19 +++++++++++++++++-- 4 files changed, 66 insertions(+), 3 deletions(-) create mode 100755 fixtures/noop-132 diff --git a/fixtures/noop-132 b/fixtures/noop-132 new file mode 100755 index 000000000..77e6da7bc --- /dev/null +++ b/fixtures/noop-132 @@ -0,0 +1,14 @@ +#!/usr/bin/env node +'use strict'; +// Interleaving the output of stdout and stderr is unpredictable and can't be +// expressed as a test. What I've tried so far to make it predictable: +// - process.stdout._handle.setBlocking(true) +// - process.stdout.once('drain', () => process.stderr.write('2')); +// - process.stdout.write('1', () => process.stderr.write('2')); +// - await delay() between calls; it works but with varying ms, based on load +// - console.log/error + +process.stdout.write('1'); +process.stdout.write('3'); +// Ideally, to demonstrate the interleaving, the next line should go before '3' +process.stderr.write('2'); diff --git a/index.js b/index.js index 0610a581c..5cfc1f02c 100644 --- a/index.js +++ b/index.js @@ -6,6 +6,7 @@ const stripFinalNewline = require('strip-final-newline'); const npmRunPath = require('npm-run-path'); const isStream = require('is-stream'); const _getStream = require('get-stream'); +const mergeStream = require('merge-stream'); const pFinally = require('p-finally'); const onExit = require('signal-exit'); const errname = require('./lib/errname'); @@ -122,6 +123,24 @@ function handleShell(fn, command, options) { return fn(file, args, options); } +function makeAllStream(spawned) { + const mixed = mergeStream(); + + if (!spawned.stdout && !spawned.stderr) { + return null; + } + + if (spawned.stdout) { + mixed.add(spawned.stdout); + } + + if (spawned.stderr) { + mixed.add(spawned.stderr); + } + + return mixed; +} + function getStream(process, stream, {encoding, buffer, maxBuffer}) { if (!process[stream]) { return null; @@ -268,16 +287,22 @@ module.exports = (command, args, options) => { if (spawned.stderr) { spawned.stderr.destroy(); } + + if (spawned.all) { + spawned.all.destroy(); + } } const handlePromise = () => pFinally(Promise.all([ processDone, getStream(spawned, 'stdout', {encoding, buffer, maxBuffer}), - getStream(spawned, 'stderr', {encoding, buffer, maxBuffer}) + getStream(spawned, 'stderr', {encoding, buffer, maxBuffer}), + getStream(spawned, 'all', {encoding, buffer, maxBuffer: maxBuffer * 2}) ]).then(arr => { const result = arr[0]; result.stdout = arr[1]; result.stderr = arr[2]; + result.all = arr[3]; if (result.error || result.code !== 0 || result.signal !== null) { const error = makeError(result, { @@ -301,6 +326,7 @@ module.exports = (command, args, options) => { return { stdout: handleOutput(parsed.options, result.stdout), stderr: handleOutput(parsed.options, result.stderr), + all: handleOutput(parsed.options, result.all), code: 0, failed: false, killed: false, @@ -314,6 +340,8 @@ module.exports = (command, args, options) => { handleInput(spawned, parsed.options.input); + spawned.all = makeAllStream(spawned); + spawned.then = (onfulfilled, onrejected) => handlePromise().then(onfulfilled, onrejected); spawned.catch = onrejected => handlePromise().catch(onrejected); @@ -339,6 +367,10 @@ module.exports.sync = (command, args, options) => { const result = childProcess.spawnSync(parsed.command, parsed.args, parsed.options); result.code = result.status; + // `spawnSync` doesn't expose the stdout/stderr before terminating, which means + // the streams can't be merged unless proxying on `options.stdio` + result.all = result.stdout + result.stderr; + if (result.error || result.status !== 0 || result.signal !== null) { const error = makeError(result, { joinedCommand, @@ -355,6 +387,7 @@ module.exports.sync = (command, args, options) => { return { stdout: handleOutput(parsed.options, result.stdout), stderr: handleOutput(parsed.options, result.stderr), + all: handleOutput(parsed.options, result.all), code: 0, failed: false, signal: null, diff --git a/package.json b/package.json index 4336d549a..7dce3d9ce 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", "is-stream": "^1.1.0", + "merge-stream": "1.0.1", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", "signal-exit": "^3.0.0", diff --git a/test.js b/test.js index c9e56f30d..0b926b43a 100644 --- a/test.js +++ b/test.js @@ -41,6 +41,19 @@ test('execa.stderr()', async t => { t.is(stderr, 'foo'); }); +test('result.all shows both `stdout` and `stderr` intermixed', async t => { + const result = await m('noop-132'); + // Due to the async nature of process.stdout/stderr on POSIX, this test + // is very unpredictable, although it should interleave the streams + // https://nodejs.org/api/process.html#process_a_note_on_process_i_os + t.is(result.all, '132'); +}); + +test('result.all shows both `stdout` and `stderr` concatenated - sync', t => { + const result = m.sync('noop-132'); + t.is(result.all, '132'); +}); + test('stdout/stderr available on errors', async t => { const err = await t.throws(m('exit', ['2'])); t.is(typeof err.stdout, 'string'); @@ -243,7 +256,8 @@ test('do not buffer stdout when `buffer` set to `false`', async t => { const promise = m('max-buffer', ['stdout', '10'], {buffer: false}); const [result, stdout] = await Promise.all([ promise, - getStream(promise.stdout) + getStream(promise.stdout), + getStream(promise.all) ]); t.is(result.stdout, undefined); @@ -254,7 +268,8 @@ test('do not buffer stderr when `buffer` set to `false`', async t => { const promise = m('max-buffer', ['stderr', '10'], {buffer: false}); const [result, stderr] = await Promise.all([ promise, - getStream(promise.stderr) + getStream(promise.stderr), + getStream(promise.all) ]); t.is(result.stderr, undefined); From 4069b17fde65d1ffb28a3c0a46d5ee525ea69d0e Mon Sep 17 00:00:00 2001 From: Tommaso Sotte Date: Thu, 7 Mar 2019 09:53:26 +0100 Subject: [PATCH 02/17] Removed `all` stream from execa.sync(); fix creating mixed stream for nothing - execa.sync() cannot provide a true interleaved `all` stream because of the implementation of child_process.spawnSync, which does not expose `stdout` and `stderr` streams until termination. We could only concatenate the streams in the end. - Does not create the mixed stream if both streams do not exist. --- index.js | 9 ++------- test.js | 5 ----- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/index.js b/index.js index 5cfc1f02c..60121c7d6 100644 --- a/index.js +++ b/index.js @@ -124,12 +124,12 @@ function handleShell(fn, command, options) { } function makeAllStream(spawned) { - const mixed = mergeStream(); - if (!spawned.stdout && !spawned.stderr) { return null; } + const mixed = mergeStream(); + if (spawned.stdout) { mixed.add(spawned.stdout); } @@ -367,10 +367,6 @@ module.exports.sync = (command, args, options) => { const result = childProcess.spawnSync(parsed.command, parsed.args, parsed.options); result.code = result.status; - // `spawnSync` doesn't expose the stdout/stderr before terminating, which means - // the streams can't be merged unless proxying on `options.stdio` - result.all = result.stdout + result.stderr; - if (result.error || result.status !== 0 || result.signal !== null) { const error = makeError(result, { joinedCommand, @@ -387,7 +383,6 @@ module.exports.sync = (command, args, options) => { return { stdout: handleOutput(parsed.options, result.stdout), stderr: handleOutput(parsed.options, result.stderr), - all: handleOutput(parsed.options, result.all), code: 0, failed: false, signal: null, diff --git a/test.js b/test.js index 0b926b43a..5c1aa8c1f 100644 --- a/test.js +++ b/test.js @@ -49,11 +49,6 @@ test('result.all shows both `stdout` and `stderr` intermixed', async t => { t.is(result.all, '132'); }); -test('result.all shows both `stdout` and `stderr` concatenated - sync', t => { - const result = m.sync('noop-132'); - t.is(result.all, '132'); -}); - test('stdout/stderr available on errors', async t => { const err = await t.throws(m('exit', ['2'])); t.is(typeof err.stdout, 'string'); From 97cde71475fc02605ffd5ac8c283437ff9005b57 Mon Sep 17 00:00:00 2001 From: Tommaso Sotte Date: Thu, 7 Mar 2019 09:54:32 +0100 Subject: [PATCH 03/17] Added `all` stream documentation and why it's not possible in execa.sync() --- readme.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index b6921faaa..bbbb3df7e 100644 --- a/readme.md +++ b/readme.md @@ -100,7 +100,7 @@ Execute a file. Think of this as a mix of `child_process.execFile` and `child_process.spawn`. -Returns a [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess), which is enhanced to also be a `Promise` for a result `Object` with `stdout` and `stderr` properties. +Returns a [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess), which is enhanced to also be a `Promise` for a result `Object` with `stdout`, `stderr` and `all` (interleaved `stdout` and `stderr` stream) properties. ### execa.stdout(file, [arguments], [options]) @@ -120,9 +120,9 @@ The `child_process` instance is enhanced to also be promise for a result object ### execa.sync(file, [arguments], [options]) -Execute a file synchronously. +Execute a file synchronously. -Returns the same result object as [`child_process.spawnSync`](https://nodejs.org/api/child_process.html#child_process_child_process_spawnsync_command_args_options). +Returns the same result object as [`child_process.spawnSync`](https://nodejs.org/api/child_process.html#child_process_child_process_spawnsync_command_args_options). It's not possible to obtain a true interleaved `all` stream as `execa` does, because `child_process.spawnSync` does not expose `stdout` and `stderr` until termination. This method throws an `Error` if the command fails. From 23799a075437b5e8dfd3398a287cd0589de70b66 Mon Sep 17 00:00:00 2001 From: Tommaso Sotte Date: Thu, 7 Mar 2019 10:21:30 +0100 Subject: [PATCH 04/17] More predictable noop-132 fixture for testing `all` stream output The delay 6000ms is approximate, it may not work on every machine. --- fixtures/noop-132 | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/fixtures/noop-132 b/fixtures/noop-132 index 77e6da7bc..62abca26f 100755 --- a/fixtures/noop-132 +++ b/fixtures/noop-132 @@ -9,6 +9,8 @@ // - console.log/error process.stdout.write('1'); -process.stdout.write('3'); -// Ideally, to demonstrate the interleaving, the next line should go before '3' -process.stderr.write('2'); +process.stderr.write('3'); + +setTimeout(() => { + process.stdout.write('2'); +}, 5000); From 40aa25142f6652e46932a373ae3d5660c0375a00 Mon Sep 17 00:00:00 2001 From: Tommaso Sotte Date: Fri, 8 Mar 2019 11:13:53 +0100 Subject: [PATCH 05/17] Merge with upstream/master; fix rename `m` -> `execa` in test.js --- .travis.yml | 1 - index.js | 190 +++++++++++++++++++------------------ lib/errname.js | 2 +- package.json | 14 +-- test.js | 248 +++++++++++++++++++++++++++--------------------- test/errname.js | 4 +- 6 files changed, 249 insertions(+), 210 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0e8f27289..f1d659d0a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,5 @@ language: node_js node_js: - '10' - '8' - - '6' after_success: - './node_modules/.bin/nyc report --reporter=text-lcov | ./node_modules/.bin/coveralls' diff --git a/index.js b/index.js index 60121c7d6..370720efa 100644 --- a/index.js +++ b/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'); @@ -15,43 +16,37 @@ const stdio = require('./lib/stdio'); const TEN_MEGABYTES = 1000 * 1000 * 10; function handleArgs(command, args, options) { - let parsed; + const parsed = crossSpawn._parse(command, args, options); + command = parsed.command; + args = parsed.args; + options = parsed.options; - options = Object.assign({ - extendEnv: true, - env: {} - }, options); - - if (options.extendEnv) { - options.env = Object.assign({}, process.env, options.env); - } - - if (options.__winShell === true) { - delete options.__winShell; - parsed = { - command, - args, - options, - file: command, - original: { - command, - args - } - }; - } else { - parsed = crossSpawn._parse(command, args, options); - } - - options = Object.assign({ + options = { maxBuffer: TEN_MEGABYTES, buffer: true, stripFinalNewline: true, preferLocal: true, - localDir: parsed.options.cwd || process.cwd(), + localDir: options.cwd || process.cwd(), encoding: 'utf8', reject: true, - cleanup: true - }, parsed.options, {windowsHide: true}); + cleanup: true, + ...options, + windowsHide: true + }; + + if (options.extendEnv !== false) { + options.env = { + ...process.env, + ...options.env + }; + } + + if (options.preferLocal) { + options.env = npmRunPath.env({ + ...options, + cwd: options.localDir + }); + } // TODO: Remove in the next major release if (options.stripEof === false) { @@ -60,26 +55,17 @@ function handleArgs(command, args, options) { options.stdio = stdio(options); - if (options.preferLocal) { - options.env = npmRunPath.env(Object.assign({}, options, {cwd: options.localDir})); - } - if (options.detached) { // #115 options.cleanup = false; } - if (process.platform === 'win32' && path.basename(parsed.command) === 'cmd.exe') { + if (process.platform === 'win32' && path.basename(command) === 'cmd.exe') { // #116 - parsed.args.unshift('/q'); + args.unshift('/q'); } - return { - command: parsed.command, - args: parsed.args, - options, - parsed - }; + return {command, args, options, parsed}; } function handleInput(spawned, input) { @@ -103,24 +89,7 @@ function handleOutput(options, value) { } function handleShell(fn, command, options) { - let file = '/bin/sh'; - let args = ['-c', command]; - - options = Object.assign({}, options); - - if (process.platform === 'win32') { - options.__winShell = true; - file = process.env.comspec || 'cmd.exe'; - args = ['/s', '/c', `"${command}"`]; - options.windowsVerbatimArguments = true; - } - - if (options.shell) { - file = options.shell; - delete options.shell; - } - - return fn(file, args, options); + return fn(command, {...options, shell: true}); } function makeAllStream(spawned) { @@ -172,43 +141,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 = ''; + const [codeString, codeNumber] = getCode(result, code); - 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}`; - } - - 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; @@ -293,16 +286,17 @@ module.exports = (command, args, options) => { } } + // TODO: Use native "finally" syntax when targeting Node.js 10 const handlePromise = () => pFinally(Promise.all([ processDone, getStream(spawned, 'stdout', {encoding, buffer, maxBuffer}), getStream(spawned, 'stderr', {encoding, buffer, maxBuffer}), getStream(spawned, 'all', {encoding, buffer, maxBuffer: maxBuffer * 2}) - ]).then(arr => { - const result = arr[0]; - result.stdout = arr[1]; - result.stderr = arr[2]; - result.all = arr[3]; + ]).then(results => { // eslint-disable-line promise/prefer-await-to-then + const result = results[0]; + result.stdout = results[1]; + result.stderr = results[2]; + result.all = results[3]; if (result.error || result.code !== 0 || result.signal !== null) { const error = makeError(result, { @@ -342,17 +336,29 @@ module.exports = (command, args, options) => { spawned.all = makeAllStream(spawned); - spawned.then = (onfulfilled, onrejected) => handlePromise().then(onfulfilled, onrejected); - spawned.catch = onrejected => handlePromise().catch(onrejected); + // eslint-disable-next-line promise/prefer-await-to-then + spawned.then = (onFulfilled, onRejected) => handlePromise().then(onFulfilled, onRejected); + spawned.catch = onRejected => handlePromise().catch(onRejected); + + // TOOD: Remove the `if`-guard when targeting Node.js 10 + if (Promise.prototype.finally) { + spawned.finally = onFinally => handlePromise().finally(onFinally); + } return spawned; }; // TODO: set `stderr: 'ignore'` when that option is implemented -module.exports.stdout = (...args) => module.exports(...args).then(x => x.stdout); +module.exports.stdout = async (...args) => { + const {stdout} = await module.exports(...args); + return stdout; +}; // TODO: set `stdout: 'ignore'` when that option is implemented -module.exports.stderr = (...args) => module.exports(...args).then(x => x.stderr); +module.exports.stderr = async (...args) => { + const {stderr} = await module.exports(...args); + return stderr; +}; module.exports.shell = (command, options) => handleShell(module.exports, command, options); diff --git a/lib/errname.js b/lib/errname.js index 562284abf..8f9e1fb1b 100644 --- a/lib/errname.js +++ b/lib/errname.js @@ -9,7 +9,7 @@ if (typeof util.getSystemErrorName === 'function') { module.exports = util.getSystemErrorName; } else { try { - uv = process.binding('uv'); + uv = process.binding('uv'); // eslint-disable-line node/no-deprecated-api if (typeof uv.errname !== 'function') { throw new TypeError('uv.errname is not a function'); diff --git a/package.json b/package.json index 7dce3d9ce..d4355ee0d 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "url": "sindresorhus.com" }, "engines": { - "node": ">=6" + "node": ">=8" }, "scripts": { "test": "xo && nyc ava" @@ -47,14 +47,14 @@ "strip-final-newline": "^2.0.0" }, "devDependencies": { - "ava": "^0.25.0", - "cat-names": "^1.0.2", - "coveralls": "^3.0.1", - "delay": "^3.0.0", + "ava": "^1.3.1", + "cat-names": "^2.0.0", + "coveralls": "^3.0.3", + "delay": "^4.1.0", "is-running": "^2.0.0", - "nyc": "^13.0.1", + "nyc": "^13.3.0", "tempfile": "^2.0.0", - "xo": "^0.23.0" + "xo": "^0.24.0" }, "nyc": { "exclude": [ diff --git a/test.js b/test.js index 5c1aa8c1f..4a9e40540 100644 --- a/test.js +++ b/test.js @@ -7,42 +7,47 @@ import getStream from 'get-stream'; import isRunning from 'is-running'; import delay from 'delay'; import tempfile from 'tempfile'; -import m from '.'; +import execa from '.'; process.env.PATH = path.join(__dirname, 'fixtures') + path.delimiter + process.env.PATH; process.env.FOO = 'foo'; +const NO_NEWLINES_REGEXP = /^[^\n]*$/; +const STDERR_STDOUT_REGEXP = /stderr[^]*stdout/; +const TIMEOUT_REGEXP = /timed out after/; + +const getExitRegExp = exitMessage => new RegExp(`failed with exit code ${exitMessage}`); + test('execa()', async t => { - const {stdout} = await m('noop', ['foo']); + const {stdout} = await execa('noop', ['foo']); t.is(stdout, 'foo'); }); if (process.platform === 'win32') { test('execa() - cmd file', async t => { - const {stdout} = await m('hello.cmd'); - + const {stdout} = await execa('hello.cmd'); t.is(stdout, 'Hello World'); }); } test('buffer', async t => { - const {stdout} = await m('noop', ['foo'], {encoding: null}); + const {stdout} = await execa('noop', ['foo'], {encoding: null}); t.true(Buffer.isBuffer(stdout)); t.is(stdout.toString(), 'foo'); }); test('execa.stdout()', async t => { - const stdout = await m.stdout('noop', ['foo']); + const stdout = await execa.stdout('noop', ['foo']); t.is(stdout, 'foo'); }); test('execa.stderr()', async t => { - const stderr = await m.stderr('noop-err', ['foo']); + const stderr = await execa.stderr('noop-err', ['foo']); t.is(stderr, 'foo'); }); test('result.all shows both `stdout` and `stderr` intermixed', async t => { - const result = await m('noop-132'); + const result = await execa('noop-132'); // Due to the async nature of process.stdout/stderr on POSIX, this test // is very unpredictable, although it should interleave the streams // https://nodejs.org/api/process.html#process_a_note_on_process_i_os @@ -50,114 +55,100 @@ test('result.all shows both `stdout` and `stderr` intermixed', async t => { }); test('stdout/stderr available on errors', async t => { - const err = await t.throws(m('exit', ['2'])); + const err = await t.throwsAsync(execa('exit', ['2']), {message: getExitRegExp('2')}); t.is(typeof err.stdout, 'string'); t.is(typeof err.stderr, 'string'); }); test('include stdout and stderr in errors for improved debugging', async t => { - const err = await t.throws(m('fixtures/error-message.js')); - t.regex(err.message, /stdout/); - t.regex(err.message, /stderr/); - t.is(err.code, 1); + await t.throwsAsync(execa('fixtures/error-message.js'), {message: STDERR_STDOUT_REGEXP, code: 1}); }); test('do not include in errors when `stdio` is set to `inherit`', async t => { - const err = await t.throws(m('fixtures/error-message.js', {stdio: 'inherit'})); - t.notRegex(err.message, /\n/); + await t.throwsAsync(execa('fixtures/error-message.js', {stdio: 'inherit'}), {message: NO_NEWLINES_REGEXP}); }); test('do not include `stderr` and `stdout` in errors when set to `inherit`', async t => { - const err = await t.throws(m('fixtures/error-message.js', {stdout: 'inherit', stderr: 'inherit'})); - t.notRegex(err.message, /\n/); + await t.throwsAsync(execa('fixtures/error-message.js', {stdout: 'inherit', stderr: 'inherit'}), {message: NO_NEWLINES_REGEXP}); }); test('do not include `stderr` and `stdout` in errors when `stdio` is set to `inherit`', async t => { - const err = await t.throws(m('fixtures/error-message.js', {stdio: [null, 'inherit', 'inherit']})); - t.notRegex(err.message, /\n/); + await t.throwsAsync(execa('fixtures/error-message.js', {stdio: [null, 'inherit', 'inherit']}), {message: NO_NEWLINES_REGEXP}); }); test('do not include `stdout` in errors when set to `inherit`', async t => { - const err = await t.throws(m('fixtures/error-message.js', {stdout: 'inherit'})); + const err = await t.throwsAsync(execa('fixtures/error-message.js', {stdout: 'inherit'}), {message: /stderr/}); t.notRegex(err.message, /stdout/); - t.regex(err.message, /stderr/); }); test('do not include `stderr` in errors when set to `inherit`', async t => { - const err = await t.throws(m('fixtures/error-message.js', {stderr: 'inherit'})); - t.regex(err.message, /stdout/); + const err = await t.throwsAsync(execa('fixtures/error-message.js', {stderr: 'inherit'}), {message: /stdout/}); t.notRegex(err.message, /stderr/); }); test('pass `stdout` to a file descriptor', async t => { const file = tempfile('.txt'); - await m('fixtures/noop', ['foo bar'], {stdout: fs.openSync(file, 'w')}); + await execa('fixtures/noop', ['foo bar'], {stdout: fs.openSync(file, 'w')}); t.is(fs.readFileSync(file, 'utf8'), 'foo bar\n'); }); test('pass `stderr` to a file descriptor', async t => { const file = tempfile('.txt'); - await m('fixtures/noop-err', ['foo bar'], {stderr: fs.openSync(file, 'w')}); + await execa('fixtures/noop-err', ['foo bar'], {stderr: fs.openSync(file, 'w')}); t.is(fs.readFileSync(file, 'utf8'), 'foo bar\n'); }); test('execa.shell()', async t => { - const {stdout} = await m.shell('node fixtures/noop foo'); + const {stdout} = await execa.shell('node fixtures/noop foo'); t.is(stdout, 'foo'); }); test('execa.sync()', t => { - const {stdout} = m.sync('noop', ['foo']); + const {stdout} = execa.sync('noop', ['foo']); t.is(stdout, 'foo'); }); 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(() => execa.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); + t.throws(() => execa.sync('node', ['fixtures/error-message.js']), {message: STDERR_STDOUT_REGEXP, code: 1}); }); test('skip throwing when using reject option in execa.sync()', t => { - const err = m.sync('node', ['fixtures/error-message.js'], {reject: false}); + const err = execa.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'); + const {stdout} = execa.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); + t.throws(() => execa.shellSync('node fixtures/error-message.js'), {message: STDERR_STDOUT_REGEXP, code: 1}); }); test('skip throwing when using reject option in execa.shellSync()', t => { - const err = m.shellSync('node fixtures/error-message.js', {reject: false}); + const err = execa.shellSync('node fixtures/error-message.js', {reject: false}); t.is(typeof err.stdout, 'string'); t.is(typeof err.stderr, 'string'); }); test('stripEof option (legacy)', async t => { - const {stdout} = await m('noop', ['foo'], {stripEof: false}); + const {stdout} = await execa('noop', ['foo'], {stripEof: false}); t.is(stdout, 'foo\n'); }); test('stripFinalNewline option', async t => { - const {stdout} = await m('noop', ['foo'], {stripFinalNewline: false}); + const {stdout} = await execa('noop', ['foo'], {stripFinalNewline: false}); t.is(stdout, 'foo\n'); }); test.serial('preferLocal option', async t => { - t.true((await m('cat-names')).stdout.length > 2); + t.true((await execa('cat-names')).stdout.length > 2); if (process.platform === 'win32') { // TODO: figure out how to make the below not hang on Windows @@ -167,7 +158,7 @@ test.serial('preferLocal option', async t => { // Account for npm adding local binaries to the PATH const _path = process.env.PATH; process.env.PATH = ''; - await t.throws(m('cat-names', {preferLocal: false}), /spawn .* ENOENT/); + await t.throwsAsync(execa('cat-names', {preferLocal: false}), /spawn .* ENOENT/); process.env.PATH = _path; }); @@ -175,20 +166,20 @@ test.serial('localDir option', async t => { const cwd = 'fixtures/local-dir'; const bin = path.resolve(cwd, 'node_modules/.bin/self-path'); - await m('npm', ['install', '--no-package-lock'], {cwd}); + await execa('npm', ['install', '--no-package-lock'], {cwd}); - const {stdout} = await m(bin, {localDir: cwd}); + const {stdout} = await execa(bin, {localDir: cwd}); t.is(path.relative(cwd, stdout), path.normalize('node_modules/self-path')); }); test('input option can be a String', async t => { - const {stdout} = await m('stdin', {input: 'foobar'}); + const {stdout} = await execa('stdin', {input: 'foobar'}); t.is(stdout, 'foobar'); }); test('input option can be a Buffer', async t => { - const {stdout} = await m('stdin', {input: 'testing12'}); + const {stdout} = await execa('stdin', {input: 'testing12'}); t.is(stdout, 'testing12'); }); @@ -196,28 +187,28 @@ test('input can be a Stream', async t => { const s = new stream.PassThrough(); s.write('howdy'); s.end(); - const {stdout} = await m('stdin', {input: s}); + const {stdout} = await execa('stdin', {input: s}); t.is(stdout, 'howdy'); }); test('you can write to child.stdin', async t => { - const child = m('stdin'); + const child = execa('stdin'); child.stdin.end('unicorns'); t.is((await child).stdout, 'unicorns'); }); test('input option can be a String - sync', t => { - const {stdout} = m.sync('stdin', {input: 'foobar'}); + const {stdout} = execa.sync('stdin', {input: 'foobar'}); t.is(stdout, 'foobar'); }); test('input option can be a Buffer - sync', t => { - const {stdout} = m.sync('stdin', {input: Buffer.from('testing12', 'utf8')}); + const {stdout} = execa.sync('stdin', {input: Buffer.from('testing12', 'utf8')}); t.is(stdout, 'testing12'); }); test('opts.stdout:ignore - stdout will not collect data', async t => { - const {stdout} = await m('stdin', { + const {stdout} = await execa('stdin', { input: 'hello', stdio: [null, 'ignore', null] }); @@ -226,29 +217,29 @@ test('opts.stdout:ignore - stdout will not collect data', async t => { test('helpful error trying to provide an input stream in sync mode', t => { t.throws( - () => m.sync('stdin', {input: new stream.PassThrough()}), + () => execa.sync('stdin', {input: new stream.PassThrough()}), /The `input` option cannot be a stream in sync mode/ ); }); test('execa() returns a promise with kill() and pid', t => { - const promise = m('noop', ['foo']); + const promise = execa('noop', ['foo']); t.is(typeof promise.kill, 'function'); t.is(typeof promise.pid, 'number'); }); test('maxBuffer affects stdout', async t => { - await t.throws(m('max-buffer', ['stdout', '11'], {maxBuffer: 10}), /stdout maxBuffer exceeded/); - await t.notThrows(m('max-buffer', ['stdout', '10'], {maxBuffer: 10})); + await t.throwsAsync(execa('max-buffer', ['stdout', '11'], {maxBuffer: 10}), /stdout maxBuffer exceeded/); + await t.notThrowsAsync(execa('max-buffer', ['stdout', '10'], {maxBuffer: 10})); }); test('maxBuffer affects stderr', async t => { - await t.throws(m('max-buffer', ['stderr', '13'], {maxBuffer: 12}), /stderr maxBuffer exceeded/); - await t.notThrows(m('max-buffer', ['stderr', '12'], {maxBuffer: 12})); + await t.throwsAsync(execa('max-buffer', ['stderr', '13'], {maxBuffer: 12}), /stderr maxBuffer exceeded/); + await t.notThrowsAsync(execa('max-buffer', ['stderr', '12'], {maxBuffer: 12})); }); test('do not buffer stdout when `buffer` set to `false`', async t => { - const promise = m('max-buffer', ['stdout', '10'], {buffer: false}); + const promise = execa('max-buffer', ['stdout', '10'], {buffer: false}); const [result, stdout] = await Promise.all([ promise, getStream(promise.stdout), @@ -260,7 +251,7 @@ test('do not buffer stdout when `buffer` set to `false`', async t => { }); test('do not buffer stderr when `buffer` set to `false`', async t => { - const promise = m('max-buffer', ['stderr', '10'], {buffer: false}); + const promise = execa('max-buffer', ['stderr', '10'], {buffer: false}); const [result, stderr] = await Promise.all([ promise, getStream(promise.stderr), @@ -272,53 +263,58 @@ test('do not buffer stderr when `buffer` set to `false`', async t => { }); test('skip throwing when using reject option', async t => { - const error = await m('exit', ['2'], {reject: false}); + const error = await execa('exit', ['2'], {reject: false}); t.is(typeof error.stdout, 'string'); t.is(typeof error.stderr, 'string'); }); +test('allow unknown exit code', async t => { + await t.throwsAsync(execa('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.throws(m('exit', ['2'])); + const {code, failed} = await execa('noop', ['foo']); + const error = await t.throwsAsync(execa('exit', ['2']), {code: 2, message: getExitRegExp('2')}); t.is(code, 0); t.false(failed); - t.is(error.code, 2); t.true(error.failed); }); test('use relative path with \'..\' chars', async t => { const pathViaParentDir = path.join('..', path.basename(__dirname), 'fixtures', 'noop'); - const {stdout} = await m(pathViaParentDir, ['foo']); + const {stdout} = await execa(pathViaParentDir, ['foo']); t.is(stdout, 'foo'); }); if (process.platform !== 'win32') { test('execa() rejects if running non-executable', async t => { - const cp = m('non-executable'); - await t.throws(cp); + const cp = execa('non-executable'); + await t.throwsAsync(cp); }); } test('error.killed is true if process was killed directly', async t => { - const cp = m('forever'); + const cp = execa('forever'); setTimeout(() => { cp.kill(); }, 100); - const error = await t.throws(cp); + const error = await t.throwsAsync(cp, {message: /was killed with SIGTERM/}); t.true(error.killed); }); // TODO: Should this really be the case, or should we improve on child_process? test('error.killed is false if process was killed indirectly', async t => { - const cp = m('forever'); + const cp = execa('forever'); setTimeout(() => { process.kill(cp.pid, 'SIGINT'); }, 100); - const error = await t.throws(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); }); @@ -338,45 +334,44 @@ if (process.platform === 'darwin') { if (process.platform !== 'win32') { test('error.signal is SIGINT', async t => { - const cp = m('forever'); + const cp = execa('forever'); setTimeout(() => { process.kill(cp.pid, 'SIGINT'); }, 100); - const error = await t.throws(cp); + const error = await t.throwsAsync(cp, {message: /was killed with SIGINT/}); t.is(error.signal, 'SIGINT'); }); test('error.signal is SIGTERM', async t => { - const cp = m('forever'); + const cp = execa('forever'); setTimeout(() => { process.kill(cp.pid, 'SIGTERM'); }, 100); - const error = await t.throws(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.throws(m('delay', ['3000', '0'], {killSignal: 'SIGHUP', timeout: 1500})); + const error = await t.throwsAsync(execa('delay', ['3000', '0'], {killSignal: 'SIGHUP', timeout: 1500, message: TIMEOUT_REGEXP})); t.is(error.signal, 'SIGHUP'); }); } test('result.signal is null for successful execution', async t => { - t.is((await m('noop')).signal, null); + t.is((await execa('noop')).signal, null); }); test('result.signal is null if process failed, but was not killed', async t => { - const error = await t.throws(m('exit', [2])); + const error = await t.throwsAsync(execa('exit', [2]), {message: getExitRegExp('2')}); t.is(error.signal, null); }); async function code(t, num) { - const error = await t.throws(m('exit', [`${num}`])); - t.is(error.code, num); + await t.throwsAsync(execa('exit', [`${num}`]), {code: num, message: getExitRegExp(num)}); } test('error.code is 2', code, 2); @@ -384,44 +379,41 @@ 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.throws(m('delay', ['60000', '0'], {timeout: 1500})); + const error = await t.throwsAsync(execa('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.throws(m('delay', ['3000', '22'], {timeout: 30000})); - + const error = await t.throwsAsync(execa('delay', ['3000', '22'], {timeout: 30000}), {code: 22, message: getExitRegExp('22')}); t.false(error.timedOut); - t.is(error.code, 22); }); test('timedOut will be false if no timeout was set and zero exit code', async t => { - const result = await m('delay', ['1000', '0']); + const result = await execa('delay', ['1000', '0']); t.false(result.timedOut); }); test('timedOut will be false if no timeout was set and non-zero exit code', async t => { - const error = await t.throws(m('delay', ['1000', '3'])); + const error = await t.throwsAsync(execa('delay', ['1000', '3']), {message: getExitRegExp('3')}); t.false(error.timedOut); }); async function errorMessage(t, expected, ...args) { - const error = await t.throws(m('exit', args)); - t.regex(error.message, expected); + await t.throwsAsync(execa('exit', args), {message: expected}); } 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.throws(m('fail', args)); + const error = await t.throwsAsync(execa('fail', args)); t.is(error.cmd, `fail${expected}`); - const result = await m('noop', args); + const result = await execa('noop', args); t.is(result.cmd, `noop${expected}`); } @@ -433,7 +425,7 @@ test(cmd, ''); async function spawnAndKill(t, signal, cleanup) { const name = cleanup ? 'sub-process' : 'sub-process-false'; - const cp = m(name); + const cp = execa(name); let pid; cp.stdout.setEncoding('utf8'); @@ -446,7 +438,7 @@ async function spawnAndKill(t, signal, cleanup) { }, 100); }); - await t.throws(cp); + await t.throwsAsync(cp); // Give everybody some time to breath and kill things await delay(200); @@ -464,10 +456,8 @@ if (process.platform !== 'win32') { test('cleanup false - SIGKILL', spawnAndKill, 'SIGKILL', false); } -// See: https://github.com/sindresorhus/execa/issues/56 -const onlyWinFailing = test[process.platform === 'win32' ? 'failing' : 'serial']; -onlyWinFailing('execa.shell() supports the `shell` option', async t => { - const {stdout} = await m.shell('noop foo', { +test('execa.shell() supports the `shell` option', async t => { + const {stdout} = await execa.shell('node fixtures/noop foo', { shell: process.platform === 'win32' ? 'cmd.exe' : '/bin/bash' }); t.is(stdout, 'foo'); @@ -478,16 +468,16 @@ if (process.platform !== 'win32') { // Try-catch here is necessary, because this test is not 100% accurate // Sometimes process can manage to accept input before exiting try { - await m(`fast-exit-${process.platform}`, [], {input: 'data'}); + await execa(`fast-exit-${process.platform}`, [], {input: 'data'}); t.pass(); } catch (error) { - t.is(error.code, 'EPIPE'); + t.is(error.code, 32); } }); } test('use environment variables by default', async t => { - const result = await m.stdout('environment'); + const result = await execa.stdout('environment'); t.deepEqual(result.split('\n'), [ 'foo', @@ -496,7 +486,7 @@ test('use environment variables by default', async t => { }); test('extend environment variables by default', async t => { - const result = await m.stdout('environment', [], {env: {BAR: 'bar'}}); + const result = await execa.stdout('environment', [], {env: {BAR: 'bar'}}); t.deepEqual(result.split('\n'), [ 'foo', @@ -504,8 +494,8 @@ test('extend environment variables by default', async t => { ]); }); -test('do not extend environment with `extendEnv` option', async t => { - const result = await m.stdout('environment', [], {env: {BAR: 'bar', PATH: process.env.PATH}, extendEnv: false}); +test('do not extend environment with `extendEnv: false`', async t => { + const result = await execa.stdout('environment', [], {env: {BAR: 'bar', PATH: process.env.PATH}, extendEnv: false}); t.deepEqual(result.split('\n'), [ 'undefined', @@ -513,8 +503,16 @@ test('do not extend environment with `extendEnv` option', async t => { ]); }); +test('use extend environment with `extendEnv: true` and `shell: true`', async t => { + process.env.TEST = 'test'; + const command = process.platform === 'win32' ? 'echo %TEST%' : 'echo $TEST'; + const stdout = await execa.stdout(command, {shell: true, env: {}, extendEnv: true}); + t.is(stdout, 'test'); + delete process.env.TEST; +}); + test('do not buffer when streaming', async t => { - const result = await getStream(m('max-buffer', ['stdout', '21'], {maxBuffer: 10}).stdout); + const result = await getStream(execa('max-buffer', ['stdout', '21'], {maxBuffer: 10}).stdout); t.is(result, '....................\n'); }); @@ -522,7 +520,7 @@ test('do not buffer when streaming', async t => { test('detach child process', async t => { const file = tempfile('.txt'); - await m('detach', [file]); + await execa('detach', [file]); await delay(5000); @@ -534,7 +532,7 @@ test('removes exit handler on exit', async t => { // FIXME: This relies on `signal-exit` internals const ee = process.__signal_exit_emitter__; - const child = m('noop'); + const child = execa('noop'); const listener = ee.listeners('exit').pop(); await new Promise((resolve, reject) => { @@ -545,3 +543,39 @@ test('removes exit handler on exit', async t => { const included = ee.listeners('exit').includes(listener); t.false(included); }); + +// TOOD: Remove the `if`-guard when targeting Node.js 10 +if (Promise.prototype.finally) { + test('finally function is executed on success', async t => { + let called = false; + const {stdout} = await execa('noop', ['foo']).finally(() => { + called = true; + }); + t.is(called, true); + t.is(stdout, 'foo'); + }); + + test('finally function is executed on failure', async t => { + let called = false; + const err = await t.throwsAsync(execa('exit', ['2']).finally(() => { + called = true; + })); + t.is(called, true); + t.is(typeof err.stdout, 'string'); + t.is(typeof err.stderr, 'string'); + }); + + test('throw in finally function bubbles up on success', async t => { + const result = await t.throwsAsync(execa('noop', ['foo']).finally(() => { + throw new Error('called'); + })); + t.is(result.message, 'called'); + }); + + test('throw in finally bubbles up on error', async t => { + const result = await t.throwsAsync(execa('exit', ['2']).finally(() => { + throw new Error('called'); + })); + t.is(result.message, 'called'); + }); +} diff --git a/test/errname.js b/test/errname.js index 97dafb3d8..d47720d99 100644 --- a/test/errname.js +++ b/test/errname.js @@ -1,7 +1,7 @@ import test from 'ava'; import errname from '../lib/errname'; -const isWin = process.platform === 'win32'; +const isWindows = process.platform === 'win32'; // Simulates failure to capture `process.binding('uv');` const fallback = code => errname.__test__(null, code); @@ -22,5 +22,5 @@ function makeTests(name, m, expected) { const unknown = 'Unknown system error -2'; -makeTests('native', errname, isWin ? unknown : 'ENOENT'); +makeTests('native', errname, isWindows ? unknown : 'ENOENT'); makeTests('fallback', fallback, unknown); From e35dcdfb6d6572019132d4f695f1b1e76d81f36f Mon Sep 17 00:00:00 2001 From: Tommaso Sotte Date: Fri, 8 Mar 2019 11:21:13 +0100 Subject: [PATCH 06/17] Fixed trailing space --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index c9f845d72..0182039a1 100644 --- a/index.js +++ b/index.js @@ -336,7 +336,7 @@ module.exports = (command, args, options) => { handleInput(spawned, parsed.options.input); spawned.all = makeAllStream(spawned); - + // eslint-disable-next-line promise/prefer-await-to-then spawned.then = (onFulfilled, onRejected) => handlePromise().then(onFulfilled, onRejected); spawned.catch = onRejected => handlePromise().catch(onRejected); From 7f430d94382bc9e7134e003d33a4d80992fc800b Mon Sep 17 00:00:00 2001 From: Tommaso Sotte Date: Sat, 9 Mar 2019 13:43:19 +0100 Subject: [PATCH 07/17] Using test.serial for testing result.all with lower timeout for noop-132 (1000ms) Testing with test.serial and a delay of only 1000ms seems to pass multiple test runs consistently. Let's see if it passes on CI testing as well. --- fixtures/noop-132 | 2 +- test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fixtures/noop-132 b/fixtures/noop-132 index 62abca26f..2e9bf482c 100755 --- a/fixtures/noop-132 +++ b/fixtures/noop-132 @@ -13,4 +13,4 @@ process.stderr.write('3'); setTimeout(() => { process.stdout.write('2'); -}, 5000); +}, 1000); diff --git a/test.js b/test.js index 4a9e40540..a09934882 100644 --- a/test.js +++ b/test.js @@ -46,7 +46,7 @@ test('execa.stderr()', async t => { t.is(stderr, 'foo'); }); -test('result.all shows both `stdout` and `stderr` intermixed', async t => { +test.serial('result.all shows both `stdout` and `stderr` intermixed', async t => { const result = await execa('noop-132'); // Due to the async nature of process.stdout/stderr on POSIX, this test // is very unpredictable, although it should interleave the streams From b500215f6c1edc83863a261ccdbd40289f7d3433 Mon Sep 17 00:00:00 2001 From: Tommaso Sotte Date: Sat, 9 Mar 2019 15:26:38 +0100 Subject: [PATCH 08/17] Added execa.all(), similar to execa.stdout() for result.all; added test --- index.js | 5 +++++ test.js | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/index.js b/index.js index 0182039a1..7f7a090c2 100644 --- a/index.js +++ b/index.js @@ -361,6 +361,11 @@ module.exports.stderr = async (...args) => { return stderr; }; +module.exports.all = async (...args) => { + const {all} = await module.exports(...args); + return all; +}; + module.exports.shell = (command, options) => handleShell(module.exports, command, options); module.exports.sync = (command, args, options) => { diff --git a/test.js b/test.js index a09934882..c8c961d5c 100644 --- a/test.js +++ b/test.js @@ -46,6 +46,11 @@ test('execa.stderr()', async t => { t.is(stderr, 'foo'); }); +test.serial('execa.all()', async t => { + const all = await execa.all('noop-132'); + t.is(all, '132'); +}); + test.serial('result.all shows both `stdout` and `stderr` intermixed', async t => { const result = await execa('noop-132'); // Due to the async nature of process.stdout/stderr on POSIX, this test From a419b4ae5d07d854ce5e88e26743e7accb863a26 Mon Sep 17 00:00:00 2001 From: Tommaso Sotte Date: Sat, 9 Mar 2019 15:37:05 +0100 Subject: [PATCH 09/17] Fixed link to Node.js docs about process I/O stream --- test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test.js b/test.js index c8c961d5c..79a0a3e9d 100644 --- a/test.js +++ b/test.js @@ -54,8 +54,8 @@ test.serial('execa.all()', async t => { test.serial('result.all shows both `stdout` and `stderr` intermixed', async t => { const result = await execa('noop-132'); // Due to the async nature of process.stdout/stderr on POSIX, this test - // is very unpredictable, although it should interleave the streams - // https://nodejs.org/api/process.html#process_a_note_on_process_i_os + // is very unpredictable, although it should interleave the streams. + // https://nodejs.org/api/process.html#process_a_note_on_process_i_o t.is(result.all, '132'); }); From bc1450adc32aa7198fe04fef886bbba48689935e Mon Sep 17 00:00:00 2001 From: Tommaso Sotte Date: Sat, 9 Mar 2019 17:34:24 +0100 Subject: [PATCH 10/17] all available on errors, if available in result (ex. no execa.sync()) --- index.js | 5 ++++- test.js | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 7f7a090c2..46d211b0f 100644 --- a/index.js +++ b/index.js @@ -163,6 +163,10 @@ function makeError(result, options) { error.cmd = joinedCommand; error.timedOut = Boolean(timedOut); + if ('all' in result) { + error.all = result.all; + } + return error; } @@ -292,7 +296,6 @@ module.exports = (command, args, options) => { getStream(spawned, 'stdout', {encoding, buffer, maxBuffer}), getStream(spawned, 'stderr', {encoding, buffer, maxBuffer}), getStream(spawned, 'all', {encoding, buffer, maxBuffer: maxBuffer * 2}) - ]).then(results => { // eslint-disable-line promise/prefer-await-to-then const result = results[0]; result.stdout = results[1]; diff --git a/test.js b/test.js index 79a0a3e9d..07680e1e9 100644 --- a/test.js +++ b/test.js @@ -59,10 +59,11 @@ test.serial('result.all shows both `stdout` and `stderr` intermixed', async t => t.is(result.all, '132'); }); -test('stdout/stderr available on errors', async t => { +test('stdout/stderr/all available on errors', async t => { const err = await t.throwsAsync(execa('exit', ['2']), {message: getExitRegExp('2')}); t.is(typeof err.stdout, 'string'); t.is(typeof err.stderr, 'string'); + t.is(typeof err.all, 'string'); }); test('include stdout and stderr in errors for improved debugging', async t => { From 29f3f12eb46220f6853daeb44f56566296c1f72d Mon Sep 17 00:00:00 2001 From: Tommaso Sotte Date: Sat, 9 Mar 2019 17:53:01 +0100 Subject: [PATCH 11/17] Added all interleaved stream to readme in Why, example and API --- readme.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index bbbb3df7e..099427655 100644 --- a/readme.md +++ b/readme.md @@ -12,6 +12,7 @@ - Higher max buffer. 10 MB instead of 200 KB. - [Executes locally installed binaries by name.](#preferlocal) - [Cleans up spawned processes when the parent process dies.](#cleanup) +- [Adds `all` interleaved output](#execafile-arguments-options) from `stdout` and `stderr`, similar to what the terminal sees. [(async only)](#execasyncfile-arguments-options) ## Install @@ -66,6 +67,7 @@ const execa = require('execa'); cmd: '/bin/sh -c exit 3', stdout: '', stderr: '', + all: '', timedOut: false } */ @@ -100,7 +102,9 @@ Execute a file. Think of this as a mix of `child_process.execFile` and `child_process.spawn`. -Returns a [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess), which is enhanced to also be a `Promise` for a result `Object` with `stdout`, `stderr` and `all` (interleaved `stdout` and `stderr` stream) properties. +Returns a [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess) which is enhanced to be a `Promise`. +It exposes an additional `all` stream, with both `stdout` and `stderr` outputs combined. +The promise result is an `Object` with `stdout`, `stderr` and `all` properties. ### execa.stdout(file, [arguments], [options]) @@ -110,6 +114,10 @@ Same as `execa()`, but returns only `stdout`. Same as `execa()`, but returns only `stderr`. +### execa.all(file, [arguments], [options]) + +Same as `execa()`, but returns only `all`. + ### execa.shell(command, [options]) Execute a command through the system shell. Prefer `execa()` whenever possible, as it's both faster and safer. @@ -122,7 +130,7 @@ The `child_process` instance is enhanced to also be promise for a result object Execute a file synchronously. -Returns the same result object as [`child_process.spawnSync`](https://nodejs.org/api/child_process.html#child_process_child_process_spawnsync_command_args_options). It's not possible to obtain a true interleaved `all` stream as `execa` does, because `child_process.spawnSync` does not expose `stdout` and `stderr` until termination. +Returns the same result object as [`child_process.spawnSync`](https://nodejs.org/api/child_process.html#child_process_child_process_spawnsync_command_args_options). It's not possible to obtain a true interleaved `all` stream as `execa()` does, because `child_process.spawnSync` does not expose `stdout` and `stderr` to be combined until termination. This method throws an `Error` if the command fails. From 43a43bd35af054fb3e5d25ec6bea96887660a10d Mon Sep 17 00:00:00 2001 From: Tommaso Sotte Date: Sun, 10 Mar 2019 15:40:34 +0100 Subject: [PATCH 12/17] Removed unnecessary comments on noop-132 and test.js --- fixtures/noop-132 | 7 ------- test.js | 3 --- 2 files changed, 10 deletions(-) diff --git a/fixtures/noop-132 b/fixtures/noop-132 index 2e9bf482c..277e817db 100755 --- a/fixtures/noop-132 +++ b/fixtures/noop-132 @@ -1,12 +1,5 @@ #!/usr/bin/env node 'use strict'; -// Interleaving the output of stdout and stderr is unpredictable and can't be -// expressed as a test. What I've tried so far to make it predictable: -// - process.stdout._handle.setBlocking(true) -// - process.stdout.once('drain', () => process.stderr.write('2')); -// - process.stdout.write('1', () => process.stderr.write('2')); -// - await delay() between calls; it works but with varying ms, based on load -// - console.log/error process.stdout.write('1'); process.stderr.write('3'); diff --git a/test.js b/test.js index 07680e1e9..3434097c3 100644 --- a/test.js +++ b/test.js @@ -53,9 +53,6 @@ test.serial('execa.all()', async t => { test.serial('result.all shows both `stdout` and `stderr` intermixed', async t => { const result = await execa('noop-132'); - // Due to the async nature of process.stdout/stderr on POSIX, this test - // is very unpredictable, although it should interleave the streams. - // https://nodejs.org/api/process.html#process_a_note_on_process_i_o t.is(result.all, '132'); }); From dc6849249de56bbe570701b3265368212a843198 Mon Sep 17 00:00:00 2001 From: Tommaso Sotte Date: Sun, 10 Mar 2019 15:45:00 +0100 Subject: [PATCH 13/17] Fixed and improved readme about all interleaved stream --- readme.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index 099427655..4d1ee58d6 100644 --- a/readme.md +++ b/readme.md @@ -103,7 +103,7 @@ Execute a file. Think of this as a mix of `child_process.execFile` and `child_process.spawn`. Returns a [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess) which is enhanced to be a `Promise`. -It exposes an additional `all` stream, with both `stdout` and `stderr` outputs combined. +It exposes an additional `all` stream, with both `stdout` and `stderr` outputs interleaved. The promise result is an `Object` with `stdout`, `stderr` and `all` properties. ### execa.stdout(file, [arguments], [options]) @@ -128,9 +128,10 @@ The `child_process` instance is enhanced to also be promise for a result object ### execa.sync(file, [arguments], [options]) -Execute a file synchronously. +Execute a file synchronously. -Returns the same result object as [`child_process.spawnSync`](https://nodejs.org/api/child_process.html#child_process_child_process_spawnsync_command_args_options). It's not possible to obtain a true interleaved `all` stream as `execa()` does, because `child_process.spawnSync` does not expose `stdout` and `stderr` to be combined until termination. +Returns the same result object as [`child_process.spawnSync`](https://nodejs.org/api/child_process.html#child_process_child_process_spawnsync_command_args_options). +It does not have the `.all` property that `execa()` has because the underlying synchronous implementation only returns `stdout` and `stderr` at the end of the execution, so they cannot be interleaved. This method throws an `Error` if the command fails. From 360bdb427801984ae5d4273fb1e7a687ea1345ee Mon Sep 17 00:00:00 2001 From: Tommaso Sotte Date: Sun, 10 Mar 2019 15:46:46 +0100 Subject: [PATCH 14/17] Removed execa.all() --- index.js | 5 ----- readme.md | 4 ---- test.js | 5 ----- 3 files changed, 14 deletions(-) diff --git a/index.js b/index.js index 46d211b0f..0c4ba452b 100644 --- a/index.js +++ b/index.js @@ -364,11 +364,6 @@ module.exports.stderr = async (...args) => { return stderr; }; -module.exports.all = async (...args) => { - const {all} = await module.exports(...args); - return all; -}; - module.exports.shell = (command, options) => handleShell(module.exports, command, options); module.exports.sync = (command, args, options) => { diff --git a/readme.md b/readme.md index 4d1ee58d6..1d3300b78 100644 --- a/readme.md +++ b/readme.md @@ -114,10 +114,6 @@ Same as `execa()`, but returns only `stdout`. Same as `execa()`, but returns only `stderr`. -### execa.all(file, [arguments], [options]) - -Same as `execa()`, but returns only `all`. - ### execa.shell(command, [options]) Execute a command through the system shell. Prefer `execa()` whenever possible, as it's both faster and safer. diff --git a/test.js b/test.js index 3434097c3..26f420d14 100644 --- a/test.js +++ b/test.js @@ -46,11 +46,6 @@ test('execa.stderr()', async t => { t.is(stderr, 'foo'); }); -test.serial('execa.all()', async t => { - const all = await execa.all('noop-132'); - t.is(all, '132'); -}); - test.serial('result.all shows both `stdout` and `stderr` intermixed', async t => { const result = await execa('noop-132'); t.is(result.all, '132'); From 06fbfff96d96a08199caacf4f8700cd0c5d724fb Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Sun, 10 Mar 2019 23:46:05 +0700 Subject: [PATCH 15/17] Update readme.md --- readme.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 1d3300b78..94fb970a6 100644 --- a/readme.md +++ b/readme.md @@ -103,7 +103,9 @@ Execute a file. Think of this as a mix of `child_process.execFile` and `child_process.spawn`. Returns a [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess) which is enhanced to be a `Promise`. -It exposes an additional `all` stream, with both `stdout` and `stderr` outputs interleaved. + +It exposes an additional `.all` stream, with `stdout` and `stderr` interleaved. + The promise result is an `Object` with `stdout`, `stderr` and `all` properties. ### execa.stdout(file, [arguments], [options]) @@ -127,7 +129,8 @@ The `child_process` instance is enhanced to also be promise for a result object Execute a file synchronously. Returns the same result object as [`child_process.spawnSync`](https://nodejs.org/api/child_process.html#child_process_child_process_spawnsync_command_args_options). -It does not have the `.all` property that `execa()` has because the underlying synchronous implementation only returns `stdout` and `stderr` at the end of the execution, so they cannot be interleaved. + +It does not have the `.all` property that `execa()` has because the [underlying synchronous implementation](https://nodejs.org/api/child_process.html#child_process_child_process_execfilesync_file_args_options) only returns `stdout` and `stderr` at the end of the execution, so they cannot be interleaved. This method throws an `Error` if the command fails. From e12846454b51df81160fc3c757b2ca06214b9d2e Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Sun, 10 Mar 2019 23:48:40 +0700 Subject: [PATCH 16/17] Update readme.md --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 94fb970a6..c47dff533 100644 --- a/readme.md +++ b/readme.md @@ -12,7 +12,7 @@ - Higher max buffer. 10 MB instead of 200 KB. - [Executes locally installed binaries by name.](#preferlocal) - [Cleans up spawned processes when the parent process dies.](#cleanup) -- [Adds `all` interleaved output](#execafile-arguments-options) from `stdout` and `stderr`, similar to what the terminal sees. [(async only)](#execasyncfile-arguments-options) +- [Adds an `.all` property](#execafile-arguments-options) with interleaved output from `stdout` and `stderr`, similar to what the terminal sees. [*(Async only)*](#execasyncfile-arguments-options) ## Install From b815a79f0b78d517e9afacf221b6671ba50ce7b6 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Sun, 10 Mar 2019 23:50:39 +0700 Subject: [PATCH 17/17] Update readme.md --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 5b48b10ed..c6d9d83ca 100644 --- a/readme.md +++ b/readme.md @@ -66,7 +66,7 @@ const execa = require('execa'); exitCodeName: 'ESRCH', stdout: '', stderr: '', - all: '', + all: '', failed: true, signal: null, cmd: 'exit 3',