mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 10:58:56 +00:00
* Fix TS types, improve child_process types * Add prettier * Add ArrayBuffer types * Add namespace Bun, improve types for SharedArrayBuffer, add toStrictEqual * Improve types, add test files for types * Update type tests * Fix typo * Add stdio * Stdio types * Use latest setup-bun * Update action * Update action * Update action Co-authored-by: Colin McDonnell <colinmcd@alum.mit.edu> Co-authored-by: Ashcon Partovi <ashcon@partovi.net>
354 lines
9.5 KiB
TypeScript
354 lines
9.5 KiB
TypeScript
import { describe, it as it_, expect as expect_ } from "bun:test";
|
|
import { gcTick } from "gc";
|
|
import {
|
|
ChildProcess,
|
|
spawn,
|
|
execFile,
|
|
exec,
|
|
fork,
|
|
spawnSync,
|
|
execFileSync,
|
|
execSync,
|
|
} from "node:child_process";
|
|
import { tmpdir } from "node:os";
|
|
|
|
const expect: typeof expect_ = (actual: unknown) => {
|
|
gcTick();
|
|
const ret = expect_(actual);
|
|
gcTick();
|
|
return ret;
|
|
};
|
|
|
|
const it: typeof it_ = (label, fn) => {
|
|
const hasDone = fn.length === 1;
|
|
if (fn.constructor.name === "AsyncFunction" && hasDone) {
|
|
return it_(label, async (done) => {
|
|
gcTick();
|
|
await fn(done);
|
|
gcTick();
|
|
});
|
|
} else if (hasDone) {
|
|
return it_(label, (done) => {
|
|
gcTick();
|
|
fn(done);
|
|
gcTick();
|
|
});
|
|
} else if (fn.constructor.name === "AsyncFunction") {
|
|
return it_(label, async () => {
|
|
gcTick();
|
|
await fn(() => {});
|
|
gcTick();
|
|
});
|
|
} else {
|
|
return it_(label, () => {
|
|
gcTick();
|
|
fn(() => {});
|
|
gcTick();
|
|
});
|
|
}
|
|
};
|
|
|
|
const debug = process.env.DEBUG ? console.log : () => {};
|
|
|
|
const platformTmpDir = require("fs").realpathSync(tmpdir());
|
|
|
|
// 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 {
|
|
// @ts-ignore
|
|
child = spawn(123);
|
|
// @ts-ignore
|
|
child2 = spawn(["echo", "hello"]);
|
|
} catch (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) => {
|
|
debug(`stdout: ${data}`);
|
|
resolve(data.toString());
|
|
});
|
|
child.stderr.on("data", (data) => {
|
|
debug(`stderr: ${data}`);
|
|
});
|
|
});
|
|
expect(SEMVER_REGEX.test(result.trim())).toBe(true);
|
|
});
|
|
|
|
it("should allow stdout to be read via .read() API", async (done) => {
|
|
const child = spawn("bun", ["-v"]);
|
|
const result: string = await new Promise((resolve) => {
|
|
let finalData = "";
|
|
child.stdout.on("error", (e) => {
|
|
done(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);
|
|
done();
|
|
});
|
|
|
|
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: platformTmpDir });
|
|
const result: string = await new Promise((resolve) => {
|
|
child.stdout.on("data", (data) => {
|
|
resolve(data.toString());
|
|
});
|
|
});
|
|
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: 3 });
|
|
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 () => {
|
|
var resolve;
|
|
const promise = new Promise<string>((resolve1) => {
|
|
resolve = resolve1;
|
|
});
|
|
process.env.NO_COLOR = "1";
|
|
const child = spawn("node", ["--help"], { argv0: "bun" });
|
|
delete process.env.NO_COLOR;
|
|
let msg = "";
|
|
|
|
child.stdout.on("data", (data) => {
|
|
msg += data.toString();
|
|
});
|
|
|
|
child.stdout.on("close", () => {
|
|
resolve(msg);
|
|
});
|
|
|
|
const result = await promise;
|
|
expect(/Open bun's Discord server/.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(Bun.which("sh"));
|
|
expect(result2.trim()).toBe(Bun.which("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"],
|
|
{ encoding: "buffer" },
|
|
(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", { encoding: "buffer" }, (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);
|
|
});
|
|
|
|
it("should allow us to pass input to the command", () => {
|
|
const result = execFileSync(
|
|
"node",
|
|
[import.meta.dir + "/spawned-child.js", "STDIN"],
|
|
{
|
|
input: "hello world!",
|
|
encoding: "utf8",
|
|
},
|
|
);
|
|
expect(result.trim()).toBe("data: hello world!");
|
|
});
|
|
});
|
|
|
|
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 proc = Bun.spawn({
|
|
cmd: ["echo", "hello"],
|
|
stdout: "pipe",
|
|
});
|
|
|
|
for await (const chunk of proc.stdout!) {
|
|
const text = new TextDecoder().decode(chunk);
|
|
expect(text.trim()).toBe("hello");
|
|
}
|
|
|
|
const result = await new Promise((resolve) => {
|
|
const maybeExited = Bun.peek(proc.exited);
|
|
if (maybeExited === proc.exited) {
|
|
proc.exited.then((code) => resolve(code));
|
|
} else {
|
|
resolve(maybeExited);
|
|
}
|
|
});
|
|
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);
|
|
// });
|
|
});
|