diff options
Diffstat (limited to 'test/js/node/child_process/child_process-node.test.js')
-rw-r--r-- | test/js/node/child_process/child_process-node.test.js | 481 |
1 files changed, 481 insertions, 0 deletions
diff --git a/test/js/node/child_process/child_process-node.test.js b/test/js/node/child_process/child_process-node.test.js new file mode 100644 index 000000000..deb3bfb86 --- /dev/null +++ b/test/js/node/child_process/child_process-node.test.js @@ -0,0 +1,481 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { ChildProcess, spawn, exec } from "node:child_process"; +import { throws, assert, createCallCheckCtx, createDoneDotAll } from "node-harness"; +import { tmpdir } from "node:os"; +const strictEqual = (a, b) => expect(a).toStrictEqual(b); +const debug = process.env.DEBUG ? console.log : () => {}; + +const platformTmpDir = require("fs").realpathSync(tmpdir()); + +const TYPE_ERR_NAME = "TypeError"; + +console.log(process.cwd()); + +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +const common = { + pwdCommand: ["pwd", []], +}; + +describe("ChildProcess.constructor", () => { + it("should be a function", () => { + strictEqual(typeof ChildProcess, "function"); + }); +}); + +describe("ChildProcess.spawn()", () => { + it("should throw on invalid options", () => { + // Verify that invalid options to spawn() throw. + const child = new ChildProcess(); + + [undefined, null, "foo", 0, 1, NaN, true, false].forEach(options => { + throws( + () => { + child.spawn(options); + }, + { + code: "ERR_INVALID_ARG_TYPE", + name: TYPE_ERR_NAME, + // message: + // 'The "options" argument must be of type object.' + + // `${common.invalidArgTypeHelper(options)}`, + }, + ); + }); + }); + + it("should throw if file is not a string", () => { + // Verify that spawn throws if file is not a string. + const child = new ChildProcess(); + [undefined, null, 0, 1, NaN, true, false, {}].forEach(file => { + throws( + () => { + child.spawn({ file }); + }, + { + code: "ERR_INVALID_ARG_TYPE", + name: TYPE_ERR_NAME, + // message: + // 'The "options.file" property must be of type string.' + + // `${common.invalidArgTypeHelper(file)}`, + }, + ); + }); + }); + + it("should throw if envPairs is not an array or undefined", () => { + // Verify that spawn throws if envPairs is not an array or undefined. + const child = new ChildProcess(); + + [null, 0, 1, NaN, true, false, {}, "foo"].forEach(envPairs => { + throws( + () => { + child.spawn({ + envPairs, + stdio: ["ignore", "ignore", "ignore", "ipc"], + }); + }, + { + code: "ERR_INVALID_ARG_TYPE", + name: TYPE_ERR_NAME, + // message: + // 'The "options.envPairs" property must be an instance of Array.' + + // common.invalidArgTypeHelper(envPairs), + }, + ); + }); + }); + + it("should throw if stdio is not an array or undefined", () => { + // Verify that spawn throws if args is not an array or undefined. + const child = new ChildProcess(); + + [null, 0, 1, NaN, true, false, {}, "foo"].forEach(args => { + throws( + () => { + child.spawn({ file: "foo", args }); + }, + { + code: "ERR_INVALID_ARG_TYPE", + name: TYPE_ERR_NAME, + // message: + // 'The "options.args" property must be an instance of Array.' + + // common.invalidArgTypeHelper(args), + }, + ); + }); + }); +}); + +describe("ChildProcess.spawn", () => { + function getChild() { + const child = new ChildProcess(); + child.spawn({ + file: "node", + // file: process.execPath, + args: ["node", "--interactive"], + cwd: process.cwd(), + stdio: ["ignore", "ignore", "ignore"], + }); + return child; + } + + it("should spawn a process", () => { + const child = getChild(); + // Test that we can call spawn + + strictEqual(Object.hasOwn(child, "pid"), true); + assert(Number.isInteger(child.pid)); + child.kill(); + }); + + it("should throw error on invalid signal", () => { + const child = getChild(); + // Try killing with invalid signal + throws( + () => { + child.kill("foo"); + }, + { code: "ERR_UNKNOWN_SIGNAL", name: TYPE_ERR_NAME }, + ); + }); +}); + +describe("ChildProcess spawn bad stdio", () => { + // Monkey patch spawn() to create a child process normally, but destroy the + // stdout and stderr streams. This replicates the conditions where the streams + // cannot be properly created. + function createChild(options, callback, done, target) { + var __originalSpawn = ChildProcess.prototype.spawn; + ChildProcess.prototype.spawn = function () { + const err = __originalSpawn.apply(this, arguments); + + this.stdout.destroy(); + this.stderr.destroy(); + + return err; + }; + + const { mustCall } = createCallCheckCtx(done); + let cmd = `bun ${import.meta.dir}/spawned-child.js`; + if (target) cmd += " " + target; + const child = exec(cmd, options, mustCall(callback)); + ChildProcess.prototype.spawn = __originalSpawn; + return child; + } + + it("should handle normal execution of child process", done => { + createChild( + {}, + (err, stdout, stderr) => { + strictEqual(err, null); + strictEqual(stdout, ""); + strictEqual(stderr, ""); + }, + done, + ); + }); + + it("should handle error event of child process", done => { + const error = new Error(`Command failed: bun ${import.meta.dir}/spawned-child.js ERROR`); + createChild( + {}, + (err, stdout, stderr) => { + strictEqual(err.message, error.message); + strictEqual(stdout, ""); + strictEqual(stderr, ""); + }, + done, + "ERROR", + ); + }); + + it("should handle killed process", done => { + createChild( + { timeout: 1 }, + (err, stdout, stderr) => { + strictEqual(err.killed, true); + strictEqual(stdout, ""); + strictEqual(stderr, ""); + }, + done, + ); + }); +}); + +describe("child_process cwd", () => { + // Spawns 'pwd' with given options, then test + // - whether the child pid is undefined or number, + // - whether the exit code equals expectCode, + // - optionally whether the trimmed stdout result matches expectData + function testCwd(options, { expectPidType, expectCode = 0, expectData }, done = () => {}) { + const createDone = createDoneDotAll(done); + const { mustCall } = createCallCheckCtx(createDone(1500)); + const exitDone = createDone(5000); + + const child = spawn(...common.pwdCommand, options); + + strictEqual(typeof child.pid, expectPidType); + + child.stdout.setEncoding("utf8"); + + // No need to assert callback since `data` is asserted. + let data = ""; + child.stdout.on("data", chunk => { + data += chunk; + }); + + // TODO: Test exit events + // // Can't assert callback, as stayed in to API: + // // _The 'exit' event may or may not fire after an error has occurred._ + child.on("exit", (code, signal) => { + try { + strictEqual(code, expectCode); + exitDone(); + } catch (err) { + exitDone(err); + } + }); + + child.on( + "close", + mustCall(() => { + expectData && strictEqual(data.trim(), expectData); + }), + ); + + return child; + } + + // TODO: Make sure this isn't important + // Currently Bun.spawn will still spawn even though cwd doesn't exist + // // Assume does-not-exist doesn't exist, expect exitCode=-1 and errno=ENOENT + // it("should throw an error when given cwd doesn't exist", () => { + // testCwd({ cwd: "does-not-exist" }, "undefined", -1).on( + // "error", + // mustCall(function (e) { + // console.log(e); + // strictEqual(e.code, "ENOENT"); + // }) + // ); + // }); + + // TODO: Make sure this isn't an important test + // it("should throw when cwd is a non-file url", () => { + // throws(() => { + // testCwd( + // { + // cwd: new URL("http://example.com/"), + // }, + // "number", + // 0, + // tmpdir.path + // ); + // }, /The URL must be of scheme file/); + + // // if (process.platform !== "win32") { + // // throws(() => { + // // testCwd( + // // { + // // cwd: new URL("file://host/dev/null"), + // // }, + // // "number", + // // 0, + // // tmpdir.path + // // ); + // // }, /File URL host must be "localhost" or empty on/); + // // } + // }); + + it("should work for valid given cwd", done => { + const tmpdir = { path: platformTmpDir }; + const createDone = createDoneDotAll(done); + + // Assume these exist, and 'pwd' gives us the right directory back + testCwd( + { cwd: tmpdir.path }, + { + expectPidType: "number", + expectCode: 0, + expectData: platformTmpDir, + }, + createDone(1500), + ); + const shouldExistDir = "/dev"; + testCwd( + { cwd: shouldExistDir }, + { + expectPidType: "number", + expectCode: 0, + expectData: shouldExistDir, + }, + createDone(1500), + ); + testCwd( + { cwd: Bun.pathToFileURL(tmpdir.path) }, + { + expectPidType: "number", + expectCode: 0, + expectData: platformTmpDir, + }, + createDone(1500), + ); + }); + + it.skip("shouldn't try to chdir to an invalid cwd", done => { + const createDone = createDoneDotAll(done); + // Spawn() shouldn't try to chdir() to invalid arg, so this should just work + testCwd({ cwd: "" }, { expectPidType: "number" }, createDone(1500)); + testCwd({ cwd: undefined }, { expectPidType: "number" }, createDone(1500)); + testCwd({ cwd: null }, { expectPidType: "number" }, createDone(1500)); + }); +}); + +describe("child_process default options", () => { + it("should use process.env as default env", done => { + globalThis.process.env.TMPDIR = platformTmpDir; + + let child = spawn("printenv", [], {}); + let response = ""; + + child.stdout.setEncoding("utf8"); + child.stdout.on("data", chunk => { + debug(`stdout: ${chunk}`); + response += chunk; + }); + + // NOTE: Original test used child.on("exit"), but this is unreliable + // because the process can exit before the stream is closed and the data is read + child.stdout.on("close", () => { + expect(response.includes(`TMPDIR=${platformTmpDir}`)).toBe(true); + done(); + }); + }); +}); + +describe("child_process double pipe", () => { + it("should allow two pipes to be used at once", done => { + // const { mustCallAtLeast, mustCall } = createCallCheckCtx(done); + const mustCallAtLeast = fn => fn; + const mustCall = fn => fn; + let grep, sed, echo; + grep = spawn("grep", ["o"], { stdio: ["pipe", "pipe", "pipe"] }); + sed = spawn("sed", ["s/o/O/"]); + echo = spawn("echo", ["hello\nnode\nand\nworld\n"]); + + // pipe grep | sed + grep.stdout.on( + "data", + mustCallAtLeast(data => { + debug(`grep stdout ${data.length}`); + if (!sed.stdin.write(data)) { + grep.stdout.pause(); + } + }), + ); + + // print sed's output + sed.stdout.on( + "data", + mustCallAtLeast(data => { + result += data.toString("utf8"); + debug(data); + }), + ); + + echo.stdout.on( + "data", + mustCallAtLeast(data => { + debug(`grep stdin write ${data.length}`); + if (!grep.stdin.write(data)) { + debug("echo stdout pause"); + echo.stdout.pause(); + } + }), + ); + + // TODO(Derrick): We don't implement the full API for this yet, + // So stdin has no 'drain' event. + // TODO(@jasnell): This does not appear to ever be + // emitted. It's not clear if it is necessary. + grep.stdin.on("drain", () => { + debug("echo stdout resume"); + echo.stdout.resume(); + }); + + // Propagate end from echo to grep + echo.stdout.on( + "end", + mustCall(() => { + debug("echo stdout end"); + grep.stdin.end(); + }), + ); + + echo.on( + "exit", + mustCall(() => { + debug("echo exit"); + }), + ); + + grep.on( + "exit", + mustCall(() => { + debug("grep exit"); + }), + ); + + sed.on( + "exit", + mustCall(() => { + debug("sed exit"); + }), + ); + + // TODO(@jasnell): This does not appear to ever be + // emitted. It's not clear if it is necessary. + sed.stdin.on("drain", () => { + grep.stdout.resume(); + }); + + // Propagate end from grep to sed + grep.stdout.on( + "end", + mustCall(() => { + debug("grep stdout end"); + sed.stdin.end(); + }), + ); + + let result = ""; + + sed.stdout.on( + "end", + mustCall(() => { + debug("result: " + result); + strictEqual(result, `hellO\nnOde\nwOrld\n`); + done(); + }), + ); + }); +}); |