diff --git a/test/index.js b/test/index.js index 8359894..160c33b 100644 --- a/test/index.js +++ b/test/index.js @@ -18,6 +18,8 @@ const FIXTURES_URL = new URL('fixtures/', import.meta.url); const fixturesPath = fileURLToPath(FIXTURES_URL); const testFixtureUrl = new URL('test.txt', FIXTURES_URL); +const nodeDirectory = path.dirname(process.execPath); + // TODO: replace with Array.fromAsync() after dropping support for Node <22.0.0 const arrayFromAsync = async asyncIterable => { const chunks = []; @@ -30,14 +32,18 @@ const arrayFromAsync = async asyncIterable => { const testString = 'test'; const secondTestString = 'secondTest'; +const thirdTestString = 'thirdTest'; +const fourthTestString = 'fourthTest'; const testUpperCase = testString.toUpperCase(); const testDoubleUpperCase = `${testUpperCase}${testUpperCase}`; const getPipeSize = command => command.split(' | ').length; const nodeHanging = ['node']; +const [nodeHangingCommand] = nodeHanging; const nodePrint = bodyString => ['node', ['-p', bodyString]]; const nodeEval = bodyString => ['node', ['-e', bodyString]]; +const nodeEvalCommandStart = 'node -e'; const nodePrintStdout = nodeEval(`console.log("${testString}")`); const nodePrintStderr = nodeEval(`console.error("${testString}")`); const nodePrintBoth = nodeEval(`console.log("${testString}"); @@ -81,44 +87,131 @@ const nodeDoubleFail = nodeEval(`process.stdin.on("data", chunk => { process.exit(2); });`); const localBinary = ['ava', ['--version']]; +const localBinaryCommand = localBinary.flat().join(' '); +const [localBinaryCommandStart] = localBinary; const nonExistentCommand = 'non-existent-command'; -const commandEvalFailStart = 'node -e'; -const messageEvalFailStart = `Command failed: ${commandEvalFailStart}`; -const messageExitEvalFailStart = `Command failed with exit code 2: ${commandEvalFailStart}`; +const assertDurationMs = (t, durationMs) => { + t.true(Number.isFinite(durationMs)); + t.true(durationMs > 0); +}; -const VERSION_REGEXP = /^\d+\.\d+\.\d+$/; +const assertNonExistent = (t, {exitCode, signalName, command, message, stderr, cause, durationMs}, commandStart = nonExistentCommand, expectedCommand = commandStart) => { + t.is(exitCode, undefined); + t.is(signalName, undefined); + t.is(command, expectedCommand); + t.is(message, `Command failed: ${expectedCommand}`); + t.is(stderr, ''); + t.true(cause.message.includes(commandStart)); + t.is(cause.code, 'ENOENT'); + t.is(cause.syscall, `spawn ${commandStart}`); + t.is(cause.path, commandStart); + assertDurationMs(t, durationMs); +}; -test('Can pass options.argv0', async t => { - const {stdout} = await nanoSpawn(...nodePrintArgv0, {argv0: testString}); - t.is(stdout, testString); -}); +const assertWindowsNonExistent = (t, {exitCode, signalName, command, message, stderr, cause, durationMs}, expectedCommand = nonExistentCommand) => { + t.is(exitCode, 1); + t.is(signalName, undefined); + t.is(command, expectedCommand); + t.is(message, `Command failed with exit code 1: ${expectedCommand}`); + t.true(stderr.includes('not recognized as an internal or external command')); + t.is(cause, undefined); + assertDurationMs(t, durationMs); +}; -test('Can pass options.argv0, shell', async t => { - const {stdout} = await nanoSpawn(...nodePrintArgv0, {argv0: testString, shell: true}); - t.is(stdout, process.execPath); -}); +const assertUnixNonExistentShell = (t, {exitCode, signalName, command, message, stderr, cause, durationMs}, expectedCommand = nonExistentCommand) => { + t.is(exitCode, 127); + t.is(signalName, undefined); + t.is(command, expectedCommand); + t.is(message, `Command failed with exit code 127: ${expectedCommand}`); + t.true(stderr.includes('not found')); + t.is(cause, undefined); + assertDurationMs(t, durationMs); +}; -test('Can pass options.stdin', async t => { - const promise = nanoSpawn(...nodePrintStdout, {stdin: 'ignore'}); - const {stdin} = await promise.nodeChildProcess; - t.is(stdin, null); - await promise; -}); +const assertUnixNotFound = (t, {exitCode, signalName, command, message, stderr, cause, durationMs}, expectedCommand = nonExistentCommand) => { + t.is(exitCode, 127); + t.is(signalName, undefined); + t.is(command, expectedCommand); + t.is(message, `Command failed with exit code 127: ${expectedCommand}`); + t.true(stderr.includes('No such file or directory')); + t.is(cause, undefined); + assertDurationMs(t, durationMs); +}; -test('Can pass options.stdout', async t => { - const promise = nanoSpawn(...nodePrintStdout, {stdout: 'ignore'}); - const {stdout} = await promise.nodeChildProcess; - t.is(stdout, null); - await promise; -}); +const assertFail = (t, {exitCode, signalName, command, message, cause, durationMs}, commandStart = nodeEvalCommandStart) => { + t.is(exitCode, 2); + t.is(signalName, undefined); + t.true(command.startsWith(commandStart)); + t.true(message.startsWith(`Command failed with exit code 2: ${commandStart}`)); + t.is(cause, undefined); + assertDurationMs(t, durationMs); +}; -test('Can pass options.stderr', async t => { - const promise = nanoSpawn(...nodePrintStdout, {stderr: 'ignore'}); - const {stderr} = await promise.nodeChildProcess; - t.is(stderr, null); +const assertSigterm = (t, {exitCode, signalName, command, message, stderr, cause, durationMs}, expectedCommand = nodeHangingCommand) => { + t.is(exitCode, undefined); + t.is(signalName, 'SIGTERM'); + t.is(command, expectedCommand); + t.is(message, `Command was terminated with SIGTERM: ${expectedCommand}`); + t.is(stderr, ''); + t.is(cause, undefined); + assertDurationMs(t, durationMs); +}; + +const earlyErrorOptions = {detached: 'true'}; + +const assertEarlyError = (t, {exitCode, signalName, command, message, stderr, cause, durationMs}, commandStart = nodeEvalCommandStart) => { + t.is(exitCode, undefined); + t.is(signalName, undefined); + t.true(command.startsWith(commandStart)); + t.true(message.startsWith(`Command failed: ${commandStart}`)); + t.is(stderr, ''); + t.true(cause.message.includes('options.detached')); + t.false(cause.message.includes('Command')); + assertDurationMs(t, durationMs); +}; + +const assertAbortError = (t, {exitCode, signalName, command, stderr, message, cause, durationMs}, expectedCause, expectedCommand = nodeHangingCommand) => { + t.is(exitCode, undefined); + t.is(signalName, undefined); + t.is(command, expectedCommand); + t.is(message, `Command failed: ${expectedCommand}`); + t.is(stderr, ''); + t.is(cause.message, 'The operation was aborted'); + t.is(cause.cause, expectedCause); + assertDurationMs(t, durationMs); +}; + +const assertErrorEvent = (t, {exitCode, signalName, command, message, stderr, cause, durationMs}, expectedCause, commandStart = nodeEvalCommandStart) => { + t.is(exitCode, undefined); + t.is(signalName, undefined); + t.true(command.startsWith(commandStart)); + t.true(message.startsWith(`Command failed: ${commandStart}`)); + t.is(stderr, ''); + t.is(cause, expectedCause); + assertDurationMs(t, durationMs); +}; + +const VERSION_REGEXP = /^\d+\.\d+\.\d+$/; + +const testArgv0 = async (t, shell) => { + const {stdout} = await nanoSpawn(...nodePrintArgv0, {argv0: testString, shell}); + t.is(stdout, shell ? process.execPath : testString); +}; + +test('Can pass options.argv0', testArgv0, false); +test('Can pass options.argv0, shell', testArgv0, true); + +const testStdOption = async (t, optionName) => { + const promise = nanoSpawn(...nodePrintStdout, {[optionName]: 'ignore'}); + const subprocess = await promise.nodeChildProcess; + t.is(subprocess[optionName], null); await promise; -}); +}; + +test('Can pass options.stdin', testStdOption, 'stdin'); +test('Can pass options.stdout', testStdOption, 'stdout'); +test('Can pass options.stderr', testStdOption, 'stderr'); test('Can pass options.stdio array', async t => { const promise = nanoSpawn(...nodePrintStdout, {stdio: ['ignore', 'pipe', 'pipe', 'pipe']}); @@ -167,18 +260,12 @@ test('options.stdio[0] can be {string: string}', async t => { test.serial('options.env augments process.env', async t => { process.env.ONE = 'one'; process.env.TWO = 'two'; - const {stdout} = await nanoSpawn('node', ['-p', 'process.env.ONE + process.env.TWO'], {env: {TWO: testString}}); + const {stdout} = await nanoSpawn(...nodePrint('process.env.ONE + process.env.TWO'), {env: {TWO: testString}}); t.is(stdout, `${process.env.ONE}${testString}`); delete process.env.ONE; delete process.env.TWO; }); -test('Can pass options object without any arguments', async t => { - const {exitCode, signalName} = await t.throwsAsync(nanoSpawn(...nodeHanging, {timeout: 1})); - t.is(exitCode, undefined); - t.is(signalName, 'SIGTERM'); -}); - test('result.exitCode|signalName on success', async t => { const {exitCode, signalName} = await nanoSpawn(...nodePrintStdout); t.is(exitCode, undefined); @@ -186,113 +273,55 @@ test('result.exitCode|signalName on success', async t => { }); test('Error on non-0 exit code', async t => { - const {exitCode, signalName, message, cause} = await t.throwsAsync(nanoSpawn(...nodeEval('process.exit(2)'))); - t.is(exitCode, 2); - t.is(signalName, undefined); - t.is(message, 'Command failed with exit code 2: node -e \'process.exit(2)\''); - t.is(cause, undefined); + const error = await t.throwsAsync(nanoSpawn(...nodeEval('process.exit(2)'))); + assertFail(t, error); }); test('Error on signal termination', async t => { - const {exitCode, signalName, message, cause} = await t.throwsAsync(nanoSpawn(...nodeHanging, {timeout: 1})); - t.is(exitCode, undefined); - t.is(signalName, 'SIGTERM'); - t.is(message, 'Command was terminated with SIGTERM: node'); - t.is(cause, undefined); + const error = await t.throwsAsync(nanoSpawn(...nodeHanging, {timeout: 1})); + assertSigterm(t, error); }); test('Error on invalid child_process options', async t => { - const {exitCode, signalName, message, cause} = await t.throwsAsync(nanoSpawn(...nodePrintStdout, {detached: 'true'})); - t.is(exitCode, undefined); - t.is(signalName, undefined); - t.true(message.startsWith(messageEvalFailStart)); - t.true(cause.message.includes('options.detached')); - t.false(cause.message.includes('Command')); + const error = await t.throwsAsync(nanoSpawn(...nodePrintStdout, earlyErrorOptions)); + assertEarlyError(t, error); }); test('Error on "error" event before spawn', async t => { - const {stderr, cause} = await t.throwsAsync(nanoSpawn(nonExistentCommand)); + const error = await t.throwsAsync(nanoSpawn(nonExistentCommand)); if (isWindows) { - t.true(stderr.includes('not recognized as an internal or external command')); + assertWindowsNonExistent(t, error); } else { - t.is(cause.code, 'ENOENT'); + assertNonExistent(t, error); } }); test('Error on "error" event during spawn', async t => { - const error = new Error(testString); - const {exitCode, signalName, message, cause} = await t.throwsAsync(nanoSpawn(...nodeHanging, {signal: AbortSignal.abort(error)})); - t.is(exitCode, undefined); - t.is(signalName, 'SIGTERM'); - t.is(message, 'Command was terminated with SIGTERM: node'); - t.is(cause, undefined); + const error = await t.throwsAsync(nanoSpawn(...nodeHanging, {signal: AbortSignal.abort()})); + assertSigterm(t, error); }); test('Error on "error" event during spawn, with iteration', async t => { - const error = new Error(testString); - const promise = nanoSpawn(...nodeHanging, {signal: AbortSignal.abort(error)}); - const {exitCode, signalName, message, cause} = await t.throwsAsync(arrayFromAsync(promise.stdout)); - t.is(exitCode, undefined); - t.is(signalName, 'SIGTERM'); - t.is(message, 'Command was terminated with SIGTERM: node'); - t.is(cause, undefined); + const promise = nanoSpawn(...nodeHanging, {signal: AbortSignal.abort()}); + const error = await t.throwsAsync(arrayFromAsync(promise.stdout)); + assertSigterm(t, error); }); // The `signal` option sends `SIGTERM`. // Whether the subprocess is terminated before or after an `error` event is emitted depends on the speed of the OS syscall. if (isLinux) { test('Error on "error" event after spawn', async t => { - const error = new Error(testString); + const cause = new Error(testString); const controller = new AbortController(); const promise = nanoSpawn(...nodeHanging, {signal: controller.signal}); await promise.nodeChildProcess; - controller.abort(error); - const {exitCode, signalName, message, cause} = await t.throwsAsync(promise); - t.is(exitCode, undefined); - t.is(signalName, undefined); - t.is(message, 'Command failed: node'); - t.is(cause.message, 'The operation was aborted'); - t.is(cause.cause, error); + controller.abort(cause); + const error = await t.throwsAsync(promise); + assertAbortError(t, error, cause); }); } -test('Error on stdin', async t => { - const error = new Error(testString); - const promise = nanoSpawn(...nodePrintStdout); - const subprocess = await promise.nodeChildProcess; - subprocess.stdin.destroy(error); - const {exitCode, signalName, message, cause} = await t.throwsAsync(promise); - t.is(exitCode, undefined); - t.is(signalName, undefined); - t.true(message.startsWith(messageEvalFailStart)); - t.is(cause, error); -}); - -test('Error on stdout', async t => { - const error = new Error(testString); - const promise = nanoSpawn(...nodePrintStderr); - const subprocess = await promise.nodeChildProcess; - subprocess.stdout.destroy(error); - const {exitCode, signalName, message, cause} = await t.throwsAsync(promise); - t.is(exitCode, undefined); - t.is(signalName, undefined); - t.true(message.startsWith(messageEvalFailStart)); - t.is(cause, error); -}); - -test('Error on stderr', async t => { - const error = new Error(testString); - const promise = nanoSpawn(...nodePrintStdout); - const subprocess = await promise.nodeChildProcess; - subprocess.stderr.destroy(error); - const {exitCode, signalName, message, cause} = await t.throwsAsync(promise); - t.is(exitCode, undefined); - t.is(signalName, undefined); - t.true(message.startsWith(messageEvalFailStart)); - t.is(cause, error); -}); - test('promise.stdout can be iterated', async t => { const promise = nanoSpawn(...nodePrintStdout); const lines = await arrayFromAsync(promise.stdout); @@ -312,13 +341,13 @@ test('promise.stderr can be iterated', async t => { }); test('promise[Symbol.asyncIterator] can be iterated', async t => { - const promise = nanoSpawn(...nodeEval(`console.log("a"); -console.log("b"); -console.error("c"); -console.error("d");`)); + const promise = nanoSpawn(...nodeEval(`console.log("${testString}"); +console.log("${secondTestString}"); +console.error("${thirdTestString}"); +console.error("${fourthTestString}");`)); const lines = await arrayFromAsync(promise); - t.deepEqual(lines, ['a', 'b', 'c', 'd']); + t.deepEqual(lines, [testString, secondTestString, thirdTestString, fourthTestString]); const {stdout, stderr, output} = await promise; t.is(stdout, ''); @@ -332,14 +361,14 @@ test.serial('promise iteration can be interleaved', async t => { import {setTimeout} from 'node:timers/promises'; for (let index = 0; index < ${length}; index += 1) { - console.log("a"); + console.log("${testString}"); await setTimeout(10); - console.error("b"); + console.error("${secondTestString}"); await setTimeout(10); }`]); const lines = await arrayFromAsync(promise); - t.deepEqual(lines, Array.from({length}, () => ['a', 'b']).flat()); + t.deepEqual(lines, Array.from({length}, () => [testString, secondTestString]).flat()); const {stdout, stderr, output} = await promise; t.is(stdout, ''); @@ -369,33 +398,33 @@ test('result.output is set', async t => { }); test('error.stdout is set', async t => { - const {exitCode, stdout, stderr, output} = await t.throwsAsync(nanoSpawn(...nodeEval(`console.log("${testString}"); + const error = await t.throwsAsync(nanoSpawn(...nodeEval(`console.log("${testString}"); process.exit(2);`))); - t.is(exitCode, 2); - t.is(stdout, testString); - t.is(stderr, ''); - t.is(output, stdout); + assertFail(t, error); + t.is(error.stdout, testString); + t.is(error.stderr, ''); + t.is(error.output, error.stdout); }); test('error.stderr is set', async t => { - const {exitCode, stdout, stderr, output} = await t.throwsAsync(nanoSpawn(...nodeEval(`console.error("${testString}"); + const error = await t.throwsAsync(nanoSpawn(...nodeEval(`console.error("${testString}"); process.exit(2);`))); - t.is(exitCode, 2); - t.is(stdout, ''); - t.is(stderr, testString); - t.is(output, stderr); + assertFail(t, error); + t.is(error.stdout, ''); + t.is(error.stderr, testString); + t.is(error.output, error.stderr); }); test('error.output is set', async t => { - const {exitCode, stdout, stderr, output} = await t.throwsAsync(nanoSpawn(...nodeEval(`console.log("${testString}"); + const error = await t.throwsAsync(nanoSpawn(...nodeEval(`console.log("${testString}"); setTimeout(() => { console.error("${secondTestString}"); process.exit(2); }, 0);`))); - t.is(exitCode, 2); - t.is(stdout, testString); - t.is(stderr, secondTestString); - t.is(output, `${stdout}\n${stderr}`); + assertFail(t, error); + t.is(error.stdout, testString); + t.is(error.stderr, secondTestString); + t.is(error.output, `${error.stdout}\n${error.stderr}`); }); test('promise.stdout has no iterations if options.stdout "ignore"', async t => { @@ -482,77 +511,43 @@ process.stderr.write("c\\nd\\n");`]); t.deepEqual(lines, ['a', 'b', 'c', 'd']); }); -test('promise.stdout handles no newline at the end', async t => { - const promise = nanoSpawn(...nodePrintNoNewline('a\nb')); - const lines = await arrayFromAsync(promise.stdout); - t.deepEqual(lines, ['a', 'b']); -}); - -test('result.stdout handles no newline at the end', async t => { - const {stdout, output} = await nanoSpawn(...nodePrintNoNewline('a\nb')); - t.is(stdout, 'a\nb'); - t.is(output, stdout); -}); - -test('promise.stdout handles newline at the end', async t => { - const promise = nanoSpawn(...nodePrintNoNewline('a\nb\n')); - const lines = await arrayFromAsync(promise.stdout); - t.deepEqual(lines, ['a', 'b']); -}); - -test('result.stdout handles newline at the end', async t => { - const {stdout, output} = await nanoSpawn(...nodePrintNoNewline('a\nb\n')); - t.is(stdout, 'a\nb'); - t.is(output, stdout); -}); - -test('promise.stdout handles newline at the beginning', async t => { - const promise = nanoSpawn(...nodePrintNoNewline('\na\nb')); - const lines = await arrayFromAsync(promise.stdout); - t.deepEqual(lines, ['', 'a', 'b']); -}); - -test('result.stdout handles newline at the beginning', async t => { - const {stdout, output} = await nanoSpawn(...nodePrintNoNewline('\na\nb')); - t.is(stdout, '\na\nb'); - t.is(output, stdout); -}); - -test('promise.stdout handles 2 newlines at the end', async t => { - const promise = nanoSpawn(...nodePrintNoNewline('a\nb\n\n')); - const lines = await arrayFromAsync(promise.stdout); - t.deepEqual(lines, ['a', 'b', '']); -}); - -test('result.stdout handles 2 newlines at the end', async t => { - const {stdout, output} = await nanoSpawn(...nodePrintNoNewline('a\nb\n\n')); - t.is(stdout, 'a\nb\n'); - t.is(output, stdout); -}); - -test('promise.stdout handles Windows newlines', async t => { - const promise = nanoSpawn(...nodePrintNoNewline('a\r\nb')); - const lines = await arrayFromAsync(promise.stdout); - t.deepEqual(lines, ['a', 'b']); -}); - -test('result.stdout handles Windows newlines', async t => { - const {stdout, output} = await nanoSpawn(...nodePrintNoNewline('a\r\nb')); - t.is(stdout, 'a\r\nb'); +const testNewline = async (t, input, expectedOutput) => { + const {stdout, output} = await nanoSpawn(...nodePrintNoNewline(input)); + t.is(stdout, expectedOutput); t.is(output, stdout); -}); +}; -test('promise.stdout handles Windows newline at the end', async t => { - const promise = nanoSpawn(...nodePrintNoNewline('a\r\nb\r\n')); +test('result.stdout handles newline at the beginning', testNewline, '\na\nb', '\na\nb'); +test('result.stdout handles newline in the middle', testNewline, 'a\nb', 'a\nb'); +test('result.stdout handles newline at the end', testNewline, 'a\nb\n', 'a\nb'); +test('result.stdout handles Windows newline at the beginning', testNewline, '\r\na\r\nb', '\r\na\r\nb'); +test('result.stdout handles Windows newline in the middle', testNewline, 'a\r\nb', 'a\r\nb'); +test('result.stdout handles Windows newline at the end', testNewline, 'a\r\nb\r\n', 'a\r\nb'); +test('result.stdout handles 2 newlines at the beginning', testNewline, '\n\na\nb', '\n\na\nb'); +test('result.stdout handles 2 newlines in the middle', testNewline, 'a\n\nb', 'a\n\nb'); +test('result.stdout handles 2 newlines at the end', testNewline, 'a\nb\n\n', 'a\nb\n'); +test('result.stdout handles 2 Windows newlines at the beginning', testNewline, '\r\n\r\na\r\nb', '\r\n\r\na\r\nb'); +test('result.stdout handles 2 Windows newlines in the middle', testNewline, 'a\r\n\r\nb', 'a\r\n\r\nb'); +test('result.stdout handles 2 Windows newlines at the end', testNewline, 'a\r\nb\r\n\r\n', 'a\r\nb\r\n'); + +const testNewlineIteration = async (t, input, expectedLines) => { + const promise = nanoSpawn(...nodePrintNoNewline(input)); const lines = await arrayFromAsync(promise.stdout); - t.deepEqual(lines, ['a', 'b']); -}); + t.deepEqual(lines, expectedLines); +}; -test('result.stdout handles Windows newline at the end', async t => { - const {stdout, output} = await nanoSpawn(...nodePrintNoNewline('a\r\nb\r\n')); - t.is(stdout, 'a\r\nb'); - t.is(output, stdout); -}); +test('promise.stdout handles newline at the beginning', testNewlineIteration, '\na\nb', ['', 'a', 'b']); +test('promise.stdout handles newline in the middle', testNewlineIteration, 'a\nb', ['a', 'b']); +test('promise.stdout handles newline at the end', testNewlineIteration, 'a\nb\n', ['a', 'b']); +test('promise.stdout handles Windows newline at the beginning', testNewlineIteration, '\r\na\r\nb', ['', 'a', 'b']); +test('promise.stdout handles Windows newline in the middle', testNewlineIteration, 'a\r\nb', ['a', 'b']); +test('promise.stdout handles Windows newline at the end', testNewlineIteration, 'a\r\nb\r\n', ['a', 'b']); +test('promise.stdout handles 2 newlines at the beginning', testNewlineIteration, '\n\na\nb', ['', '', 'a', 'b']); +test('promise.stdout handles 2 newlines in the middle', testNewlineIteration, 'a\n\nb', ['a', '', 'b']); +test('promise.stdout handles 2 newlines at the end', testNewlineIteration, 'a\nb\n\n', ['a', 'b', '']); +test('promise.stdout handles 2 Windows newlines at the beginning', testNewlineIteration, '\r\n\r\na\r\nb', ['', '', 'a', 'b']); +test('promise.stdout handles 2 Windows newlines in the middle', testNewlineIteration, 'a\r\n\r\nb', ['a', '', 'b']); +test('promise.stdout handles 2 Windows newlines at the end', testNewlineIteration, 'a\r\nb\r\n\r\n', ['a', 'b', '']); const multibyteString = '.\u{1F984}.'; const multibyteUint8Array = new TextEncoder().encode(multibyteString); @@ -584,293 +579,126 @@ test.serial('result.stdout works with multibyte sequences', async t => { t.is(output, stdout); }); -const destroyStdout = async ({nodeChildProcess}, error) => { - const {stdout} = await nodeChildProcess; - stdout.destroy(error); +const destroySubprocessStream = async ({nodeChildProcess}, error, streamName) => { + const subprocess = await nodeChildProcess; + subprocess[streamName].destroy(error); }; -const destroyStderr = async ({nodeChildProcess}, error) => { - const {stderr} = await nodeChildProcess; - stderr.destroy(error); -}; - -test('Handles promise.stdout error', async t => { +const testStreamError = async (t, streamName) => { const promise = nanoSpawn(...nodePrintStdout); - const error = new Error(testString); - destroyStdout(promise, error); - const {cause} = await t.throwsAsync(arrayFromAsync(promise.stdout)); - t.is(cause, error); - const {stdout, output} = await t.throwsAsync(promise); - t.is(stdout, ''); - t.is(output, ''); -}); + const cause = new Error(testString); + destroySubprocessStream(promise, cause, streamName); + const error = await t.throwsAsync(promise); + assertErrorEvent(t, error, cause); +}; -test('Handles promise.stderr error', async t => { - const promise = nanoSpawn(...nodePrintStderr); - const error = new Error(testString); - destroyStderr(promise, error); - const {cause} = await t.throwsAsync(arrayFromAsync(promise.stderr)); - t.is(cause, error); - const {stderr, output} = await t.throwsAsync(promise); - t.is(stderr, ''); - t.is(output, ''); -}); +test('Handles subprocess.stdin error', testStreamError, 'stdin'); +test('Handles subprocess.stdout error', testStreamError, 'stdout'); +test('Handles subprocess.stderr error', testStreamError, 'stderr'); -test('Handles promise.stdout error in promise[Symbol.asyncIterator]', async t => { +const testStreamIterateError = async (t, streamName) => { const promise = nanoSpawn(...nodePrintStdout); - const error = new Error(testString); - destroyStdout(promise, error); - const {cause} = await t.throwsAsync(arrayFromAsync(promise)); - t.is(cause, error); - const {stdout, output} = await t.throwsAsync(promise); - t.is(stdout, ''); - t.is(output, ''); -}); + const cause = new Error(testString); + destroySubprocessStream(promise, cause, streamName); + const error = await t.throwsAsync(arrayFromAsync(promise[streamName])); + assertErrorEvent(t, error, cause); + const promiseError = await t.throwsAsync(promise); + assertErrorEvent(t, promiseError, cause); + t.is(promiseError[streamName], ''); + t.is(promiseError.output, ''); +}; -test('Handles promise.stderr error in promise[Symbol.asyncIterator]', async t => { - const promise = nanoSpawn(...nodePrintStderr); - const error = new Error(testString); - destroyStderr(promise, error); - const {cause} = await t.throwsAsync(arrayFromAsync(promise)); - t.is(cause, error); - const {stderr, output} = await t.throwsAsync(promise); - t.is(stderr, ''); - t.is(output, ''); -}); +test('Handles promise.stdout error', testStreamIterateError, 'stdout'); +test('Handles promise.stderr error', testStreamIterateError, 'stderr'); -test('Handles result.stdout error', async t => { +const testStreamIterateAllError = async (t, streamName) => { const promise = nanoSpawn(...nodePrintStdout); - const error = new Error(testString); - destroyStdout(promise, error); - const {cause} = await t.throwsAsync(promise); - t.is(cause, error); -}); + const cause = new Error(testString); + destroySubprocessStream(promise, cause, streamName); + const error = await t.throwsAsync(arrayFromAsync(promise)); + assertErrorEvent(t, error, cause); + const promiseError = await t.throwsAsync(promise); + assertErrorEvent(t, promiseError, cause); + t.is(promiseError[streamName], ''); + t.is(promiseError.output, ''); +}; -test('Handles result.stderr error', async t => { - const promise = nanoSpawn(...nodePrintStdout); - const error = new Error(testString); - destroyStderr(promise, error); - const {cause} = await t.throwsAsync(promise); - t.is(cause, error); -}); +test('Handles promise.stdout error in promise[Symbol.asyncIterator]', testStreamIterateAllError, 'stdout'); +test('Handles promise.stderr error in promise[Symbol.asyncIterator]', testStreamIterateAllError, 'stderr'); -test.serial('promise.stdout iteration break waits for the subprocess success', async t => { - const promise = nanoSpawn(...nodePassThroughPrint); - let done = false; +// eslint-disable-next-line max-params +const iterateOnOutput = async (t, promise, state, cause, shouldThrow, promiseType) => { + const iterable = promiseType === '' ? promise : promise[promiseType]; // eslint-disable-next-line no-unreachable-loop - for await (const line of promise.stdout) { + for await (const line of iterable) { t.is(line, testString); - globalThis.setTimeout(async () => { - const {stdin, stdout} = await promise.nodeChildProcess; - t.true(stdout.readable); - t.true(stdin.writable); - stdin.end(secondTestString); - done = true; - }, 1e2); - break; - } - - t.true(done); - const {stdout, output} = await promise; - t.is(stdout, ''); - t.is(output, ''); -}); -test.serial('promise[Symbol.asyncIterator] iteration break waits for the subprocess success', async t => { - const promise = nanoSpawn(...nodePassThroughPrint); - let done = false; - - // eslint-disable-next-line no-unreachable-loop - for await (const line of promise) { - t.is(line, testString); globalThis.setTimeout(async () => { const {stdin, stdout} = await promise.nodeChildProcess; t.true(stdout.readable); t.true(stdin.writable); stdin.end(secondTestString); - done = true; + state.done = true; }, 1e2); - break; - } - t.true(done); - const {stdout, output} = await promise; - t.is(stdout, ''); - t.is(output, ''); -}); - -test.serial('promise.stdout iteration exception waits for the subprocess success', async t => { - const promise = nanoSpawn(...nodePassThroughPrint); - let done = false; - - const cause = new Error(testString); - try { - // eslint-disable-next-line no-unreachable-loop - for await (const line of promise.stdout) { - t.is(line, testString); - globalThis.setTimeout(async () => { - const {stdin, stdout} = await promise.nodeChildProcess; - t.true(stdout.readable); - t.true(stdin.writable); - stdin.end(secondTestString); - done = true; - }, 1e2); + if (shouldThrow) { throw cause; + } else { + break; } - } catch (error) { - t.is(error, cause); } +}; - t.true(done); - const {stdout, output} = await promise; - t.is(stdout, ''); - t.is(output, ''); -}); - -test.serial('promise[Symbol.asyncIterator] iteration exception waits for the subprocess success', async t => { +const testIteration = async (t, shouldThrow, promiseType) => { const promise = nanoSpawn(...nodePassThroughPrint); - let done = false; - + const state = {done: false}; const cause = new Error(testString); + try { - // eslint-disable-next-line no-unreachable-loop - for await (const line of promise) { - t.is(line, testString); - globalThis.setTimeout(async () => { - const {stdin, stdout} = await promise.nodeChildProcess; - t.true(stdout.readable); - t.true(stdin.writable); - stdin.end(secondTestString); - done = true; - }, 1e2); - throw cause; - } + await iterateOnOutput(t, promise, state, cause, shouldThrow, promiseType); } catch (error) { t.is(error, cause); } - t.true(done); + t.true(state.done); + const {stdout, output} = await promise; t.is(stdout, ''); t.is(output, ''); -}); - -test.serial('promise.stdout iteration break waits for the subprocess failure', async t => { - const promise = nanoSpawn(...nodePassThroughPrintFail); - let done = false; - - let cause; - try { - // eslint-disable-next-line no-unreachable-loop - for await (const line of promise.stdout) { - t.is(line, testString); - globalThis.setTimeout(async () => { - const {stdin, stdout} = await promise.nodeChildProcess; - t.true(stdout.readable); - t.true(stdin.writable); - stdin.end(secondTestString); - done = true; - }, 1e2); - break; - } - } catch (error) { - cause = error; - } - - t.true(done); - const error = await t.throwsAsync(promise); - t.is(error, cause); - t.is(error.stdout, ''); - t.is(error.output, ''); -}); - -test.serial('promise[Symbol.asyncIterator] iteration break waits for the subprocess failure', async t => { - const promise = nanoSpawn(...nodePassThroughPrintFail); - let done = false; - - let cause; - try { - // eslint-disable-next-line no-unreachable-loop - for await (const line of promise) { - t.is(line, testString); - globalThis.setTimeout(async () => { - const {stdin, stdout} = await promise.nodeChildProcess; - t.true(stdout.readable); - t.true(stdin.writable); - stdin.end(secondTestString); - done = true; - }, 1e2); - break; - } - } catch (error) { - cause = error; - } +}; - t.true(done); - const error = await t.throwsAsync(promise); - t.is(error, cause); - t.is(error.stdout, ''); - t.is(error.output, ''); -}); +test.serial('promise.stdout iteration break waits for the subprocess success', testIteration, false, 'stdout'); +test.serial('promise[Symbol.asyncIterator] iteration break waits for the subprocess success', testIteration, false, ''); +test.serial('promise.stdout iteration exception waits for the subprocess success', testIteration, true, 'stdout'); +test.serial('promise[Symbol.asyncIterator] iteration exception waits for the subprocess success', testIteration, true, ''); -test.serial('promise.stdout iteration exception waits for the subprocess failure', async t => { +const testIterationFail = async (t, shouldThrow, promiseType) => { const promise = nanoSpawn(...nodePassThroughPrintFail); - let done = false; - + const state = {done: false}; const cause = new Error(testString); + let caughtError; + try { - // eslint-disable-next-line no-unreachable-loop - for await (const line of promise.stdout) { - t.is(line, testString); - globalThis.setTimeout(async () => { - const {stdin, stdout} = await promise.nodeChildProcess; - t.true(stdout.readable); - t.true(stdin.writable); - stdin.end(secondTestString); - done = true; - }, 1e2); - throw cause; - } + await iterateOnOutput(t, promise, state, cause, shouldThrow, promiseType); } catch (error) { - t.is(error, cause); + t.is(error === cause, shouldThrow); + caughtError = error; } - t.true(done); - const error = await t.throwsAsync(promise); - t.not(error, cause); - t.is(error.stdout, ''); - t.is(error.output, ''); -}); + t.true(state.done); -test.serial('promise[Symbol.asyncIterator] iteration exception waits for the subprocess failure', async t => { - const promise = nanoSpawn(...nodePassThroughPrintFail); - let done = false; + const promiseError = await t.throwsAsync(promise); + assertFail(t, promiseError); + t.is(promiseError === caughtError, !shouldThrow); + t.is(promiseError.stdout, ''); + t.is(promiseError.output, ''); +}; - const cause = new Error(testString); - try { - // eslint-disable-next-line no-unreachable-loop - for await (const line of promise) { - t.is(line, testString); - globalThis.setTimeout(async () => { - const {stdin, stdout} = await promise.nodeChildProcess; - t.true(stdout.readable); - t.true(stdin.writable); - stdin.end(secondTestString); - done = true; - }, 1e2); - throw cause; - } - } catch (error) { - t.is(error, cause); - } - - t.true(done); - const error = await t.throwsAsync(promise); - t.not(error, cause); - t.is(error.stdout, ''); - t.is(error.output, ''); -}); +test.serial('promise.stdout iteration break waits for the subprocess failure', testIterationFail, false, 'stdout'); +test.serial('promise[Symbol.asyncIterator] iteration break waits for the subprocess failure', testIterationFail, false, ''); +test.serial('promise.stdout iteration exception waits for the subprocess failure', testIterationFail, true, 'stdout'); +test.serial('promise[Symbol.asyncIterator] iteration exception waits for the subprocess failure', testIterationFail, true, ''); test('Returns a promise', async t => { const promise = nanoSpawn(...nodePrintStdout); @@ -885,54 +713,42 @@ test('promise.nodeChildProcess is set', async t => { const nodeChildProcess = await promise.nodeChildProcess; nodeChildProcess.kill(); - const {signalName} = await t.throwsAsync(promise); - t.is(signalName, 'SIGTERM'); + const error = await t.throwsAsync(promise); + assertSigterm(t, error); }); -test('result.command is defined', async t => { +test('result.command does not quote normal arguments', async t => { const {command} = await nanoSpawn('node', ['--version']); t.is(command, 'node --version'); }); -test('result.command quotes spaces', async t => { - const {command, stdout} = await nanoSpawn(...nodePrint('". ."')); - t.is(command, 'node -p \'". ."\''); - t.is(stdout, '. .'); -}); - -test('result.command quotes single quotes', async t => { - const {command, stdout} = await nanoSpawn(...nodePrint('"\'"')); - t.is(command, 'node -p \'"\'\\\'\'"\''); - t.is(stdout, '\''); -}); - -test('result.command quotes unusual characters', async t => { - const {command, stdout} = await nanoSpawn(...nodePrint('","')); - t.is(command, 'node -p \'","\''); - t.is(stdout, ','); -}); +const testCommandEscaping = async (t, input, expectedCommand) => { + const {command, stdout} = await nanoSpawn(...nodePrint(`"${input}"`)); + t.is(command, `node -p '"${expectedCommand}"'`); + t.is(stdout, input); +}; -test('result.command strips ANSI sequences', async t => { - const {command, stdout} = await nanoSpawn(...nodePrint(`"${red(testString)}"`)); - t.is(command, `node -p '"${testString}"'`); - t.is(stdout, red(testString)); -}); +test('result.command quotes spaces', testCommandEscaping, '. .', '. .'); +test('result.command quotes single quotes', testCommandEscaping, '\'', '\'\\\'\''); +test('result.command quotes unusual characters', testCommandEscaping, ',', ','); +test('result.command strips ANSI sequences', testCommandEscaping, red(testString), testString); test('result.durationMs is set', async t => { const {durationMs} = await nanoSpawn(...nodePrintStdout); - t.true(Number.isFinite(durationMs)); - t.true(durationMs > 0); + assertDurationMs(t, durationMs); }); test('error.durationMs is set', async t => { const {durationMs} = await t.throwsAsync(nanoSpawn('node', ['--unknown'])); - t.true(Number.isFinite(durationMs)); - t.true(durationMs > 0); + assertDurationMs(t, durationMs); }); if (isWindows) { + test('Current OS uses node.exe', t => { + t.true(process.execPath.endsWith('\\node.exe')); + }); + const testExe = async (t, shell) => { - t.is(path.extname(process.execPath), '.exe'); const {stdout} = await nanoSpawn(process.execPath, ['--version'], {shell}); t.is(stdout, process.version); }; @@ -951,41 +767,24 @@ if (isWindows) { t.is(stdout, process.execPath); }); - test('.exe detection with explicit file extension', async t => { - const {stdout} = await nanoSpawn(process.execPath, ['-p', 'process.argv0'], {argv0: testString}); - t.is(stdout, testString); - }); - - test('.exe detection with explicit file extension, case insensitive', async t => { - const {stdout} = await nanoSpawn(process.execPath.toUpperCase(), ['-p', 'process.argv0'], {argv0: testString}); + const testExeDetection = async (t, execPath) => { + const {stdout} = await nanoSpawn(execPath, ['-p', 'process.argv0'], {argv0: testString}); t.is(stdout, testString); - }); - - test('.exe detection with file paths without file extension', async t => { - const {stdout} = await nanoSpawn(process.execPath.replace('.exe', ''), ['-p', 'process.argv0'], {argv0: testString}); - t.is(stdout, testString); - }); + }; - test('.exe detection with Unix slashes', async t => { - t.true(process.execPath.endsWith('\\node.exe')); - const {stdout} = await nanoSpawn(process.execPath.replace('\\node.exe', '/node.exe'), ['-p', 'process.argv0'], {argv0: testString}); - t.is(stdout, testString); - }); + test('.exe detection with explicit file extension', testExeDetection, process.execPath); + test('.exe detection with explicit file extension, case insensitive', testExeDetection, process.execPath.toUpperCase()); + test('.exe detection with file paths without file extension', testExeDetection, process.execPath.replace('.exe', '')); + test('.exe detection with Unix slashes', testExeDetection, process.execPath.replace('\\node.exe', '/node.exe')); - test('.exe detection with custom Path', async t => { - const {stdout} = await nanoSpawn(...nodePrintArgv0, {argv0: testString, env: {[pathKey()]: path.dirname(process.execPath)}}); + const testPathValue = async (t, pathValue) => { + const {stdout} = await nanoSpawn(...nodePrintArgv0, {argv0: testString, env: {[pathKey()]: pathValue}}); t.is(stdout, testString); - }); + }; - test('.exe detection with custom Path and leading ;', async t => { - const {stdout} = await nanoSpawn(...nodePrintArgv0, {argv0: testString, env: {[pathKey()]: `;${path.dirname(process.execPath)}`}}); - t.is(stdout, testString); - }); - - test('.exe detection with custom Path and double quoting', async t => { - const {stdout} = await nanoSpawn(...nodePrintArgv0, {argv0: testString, env: {[pathKey()]: `"${path.dirname(process.execPath)}"`}}); - t.is(stdout, testString); - }); + test('.exe detection with custom Path', testPathValue, nodeDirectory); + test('.exe detection with custom Path and leading ;', testPathValue, `;${nodeDirectory}`); + test('.exe detection with custom Path and double quoting', testPathValue, `"${nodeDirectory}"`); const testCom = async (t, shell) => { const {stdout} = await nanoSpawn('tree.com', [fileURLToPath(FIXTURES_URL), '/f'], {shell}); @@ -1022,13 +821,12 @@ if (isWindows) { }); const testPathExtension = async (t, shell) => { - const {exitCode, stderr} = await t.throwsAsync(nanoSpawn('spawnecho', [testString], { + const error = await t.throwsAsync(nanoSpawn('spawnecho', [testString], { env: {PATHEXT: '.COM'}, cwd: FIXTURES_URL, shell, })); - t.is(exitCode, 1); - t.true(stderr.includes('not recognized as an internal or external command')); + assertWindowsNonExistent(t, error, `spawnecho ${testString}`); }; test('Can set PATHEXT', testPathExtension, undefined); @@ -1093,12 +891,8 @@ if (isWindows) { test('Escapes argument when setting shell option, "(foo|bar>baz|foz)"', testEscape, '"(foo|bar>baz|foz)"'); test('Cannot run shebangs', async t => { - const {message, exitCode, signalName, stderr, cause} = await t.throwsAsync(nanoSpawn('./shebang.js', {cwd: FIXTURES_URL})); - t.is(signalName, undefined); - t.is(exitCode, 1); - t.is(message, 'Command failed with exit code 1: ./shebang.js'); - t.true(stderr.includes('not recognized as an internal or external command')); - t.is(cause, undefined); + const error = await t.throwsAsync(nanoSpawn('./shebang.js', {cwd: FIXTURES_URL})); + assertWindowsNonExistent(t, error, './shebang.js'); }); } else { test('Can run shebangs', async t => { @@ -1118,40 +912,22 @@ test('Does not double escape shell strings', async t => { }); test('Handles non-existing command', async t => { - const {message, exitCode, signalName, stderr, cause} = await t.throwsAsync(nanoSpawn(nonExistentCommand)); + const error = await t.throwsAsync(nanoSpawn(nonExistentCommand)); if (isWindows) { - t.is(signalName, undefined); - t.is(exitCode, 1); - t.is(message, 'Command failed with exit code 1: non-existent-command'); - t.true(stderr.includes('not recognized as an internal or external command')); - t.is(cause, undefined); + assertWindowsNonExistent(t, error); } else { - t.is(signalName, undefined); - t.is(exitCode, undefined); - t.is(message, 'Command failed: non-existent-command'); - t.is(stderr, ''); - t.true(cause.message.includes(nonExistentCommand)); - t.is(cause.code, 'ENOENT'); - t.is(cause.syscall, 'spawn non-existent-command'); + assertNonExistent(t, error); } }); test('Handles non-existing command, shell', async t => { - const {message, exitCode, signalName, stderr, cause} = await t.throwsAsync(nanoSpawn(nonExistentCommand, {shell: true})); + const error = await t.throwsAsync(nanoSpawn(nonExistentCommand, {shell: true})); if (isWindows) { - t.is(signalName, undefined); - t.is(exitCode, 1); - t.is(message, 'Command failed with exit code 1: non-existent-command'); - t.true(stderr.includes('not recognized as an internal or external command')); - t.is(cause, undefined); + assertWindowsNonExistent(t, error); } else { - t.is(signalName, undefined); - t.is(exitCode, 127); - t.is(message, 'Command failed with exit code 127: non-existent-command'); - t.true(stderr.includes('not found')); - t.is(cause, undefined); + assertUnixNonExistentShell(t, error); } }); @@ -1169,27 +945,24 @@ test('options.preferLocal true runs local npm binaries', testLocalBinaryExec, un test('options.preferLocal true runs local npm binaries with options.cwd string', testLocalBinaryExec, fixturesPath); test('options.preferLocal true runs local npm binaries with options.cwd URL', testLocalBinaryExec, FIXTURES_URL); -if (!isWindows) { - const testPathVariable = async (t, pathName) => { - const {stdout} = await nanoSpawn(...localBinary, {preferLocal: true, env: {Path: undefined, [pathName]: path.dirname(process.execPath)}}); - t.regex(stdout, VERSION_REGEXP); - }; +const testPathVariable = async (t, pathName) => { + const {stdout} = await nanoSpawn(...localBinary, {preferLocal: true, env: {PATH: undefined, Path: undefined, [pathName]: isWindows ? process.env[pathKey()] : nodeDirectory}}); + t.regex(stdout, VERSION_REGEXP); +}; - test('options.preferLocal true uses options.env.PATH when set', testPathVariable, 'PATH'); - test('options.preferLocal true uses options.env.Path when set', testPathVariable, 'Path'); -} +test('options.preferLocal true uses options.env.PATH when set', testPathVariable, 'PATH'); +test('options.preferLocal true uses options.env.Path when set', testPathVariable, 'Path'); const testNoLocal = async (t, preferLocal) => { const PATH = process.env[pathKey()] .split(path.delimiter) .filter(pathPart => !pathPart.includes(path.join('node_modules', '.bin'))) .join(path.delimiter); - const {stderr, cause} = await t.throwsAsync(nanoSpawn(...localBinary, {preferLocal, env: {Path: undefined, PATH}})); + const error = await t.throwsAsync(nanoSpawn(...localBinary, {preferLocal, env: {Path: undefined, PATH}})); if (isWindows) { - t.true(stderr.includes('\'ava\' is not recognized as an internal or external command')); + assertWindowsNonExistent(t, error, localBinaryCommand); } else { - t.is(cause.code, 'ENOENT'); - t.is(cause.path, localBinary[0]); + assertNonExistent(t, error, localBinaryCommandStart, localBinaryCommand); } }; @@ -1197,27 +970,19 @@ test('options.preferLocal undefined does not run local npm binaries', testNoLoca test('options.preferLocal false does not run local npm binaries', testNoLocal, false); test('options.preferLocal true uses options.env when empty', async t => { - const {exitCode, stderr, cause} = await t.throwsAsync(nanoSpawn(...localBinary, {preferLocal: true, env: {PATH: undefined, Path: undefined}})); + const error = await t.throwsAsync(nanoSpawn(...localBinary, {preferLocal: true, env: {PATH: undefined, Path: undefined}})); if (isWindows) { - t.is(cause.code, 'ENOENT'); + assertNonExistent(t, error, 'cmd.exe', localBinaryCommand); } else { - t.is(exitCode, 127); - t.true(stderr.includes('No such file')); + assertUnixNotFound(t, error, localBinaryCommand); } }); -if (isWindows) { - test('options.preferLocal true runs local npm binaries with process.env.Path', async t => { - const {stdout} = await nanoSpawn(...localBinary, {preferLocal: true, env: {PATH: undefined, Path: process.env[pathKey()]}}); - t.regex(stdout, VERSION_REGEXP); - }); -} - test('options.preferLocal true does not add node_modules/.bin if already present', async t => { const localDirectory = fileURLToPath(new URL('node_modules/.bin', import.meta.url)); const currentPath = process.env[pathKey()]; const pathValue = `${localDirectory}${path.delimiter}${currentPath}`; - const {stdout} = await nanoSpawn('node', ['-p', `process.env.${pathKey()}`], {preferLocal: true, env: {[pathKey()]: pathValue}}); + const {stdout} = await nanoSpawn(...nodePrint(`process.env.${pathKey()}`), {preferLocal: true, env: {[pathKey()]: pathValue}}); t.is( stdout.split(path.delimiter).filter(pathPart => pathPart === localDirectory).length - currentPath.split(path.delimiter).filter(pathPart => pathPart === localDirectory).length, @@ -1265,10 +1030,18 @@ test('Can run OS binaries', async t => { const nodeCliFlag = '--jitless'; const inspectCliFlag = '--inspect-port=8091'; -test('Keeps Node flags', async t => { - const {stdout} = await nanoSpawn('node', [nodeCliFlag, 'node-flags.js'], {cwd: FIXTURES_URL}); - t.true(stdout.includes(nodeCliFlag)); -}); +const testNodeFlags = async (t, binaryName, fixtureName, hasFlag) => { + const {stdout} = await nanoSpawn(binaryName, [nodeCliFlag, fixtureName], {cwd: FIXTURES_URL}); + t.is(stdout.includes(nodeCliFlag), hasFlag); +}; + +test('Keeps Node flags', testNodeFlags, 'node', 'node-flags.js', true); +test('Does not keep Node flags, full path', testNodeFlags, 'node', 'node-flags-path.js', false); + +if (isWindows) { + test('Keeps Node flags, node.exe', testNodeFlags, 'node.exe', 'node-flags.js', true); + test('Keeps Node flags, case-insensitive', testNodeFlags, 'NODE', 'node-flags.js', true); +} test('Does not keep --inspect* Node flags', async t => { const {stdout} = await nanoSpawn('node', [nodeCliFlag, inspectCliFlag, 'node-flags.js'], {cwd: FIXTURES_URL}); @@ -1276,23 +1049,6 @@ test('Does not keep --inspect* Node flags', async t => { t.false(stdout.includes(inspectCliFlag)); }); -test('Does not keep Node flags, full path', async t => { - const {stdout} = await nanoSpawn('node', [nodeCliFlag, 'node-flags-path.js'], {cwd: FIXTURES_URL}); - t.false(stdout.includes(nodeCliFlag)); -}); - -if (isWindows) { - test('Keeps Node flags, node.exe', async t => { - const {stdout} = await nanoSpawn('node.exe', [nodeCliFlag, 'node-flags.js'], {cwd: FIXTURES_URL}); - t.true(stdout.includes(nodeCliFlag)); - }); - - test('Keeps Node flags, case-insensitive', async t => { - const {stdout} = await nanoSpawn('NODE', [nodeCliFlag, 'node-flags.js'], {cwd: FIXTURES_URL}); - t.true(stdout.includes(nodeCliFlag)); - }); -} - const TEST_NODE_VERSION = '18.0.0'; test.serial('Keeps Node version', async t => { @@ -1303,89 +1059,69 @@ test.serial('Keeps Node version', async t => { }); test('.pipe() success', async t => { - const {stdout, output, command, durationMs} = await nanoSpawn(...nodePrintStdout) - .pipe(...nodeToUpperCase); + const {stdout, output, command, durationMs} = await nanoSpawn(...nodePrintStdout).pipe(...nodeToUpperCase); t.is(stdout, testUpperCase); t.is(output, stdout); t.is(getPipeSize(command), 2); - t.true(durationMs > 0); + assertDurationMs(t, durationMs); }); test('.pipe() source fails', async t => { - const {exitCode, stdout, output, command, durationMs} = await t.throwsAsync(nanoSpawn(...nodePrintFail) - .pipe(...nodeToUpperCase)); - t.is(exitCode, 2); - t.is(stdout, testString); - t.is(output, stdout); - t.is(getPipeSize(command), 1); - t.true(durationMs > 0); + const error = await t.throwsAsync(nanoSpawn(...nodePrintFail).pipe(...nodeToUpperCase)); + assertFail(t, error); + t.is(error.stdout, testString); + t.is(error.output, error.stdout); + t.is(getPipeSize(error.command), 1); }); test('.pipe() source fails due to child_process invalid option', async t => { - const {exitCode, cause, command, durationMs} = await t.throwsAsync(nanoSpawn(...nodePrintStdout, {detached: 'true'}) - .pipe(...nodeToUpperCase)); - t.is(exitCode, undefined); - t.true(cause.message.includes('options.detached')); - t.is(getPipeSize(command), 1); - t.true(durationMs > 0); + const error = await t.throwsAsync(nanoSpawn(...nodePrintStdout, earlyErrorOptions).pipe(...nodeToUpperCase)); + assertEarlyError(t, error); + t.is(getPipeSize(error.command), 1); }); test('.pipe() source fails due to stream error', async t => { const first = nanoSpawn(...nodePrintStdout); const second = first.pipe(...nodeToUpperCase); - const error = new Error(testString); + const cause = new Error(testString); const subprocess = await first.nodeChildProcess; - subprocess.stdout.destroy(error); - const {exitCode, signalName, message, cause} = await t.throwsAsync(second); - t.is(exitCode, undefined); - t.is(signalName, undefined); - t.true(message.startsWith(messageEvalFailStart)); - t.is(cause, error); + subprocess.stdout.destroy(cause); + const error = await t.throwsAsync(second); + assertErrorEvent(t, error, cause); }); test('.pipe() destination fails', async t => { - const {exitCode, stdout, command, durationMs} = await t.throwsAsync(nanoSpawn(...nodePrintStdout) - .pipe(...nodeToUpperCaseFail)); - t.is(exitCode, 2); - t.is(stdout, testUpperCase); - t.is(getPipeSize(command), 2); - t.true(durationMs > 0); + const error = await t.throwsAsync(nanoSpawn(...nodePrintStdout).pipe(...nodeToUpperCaseFail)); + assertFail(t, error); + t.is(error.stdout, testUpperCase); + t.is(getPipeSize(error.command), 2); }); test('.pipe() destination fails due to child_process invalid option', async t => { - const {exitCode, cause, command, durationMs} = await t.throwsAsync(nanoSpawn(...nodePrintStdout) - .pipe(...nodeToUpperCase, {detached: 'true'})); - t.is(exitCode, undefined); - t.true(cause.message.includes('options.detached')); - t.is(getPipeSize(command), 2); - t.true(durationMs > 0); + const error = await t.throwsAsync(nanoSpawn(...nodePrintStdout).pipe(...nodeToUpperCase, earlyErrorOptions)); + assertEarlyError(t, error); + t.is(getPipeSize(error.command), 2); }); test('.pipe() destination fails due to stream error', async t => { const first = nanoSpawn(...nodePrintStdout); const second = first.pipe(...nodeToUpperCase); - const error = new Error(testString); + const cause = new Error(testString); const subprocess = await second.nodeChildProcess; - subprocess.stdin.destroy(error); - const {exitCode, signalName, message, cause} = await t.throwsAsync(second); - t.is(exitCode, undefined); - t.is(signalName, undefined); - t.true(message.startsWith(messageEvalFailStart)); - t.is(cause, error); + subprocess.stdin.destroy(cause); + const error = await t.throwsAsync(second); + assertErrorEvent(t, error, cause); }); test('.pipe() source and destination fail', async t => { - const {exitCode, stdout, command, durationMs} = await t.throwsAsync(nanoSpawn(...nodePrintFail) - .pipe(...nodeToUpperCaseFail)); - t.is(exitCode, 2); - t.is(stdout, testString); - t.is(getPipeSize(command), 1); - t.true(durationMs > 0); + const error = await t.throwsAsync(nanoSpawn(...nodePrintFail).pipe(...nodeToUpperCaseFail)); + assertFail(t, error); + t.is(error.stdout, testString); + t.is(getPipeSize(error.command), 1); }); test('.pipe().pipe() success', async t => { - const first = nanoSpawn(...nodePrintStdout) - .pipe(...nodeToUpperCase); + const first = nanoSpawn(...nodePrintStdout).pipe(...nodeToUpperCase); const secondResult = await first.pipe(...nodeDouble); const firstResult = await first; t.is(firstResult.stdout, testUpperCase); @@ -1394,122 +1130,110 @@ test('.pipe().pipe() success', async t => { t.is(secondResult.output, secondResult.stdout); t.is(getPipeSize(firstResult.command), 2); t.is(getPipeSize(secondResult.command), 3); - t.true(firstResult.durationMs > 0); + assertDurationMs(t, firstResult.durationMs); t.true(secondResult.durationMs > firstResult.durationMs); }); test('.pipe().pipe() first source fail', async t => { - const first = nanoSpawn(...nodePrintFail) - .pipe(...nodeToUpperCase); - const secondResult = await t.throwsAsync(first.pipe(...nodeDouble)); - const firstResult = await t.throwsAsync(first); - t.is(firstResult, secondResult); - t.is(firstResult.stdout, testString); - t.is(firstResult.output, firstResult.stdout); - t.is(getPipeSize(firstResult.command), 1); - t.true(firstResult.durationMs > 0); + const first = nanoSpawn(...nodePrintFail).pipe(...nodeToUpperCase); + const secondError = await t.throwsAsync(first.pipe(...nodeDouble)); + const firstError = await t.throwsAsync(first); + assertFail(t, firstError); + t.is(firstError, secondError); + t.is(firstError.stdout, testString); + t.is(firstError.output, firstError.stdout); + t.is(getPipeSize(firstError.command), 1); }); test('.pipe().pipe() second source fail', async t => { - const first = nanoSpawn(...nodePrintStdout) - .pipe(...nodeToUpperCaseFail); - const secondResult = await t.throwsAsync(first.pipe(...nodeDouble)); - const firstResult = await t.throwsAsync(first); - t.is(firstResult, secondResult); - t.is(firstResult.stdout, testUpperCase); - t.is(firstResult.output, firstResult.stdout); - t.is(getPipeSize(firstResult.command), 2); - t.true(firstResult.durationMs > 0); + const first = nanoSpawn(...nodePrintStdout).pipe(...nodeToUpperCaseFail); + const secondError = await t.throwsAsync(first.pipe(...nodeDouble)); + const firstError = await t.throwsAsync(first); + assertFail(t, firstError); + t.is(firstError, secondError); + t.is(firstError.stdout, testUpperCase); + t.is(firstError.output, firstError.stdout); + t.is(getPipeSize(firstError.command), 2); }); test('.pipe().pipe() destination fail', async t => { - const first = nanoSpawn(...nodePrintStdout) - .pipe(...nodeToUpperCase); - const secondResult = await t.throwsAsync(first.pipe(...nodeDoubleFail)); + const first = nanoSpawn(...nodePrintStdout).pipe(...nodeToUpperCase); + const secondError = await t.throwsAsync(first.pipe(...nodeDoubleFail)); const firstResult = await first; - t.not(firstResult, secondResult); + assertFail(t, secondError); t.is(firstResult.stdout, testUpperCase); t.is(firstResult.output, firstResult.stdout); - t.is(secondResult.stdout, testDoubleUpperCase); - t.is(secondResult.output, secondResult.stdout); + t.is(secondError.stdout, testDoubleUpperCase); + t.is(secondError.output, secondError.stdout); t.is(getPipeSize(firstResult.command), 2); - t.is(getPipeSize(secondResult.command), 3); - t.true(firstResult.durationMs > 0); - t.true(secondResult.durationMs > 0); + t.is(getPipeSize(secondError.command), 3); + assertDurationMs(t, firstResult.durationMs); }); test('.pipe().pipe() all fail', async t => { - const first = nanoSpawn(...nodePrintFail) - .pipe(...nodeToUpperCaseFail); - const secondResult = await t.throwsAsync(first.pipe(...nodeDoubleFail)); - const firstResult = await t.throwsAsync(first); - t.is(firstResult, secondResult); - t.is(firstResult.stdout, testString); - t.is(firstResult.output, firstResult.stdout); - t.is(getPipeSize(firstResult.command), 1); - t.true(firstResult.durationMs > 0); + const first = nanoSpawn(...nodePrintFail).pipe(...nodeToUpperCaseFail); + const secondError = await t.throwsAsync(first.pipe(...nodeDoubleFail)); + const firstError = await t.throwsAsync(first); + assertFail(t, firstError); + t.is(firstError, secondError); + t.is(firstError.stdout, testString); + t.is(firstError.output, firstError.stdout); + t.is(getPipeSize(firstError.command), 1); }); // Cannot guarantee that `cat` exists on Windows if (!isWindows) { test('.pipe() without arguments', async t => { - const {stdout} = await nanoSpawn(...nodePrintStdout) - .pipe('cat'); + const {stdout} = await nanoSpawn(...nodePrintStdout).pipe('cat'); t.is(stdout, testString); }); } test('.pipe() with options', async t => { const argv0 = 'Foo'; - const {stdout} = await nanoSpawn(...nodePrintStdout) - .pipe(...nodeEval(`process.stdin.on("data", chunk => { - console.log(chunk.toString().trim() + process.argv0); - });`), {argv0}); + const {stdout} = await nanoSpawn(...nodePrintStdout).pipe(...nodeEval(`process.stdin.on("data", chunk => { + console.log(chunk.toString().trim() + process.argv0); +});`), {argv0}); t.is(stdout, `${testString}${argv0}`); }); test.serial('.pipe() which does not read stdin, source ends first', async t => { - const {stdout, output} = await nanoSpawn(...nodePrintStdout) - .pipe(...nodePrintSleep); + const {stdout, output} = await nanoSpawn(...nodePrintStdout).pipe(...nodePrintSleep); t.is(stdout, testString); t.is(output, stdout); }); test.serial('.pipe() which does not read stdin, source fails first', async t => { - const {stdout, output} = await t.throwsAsync(nanoSpawn(...nodePrintFail) - .pipe(...nodePrintSleep)); - t.is(stdout, testString); - t.is(output, stdout); + const error = await t.throwsAsync(nanoSpawn(...nodePrintFail).pipe(...nodePrintSleep)); + assertFail(t, error); + t.is(error.stdout, testString); + t.is(error.output, error.stdout); }); test.serial('.pipe() which does not read stdin, source ends last', async t => { - const {stdout, output} = await nanoSpawn(...nodePrintSleep) - .pipe(...nodePrintStdout); + const {stdout, output} = await nanoSpawn(...nodePrintSleep).pipe(...nodePrintStdout); t.is(stdout, testString); t.is(output, stdout); }); test.serial('.pipe() which does not read stdin, source fails last', async t => { - const {stdout, output} = await t.throwsAsync(nanoSpawn(...nodePrintStdout) - .pipe(...nodePrintSleepFail)); - t.is(stdout, testString); - t.is(output, stdout); + const error = await t.throwsAsync(nanoSpawn(...nodePrintStdout).pipe(...nodePrintSleepFail)); + assertFail(t, error); + t.is(error.stdout, testString); + t.is(error.output, error.stdout); }); test('.pipe() which has hanging stdin', async t => { - const {signalName, command, stdout, output} = await t.throwsAsync(nanoSpawn('node', {timeout: 1e3}) - .pipe(...nodePassThrough)); - t.is(signalName, 'SIGTERM'); - t.is(command, 'node'); - t.is(stdout, ''); - t.is(output, ''); + const error = await t.throwsAsync(nanoSpawn(...nodeHanging, {timeout: 1e3}).pipe(...nodePassThrough)); + assertSigterm(t, error); + t.is(error.stdout, ''); + t.is(error.output, ''); }); test('.pipe() with stdin stream in source', async t => { const stream = createReadStream(testFixtureUrl); await once(stream, 'open'); - const {stdout} = await nanoSpawn(...nodePassThrough, {stdin: stream}) - .pipe(...nodeToUpperCase); + const {stdout} = await nanoSpawn(...nodePassThrough, {stdin: stream}).pipe(...nodeToUpperCase); t.is(stdout, testUpperCase); }); @@ -1517,8 +1241,7 @@ test('.pipe() with stdin stream in destination', async t => { const stream = createReadStream(testFixtureUrl); await once(stream, 'open'); await t.throwsAsync( - nanoSpawn(...nodePassThrough) - .pipe(...nodeToUpperCase, {stdin: stream}), + nanoSpawn(...nodePassThrough).pipe(...nodeToUpperCase, {stdin: stream}), {message: 'The "stdin" option must be set on the first "nanoSpawn()" call in the pipeline.'}); }); @@ -1526,8 +1249,7 @@ test('.pipe() with stdout stream in destination', async t => { await temporaryWriteTask('', async temporaryPath => { const stream = createWriteStream(temporaryPath); await once(stream, 'open'); - const {stdout} = await nanoSpawn(...nodePrintStdout) - .pipe(...nodePassThrough, {stdout: stream}); + const {stdout} = await nanoSpawn(...nodePrintStdout).pipe(...nodePassThrough, {stdout: stream}); t.is(stdout, ''); t.is(await readFile(temporaryPath, 'utf8'), `${testString}\n`); }); @@ -1538,16 +1260,14 @@ test('.pipe() with stdout stream in source', async t => { const stream = createWriteStream(temporaryPath); await once(stream, 'open'); await t.throwsAsync( - nanoSpawn(...nodePrintStdout, {stdout: stream}) - .pipe(...nodePassThrough), + nanoSpawn(...nodePrintStdout, {stdout: stream}).pipe(...nodePassThrough), {message: 'The "stdout" option must be set on the last "nanoSpawn()" call in the pipeline.'}, ); }); }); test('.pipe() + stdout/stderr iteration', async t => { - const promise = nanoSpawn(...nodePrintStdout) - .pipe(...nodeToUpperCase); + const promise = nanoSpawn(...nodePrintStdout).pipe(...nodeToUpperCase); const lines = await arrayFromAsync(promise); t.deepEqual(lines, [testUpperCase]); const {stdout, stderr, output} = await promise; @@ -1557,8 +1277,7 @@ test('.pipe() + stdout/stderr iteration', async t => { }); test('.pipe() + stdout iteration', async t => { - const promise = nanoSpawn(...nodePrintStdout) - .pipe(...nodeToUpperCase); + const promise = nanoSpawn(...nodePrintStdout).pipe(...nodeToUpperCase); const lines = await arrayFromAsync(promise.stdout); t.deepEqual(lines, [testUpperCase]); const {stdout, output} = await promise; @@ -1567,8 +1286,7 @@ test('.pipe() + stdout iteration', async t => { }); test('.pipe() + stderr iteration', async t => { - const promise = nanoSpawn(...nodePrintStdout) - .pipe(...nodeToUpperCaseStderr); + const promise = nanoSpawn(...nodePrintStdout).pipe(...nodeToUpperCaseStderr); const lines = await arrayFromAsync(promise.stderr); t.deepEqual(lines, [testUpperCase]); const {stderr, output} = await promise; @@ -1577,31 +1295,23 @@ test('.pipe() + stderr iteration', async t => { }); test('.pipe() + stdout iteration, source fail', async t => { - const promise = nanoSpawn(...nodePrintFail) - .pipe(...nodeToUpperCase); - const {exitCode, stdout, message, command, durationMs} = await t.throwsAsync(arrayFromAsync(promise.stdout)); - t.is(exitCode, 2); - t.is(stdout, testString); - t.true(message.startsWith(messageExitEvalFailStart)); - t.true(command.startsWith(commandEvalFailStart)); - t.true(durationMs > 0); - const error = await t.throwsAsync(promise); + const promise = nanoSpawn(...nodePrintFail).pipe(...nodeToUpperCase); + const error = await t.throwsAsync(arrayFromAsync(promise.stdout)); + assertFail(t, error); t.is(error.stdout, testString); - t.is(error.output, error.stdout); + const secondError = await t.throwsAsync(promise); + t.is(secondError.stdout, testString); + t.is(secondError.output, secondError.stdout); }); test('.pipe() + stdout iteration, destination fail', async t => { - const promise = nanoSpawn(...nodePrintStdout) - .pipe(...nodeToUpperCaseFail); - const {exitCode, stdout, message, command, durationMs} = await t.throwsAsync(arrayFromAsync(promise.stdout)); - t.is(exitCode, 2); - t.is(stdout, ''); - t.true(message.startsWith(messageExitEvalFailStart)); - t.true(command.startsWith(commandEvalFailStart)); - t.true(durationMs > 0); - const error = await t.throwsAsync(promise); + const promise = nanoSpawn(...nodePrintStdout).pipe(...nodeToUpperCaseFail); + const error = await t.throwsAsync(arrayFromAsync(promise.stdout)); + assertFail(t, error); t.is(error.stdout, ''); - t.is(error.output, ''); + const secondError = await t.throwsAsync(promise); + t.is(secondError.stdout, ''); + t.is(secondError.output, ''); }); test('.pipe() with EPIPE', async t => { @@ -1610,8 +1320,7 @@ test('.pipe() with EPIPE', async t => { }, 0); process.stdout.on("error", () => { process.exit(); -});`)) - .pipe('head', ['-n', '2']); +});`)).pipe('head', ['-n', '2']); const lines = await arrayFromAsync(promise); t.deepEqual(lines, [testString, testString]); const {stdout, output} = await promise;