Files
bun.sh/test/bun.js/spawn.test.ts
Jarred Sumner b18e4064a2 Make node streams faster (#1502)
* Make node streams faster

* Fix for macOS, improve performance, handle ref and unref

Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
2022-11-13 19:14:44 -08:00

328 lines
9.2 KiB
TypeScript

import { readableStreamToText, spawn, spawnSync, write } from "bun";
import { describe, expect, it } from "bun:test";
import { gcTick as _gcTick } from "./gc";
import { rmdirSync, unlinkSync, rmSync, writeFileSync } from "node:fs";
for (let [gcTick, label] of [
[_gcTick, "gcTick"],
[() => {}, "no gc tick"],
] as const) {
describe(label, () => {
describe("spawnSync", () => {
const hugeString = "hello".repeat(10000).slice();
it("as an array", () => {
const { stdout } = spawnSync(["echo", "hi"]);
// stdout is a Buffer
const text = stdout!.toString();
expect(text).toBe("hi\n");
});
it("Uint8Array works as stdin", async () => {
const { stdout, stderr } = spawnSync({
cmd: ["cat"],
stdin: new TextEncoder().encode(hugeString),
});
expect(stdout!.toString()).toBe(hugeString);
expect(stderr!.byteLength).toBe(0);
});
it("check exit code", async () => {
const { exitCode: exitCode1 } = spawnSync({
cmd: ["ls"],
});
const { exitCode: exitCode2 } = spawnSync({
cmd: ["false"],
});
expect(exitCode1).toBe(0);
expect(exitCode2).toBe(1);
});
});
describe("spawn", () => {
const hugeString = "hello".repeat(10000).slice();
it("as an array", async () => {
const { stdout, exited } = spawn(["echo", "hello"], {
stdout: "pipe",
});
gcTick();
expect(await new Response(stdout).text()).toBe("hello\n");
});
it("as an array with options object", async () => {
const { stdout } = spawn(["printenv", "FOO"], {
cwd: "/tmp",
env: {
...process.env,
FOO: "bar",
},
stdin: null,
stdout: "pipe",
stderr: "inherit",
});
const text = await new Response(stdout).text();
expect(text).toBe("bar\n");
});
it("Uint8Array works as stdin", async () => {
rmSync("/tmp/out.123.txt", { force: true });
gcTick();
const { exited } = spawn({
cmd: ["cat"],
stdin: new TextEncoder().encode(hugeString),
stdout: Bun.file("/tmp/out.123.txt"),
});
await exited;
expect(await Bun.file("/tmp/out.123.txt").text()).toBe(hugeString);
});
it("check exit code", async () => {
const exitCode1 = await spawn({
cmd: ["ls"],
}).exited;
const exitCode2 = await spawn({
cmd: ["false"],
}).exited;
expect(exitCode1).toBe(0);
expect(exitCode2).toBe(1);
});
it("nothing to stdout and sleeping doesn't keep process open 4ever", async () => {
const proc = spawn({
cmd: ["sleep", "0.1"],
});
for await (const _ of proc.stdout!) {
throw new Error("should not happen");
}
});
it("check exit code from onExit", async () => {
var exitCode1, exitCode2;
await new Promise<void>((resolve) => {
var counter = 0;
spawn({
cmd: ["ls"],
onExit(code) {
exitCode1 = code;
counter++;
if (counter === 2) {
resolve();
}
},
});
spawn({
cmd: ["false"],
onExit(code) {
exitCode2 = code;
counter++;
if (counter === 2) {
resolve();
}
},
});
});
expect(exitCode1).toBe(0);
expect(exitCode2).toBe(1);
});
it("Blob works as stdin", async () => {
rmSync("/tmp/out.123.txt", { force: true });
gcTick();
const { exited } = spawn({
cmd: ["cat"],
stdin: new Blob([new TextEncoder().encode(hugeString)]),
stdout: Bun.file("/tmp/out.123.txt"),
});
await exited;
expect(await Bun.file("/tmp/out.123.txt").text()).toBe(hugeString);
});
it("Bun.file() works as stdout", async () => {
rmSync("/tmp/out.123.txt", { force: true });
gcTick();
const { exited } = spawn({
cmd: ["echo", "hello"],
stdout: Bun.file("/tmp/out.123.txt"),
});
await exited;
gcTick();
expect(await Bun.file("/tmp/out.123.txt").text()).toBe("hello\n");
});
it("Bun.file() works as stdin", async () => {
await write(Bun.file("/tmp/out.456.txt"), "hello there!");
gcTick();
const { stdout } = spawn({
cmd: ["cat"],
stdout: "pipe",
stdin: Bun.file("/tmp/out.456.txt"),
});
gcTick();
expect(await readableStreamToText(stdout!)).toBe("hello there!");
});
it("Bun.file() works as stdin and stdout", async () => {
writeFileSync("/tmp/out.456.txt", "hello!");
gcTick();
writeFileSync("/tmp/out.123.txt", "wrong!");
gcTick();
const { exited } = spawn({
cmd: ["cat"],
stdout: Bun.file("/tmp/out.123.txt"),
stdin: Bun.file("/tmp/out.456.txt"),
});
gcTick();
await exited;
expect(await Bun.file("/tmp/out.456.txt").text()).toBe("hello!");
gcTick();
expect(await Bun.file("/tmp/out.123.txt").text()).toBe("hello!");
});
it("stdout can be read", async () => {
await Bun.write("/tmp/out.txt", hugeString);
gcTick();
const { stdout } = spawn({
cmd: ["cat", "/tmp/out.txt"],
stdout: "pipe",
});
gcTick();
const text = await readableStreamToText(stdout!);
gcTick();
expect(text).toBe(hugeString);
});
it("kill(1) works", async () => {
const process = spawn({
cmd: ["bash", "-c", "sleep 1000"],
stdout: "pipe",
});
gcTick();
const prom = process.exited;
process.kill(1);
await prom;
});
it("kill() works", async () => {
const process = spawn({
cmd: ["bash", "-c", "sleep 1000"],
stdout: "pipe",
});
gcTick();
const prom = process.exited;
process.kill();
await prom;
});
it("stdin can be read and stdout can be written", async () => {
const proc = spawn({
cmd: ["bash", import.meta.dir + "/bash-echo.sh"],
stdout: "pipe",
stdin: "pipe",
stderr: "inherit",
});
proc.stdin!.write(hugeString);
await proc.stdin!.end();
var text = "";
var reader = proc.stdout!.getReader();
var done = false;
while (!done) {
var { value, done } = await reader.read();
if (value) text += new TextDecoder().decode(value);
if (done && text.length === 0) {
reader.releaseLock();
reader = proc.stdout!.getReader();
done = false;
}
}
expect(text.trim().length).toBe(hugeString.length);
expect(text.trim()).toBe(hugeString);
gcTick();
await proc.exited;
});
describe("pipe", () => {
function huge() {
return spawn({
cmd: ["echo", hugeString],
stdout: "pipe",
stdin: "pipe",
stderr: "inherit",
});
}
function helloWorld() {
return spawn({
cmd: ["echo", "hello"],
stdout: "pipe",
stdin: "pipe",
});
}
const fixtures = [
[helloWorld, "hello"],
[huge, hugeString],
] as const;
for (const [callback, fixture] of fixtures) {
describe(fixture.slice(0, 12), () => {
describe("should should allow reading stdout", () => {
it("before exit", async () => {
const process = callback();
const output = await readableStreamToText(process.stdout!);
const expected = fixture + "\n";
expect(output.length).toBe(expected.length);
expect(output).toBe(expected);
await process.exited;
});
it("before exit (chunked)", async () => {
const process = callback();
var output = "";
var reader = process.stdout!.getReader();
var done = false;
while (!done) {
var { value, done } = await reader.read();
if (value) output += new TextDecoder().decode(value);
}
const expected = fixture + "\n";
expect(output.length).toBe(expected.length);
expect(output).toBe(expected);
await process.exited;
});
it("after exit", async () => {
const process = callback();
await process.stdin!.end();
const output = await readableStreamToText(process.stdout!);
const expected = fixture + "\n";
expect(output.length).toBe(expected.length);
expect(output).toBe(expected);
await process.exited;
});
});
});
}
});
});
});
}