aboutsummaryrefslogtreecommitdiff
path: root/test/bun.js
diff options
context:
space:
mode:
Diffstat (limited to 'test/bun.js')
-rw-r--r--test/bun.js/child_process-node.test.js570
-rw-r--r--test/bun.js/child_process.test.ts279
2 files changed, 849 insertions, 0 deletions
diff --git a/test/bun.js/child_process-node.test.js b/test/bun.js/child_process-node.test.js
new file mode 100644
index 000000000..10135affa
--- /dev/null
+++ b/test/bun.js/child_process-node.test.js
@@ -0,0 +1,570 @@
+import { describe, expect, it } from "bun:test";
+import { ChildProcess, spawn, exec } from "node:child_process";
+import { EOL } from "node:os";
+import assertNode from "node:assert";
+import { inspect } from "node:util";
+
+const debug = console.log;
+
+// 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 = {
+ // // TODO: Fix the implementations of these functions, they may be ruining everything...
+ // mustCallAtLeast: function mustCallAtLeast(callback) {
+ // return (...args) => {
+ // callback(...args);
+ // expect(true).toBe(true);
+ // };
+ // },
+ // mustCall: function mustCall(callback) {
+ // return (...args) => {
+ // callback(...args);
+ // expect(true).toBe(true);
+ // };
+ // },
+ pwdCommand: ["pwd", []],
+};
+
+const mustCallChecks = [];
+
+function runCallChecks(exitCode) {
+ if (exitCode !== 0) return;
+
+ const failed = mustCallChecks.filter(function (context) {
+ if ("minimum" in context) {
+ context.messageSegment = `at least ${context.minimum}`;
+ return context.actual < context.minimum;
+ }
+ context.messageSegment = `exactly ${context.exact}`;
+ return context.actual !== context.exact;
+ });
+
+ failed.forEach(function (context) {
+ console.log(
+ "Mismatched %s function calls. Expected %s, actual %d.",
+ context.name,
+ context.messageSegment,
+ context.actual
+ );
+ console.log(context.stack.split("\n").slice(2).join("\n"));
+ });
+
+ if (failed.length) process.exit(1);
+}
+
+function mustCall(fn, exact) {
+ return _mustCallInner(fn, exact, "exact");
+}
+
+function mustSucceed(fn, exact) {
+ return mustCall(function (err, ...args) {
+ assert.ifError(err);
+ if (typeof fn === "function") return fn.apply(this, args);
+ }, exact);
+}
+
+function mustCallAtLeast(fn, minimum) {
+ return _mustCallInner(fn, minimum, "minimum");
+}
+
+function _mustCallInner(fn, criteria = 1, field) {
+ if (process._exiting)
+ throw new Error("Cannot use common.mustCall*() in process exit handler");
+ if (typeof fn === "number") {
+ criteria = fn;
+ fn = noop;
+ } else if (fn === undefined) {
+ fn = noop;
+ }
+
+ if (typeof criteria !== "number")
+ throw new TypeError(`Invalid ${field} value: ${criteria}`);
+
+ const context = {
+ [field]: criteria,
+ actual: 0,
+ stack: inspect(new Error()),
+ name: fn.name || "<anonymous>",
+ };
+
+ // Add the exit listener only once to avoid listener leak warnings
+ if (mustCallChecks.length === 0) process.on("exit", runCallChecks);
+
+ mustCallChecks.push(context);
+
+ const _return = function () {
+ // eslint-disable-line func-style
+ context.actual++;
+ return fn.apply(this, arguments);
+ };
+ // Function instances have own properties that may be relevant.
+ // Let's replicate those properties to the returned function.
+ // Refs: https://tc39.es/ecma262/#sec-function-instances
+ Object.defineProperties(_return, {
+ name: {
+ value: fn.name,
+ writable: false,
+ enumerable: false,
+ configurable: true,
+ },
+ length: {
+ value: fn.length,
+ writable: false,
+ enumerable: false,
+ configurable: true,
+ },
+ });
+ return _return;
+}
+
+const strictEqual = (...args) => {
+ let error = null;
+ try {
+ assertNode.strictEqual(...args);
+ } catch (err) {
+ error = err;
+ }
+ expect(error).toBe(null);
+};
+
+const throws = (...args) => {
+ let error = null;
+ try {
+ assertNode.throws(...args);
+ } catch (err) {
+ error = err;
+ }
+ expect(error).toBe(null);
+};
+
+const assert = (...args) => {
+ let error = null;
+ try {
+ assertNode(...args);
+ } catch (err) {
+ error = err;
+ }
+ expect(error).toBe(null);
+};
+
+const assertOk = (...args) => {
+ let error = null;
+ try {
+ assertNode.ok(...args);
+ } catch (err) {
+ error = err;
+ }
+ expect(error).toBe(null);
+};
+
+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: "TypeError",
+ // 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: "TypeError",
+ // 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: "TypeError",
+ // 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: "TypeError",
+ // message:
+ // 'The "options.args" property must be an instance of Array.' +
+ // common.invalidArgTypeHelper(args),
+ }
+ );
+ });
+ });
+});
+
+describe("ChildProcess.spawn", () => {
+ const child = new ChildProcess();
+ child.spawn({
+ file: "bun",
+ // file: process.execPath,
+ args: ["--interactive"],
+ cwd: process.cwd(),
+ stdio: "pipe",
+ });
+
+ it("should spawn a process", () => {
+ // Test that we can call spawn
+
+ strictEqual(Object.hasOwn(child, "pid"), true);
+ assert(Number.isInteger(child.pid));
+ });
+
+ it("should throw error on invalid signal", () => {
+ // Try killing with invalid signal
+ throws(
+ () => {
+ child.kill("foo");
+ },
+ { code: "ERR_UNKNOWN_SIGNAL", name: "TypeError" }
+ );
+ });
+
+ it("should die when killed", () => {
+ strictEqual(child.kill(), true);
+ });
+});
+
+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.
+ const original = ChildProcess.prototype.spawn;
+
+ ChildProcess.prototype.spawn = function () {
+ const err = original.apply(this, arguments);
+
+ this.stdout.destroy();
+ this.stderr.destroy();
+ this.stdout = null;
+ this.stderr = null;
+
+ return err;
+ };
+
+ function createChild(options, callback) {
+ const cmd = `"${process.execPath}" "${import.meta.path}" child`;
+ return exec(cmd, options, mustCall(callback));
+ }
+
+ it("should handle normal execution of child process", () => {
+ createChild({}, (err, stdout, stderr) => {
+ strictEqual(err, null);
+ strictEqual(stdout, "");
+ strictEqual(stderr, "");
+ });
+ });
+
+ it("should handle error event of child process", () => {
+ const error = new Error("foo");
+ const child = createChild({}, (err, stdout, stderr) => {
+ strictEqual(err, error);
+ strictEqual(stdout, "");
+ strictEqual(stderr, "");
+ });
+
+ child.emit("error", error);
+ });
+
+ it("should handle killed process", () => {
+ createChild({ timeout: 1 }, (err, stdout, stderr) => {
+ strictEqual(err.killed, true);
+ strictEqual(stdout, "");
+ strictEqual(stderr, "");
+ });
+ });
+
+ ChildProcess.prototype.spawn = original;
+});
+
+describe("child_process cwd", () => {
+ const tmpdir = { path: Bun.env.TMPDIR };
+
+ // 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) {
+ 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", function (chunk) {
+ data += chunk;
+ });
+
+ // 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", function (code, signal) {
+ strictEqual(code, expectCode).bind(this);
+ });
+
+ child.on(
+ "close",
+ mustCall(function () {
+ 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", () => {
+ // Assume these exist, and 'pwd' gives us the right directory back
+ testCwd({ cwd: tmpdir.path }, "number", 0, tmpdir.path);
+ const shouldExistDir = "/dev";
+ testCwd({ cwd: shouldExistDir }, "number", 0, shouldExistDir);
+ testCwd({ cwd: Bun.pathToFileURL(tmpdir.path) }, "number", 0, tmpdir.path);
+ });
+
+ it("shouldn't try to chdir to an invalid cwd", () => {
+ // Spawn() shouldn't try to chdir() to invalid arg, so this should just work
+ testCwd({ cwd: "" }, "number");
+ testCwd({ cwd: undefined }, "number");
+ testCwd({ cwd: null }, "number");
+ });
+});
+
+describe("child_process default options", () => {
+ process.env.HELLO = "WORLD";
+
+ let child = spawn("/usr/bin/env", [], {});
+ let response = "";
+
+ child.stdout.setEncoding("utf8");
+
+ it("should use process.env as default env", () => {
+ child.stdout.on("data", function (chunk) {
+ debug(`stdout: ${chunk}`);
+ response += chunk;
+ });
+
+ process.on("exit", function () {
+ assertOk(
+ response.includes("HELLO=WORLD"),
+ "spawn did not use process.env as default " +
+ `(process.env.HELLO = ${process.env.HELLO})`
+ );
+ });
+ });
+
+ delete process.env.HELLO;
+});
+
+describe("child_process double pipe", () => {
+ let grep, sed, echo;
+ grep = spawn("grep", ["o"]);
+ sed = spawn("sed", ["s/o/O/"]);
+ echo = spawn("echo", ["hello\nnode\nand\nworld\n"]);
+
+ it("should allow two pipes to be used at once", () => {
+ // pipe echo | grep
+ echo.stdout.on(
+ "data",
+ mustCallAtLeast((data) => {
+ debug(`grep stdin write ${data.length}`);
+ if (!grep.stdin.write(data)) {
+ 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", (data) => {
+ // echo.stdout.resume();
+ // });
+
+ // Propagate end from echo to grep
+ echo.stdout.on(
+ "end",
+ mustCall((code) => {
+ grep.stdin.end();
+ })
+ );
+
+ echo.on(
+ "exit",
+ mustCall(() => {
+ debug("echo exit");
+ })
+ );
+
+ grep.on(
+ "exit",
+ mustCall(() => {
+ debug("grep exit");
+ })
+ );
+
+ sed.on(
+ "exit",
+ mustCall(() => {
+ debug("sed exit");
+ })
+ );
+
+ // pipe grep | sed
+ grep.stdout.on(
+ "data",
+ mustCallAtLeast((data) => {
+ debug(`grep stdout ${data.length}`);
+ if (!sed.stdin.write(data)) {
+ grep.stdout.pause();
+ }
+ })
+ );
+
+ // // TODO(@jasnell): This does not appear to ever be
+ // // emitted. It's not clear if it is necessary.
+ // sed.stdin.on("drain", (data) => {
+ // grep.stdout.resume();
+ // });
+
+ // Propagate end from grep to sed
+ grep.stdout.on(
+ "end",
+ mustCall((code) => {
+ debug("grep stdout end");
+ sed.stdin.end();
+ })
+ );
+
+ let result = "";
+
+ // print sed's output
+ sed.stdout.on(
+ "data",
+ mustCallAtLeast((data) => {
+ result += data.toString("utf8", 0, data.length);
+ debug(data);
+ })
+ );
+
+ sed.stdout.on(
+ "end",
+ mustCall((code) => {
+ strictEqual(result, `hellO${EOL}nOde${EOL}wOrld${EOL}`);
+ })
+ );
+ });
+});
diff --git a/test/bun.js/child_process.test.ts b/test/bun.js/child_process.test.ts
new file mode 100644
index 000000000..fd1c27ae7
--- /dev/null
+++ b/test/bun.js/child_process.test.ts
@@ -0,0 +1,279 @@
+import { describe, it, expect } from "bun:test";
+import {
+ ChildProcess,
+ spawn,
+ execFile,
+ exec,
+ fork,
+ spawnSync,
+ execFileSync,
+ execSync,
+} from "node:child_process";
+
+// Semver regex: https://gist.github.com/jhorsman/62eeea161a13b80e39f5249281e17c39?permalink_comment_id=2896416#gistcomment-2896416
+// Not 100% accurate, but good enough for this test
+const SEMVER_REGEX =
+ /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-[a-zA-Z\d][-a-zA-Z.\d]*)?(\+[a-zA-Z\d][-a-zA-Z.\d]*)?$/;
+
+describe("ChildProcess.spawn()", () => {
+ it("should emit `spawn` on spawn", async () => {
+ const proc = new ChildProcess();
+ const result = await new Promise((resolve) => {
+ proc.on("spawn", () => {
+ resolve(true);
+ });
+ proc.spawn({ file: "bun", args: ["bun", "-v"] });
+ });
+ expect(result).toBe(true);
+ });
+
+ it("should emit `exit` when killed", async () => {
+ const proc = new ChildProcess();
+ const result = await new Promise((resolve) => {
+ proc.on("exit", () => {
+ resolve(true);
+ });
+
+ proc.spawn({ file: "bun", args: ["bun", "-v"] });
+ proc.kill();
+ });
+ expect(result).toBe(true);
+ });
+});
+
+describe("spawn()", () => {
+ it("should spawn a process", () => {
+ const child = spawn("echo", ["hello"]);
+ expect(!!child).toBe(true);
+ });
+
+ it("should disallow invalid filename", () => {
+ let child;
+ let child2;
+ try {
+ child = spawn(123);
+ child2 = spawn(["echo", "hello"]);
+ } catch (e) {
+ console.error(e);
+ }
+ expect(!!child).toBe(false);
+ expect(!!child2).toBe(false);
+ });
+
+ it("should allow stdout to be read via Node stream.Readable `data` events", async () => {
+ const child = spawn("bun", ["-v"]);
+ const result: string = await new Promise((resolve) => {
+ child.stdout.on("error", (e) => {
+ console.error(e);
+ });
+ child.stdout.on("data", (data) => {
+ console.log(`stdout: ${data}`);
+ resolve(data.toString());
+ });
+ child.stderr.on("data", (data) => {
+ console.log(`stderr: ${data}`);
+ });
+ });
+ expect(SEMVER_REGEX.test(result.trim())).toBe(true);
+ });
+
+ it("should allow stdout to be read via .read() API", async () => {
+ const child = spawn("bun", ["-v"]);
+ const result: string = await new Promise((resolve) => {
+ let finalData = "";
+ child.stdout.on("error", (e) => {
+ console.error(e);
+ });
+ child.stdout.on("readable", () => {
+ let data;
+
+ while ((data = child.stdout.read()) !== null) {
+ finalData += data.toString();
+ }
+ resolve(finalData);
+ });
+ });
+ expect(SEMVER_REGEX.test(result.trim())).toBe(true);
+ });
+
+ it("should accept stdio option with 'ignore' for no stdio fds", async () => {
+ const child1 = spawn("bun", ["-v"], {
+ stdio: "ignore",
+ });
+ const child2 = spawn("bun", ["-v"], {
+ stdio: ["ignore", "ignore", "ignore"],
+ });
+
+ expect(!!child1).toBe(true);
+ expect(child1.stdin).toBe(null);
+ expect(child1.stdout).toBe(null);
+ expect(child1.stderr).toBe(null);
+
+ expect(!!child2).toBe(true);
+ expect(child2.stdin).toBe(null);
+ expect(child2.stdout).toBe(null);
+ expect(child2.stderr).toBe(null);
+ });
+
+ it("should allow us to set cwd", async () => {
+ const child = spawn("pwd", { cwd: process.env.TMPDIR });
+ const result: string = await new Promise((resolve) => {
+ child.stdout.on("data", (data) => {
+ resolve(data.toString());
+ });
+ });
+ const platformTmpDir = `${process.platform === "darwin" ? "/private" : ""}${
+ process.env.TMPDIR
+ }`;
+ expect(`${result.trim()}/`).toBe(platformTmpDir);
+ });
+
+ it("should allow us to write to stdin", async () => {
+ const child = spawn("tee");
+ const result: string = await new Promise((resolve) => {
+ child.stdin.write("hello");
+ child.stdout.on("data", (data) => {
+ resolve(data.toString());
+ });
+ });
+ expect(result.trim()).toBe("hello");
+ });
+
+ it("should allow us to timeout hanging processes", async () => {
+ const child = spawn("sleep", ["2"], { timeout: 400 });
+ const start = performance.now();
+ let end;
+ await new Promise((resolve) => {
+ child.on("exit", () => {
+ end = performance.now();
+ resolve(true);
+ });
+ });
+ expect(end - start < 2000).toBe(true);
+ });
+
+ it("should allow us to set env", async () => {
+ const child = spawn("env", { env: { TEST: "test" } });
+ const result: string = await new Promise((resolve) => {
+ child.stdout.on("data", (data) => {
+ resolve(data.toString());
+ });
+ });
+ expect(/TEST\=test/.test(result)).toBe(true);
+ });
+
+ it("should allow explicit setting of argv0", async () => {
+ const child = spawn("node", ["--help"], { argv0: "bun" });
+ const result: string = await new Promise((resolve) => {
+ let msg;
+ child.stdout.on("data", (data) => {
+ msg += data.toString();
+ });
+
+ child.stdout.on("close", () => {
+ resolve(msg);
+ });
+ });
+ expect(/bun:/.test(result)).toBe(true);
+ });
+
+ it("should allow us to spawn in a shell", async () => {
+ const result1: string = await new Promise((resolve) => {
+ const child1 = spawn("echo", ["$0"], { shell: true });
+ child1.stdout.on("data", (data) => {
+ resolve(data.toString());
+ });
+ });
+ const result2: string = await new Promise((resolve) => {
+ const child2 = spawn("echo", ["$0"], { shell: "bash" });
+ child2.stdout.on("data", (data) => {
+ resolve(data.toString());
+ });
+ });
+ expect(result1.trim()).toBe("/bin/sh");
+ expect(result2.trim()).toBe("/bin/bash");
+ });
+ it("should spawn a process synchronously", () => {
+ const { stdout } = spawnSync("echo", ["hello"], { encoding: "utf8" });
+ expect(stdout.trim()).toBe("hello");
+ });
+});
+
+describe("execFile()", () => {
+ it("should execute a file", async () => {
+ const result: Buffer = await new Promise((resolve, reject) => {
+ execFile("bun", ["-v"], (error, stdout, stderr) => {
+ if (error) {
+ reject(error);
+ }
+ resolve(stdout);
+ });
+ });
+ expect(SEMVER_REGEX.test(result.toString().trim())).toBe(true);
+ });
+});
+
+describe("exec()", () => {
+ it("should execute a command in a shell", async () => {
+ const result: Buffer = await new Promise((resolve, reject) => {
+ exec("bun -v", (error, stdout, stderr) => {
+ if (error) {
+ reject(error);
+ }
+ resolve(stdout);
+ });
+ });
+ expect(SEMVER_REGEX.test(result.toString().trim())).toBe(true);
+ });
+});
+
+describe("fork()", () => {
+ it("should throw an error when used", () => {
+ let err;
+ try {
+ fork("index.js");
+ } catch (e) {
+ err = e;
+ }
+ expect(err instanceof Error).toBe(true);
+ });
+});
+
+describe("spawnSync()", () => {
+ it("should spawn a process synchronously", () => {
+ const { stdout } = spawnSync("echo", ["hello"], { encoding: "utf8" });
+ expect(stdout.trim()).toBe("hello");
+ });
+});
+
+describe("execFileSync()", () => {
+ it("should execute a file synchronously", () => {
+ const result = execFileSync("bun", ["-v"], { encoding: "utf8" });
+ expect(SEMVER_REGEX.test(result.trim())).toBe(true);
+ });
+});
+
+describe("execSync()", () => {
+ it("should execute a command in the shell synchronously", () => {
+ const result = execSync("bun -v", { encoding: "utf8" });
+ expect(SEMVER_REGEX.test(result.trim())).toBe(true);
+ });
+});
+
+// describe("Bun.spawn()", () => {
+// it("should return exit code 0 on successful execution", async () => {
+// const result = await new Promise((resolve) => {
+// Bun.spawn({
+// cmd: ["echo", "hello"],
+// encoding: "utf8",
+// onExit: (code) => resolve(code),
+// stdout: "inherit",
+// });
+// });
+// expect(result).toBe(0);
+// });
+// it("should fail when given an invalid cwd", () => {
+// const child = Bun.spawn({ cmd: ["echo", "hello"], cwd: "/invalid" });
+// expect(child.pid).toBe(undefined);
+// });
+// });