mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 10:58:56 +00:00
457 lines
14 KiB
TypeScript
457 lines
14 KiB
TypeScript
import { semver, write } from "bun";
|
|
import { afterAll, beforeEach, describe, expect, it } from "bun:test";
|
|
import fs from "fs";
|
|
import { bunEnv, bunExe, isWindows, nodeExe, runBunInstall, shellExe, tmpdirSync } from "harness";
|
|
import { ChildProcess, exec, execFile, execFileSync, execSync, spawn, spawnSync } from "node:child_process";
|
|
import { promisify } from "node:util";
|
|
import path from "path";
|
|
const debug = process.env.DEBUG ? console.log : () => {};
|
|
|
|
const originalProcessEnv = process.env;
|
|
beforeEach(() => {
|
|
process.env = { ...bunEnv };
|
|
// Github actions might filter these out
|
|
for (const key in process.env) {
|
|
if (key.toUpperCase().startsWith("TLS_")) {
|
|
delete process.env[key];
|
|
}
|
|
}
|
|
});
|
|
|
|
afterAll(() => {
|
|
process.env = originalProcessEnv;
|
|
});
|
|
|
|
function isValidSemver(string: string): boolean {
|
|
const cmp = string.replaceAll("-debug", "").trim();
|
|
const valid = semver.satisfies(cmp, "*");
|
|
|
|
if (!valid) {
|
|
console.error(`Invalid semver: ${JSON.stringify(cmp)}`);
|
|
}
|
|
|
|
return valid;
|
|
}
|
|
|
|
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);
|
|
});
|
|
// @ts-ignore
|
|
proc.spawn({ file: bunExe(), args: [bunExe(), "-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);
|
|
});
|
|
// @ts-ignore
|
|
proc.spawn({ file: bunExe(), args: [bunExe(), "-v"] });
|
|
proc.kill();
|
|
});
|
|
expect(result).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("spawn()", () => {
|
|
it("should spawn a process", () => {
|
|
const child = spawn("bun", ["-v"]);
|
|
expect(!!child).toBe(true);
|
|
});
|
|
|
|
it("should use cwd from options to search for executables", async () => {
|
|
const tmpdir = tmpdirSync();
|
|
await Promise.all([
|
|
write(
|
|
path.join(tmpdir, "package.json"),
|
|
JSON.stringify({
|
|
name: "foo",
|
|
dependencies: {
|
|
foo: "file:foo-1.2.3.tgz",
|
|
},
|
|
}),
|
|
),
|
|
fs.promises.cp(path.join(import.meta.dir, "fixtures", "foo-1.2.3.tgz"), path.join(tmpdir, "foo-1.2.3.tgz")),
|
|
]);
|
|
await runBunInstall(bunEnv, tmpdir);
|
|
|
|
const { exitCode, out } = await new Promise<any>(resolve => {
|
|
const child = spawn("./node_modules/.bin/foo", { cwd: tmpdir, env: bunEnv });
|
|
child.on("exit", async exitCode => {
|
|
const out = await new Response(child.stdout).text();
|
|
resolve({ exitCode, out });
|
|
});
|
|
});
|
|
expect(out).toBe("hello bun!\n");
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
it("should disallow invalid filename", () => {
|
|
// @ts-ignore
|
|
expect(() => spawn(123)).toThrow({
|
|
message: 'The "file" argument must be of type string. Received 123',
|
|
code: "ERR_INVALID_ARG_TYPE",
|
|
});
|
|
});
|
|
|
|
it("should allow stdout to be read via Node stream.Readable `data` events", async () => {
|
|
const child = spawn(bunExe(), ["-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(isValidSemver(result.trim().replace("-debug", ""))).toBe(true);
|
|
});
|
|
|
|
it("should allow stdout to be read via .read() API", async () => {
|
|
const child = spawn(bunExe(), ["-v"]);
|
|
const result: string = await new Promise((resolve, reject) => {
|
|
let finalData = "";
|
|
child.stdout.on("error", e => {
|
|
reject(e);
|
|
});
|
|
child.stdout.on("readable", () => {
|
|
let data;
|
|
|
|
while ((data = child.stdout.read()) !== null) {
|
|
finalData += data.toString();
|
|
}
|
|
resolve(finalData);
|
|
});
|
|
});
|
|
expect(isValidSemver(result.trim())).toBe(true);
|
|
});
|
|
|
|
it("should accept stdio option with 'ignore' for no stdio fds", async () => {
|
|
const child1 = spawn(bunExe(), ["-v"], {
|
|
stdio: "ignore",
|
|
});
|
|
const child2 = spawn(bunExe(), ["-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 tmpdir = tmpdirSync();
|
|
const result: string = await new Promise(resolve => {
|
|
const child = spawn(bunExe(), ["-e", "console.log(process.cwd())"], { cwd: tmpdir, env: bunEnv });
|
|
child.stdout.on("data", data => {
|
|
resolve(data.toString());
|
|
});
|
|
});
|
|
expect(result.trim()).toBe(tmpdir);
|
|
});
|
|
|
|
it("should allow us to write to stdin", async () => {
|
|
const result: string = await new Promise(resolve => {
|
|
const child = spawn(bunExe(), ["-e", "process.stdin.pipe(process.stdout)"], { env: bunEnv });
|
|
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(shellExe(), ["-c", "sleep", "2"], { timeout: 3 });
|
|
const start = performance.now();
|
|
let end: number;
|
|
await new Promise(resolve => {
|
|
child.on("exit", () => {
|
|
end = performance.now();
|
|
resolve(true);
|
|
});
|
|
});
|
|
expect(end!).toBeDefined();
|
|
expect(end! - start < 2000).toBe(true);
|
|
});
|
|
|
|
it("should allow us to set env", async () => {
|
|
async function getChildEnv(env: any): Promise<object> {
|
|
const result: string = await new Promise(resolve => {
|
|
const child = spawn(bunExe(), ["-e", "process.stdout.write(JSON.stringify(process.env))"], { env });
|
|
child.stdout.on("data", data => {
|
|
resolve(data.toString());
|
|
});
|
|
});
|
|
return JSON.parse(result);
|
|
}
|
|
|
|
// on Windows, there's a set of environment variables which are always set
|
|
if (isWindows) {
|
|
expect(await getChildEnv({ TEST: "test" })).toMatchObject({ TEST: "test" });
|
|
expect(await getChildEnv({})).toMatchObject({});
|
|
expect(await getChildEnv(undefined)).not.toStrictEqual({});
|
|
expect(await getChildEnv(null)).not.toStrictEqual({});
|
|
} else {
|
|
expect(await getChildEnv({ TEST: "test" })).toStrictEqual({ TEST: "test" });
|
|
expect(await getChildEnv({})).toStrictEqual({});
|
|
expect(await getChildEnv(undefined)).toStrictEqual(process.env);
|
|
expect(await getChildEnv(null)).toStrictEqual(process.env);
|
|
}
|
|
});
|
|
|
|
it("should allow explicit setting of argv0", async () => {
|
|
var resolve: (_?: any) => void;
|
|
const promise = new Promise<string>(resolve1 => {
|
|
resolve = resolve1;
|
|
});
|
|
process.env.NO_COLOR = "1";
|
|
const node = nodeExe();
|
|
const bun = bunExe();
|
|
const child = spawn(
|
|
node || bun,
|
|
["-e", "console.log(JSON.stringify([process.argv0, fs.realpathSync(process.argv[0])]))"],
|
|
{
|
|
argv0: bun,
|
|
stdio: ["inherit", "pipe", "inherit"],
|
|
},
|
|
);
|
|
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(JSON.parse(result)).toStrictEqual([bun, fs.realpathSync(node || bun)]);
|
|
});
|
|
|
|
it("should allow us to spawn in the default shell", async () => {
|
|
const shellPath: string = await new Promise(resolve => {
|
|
const child = spawn("echo", [isWindows ? "$PSHOME" : "$SHELL"], { shell: true });
|
|
child.stdout.on("data", data => {
|
|
resolve(data.toString().trim());
|
|
});
|
|
});
|
|
|
|
// On Windows, the default shell is cmd.exe, which does not support this
|
|
if (isWindows) {
|
|
expect(shellPath).not.toBeEmpty();
|
|
} else {
|
|
expect(fs.existsSync(shellPath), `${shellPath} does not exist`).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("should allow us to spawn in a specified shell", async () => {
|
|
const shell = shellExe();
|
|
const shellPath: string = await new Promise(resolve => {
|
|
const child = spawn("echo", [isWindows ? "$PSHOME" : "$SHELL"], { shell });
|
|
child.stdout.on("data", data => {
|
|
resolve(data.toString().trim());
|
|
});
|
|
});
|
|
expect(fs.existsSync(shellPath), `${shellPath} does not exist`).toBe(true);
|
|
});
|
|
|
|
it("should spawn a process synchronously", () => {
|
|
const { stdout } = spawnSync("bun", ["-v"], { encoding: "utf8" });
|
|
expect(isValidSemver(stdout.trim())).toBe(true);
|
|
});
|
|
|
|
describe("stdio", () => {
|
|
it("ignore", () => {
|
|
const child = spawn(bunExe(), ["-v"], { stdio: "ignore" });
|
|
expect(!!child).toBe(true);
|
|
expect(child.stdout).toBeNull();
|
|
expect(child.stderr).toBeNull();
|
|
});
|
|
it("inherit", () => {
|
|
const child = spawn(bunExe(), ["-v"], { stdio: "inherit" });
|
|
expect(!!child).toBe(true);
|
|
expect(child.stdout).toBeNull();
|
|
expect(child.stderr).toBeNull();
|
|
});
|
|
it("pipe", () => {
|
|
const child = spawn(bunExe(), ["-v"], { stdio: "pipe" });
|
|
expect(!!child).toBe(true);
|
|
expect(child.stdout).not.toBeNull();
|
|
expect(child.stderr).not.toBeNull();
|
|
});
|
|
it.todo("overlapped", () => {
|
|
const child = spawn(bunExe(), ["-v"], { stdio: "overlapped" });
|
|
expect(!!child).toBe(true);
|
|
expect(child.stdout).not.toBeNull();
|
|
expect(child.stderr).not.toBeNull();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("execFile()", () => {
|
|
it("should execute a file", async () => {
|
|
const result: Buffer = await new Promise((resolve, reject) => {
|
|
execFile(bunExe(), ["-v"], { encoding: "buffer" }, (error, stdout, stderr) => {
|
|
if (error) {
|
|
reject(error);
|
|
}
|
|
resolve(stdout);
|
|
});
|
|
});
|
|
expect(isValidSemver(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(isValidSemver(result.toString().trim())).toBe(true);
|
|
});
|
|
|
|
it("should return an object w/ stdout and stderr when promisified", async () => {
|
|
const result = await promisify(exec)("bun -v");
|
|
expect(typeof result).toBe("object");
|
|
expect(typeof result.stdout).toBe("string");
|
|
expect(typeof result.stderr).toBe("string");
|
|
|
|
const { stdout, stderr } = result;
|
|
expect(isValidSemver(stdout.trim())).toBe(true);
|
|
expect(stderr.trim()).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("spawnSync()", () => {
|
|
it("should spawn a process synchronously", () => {
|
|
const { stdout } = spawnSync("bun", ["-v"], { encoding: "utf8" });
|
|
expect(isValidSemver(stdout.trim())).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("execFileSync()", () => {
|
|
it("should execute a file synchronously", () => {
|
|
const result = execFileSync(bunExe(), ["-v"], { encoding: "utf8", env: process.env });
|
|
expect(isValidSemver(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",
|
|
env: process.env,
|
|
});
|
|
expect(result.trim()).toBe("data: hello world!");
|
|
});
|
|
});
|
|
|
|
describe("execSync()", () => {
|
|
it("should execute a command in the shell synchronously", () => {
|
|
const result = execSync(bunExe() + " -v", { encoding: "utf8", env: bunEnv });
|
|
expect(isValidSemver(result.trim())).toBe(true);
|
|
});
|
|
});
|
|
|
|
it("should call close and exit before process exits", async () => {
|
|
const proc = Bun.spawn({
|
|
cmd: [bunExe(), path.join("fixtures", "child-process-exit-event.js")],
|
|
cwd: import.meta.dir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stdin: "inherit",
|
|
stderr: "inherit",
|
|
});
|
|
const data = await new Response(proc.stdout).text();
|
|
expect(data).toContain("closeHandler called");
|
|
expect(data).toContain("exithHandler called");
|
|
expect(await proc.exited).toBe(0);
|
|
});
|
|
|
|
it("it accepts stdio passthrough", async () => {
|
|
const package_dir = tmpdirSync();
|
|
|
|
await fs.promises.writeFile(
|
|
path.join(package_dir, "package.json"),
|
|
JSON.stringify({
|
|
"name": "npm-run-all-test",
|
|
"version": "1.0.0",
|
|
"type": "module",
|
|
"scripts": {
|
|
"all": "run-p echo-hello echo-world",
|
|
"echo-hello": "echo hello",
|
|
"echo-world": "echo world",
|
|
},
|
|
"devDependencies": {
|
|
"npm-run-all": "4.1.5",
|
|
},
|
|
}),
|
|
);
|
|
|
|
let { stdout, stderr, exited } = Bun.spawn({
|
|
cmd: [bunExe(), "install"],
|
|
cwd: package_dir,
|
|
stdio: ["inherit", "inherit", "inherit"],
|
|
env: bunEnv,
|
|
});
|
|
expect(await exited).toBe(0);
|
|
|
|
({ stdout, stderr, exited } = Bun.spawn({
|
|
cmd: [bunExe(), "--bun", "run", "all"],
|
|
cwd: package_dir,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
env: bunEnv,
|
|
}));
|
|
const [err, out, exitCode] = await Promise.all([new Response(stderr).text(), new Response(stdout).text(), exited]);
|
|
try {
|
|
// This command outputs in either `["hello", "world"]` or `["world", "hello"]` order.
|
|
expect([err.split("\n")[0], ...err.split("\n").slice(1, -1).sort(), err.split("\n").at(-1)]).toEqual([
|
|
"$ run-p echo-hello echo-world",
|
|
"$ echo hello",
|
|
"$ echo world",
|
|
"",
|
|
]);
|
|
expect(out.split("\n").slice(0, -1).sort()).toStrictEqual(["hello", "world"].sort());
|
|
expect(exitCode).toBe(0);
|
|
} catch (e) {
|
|
console.error({ exitCode });
|
|
console.log(err);
|
|
console.log(out);
|
|
throw e;
|
|
}
|
|
}, 10000);
|
|
|
|
it.if(!isWindows)("spawnSync correctly reports signal codes", () => {
|
|
const trapCode = `
|
|
process.kill(process.pid, "SIGTRAP");
|
|
`;
|
|
|
|
const { signal } = spawnSync(bunExe(), ["-e", trapCode]);
|
|
|
|
expect(signal).toBe("SIGTRAP");
|
|
});
|