aboutsummaryrefslogtreecommitdiff
path: root/test/js/node/child_process/child_process-node.test.js
diff options
context:
space:
mode:
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.js481
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();
+ }),
+ );
+ });
+});