diff --git a/test.js b/test.js index 25fba2d..e295698 100644 --- a/test.js +++ b/test.js @@ -83,42 +83,62 @@ const nodeDoubleFail = nodeEval(`process.stdin.on("data", chunk => { const localBinary = ['ava', ['--version']]; const nonExistentCommand = 'non-existent-command'; +const assertWindowsNonExistent = (t, {exitCode, signalName, command, message, stderr, cause}, commandStart = nonExistentCommand) => { + t.is(exitCode, 1); + t.is(signalName, undefined); + t.true(command.startsWith(commandStart)); + t.true(message.startsWith(`Command failed with exit code 1: ${commandStart}`)); + t.true(stderr.includes('not recognized as an internal or external command')); + t.is(cause, undefined); +}; + +const assertSigterm = (t, {exitCode, signalName, message, cause}) => { + t.is(exitCode, undefined); + t.is(signalName, 'SIGTERM'); + t.is(message, 'Command was terminated with SIGTERM: node'); + t.is(cause, undefined); +}; + const commandEvalFailStart = 'node -e'; const messageEvalFailStart = `Command failed: ${commandEvalFailStart}`; -const messageExitEvalFailStart = `Command failed with exit code 2: ${commandEvalFailStart}`; +const earlyErrorOptions = {detached: 'true'}; -const VERSION_REGEXP = /^\d+\.\d+\.\d+$/; +const assertEarlyError = (t, {exitCode, signalName, message, cause}) => { + 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')); +}; -test('Can pass options.argv0', async t => { - const {stdout} = await nanoSpawn(...nodePrintArgv0, {argv0: testString}); - t.is(stdout, testString); -}); +const assertFail = (t, {exitCode, signalName, command, message, cause}, commandStart = commandEvalFailStart) => { + 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); +}; -test('Can pass options.argv0, shell', async t => { - const {stdout} = await nanoSpawn(...nodePrintArgv0, {argv0: testString, shell: true}); - t.is(stdout, process.execPath); -}); +const VERSION_REGEXP = /^\d+\.\d+\.\d+$/; -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 testArgv0 = async (t, shell) => { + const {stdout} = await nanoSpawn(...nodePrintArgv0, {argv0: testString, shell}); + t.is(stdout, shell ? process.execPath : testString); +}; -test('Can pass options.stdout', async t => { - const promise = nanoSpawn(...nodePrintStdout, {stdout: 'ignore'}); - const {stdout} = await promise.nodeChildProcess; - t.is(stdout, null); - await promise; -}); +test('Can pass options.argv0', testArgv0, false); +test('Can pass options.argv0, shell', testArgv0, true); -test('Can pass options.stderr', async t => { - const promise = nanoSpawn(...nodePrintStdout, {stderr: 'ignore'}); - const {stderr} = await promise.nodeChildProcess; - t.is(stderr, null); +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 +187,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,57 +200,39 @@ 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'); + t.is(error.cause.code, 'ENOENT'); } }); 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`. @@ -369,33 +365,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 => { @@ -885,8 +881,8 @@ 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 => { @@ -1022,13 +1018,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'); }; test('Can set PATHEXT', testPathExtension, undefined); @@ -1093,12 +1088,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 +1109,32 @@ 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'); + t.is(error.signalName, undefined); + t.is(error.exitCode, undefined); + t.is(error.message, 'Command failed: non-existent-command'); + t.is(error.stderr, ''); + t.true(error.cause.message.includes(nonExistentCommand)); + t.is(error.cause.code, 'ENOENT'); + t.is(error.cause.syscall, 'spawn non-existent-command'); } }); 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); + t.is(error.signalName, undefined); + t.is(error.exitCode, 127); + t.is(error.message, 'Command failed with exit code 127: non-existent-command'); + t.true(error.stderr.includes('not found')); + t.is(error.cause, undefined); } }); @@ -1184,12 +1167,12 @@ const testNoLocal = async (t, preferLocal) => { .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, localBinary[0]); } else { - t.is(cause.code, 'ENOENT'); - t.is(cause.path, localBinary[0]); + t.is(error.cause.code, 'ENOENT'); + t.is(error.cause.path, localBinary[0]); } }; @@ -1217,7 +1200,7 @@ test('options.preferLocal true does not add node_modules/.bin if already present 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, @@ -1312,22 +1295,21 @@ test('.pipe() success', async t => { }); test('.pipe() source fails', async t => { - const {exitCode, stdout, output, command, durationMs} = await t.throwsAsync(nanoSpawn(...nodePrintFail) + const error = 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); + assertFail(t, error); + t.is(error.stdout, testString); + t.is(error.output, error.stdout); + t.is(getPipeSize(error.command), 1); + t.true(error.durationMs > 0); }); test('.pipe() source fails due to child_process invalid option', async t => { - const {exitCode, cause, command, durationMs} = await t.throwsAsync(nanoSpawn(...nodePrintStdout, {detached: 'true'}) + const error = await t.throwsAsync(nanoSpawn(...nodePrintStdout, earlyErrorOptions) .pipe(...nodeToUpperCase)); - t.is(exitCode, undefined); - t.true(cause.message.includes('options.detached')); - t.is(getPipeSize(command), 1); - t.true(durationMs > 0); + assertEarlyError(t, error); + t.is(getPipeSize(error.command), 1); + t.true(error.durationMs > 0); }); test('.pipe() source fails due to stream error', async t => { @@ -1344,21 +1326,20 @@ test('.pipe() source fails due to stream error', async t => { }); test('.pipe() destination fails', async t => { - const {exitCode, stdout, command, durationMs} = await t.throwsAsync(nanoSpawn(...nodePrintStdout) + const error = await t.throwsAsync(nanoSpawn(...nodePrintStdout) .pipe(...nodeToUpperCaseFail)); - t.is(exitCode, 2); - t.is(stdout, testUpperCase); - t.is(getPipeSize(command), 2); - t.true(durationMs > 0); + assertFail(t, error); + t.is(error.stdout, testUpperCase); + t.is(getPipeSize(error.command), 2); + t.true(error.durationMs > 0); }); 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); + t.true(error.durationMs > 0); }); test('.pipe() destination fails due to stream error', async t => { @@ -1375,12 +1356,12 @@ test('.pipe() destination fails due to stream error', async t => { }); test('.pipe() source and destination fail', async t => { - const {exitCode, stdout, command, durationMs} = await t.throwsAsync(nanoSpawn(...nodePrintFail) + const error = await t.throwsAsync(nanoSpawn(...nodePrintFail) .pipe(...nodeToUpperCaseFail)); - t.is(exitCode, 2); - t.is(stdout, testString); - t.is(getPipeSize(command), 1); - t.true(durationMs > 0); + assertFail(t, error); + t.is(error.stdout, testString); + t.is(getPipeSize(error.command), 1); + t.true(error.durationMs > 0); }); test('.pipe().pipe() success', async t => { @@ -1497,12 +1478,12 @@ test.serial('.pipe() which does not read stdin, source fails last', async t => { }); test('.pipe() which has hanging stdin', async t => { - const {signalName, command, stdout, output} = await t.throwsAsync(nanoSpawn('node', {timeout: 1e3}) + const error = await t.throwsAsync(nanoSpawn('node', {timeout: 1e3}) .pipe(...nodePassThrough)); - t.is(signalName, 'SIGTERM'); - t.is(command, 'node'); - t.is(stdout, ''); - t.is(output, ''); + assertSigterm(t, error); + t.is(error.command, 'node'); + t.is(error.stdout, ''); + t.is(error.output, ''); }); test('.pipe() with stdin stream in source', async t => { @@ -1579,29 +1560,25 @@ 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 error = await t.throwsAsync(arrayFromAsync(promise.stdout)); + assertFail(t, error); t.is(error.stdout, testString); - t.is(error.output, error.stdout); + t.true(error.durationMs > 0); + 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 error = await t.throwsAsync(arrayFromAsync(promise.stdout)); + assertFail(t, error); t.is(error.stdout, ''); - t.is(error.output, ''); + t.true(error.durationMs > 0); + const secondError = await t.throwsAsync(promise); + t.is(secondError.stdout, ''); + t.is(secondError.output, ''); }); test('.pipe() with EPIPE', async t => {