initial commit

This commit is contained in:
i2p
2026-08-27 21:09:14 +00:00
commit a5b6d59437
12681 changed files with 3253832 additions and 0 deletions
@@ -0,0 +1,217 @@
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
exports[`bun <file.md> renders headings with underlines 1`] = `
"\x1B[1m\x1B[35mHeading 1\x1B[0m
\x1B[2m═════════\x1B[0m
\x1B[1m\x1B[36mHeading 2\x1B[0m
\x1B[2m─────────\x1B[0m
\x1B[1m\x1B[33mHeading 3\x1B[0m
body\x1B[0m
"
`;
exports[`bun <file.md> renders bold, italic, strikethrough, inline code 1`] = `
"\x1B[1mbold\x1B[22m \x1B[3mitalic\x1B[23m \x1B[9mstrike\x1B[29m \x1B[48;5;236m\x1B[38;5;215mcode\x1B[39m\x1B[49m regular\x1B[0m
"
`;
exports[`bun <file.md> renders ordered, unordered, and task lists 1`] = `
" \x1B[36m1. \x1B[0mfirst
\x1B[36m2. \x1B[0msecond
\x1B[36m3. \x1B[0mthird
\x1B[36m• \x1B[0mapple\x1B[0m
\x1B[36m• \x1B[0mbanana\x1B[0m
\x1B[36m• \x1B[0mcherry\x1B[0m
\x1B[2m☐ \x1B[0mtodo\x1B[0m
\x1B[32m☒ \x1B[0mdone\x1B[0m
"
`;
exports[`bun <file.md> renders blockquotes and nested blockquotes 1`] = `
"\x1B[38;5;242m│ \x1B[39mA quote\x1B[0m
\x1B[38;5;242m│\x1B[39m
\x1B[38;5;242m│ │ \x1B[39mNested quote\x1B[0m
"
`;
exports[`bun <file.md> renders horizontal rules 1`] = `
"above\x1B[0m
\x1B[2m────────────────────────────────────────────────────────────\x1B[0m
below\x1B[0m
"
`;
exports[`bun <file.md> renders fenced code block with JS syntax highlighting 1`] = `
"\x1B[2m┌─ \x1B[0m\x1B[2m\x1B[3mjs\x1B[0m
\x1B[2m│ \x1B[0m\x1B[0m\x1B[35mconst\x1B[0m name = \x1B[0m\x1B[32m"world"\x1B[0m\x1B[0m\x1B[2m;\x1B[0m
\x1B[2m│ \x1B[0mconsole\x1B[0m\x1B[3m\x1B[1m.log\x1B[0m(\x1B[0m\x1B[32m\`hello \x1B[0m\${name}\x1B[0m\x1B[32m\`\x1B[0m)\x1B[0m\x1B[2m;\x1B[0m
\x1B[2m└─\x1B[0m
"
`;
exports[`bun <file.md> renders fenced code block without language 1`] = `
"\x1B[2m\x1B[2m┌─\x1B[0m
\x1B[2m│ \x1B[0mplain text
\x1B[2m│ \x1B[0mno highlighting
\x1B[2m└─\x1B[0m
"
`;
exports[`bun <file.md> renders link text with url fallback when no TTY 1`] = `
"Visit \x1B[34m\x1B[4mBun\x1B[24m\x1B[39m\x1B[2m (https://bun.com\x1B[2m)\x1B[39m\x1B[22m today.\x1B[0m
"
`;
exports[`bun <file.md> renders link with text + url pair fallback 1`] = `
"see \x1B[34m\x1B[4mBun\x1B[24m\x1B[39m\x1B[2m (https://bun.com\x1B[2m)\x1B[39m\x1B[22m\x1B[0m
"
`;
exports[`bun <file.md> renders images as alt text with link 1`] = `
"\x1B[35m📷 an image\x1B[0m\x1B[0m
"
`;
exports[`bun <file.md> renders wikilinks 1`] = `
"see \x1B[34m[[SomePage]]\x1B[39m for more\x1B[0m
"
`;
exports[`bun <file.md> falls back to literal text when \`[[\` never closes 1`] = `
"foo [[bar baz\x1B[0m
"
`;
exports[`bun <file.md> renders simple table with alignment 1`] = `
"\x1B[2m┌───────┬─────┬──────┐\x1B[0m
\x1B[2m│\x1B[0m \x1B[1mName\x1B[0m \x1B[2m│\x1B[0m \x1B[1mAge\x1B[0m \x1B[2m│\x1B[0m \x1B[1mCity\x1B[0m \x1B[2m│\x1B[0m
\x1B[2m├───────┼─────┼──────┤\x1B[0m
\x1B[2m│\x1B[0m Alice \x1B[2m│\x1B[0m 30 \x1B[2m│\x1B[0m NYC \x1B[2m│\x1B[0m
\x1B[2m│\x1B[0m Bob \x1B[2m│\x1B[0m 25 \x1B[2m│\x1B[0m LA \x1B[2m│\x1B[0m
\x1B[2m└───────┴─────┴──────┘\x1B[0m
"
`;
exports[`bun <file.md> renders table with CJK multi-width graphemes 1`] = `
"\x1B[2m┌───────┬─────────┬──────┐\x1B[0m
\x1B[2m│\x1B[0m \x1B[1m名前\x1B[0m \x1B[2m│\x1B[0m \x1B[1m 言語\x1B[0m \x1B[2m│\x1B[0m \x1B[1m都市\x1B[0m \x1B[2m│\x1B[0m
\x1B[2m├───────┼─────────┼──────┤\x1B[0m
\x1B[2m│\x1B[0m 山田 \x1B[2m│\x1B[0m 日本語 \x1B[2m│\x1B[0m 東京 \x1B[2m│\x1B[0m
\x1B[2m│\x1B[0m Alice \x1B[2m│\x1B[0m English \x1B[2m│\x1B[0m NYC \x1B[2m│\x1B[0m
\x1B[2m└───────┴─────────┴──────┘\x1B[0m
"
`;
exports[`bun <file.md> renders table with emoji graphemes 1`] = `
"\x1B[2m┌────────┬────────┐\x1B[0m
\x1B[2m│\x1B[0m \x1B[1mStatus\x1B[0m \x1B[2m│\x1B[0m \x1B[1mLabel\x1B[0m \x1B[2m│\x1B[0m
\x1B[2m├────────┼────────┤\x1B[0m
\x1B[2m│\x1B[0m ✅ \x1B[2m│\x1B[0m pass \x1B[2m│\x1B[0m
\x1B[2m│\x1B[0m ❌ \x1B[2m│\x1B[0m fail \x1B[2m│\x1B[0m
\x1B[2m│\x1B[0m 🚀 \x1B[2m│\x1B[0m launch \x1B[2m│\x1B[0m
\x1B[2m└────────┴────────┘\x1B[0m
"
`;
exports[`bun <file.md> renders combining characters without breaking alignment 1`] = `
"\x1B[2m┌───────┬──────┐\x1B[0m
\x1B[2m│\x1B[0m \x1B[1mName\x1B[0m \x1B[2m│\x1B[0m \x1B[1mNote\x1B[0m \x1B[2m│\x1B[0m
\x1B[2m├───────┼──────┤\x1B[0m
\x1B[2m│\x1B[0m café \x1B[2m│\x1B[0m hot \x1B[2m│\x1B[0m
\x1B[2m│\x1B[0m naïve \x1B[2m│\x1B[0m ok \x1B[2m│\x1B[0m
\x1B[2m└───────┴──────┘\x1B[0m
"
`;
exports[`bun <file.md> renders inline formatting inside table cells 1`] = `
"\x1B[2m┌───────┬────────────────────────┐\x1B[0m
\x1B[2m│\x1B[0m \x1B[1mName\x1B[0m \x1B[2m│\x1B[0m \x1B[1mStyle\x1B[0m \x1B[2m│\x1B[0m
\x1B[2m├───────┼────────────────────────┤\x1B[0m
\x1B[2m│\x1B[0m \x1B[1mAlice\x1B[22m \x1B[2m│\x1B[0m \x1B[3meditor\x1B[23m \x1B[2m│\x1B[0m
\x1B[2m│\x1B[0m \x1B[48;5;236m\x1B[38;5;215mbob\x1B[39m\x1B[49m \x1B[2m│\x1B[0m \x1B[34m\x1B[4mlink\x1B[24m\x1B[39m\x1B[2m (https://bun.com\x1B[2m)\x1B[39m\x1B[22m \x1B[2m│\x1B[0m
\x1B[2m└───────┴────────────────────────┘\x1B[0m
"
`;
exports[`bun <file.md> renders inline formatting inside headings 1`] = `
"\x1B[1m\x1B[35mHello \x1B[1mbold\x1B[22m\x1B[1m\x1B[35m and \x1B[3mitalic\x1B[23m\x1B[1m\x1B[35m heading\x1B[0m
\x1B[2m═════════════════════════════\x1B[0m
"
`;
exports[`bun <file.md> nested code span inside bold keeps outer bold 1`] = `
"\x1B[1mbefore \x1B[48;5;236m\x1B[38;5;215mcode\x1B[39m\x1B[49m\x1B[1m after\x1B[22m tail\x1B[0m
"
`;
exports[`bun <file.md> renders mixed inline styles with autolinks 1`] = `
"Check \x1B[1m\x1B[34m\x1B[4mhttps://bun.com\x1B[24m\x1B[39m\x1B[1m\x1B[2m (https://bun.com\x1B[2m)\x1B[39m\x1B[22m\x1B[1m\x1B[22m and \x1B[34m\x1B[[email protected]\x1B[24m\x1B[39m\x1B[2m (
mailto:[email protected]\x1B[2m)\x1B[39m\x1B[22m!\x1B[0m
"
`;
exports[`bun <file.md> renders nested lists 1`] = `
" \x1B[36m• \x1B[0mouter
\x1B[36m• \x1B[0minner 1
\x1B[36m• \x1B[0minner 2
\x1B[36m• \x1B[0mdeep
\x1B[36m• \x1B[0msecond outer
"
`;
exports[`bun <file.md> runs without colors when NO_COLOR is set 1`] = `
"Hello
=====
world
"
`;
exports[`bun <file.md> renders via \`bun run ./file.md\` 1`] = `
"\x1B[1m\x1B[35mTitle\x1B[0m
\x1B[2m═════\x1B[0m
body\x1B[0m
"
`;
exports[`bun <file.md> renders .markdown extension too 1`] = `
"\x1B[1m\x1B[35myep\x1B[0m
\x1B[2m═══\x1B[0m
"
`;
exports[`bun <file.md> wraps inline code spans within COLUMNS, never mid-word 1`] = `
"After modifying \x1B[48;5;236m\x1B[38;5;215mcoreBeeps.ts\x1B[39m\x1B[49m, \x1B[48;5;236m\x1B[38;5;215mcontrolBoops.ts\x1B[39m\x1B[49m
, or \x1B[48;5;236m\x1B[38;5;215mtoolBoops.ts\x1B[39m\x1B[49m, run \x1B[48;5;236m\x1B[38;5;215mbun run build:boop\x1B[39m\x1B[49m to
regenerate. \x1B[1mDo not edit \x1B[48;5;236m\x1B[38;5;215m*.generated.ts\x1B[39m\x1B[49m\x1B[1m files
directly.\x1B[22m\x1B[0m
"
`;
exports[`bun <file.md> hard-breaks an overlong CJK word at the visible-width boundary 1`] = `
"A 测试测试测试测试测试测试测试
测试测试测试 wide.\x1B[0m
"
`;
exports[`bun <file.md> shrinks table columns to fit COLUMNS and wraps cell content 1`] = `
"\x1B[2m┌───────────────────────┬────────────────────────┐\x1B[0m
\x1B[2m│\x1B[0m \x1B[1mPath\x1B[0m \x1B[2m│\x1B[0m \x1B[1mScope\x1B[0m \x1B[2m│\x1B[0m
\x1B[2m├───────────────────────┼────────────────────────┤\x1B[0m
\x1B[2m│\x1B[0m \x1B[48;5;236m\x1B[38;5;215mdocs/guide.md\x1B[39m\x1B[49m \x1B[2m│\x1B[0m Top-level \x1B[2m│\x1B[0m
\x1B[2m│\x1B[0m \x1B[2m│\x1B[0m architecture: module \x1B[2m│\x1B[0m
\x1B[2m│\x1B[0m \x1B[2m│\x1B[0m map, startup flow, \x1B[2m│\x1B[0m
\x1B[2m│\x1B[0m \x1B[2m│\x1B[0m state management \x1B[2m│\x1B[0m
\x1B[2m│\x1B[0m \x1B[48;5;236m\x1B[38;5;215mdocs/components/desig\x1B[0m \x1B[2m│\x1B[0m Design system \x1B[2m│\x1B[0m
\x1B[2m│\x1B[0m \x1B[38;5;215m\x1B[48;5;236mn-system/guide.md\x1B[39m\x1B[49m \x1B[2m│\x1B[0m component API \x1B[2m│\x1B[0m
\x1B[2m│\x1B[0m \x1B[2m│\x1B[0m (Dialog, Tabs, \x1B[2m│\x1B[0m
\x1B[2m│\x1B[0m \x1B[2m│\x1B[0m StatusIcon, etc.) \x1B[2m│\x1B[0m
\x1B[2m└───────────────────────┴────────────────────────┘\x1B[0m
"
`;
+114
View File
@@ -0,0 +1,114 @@
import { describe, expect, test } from "bun:test";
import { join } from "path";
import { bunEnv, bunExe, fakeNodeRun, tempDir } from "../../harness";
describe("fake node cli", () => {
test("the node cli actually works", () => {
using temp = tempDir("fake-node", {
"index.ts": "console.log(Bun.version)",
});
expect(fakeNodeRun(temp, join(temp, "index.ts")).stdout).toBe(Bun.version);
});
test("doesnt resolve bins", () => {
using temp = tempDir("fake-node", {
"vite.js": "console.log('pass')",
"node_modules/.bin/vite": "#!/usr/bin/sh\necho fail && exit 1",
});
expect(fakeNodeRun(temp, "vite").stdout).toBe("pass");
});
test("doesnt resolve scripts", () => {
using temp = tempDir("fake-node", {
"vite.js": "console.log('pass')",
"package.json": '{"scripts":{"vite":"echo fail && exit 1"}}',
});
expect(fakeNodeRun(temp, "vite").stdout).toBe("pass");
});
test("can run a script named run.js", () => {
using temp = tempDir("fake-node", {
"run.js": "console.log('pass')",
"run/index.js": "console.log('fail')",
"node_modules/run/index.js": "console.log('fail')",
});
expect(fakeNodeRun(temp, "run").stdout).toBe("pass");
});
describe("entrypoint file extension picking", () => {
// Bun supports JSX and TS, and node doesnt, so our behavior here differs a bit
// Hopefully these priorization rules will not break any node apps.
test("picks tsx over any other ext", () => {
using temp = tempDir("fake-node", {
"build.js": "console.log('fail (build.js)')",
"build.jsx": "console.log('fail (build.jsx)')",
"build.cjs": "console.log('fail (build.cjs)')",
"build.mjs": "console.log('fail (build.mjs)')",
"build.ts": "console.log('fail (build.ts)')",
"build.cts": "console.log('fail (build.cts)')",
"build.mts": "console.log('fail (build.mts)')",
"build.tsx": "console.log('pass')",
});
expect(fakeNodeRun(temp, "build").stdout).toBe("pass");
});
test("picks jsx over ts", () => {
using temp = tempDir("fake-node", {
"build.js": "console.log('fail (build.js)')",
"build.jsx": "console.log('pass')",
"build.cjs": "console.log('fail (build.cjs)')",
"build.mjs": "console.log('fail (build.mjs)')",
"build.ts": "console.log('fail (build.ts)')",
"build.cts": "console.log('fail (build.cts)')",
"build.mts": "console.log('fail (build.mts)')",
});
expect(fakeNodeRun(temp, "build").stdout).toBe("pass");
});
test("picks mts over ts", () => {
using temp = tempDir("fake-node", {
"build.js": "console.log('fail (build.js)')",
"build.cjs": "console.log('fail (build.cjs)')",
"build.mjs": "console.log('fail (build.mjs)')",
"build.ts": "console.log('fail (build.ts)')",
"build.cts": "console.log('fail (build.cts)')",
"build.mts": "console.log('pass')",
});
expect(fakeNodeRun(temp, "build").stdout).toBe("pass");
});
test("picks ts over js/cjs/etc", () => {
using temp = tempDir("fake-node", {
"build.js": "console.log('fail (build.js)')",
"build.cjs": "console.log('fail (build.cjs)')",
"build.mjs": "console.log('fail (build.mjs)')",
"build.ts": "console.log('pass')",
"build.cts": "console.log('fail (build.cts)')",
});
expect(fakeNodeRun(temp, "build").stdout).toBe("pass");
});
});
test("node -e ", () => {
using temp = tempDir("fake-node", {});
expect(fakeNodeRun(temp, ["-e", "console.log('pass')"]).stdout).toBe("pass");
});
test("process args work", () => {
using temp = tempDir("fake-node", {
"index.js": "console.log(JSON.stringify(process.argv.slice(1)))",
});
expect(fakeNodeRun(temp, ["index", "a", "b", "c"]).stdout).toBe(
// note: no extension here is INTENTIONAL
JSON.stringify([join(temp, "index"), "a", "b", "c"]),
);
});
// Bare `node` now matches Node.js: a TTY stdin enters the REPL, a
// non-TTY stdin (pipe) prints "Missing script". fakeNodeRun's default
// stdin is platform-dependent (Windows may inherit a console), so pin
// a piped stdin here.
test("no args with piped stdin errors with 'Missing script'", () => {
using temp = tempDir("fake-node", {});
const result = Bun.spawnSync([bunExe(), "--bun", "node"], {
cwd: temp,
env: { ...bunEnv, NODE_ENV: undefined },
stdin: Buffer.alloc(0),
});
expect(result.stderr.toString()).toContain("Missing script");
expect(result.success).toBe(false);
});
});
@@ -0,0 +1,76 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import { readdirSync, rmSync } from "node:fs";
import { join } from "node:path";
// Regression: when runtime auto-install (`enqueueDependencyToRoot`) finds a
// cached manifest on disk but not the extracted tarball, it calls
// `Lockfile.Package.fromNPM` which grows `lockfile.buffers.dependencies`.
// The `dependency` argument was a pointer directly into that buffer, so the
// subsequent `.extract` branch read `dependency.behavior` from freed memory
// (ASAN: use-after-poison).
//
// This test uses real npm like the neighbouring run-autoinstall.test.ts; the
// package only needs to have at least one dependency of its own so that
// `fromNPM` reallocates the single-entry dependencies buffer.
test("auto-install with cached manifest but missing tarball does not read a dangling dependency pointer", async () => {
using dir = tempDir("autoinstall-cached-manifest", {});
const cacheDir = join(String(dir), ".bun-cache");
const env = {
...bunEnv,
BUN_INSTALL_CACHE_DIR: cacheDir,
};
// Use `-e` so the `require()` is resolved at runtime through
// `Bun__resolveSync` → `enqueueDependencyToRoot` (the code path that was
// passing a pointer directly into the lockfile buffer). Loading from a
// file instead lets the transpiler resolve the import through a
// different path that already used a stack copy.
const script = `try { require("is-even"); } catch {} console.log("ok");`;
// First run: downloads manifests and tarballs into the cache. The manifest
// isn't cached yet, so the safe `processDependencyListItem` path (stack
// copy) is taken.
{
await using proc = Bun.spawn({
cmd: [bunExe(), "--install=force", "-e", script],
env,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("ok");
expect(exitCode).toBe(0);
}
// Keep the `.npm` manifest files, drop every extracted tarball so the next
// run hits the `.extract` path with a warm manifest cache. Assert both
// kinds actually exist first so this test doesn't silently become a no-op
// if the first run's auto-install didn't reach the registry.
const entries = readdirSync(cacheDir);
const manifests = entries.filter(e => e.endsWith(".npm"));
const extracted = entries.filter(e => !e.endsWith(".npm"));
expect(manifests.length).toBeGreaterThan(0);
expect(extracted.length).toBeGreaterThan(0);
for (const entry of extracted) {
rmSync(join(cacheDir, entry), { recursive: true, force: true });
}
// Second run: manifest is loaded from disk, `fromNPM` appends the package's
// dependencies (reallocating the buffer), then the tarball download path
// runs. Previously this read `dependency.behavior` from the freed buffer.
{
await using proc = Bun.spawn({
cmd: [bunExe(), "--install=force", "-e", script],
env,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("ok");
expect(exitCode).toBe(0);
}
});
@@ -0,0 +1,19 @@
exports[0] = 0;
Object.defineProperty(exports, "1", {
value: 1,
enumerable: true,
});
Object.defineProperty(exports, "2", {
get: () => {
return 3;
},
enumerable: true,
});
exports[3] = 4;
Object.defineProperty(exports, "4", {
get() {
throw new Error("4");
},
enumerable: true,
});
+13
View File
@@ -0,0 +1,13 @@
Object.defineProperty(exports, "a", {
value: 1,
enumerable: false,
});
exports.b = 2;
let fn = function () {
return 3;
};
// Node doesn't support non-enumerable getters/setters
Object.defineProperty(exports, "c", {
get: fn,
enumerable: false,
});
+7
View File
@@ -0,0 +1,7 @@
// @bun @bun-cjs
function f() {
console.log("This module has been evaluated!");
}
f();
+43
View File
@@ -0,0 +1,43 @@
const dest = require.resolve("./leak-fixture-small-ast.js");
// ASAN's quarantine retains freed allocations (default 256 MB) so RSS deltas
// run far higher under bun-asan; widen the threshold to avoid false positives.
const isASAN = process.execPath.includes("bun-asan");
const rss =
process.platform === "darwin" && typeof Bun !== "undefined" && typeof Bun.unsafe.memoryFootprint === "function"
? Bun.unsafe.memoryFootprint
: process.memoryUsage.rss;
let gc = globalThis.gc;
if (typeof Bun !== "undefined") {
gc = () => Bun.gc(true);
}
if (!gc) {
gc = () => {};
}
gc();
for (let i = 0; i < 5; i++) {
delete require.cache[dest];
require(dest);
}
gc();
const baseline = rss();
for (let i = 0; i < 10000; i++) {
delete require.cache[dest];
require(dest);
}
gc();
setTimeout(() => {
let diff = rss() - baseline;
diff = (diff / 1024 / 1024) | 0;
console.log({ leaked: diff + " MB" });
if (diff > (isASAN ? 320 : 48)) {
console.log("\n--fail--\n");
process.exit(1);
} else {
console.log("\n--pass--\n");
}
}, 16);
+20
View File
@@ -0,0 +1,20 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
import { join } from "path";
describe.concurrent("commonjs-invalid", () => {
test("Loading an invalid commonjs module", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "run", join(import.meta.dir, "cjs-fixture-bad.cjs")],
env: bunEnv,
stdout: "inherit",
stderr: "pipe",
stdin: "inherit",
});
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
expect(stderr.trim()).toContain("Expected CommonJS module to have a function wrapper");
expect(exitCode).toBe(1);
});
});
+19
View File
@@ -0,0 +1,19 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
import { join } from "path";
describe.concurrent("commonjs-no-export", () => {
test("CommonJS entry point with no exports", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--bun", join(import.meta.dir, "commonjs-no-exports-fixture.js")],
env: bunEnv,
stderr: "inherit",
stdout: "pipe",
});
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
expect(stdout.trim().endsWith("--pass--")).toBe(true);
expect(exitCode).toBe(0);
});
});
+7
View File
@@ -0,0 +1,7 @@
// see https://www.npmjs.com/package/checkpoint-client
require("node:fs");
eval("__dirname");
eval("__filename");
console.log("--pass--");
+447
View File
@@ -0,0 +1,447 @@
import { describe, expect, test } from "bun:test";
import { readdirSync, readFileSync } from "fs";
import { bunEnv, bunExe, isWindows, tempDir } from "harness";
import { join } from "path";
// Every workload below is time-bounded for 100ms. On Windows JSC's
// SamplingProfiler effectively ticks at the ~15.6ms default timer quantum, and
// the entry module evaluates via an async fetch/link/evaluate chain — so
// shorter windows (the previous 32/50ms) can elapse before a single sample is
// taken, leaving "No samples collected." in --cpu-prof-md output.
describe.concurrent("--cpu-prof", () => {
test("generates CPU profile with default name", async () => {
using dir = tempDir("cpu-prof", {
"test.js": `
// CPU-intensive task
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const now = performance.now();
while (now + 100 > performance.now()) {
Bun.inspect(fibonacci(20));
}
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "--cpu-prof", "test.js"],
cwd: String(dir),
env: bunEnv,
stdout: "inherit",
stderr: "inherit",
});
const exitCode = await proc.exited;
// Check that a .cpuprofile file was created
const files = readdirSync(String(dir));
const profileFiles = files.filter(f => f.endsWith(".cpuprofile"));
expect(profileFiles.length).toBeGreaterThan(0);
expect(exitCode).toBe(0);
// Read and validate the profile
const profilePath = join(String(dir), profileFiles[0]);
const profileContent = readFileSync(profilePath, "utf-8");
const profile = JSON.parse(profileContent);
// Validate Chrome CPU Profiler format
expect(profile).toHaveProperty("nodes");
expect(profile).toHaveProperty("startTime");
expect(profile).toHaveProperty("endTime");
expect(profile).toHaveProperty("samples");
expect(profile).toHaveProperty("timeDeltas");
expect(Array.isArray(profile.nodes)).toBe(true);
expect(Array.isArray(profile.samples)).toBe(true);
expect(Array.isArray(profile.timeDeltas)).toBe(true);
// Validate root node
expect(profile.nodes.length).toBeGreaterThan(0);
const rootNode = profile.nodes[0];
expect(rootNode.id).toBe(1);
expect(rootNode.callFrame.functionName).toBe("(root)");
// Validate node structure
profile.nodes.forEach((node: any) => {
expect(node).toHaveProperty("id");
expect(node).toHaveProperty("callFrame");
expect(node).toHaveProperty("hitCount");
expect(node.callFrame).toHaveProperty("functionName");
expect(node.callFrame).toHaveProperty("scriptId");
expect(node.callFrame).toHaveProperty("url");
expect(node.callFrame).toHaveProperty("lineNumber");
expect(node.callFrame).toHaveProperty("columnNumber");
});
// Validate samples point to valid nodes
const nodeIds = new Set(profile.nodes.map((n: any) => n.id));
profile.samples.forEach((sample: number) => {
expect(nodeIds.has(sample)).toBe(true);
});
// Validate time deltas
expect(profile.timeDeltas.length).toBe(profile.samples.length);
// For very fast programs, start and end times might be equal or very close
expect(profile.startTime).toBeLessThanOrEqual(profile.endTime);
// CRITICAL: Validate timestamps are positive and in microseconds
// Chrome DevTools requires timestamps in microseconds since Unix epoch
// A valid timestamp should be > 1000000000000000 (around year 2001)
// and < 3000000000000000 (around year 2065)
expect(profile.startTime).toBeGreaterThan(1000000000000000);
expect(profile.startTime).toBeLessThan(3000000000000000);
expect(profile.endTime).toBeGreaterThan(1000000000000000);
expect(profile.endTime).toBeLessThan(3000000000000000);
});
test("--cpu-prof-name sets custom filename", async () => {
using dir = tempDir("cpu-prof-name", {
"test.js": `
function loop() {
const end = Date.now() + 100;
while (Date.now() < end) {}
}
loop();
`,
});
const customName = "my-profile.cpuprofile";
await using proc = Bun.spawn({
cmd: [bunExe(), "--cpu-prof", "--cpu-prof-name", customName, "test.js"],
cwd: String(dir),
env: bunEnv,
stdout: "inherit",
stderr: "inherit",
});
const exitCode = await proc.exited;
const files = readdirSync(String(dir));
expect(files).toContain(customName);
expect(exitCode).toBe(0);
});
// On Windows the path buffer is ~98 KB and the CreateProcess command-line
// limit is ~32 KB, so an overflowing CLI argument cannot be delivered. On
// POSIX 5000 bytes exceeds the fixed path buffer (Linux 4096, macOS 1024);
// 2500 + 2500 exercises the combined bound with components that fit
// individually on Linux.
test.skipIf(isWindows).each([
["--cpu-prof-dir", ["--cpu-prof-dir", Buffer.alloc(5000, "d").toString()]],
["--cpu-prof-name", ["--cpu-prof-name", Buffer.alloc(5000, "n").toString()]],
[
"--cpu-prof-dir + --cpu-prof-name combined",
["--cpu-prof-dir", Buffer.alloc(2500, "d").toString(), "--cpu-prof-name", Buffer.alloc(2500, "n").toString()],
],
] as const)("%s longer than PATH_MAX reports an error instead of panicking", async (_label, args) => {
using dir = tempDir("cpu-prof-pathmax", {
"test.js": `const end = Date.now() + 60; while (Date.now() < end) {}`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "--cpu-prof", ...args, "test.js"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("");
expect(stderr).toContain("FilenameTooLong");
expect(stderr).toContain("Failed to write CPU profile");
expect(exitCode).toBe(0);
expect(proc.signalCode).toBeNull();
});
test("--cpu-prof-dir sets custom directory", async () => {
using dir = tempDir("cpu-prof-dir", {
"test.js": `
function loop() {
const end = Date.now() + 100;
while (Date.now() < end) {}
}
loop();
`,
"profiles": {},
});
await using proc = Bun.spawn({
cmd: [bunExe(), "--cpu-prof", "--cpu-prof-dir", "profiles", "test.js"],
cwd: String(dir),
env: bunEnv,
stdout: "inherit",
stderr: "inherit",
});
const exitCode = await proc.exited;
const profilesDir = join(String(dir), "profiles");
const files = readdirSync(profilesDir);
const profileFiles = files.filter(f => f.endsWith(".cpuprofile"));
expect(profileFiles.length).toBeGreaterThan(0);
expect(exitCode).toBe(0);
});
test("profile captures function names", async () => {
using dir = tempDir("cpu-prof-functions", {
"test.js": `
function myFunction() {
let sum = 0;
const end = performance.now() + 100;
while (performance.now() < end) {
for (let i = 0; i < 1000; i++) sum += i;
}
return sum;
}
myFunction();
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "--cpu-prof", "test.js"],
cwd: String(dir),
env: bunEnv,
stdout: "inherit",
stderr: "inherit",
});
const exitCode = await proc.exited;
const files = readdirSync(String(dir));
const profileFiles = files.filter(f => f.endsWith(".cpuprofile"));
expect(profileFiles.length).toBeGreaterThan(0);
const profilePath = join(String(dir), profileFiles[0]);
const profile = JSON.parse(readFileSync(profilePath, "utf-8"));
// Check that we captured some meaningful function names
const functionNames = profile.nodes.map((n: any) => n.callFrame.functionName);
expect(functionNames.some((name: string) => name !== "(root)" && name !== "(program)")).toBe(true);
expect(exitCode).toBe(0);
});
test("--cpu-prof-md generates markdown format profile", async () => {
using dir = tempDir("cpu-prof-md", {
"test.js": `
// CPU-intensive task for text profile
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
function main() {
const now = performance.now();
while (now + 100 > performance.now()) {
Bun.inspect(fibonacci(20));
}
}
main();
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "--cpu-prof", "--cpu-prof-md", "test.js"],
cwd: String(dir),
env: bunEnv,
stdout: "inherit",
stderr: "inherit",
});
const exitCode = await proc.exited;
// Check that a .md file was created (not .cpuprofile)
const files = readdirSync(String(dir));
const mdFiles = files.filter(f => f.endsWith(".md") && f.startsWith("CPU."));
expect(mdFiles.length).toBeGreaterThan(0);
expect(exitCode).toBe(0);
// Read and validate the markdown profile format
const profilePath = join(String(dir), mdFiles[0]);
const profileContent = readFileSync(profilePath, "utf-8");
// Validate the markdown format has expected sections
expect(profileContent).toContain("# CPU Profile");
expect(profileContent).toContain("## Hot Functions (Self Time)");
expect(profileContent).toContain("## Call Tree (Total Time)");
expect(profileContent).toContain("## Function Details");
expect(profileContent).toContain("## Files");
// Validate header contains summary info in markdown table
expect(profileContent).toMatch(/\| Duration \| Samples \| Interval \| Functions \|/);
// Validate function details have caller/callee info
expect(profileContent).toContain("**Called by:**");
expect(profileContent).toContain("**Calls:**");
});
test("--cpu-prof-md with custom name", async () => {
using dir = tempDir("cpu-prof-md-name", {
"test.js": `
function loop() {
const end = Date.now() + 100;
while (Date.now() < end) {}
}
loop();
`,
});
const customName = "my-profile.md";
// --cpu-prof-md works standalone, no need for --cpu-prof
await using proc = Bun.spawn({
cmd: [bunExe(), "--cpu-prof-md", "--cpu-prof-name", customName, "test.js"],
cwd: String(dir),
env: bunEnv,
stdout: "inherit",
stderr: "inherit",
});
const exitCode = await proc.exited;
const files = readdirSync(String(dir));
expect(files).toContain(customName);
expect(exitCode).toBe(0);
// Validate it's markdown format
const profileContent = readFileSync(join(String(dir), customName), "utf-8");
expect(profileContent).toContain("# CPU Profile");
});
test("--cpu-prof-md shows function details with relationships", async () => {
using dir = tempDir("cpu-prof-md-details", {
"test.js": `
function workA() {
let sum = 0;
for (let i = 0; i < 500000; i++) sum += i;
return sum;
}
function workB() {
let sum = 0;
for (let i = 0; i < 500000; i++) sum += i;
return sum;
}
function main() {
const now = performance.now();
while (now + 100 > performance.now()) {
workA();
workB();
}
}
main();
`,
});
// --cpu-prof-md works standalone
await using proc = Bun.spawn({
cmd: [bunExe(), "--cpu-prof-md", "test.js"],
cwd: String(dir),
env: bunEnv,
stdout: "inherit",
stderr: "inherit",
});
const exitCode = await proc.exited;
expect(exitCode).toBe(0);
const files = readdirSync(String(dir));
const mdFiles = files.filter(f => f.endsWith(".md") && f.startsWith("CPU."));
expect(mdFiles.length).toBeGreaterThan(0);
const profileContent = readFileSync(join(String(dir), mdFiles[0]), "utf-8");
// Check markdown sections
expect(profileContent).toMatch(/## Hot Functions \(Self Time\)/);
expect(profileContent).toMatch(/## Call Tree \(Total Time\)/);
expect(profileContent).toMatch(/## Function Details/);
expect(profileContent).toMatch(/## Files/);
// Check function detail headers (### `functionName`)
expect(profileContent).toMatch(/^### `/m);
});
test("--cpu-prof-md works standalone without --cpu-prof", async () => {
using dir = tempDir("cpu-prof-md-standalone", {
"test.js": `
function loop() {
const end = Date.now() + 100;
while (Date.now() < end) {}
}
loop();
`,
});
// Use ONLY --cpu-prof-md without --cpu-prof
await using proc = Bun.spawn({
cmd: [bunExe(), "--cpu-prof-md", "test.js"],
cwd: String(dir),
env: bunEnv,
stdout: "inherit",
stderr: "inherit",
});
const exitCode = await proc.exited;
// Check that a .md file was created
const files = readdirSync(String(dir));
const mdFiles = files.filter(f => f.endsWith(".md") && f.startsWith("CPU."));
expect(mdFiles.length).toBeGreaterThan(0);
expect(exitCode).toBe(0);
// Validate it's the markdown format
const profileContent = readFileSync(join(String(dir), mdFiles[0]), "utf-8");
expect(profileContent).toContain("# CPU Profile");
});
test("--cpu-prof and --cpu-prof-md together creates both files", async () => {
using dir = tempDir("cpu-prof-both-formats", {
"test.js": `
function loop() {
const end = Date.now() + 100;
while (Date.now() < end) {}
}
loop();
`,
});
// Use both flags together
await using proc = Bun.spawn({
cmd: [bunExe(), "--cpu-prof", "--cpu-prof-md", "test.js"],
cwd: String(dir),
env: bunEnv,
stdout: "inherit",
stderr: "inherit",
});
const exitCode = await proc.exited;
// Check that both .cpuprofile and .md files were created
const files = readdirSync(String(dir));
const jsonFiles = files.filter(f => f.endsWith(".cpuprofile"));
const mdFiles = files.filter(f => f.endsWith(".md") && f.startsWith("CPU."));
expect(jsonFiles.length).toBeGreaterThan(0);
expect(mdFiles.length).toBeGreaterThan(0);
expect(exitCode).toBe(0);
// Validate JSON file
const jsonContent = readFileSync(join(String(dir), jsonFiles[0]), "utf-8");
const profile = JSON.parse(jsonContent);
expect(profile).toHaveProperty("nodes");
expect(profile).toHaveProperty("samples");
// Validate markdown file
const mdContent = readFileSync(join(String(dir), mdFiles[0]), "utf-8");
expect(mdContent).toContain("# CPU Profile");
});
});
@@ -0,0 +1,62 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, mergeWindowEnvs, tempDir } from "harness";
import path from "path";
// NOTE: kept separate from run-crash-handler.test.ts on purpose — that file
// is skip-listed in test/expectations.txt (its segfault-reporter test is
// broken), so anything added there never runs in CI.
describe.concurrent("crash report command character", () => {
// Crash while a given subcommand is running and return the command
// character from the trace string printed to stderr:
// {base}/{version}/{platform char}{command char}{remainder}
// Expected characters must stay in sync with `Command.Tag.char()`
// (src/options_types/command_tag.rs) and bun.report's decoder.
async function commandCharFromCrash(args: string[]): Promise<string> {
using server = Bun.serve({ port: 0, fetch: () => new Response("OK") });
const base = new URL(server.url).origin;
// No cwd override: on Windows the crash reporter spawns a detached child
// that inherits the crashing process's cwd, which would keep a tempDir
// cwd alive past the test and make its cleanup fail with EBUSY.
await using proc = Bun.spawn({
cmd: [bunExe(), ...args],
env: mergeWindowEnvs([
bunEnv,
{
BUN_CRASH_REPORT_URL: base,
BUN_ENABLE_CRASH_REPORTING: "1",
},
]),
stdio: ["ignore", "pipe", "pipe"],
});
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
expect(exitCode).not.toBe(0);
const trace = stderr.match(new RegExp(`${base.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/\\S+`));
expect(trace).not.toBeNull();
const payload = new URL(trace![0]).pathname.split("/")[2];
expect(payload.length).toBeGreaterThan(2);
return payload[1];
}
const fixture = path.join(import.meta.dir, "fixture-crash.js");
test("bun <script> encodes AutoCommand", async () => {
expect(await commandCharFromCrash([fixture, "panic"])).toBe("a");
});
test("bun run <script> encodes RunCommand", async () => {
expect(await commandCharFromCrash(["run", fixture, "panic"])).toBe("r");
});
test("bun test encodes TestCommand", async () => {
using dir = tempDir("crash-report-cmd-char", {
"crash.fixture.test.js": `
import { crash_handler } from "bun:internal-for-testing";
crash_handler.panic();
`,
});
const testFile = path.join(String(dir), "crash.fixture.test.js");
expect(await commandCharFromCrash(["test", testFile])).toBe("t");
});
});
View File
+18
View File
@@ -0,0 +1,18 @@
import { describe, expect, it } from "bun:test";
import { bunEnv, bunExe } from "harness";
import { join } from "path";
describe.concurrent("empty-file", () => {
it("should execute empty scripts", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--bun", join(import.meta.dir, "empty-file.js")],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBeEmpty();
expect(stderr).toBeEmpty();
expect(exitCode).toBe(0);
});
});
File diff suppressed because it is too large Load Diff
+36
View File
@@ -0,0 +1,36 @@
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const dest = await import.meta.resolve("./esm-leak-fixture-large-ast.mjs");
// ASAN's quarantine retains freed allocations (default 256 MB) so RSS deltas
// run far higher under bun-asan; widen the threshold to avoid false positives.
const isASAN = process.execPath.includes("bun-asan");
const rss =
process.platform === "darwin" && typeof Bun !== "undefined" && typeof Bun.unsafe.memoryFootprint === "function"
? Bun.unsafe.memoryFootprint
: process.memoryUsage.rss;
if (typeof Bun !== "undefined") Bun.gc(true);
for (let i = 0; i < 5; i++) {
delete require.cache[dest];
await import(dest);
}
if (typeof Bun !== "undefined") Bun.gc(true);
const baseline = rss();
for (let i = 0; i < 50; i++) {
delete require.cache[dest];
await import(dest);
}
if (typeof Bun !== "undefined") Bun.gc(true);
setTimeout(() => {
let diff = rss() - baseline;
diff = (diff / 1024 / 1024) | 0;
console.log({ leaked: diff + " MB" });
if (diff > (isASAN ? 400 : 120)) {
console.log("\n--fail--\n");
process.exit(1);
} else {
console.log("\n--pass--\n");
}
}, 16);
+45
View File
@@ -0,0 +1,45 @@
import { expect, test } from "bun:test";
import * as CJSArrayLike from "./cjs-defineProperty-arraylike.cjs";
import * as CJS from "./cjs-defineProperty-fixture.cjs";
import * as Self from "./esm-defineProperty.test.ts";
// https://github.com/oven-sh/bun/issues/4432
test("defineProperty", () => {
expect(CJS.a).toBe(1);
expect(CJS.b).toBe(2);
// non-enumerable getter/setter are not copied, matching node.js
expect(CJS.c).toBe(undefined);
expect(Bun.inspect(CJS.default)).toBe(`{\n a: 1,\n b: 2,\n c: [Getter],\n}`);
});
export const __esModule = true;
test("shows __esModule if it was exported", () => {
expect(Bun.inspect(Self)).toBe(`Module {
__esModule: true,
}`);
expect(Object.getOwnPropertyNames(Self)).toContain("__esModule");
});
test("arraylike", () => {
expect(CJSArrayLike[0]).toBe(0);
expect(CJSArrayLike[1]).toBe(1);
expect(CJSArrayLike[2]).toBe(3);
expect(CJSArrayLike[3]).toBe(4);
expect(CJSArrayLike[4]).toBe(undefined);
expect(CJSArrayLike).toHaveProperty("4");
expect(Object.getOwnPropertyNames(CJSArrayLike)).not.toContain("__esModule");
expect(Object.getOwnPropertyNames(CJSArrayLike.default)).not.toContain("__esModule");
expect(Bun.inspect(CJSArrayLike)).toBe(`Module {
"0": 0,
"1": 1,
"2": 3,
"3": 4,
"4": undefined,
default: {
"0": 0,
"1": 1,
"2": [Getter],
"3": 4,
"4": [Getter],
},
}`);
});
+48
View File
@@ -0,0 +1,48 @@
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const dest = require.resolve("./leak-fixture-small-ast.js");
// ASAN's quarantine retains freed allocations (default 256 MB) so RSS deltas
// run far higher under bun-asan; widen the threshold to avoid false positives.
const isASAN = process.execPath.includes("bun-asan");
const rss =
process.platform === "darwin" && typeof Bun !== "undefined" && typeof Bun.unsafe.memoryFootprint === "function"
? Bun.unsafe.memoryFootprint
: process.memoryUsage.rss;
if (typeof Bun !== "undefined") Bun.gc(true);
for (let i = 0; i < 5; i++) {
delete require.cache[dest];
await import(dest);
}
if (typeof Bun !== "undefined") Bun.gc(true);
const baseline = rss();
for (let i = 0; i < 100000; i++) {
delete require.cache[dest];
await import(dest);
}
if (typeof Bun !== "undefined") Bun.gc(true);
setTimeout(() => {
let diff = rss() - baseline;
diff = (diff / 1024 / 1024) | 0;
console.log({ leaked: diff + " MB" });
// This test seems to be more flaky on slow filesystems.
// This used to be 40 MB, but the original version of Bun which this triggered on would reach 120 MB
// so we can increase it to 100 and still catch the leak.
//
// bunx [email protected] --smol test/cli/run/esm-fixture-leak-small.mjs
// {
// leaked: "100 MB"
// }
// bunx [email protected] --smol test/cli/run/esm-fixture-leak-small.mjs
// {
// leaked: "38 MB",
// }
if (diff >= (isASAN ? 500 : 100)) {
console.log("\n--fail--\n");
process.exit(1);
} else {
console.log("\n--pass--\n");
}
}, 24);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
let crash_handler;
try {
crash_handler = require("bun:internal-for-testing").crash_handler;
} catch {
console.error("This version of bun does not have internal-for-testing exposed");
console.error("BUN_GARBAGE_COLLECTOR_LEVEL=0 BUN_FEATURE_FLAG_INTERNAL_FOR_TESTING=1 bun");
process.exit(1);
}
const approach = process.argv[2];
if (approach in crash_handler) {
crash_handler[approach]();
} else {
console.error(
"usage: bun fixture-crash.js <segfault|segfaultInDll|panic|rootError|outOfMemory|abort|trap|raiseIgnoringPanicHandler>",
);
}
+18
View File
@@ -0,0 +1,18 @@
const onlyCheck = process.env.ONLY_CHECK_TTY === "0";
import { dlopen } from "bun:ffi";
const suffix = process.platform === "darwin" ? "dylib" : "so.6";
const { tcgetattr, tcsetattr } = dlopen(`libc.${suffix}`, {
"tcgetattr": {
"args": ["int", "pointer"],
"result": "int",
},
}).symbols;
var termios = new Buffer(256);
var dataView = new DataView(termios.buffer);
const rc = tcgetattr(0, dataView);
if (rc === 0) {
throw new Error("tcgetattr failed");
}
await Bun.write(1, termios.toString("hex"));
+1
View File
@@ -0,0 +1 @@
export const Fragment = () => {};
+69
View File
@@ -0,0 +1,69 @@
# Basic filesystem with FUSE
# Used to ensure bun can run files mounted on FUSE
# The filesystem will appear to have `main.js` containing:
# console.log("hello world");
# and `main-symlink.js` as a symlink to `main.js`.
import fuse
import errno, stat, os
fuse.fuse_python_api = (0, 2)
script = b'console.log("hello world");\n'
class TestingFs(fuse.Fuse):
def getattr(self, path):
st = fuse.Stat()
if path == "/":
st.st_mode = stat.S_IFDIR | 0o755
st.st_nlink = 2
elif path == "/main.js":
st.st_mode = stat.S_IFREG | 0o644
st.st_nlink = 1
st.st_size = len(script)
elif path == "/main-symlink.js":
st.st_mode = stat.S_IFLNK | 0o644
st.st_nlink = 1
st.st_size = len("main.js")
else:
return -errno.ENOENT
return st
def readdir(self, path, offset):
for r in ".", "..", "main.js", "main-symlink.js":
yield fuse.Direntry(r)
def open(self, path, flags):
if path != "/main.js" and path != "/main-symlink.js":
return -errno.ENOENT
mask = os.O_RDONLY | os.O_WRONLY | os.O_RDWR
if (flags & mask) != os.O_RDONLY:
return -errno.EACCES
def read(self, path, size, offset):
if path != "/main.js":
return -errno.ENOENT
if offset < len(script):
if offset + size > len(script):
size = len(script) - offset
return script[offset : offset + size]
return b""
def readlink(self, path):
if path != "/main-symlink.js":
return -errno.ENOENT
return "main.js"
def main():
server = TestingFs(
version=fuse.__version__,
usage="\nSmall filesystem made for testing Bun's ability to run files off of FUSE",
dash_s_do="setsingle",
)
server.parse(errex=1)
server.main()
if __name__ == "__main__":
main()
+133
View File
@@ -0,0 +1,133 @@
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
int stdout_pipe[2], stderr_pipe[2];
pid_t pid;
int status;
char stdout_buffer[4096] = {0};
char stderr_buffer[4096] = {0};
// Create pipes for stdout and stderr
if (pipe(stdout_pipe) == -1 || pipe(stderr_pipe) == -1) {
perror("pipe");
return 1;
}
// Create garbage environment variables with stack buffers containing
// arbitrary bytes
char garbage1[64];
char garbage2[64];
char garbage3[64];
char garbage4[64];
char garbage5[64];
// Fill with arbitrary non-ASCII/UTF-8 bytes
for (int i = 0; i < 63; i++) {
garbage1[i] = (char)(0x80 + (i % 128)); // Invalid UTF-8 start bytes
garbage2[i] = (char)(0xFF - (i % 256)); // High bytes
garbage3[i] = (char)(i * 3 + 128); // Mixed garbage
garbage4[i] = (char)(0xC0 | (i & 0x1F)); // Invalid UTF-8 sequences
}
garbage1[63] = '\0';
garbage2[63] = '\0';
garbage3[63] = '\0';
garbage4[63] = '\0';
for (int i = 0; i < 10; i++) {
garbage5[i] = (char)(0x80 + (i % 128));
}
garbage5[10] = '=';
garbage5[11] = 0x81;
garbage5[12] = 0xF5;
garbage5[13] = 0xC1;
garbage5[14] = 0xC2;
char *garbage_env[] = {
garbage5,
// garbage1,
// garbage2,
// garbage3,
// garbage4,
"PATH=/usr/bin:/bin", // Keep PATH so we can find commands
"BUN_DEBUG_QUIET_LOGS=1", "OOGA=booga", "OOGA=laskdjflsdf", NULL};
pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) {
// Child process
close(stdout_pipe[0]); // Close read end
close(stderr_pipe[0]); // Close read end
// Redirect stdout and stderr to pipes
dup2(stdout_pipe[1], STDOUT_FILENO);
dup2(stderr_pipe[1], STDERR_FILENO);
close(stdout_pipe[1]);
close(stderr_pipe[1]);
char *BUN_PATH = getenv("BUN_PATH");
if (BUN_PATH == NULL) {
fprintf(stderr, "Missing BUN_PATH!\n");
fflush(stderr);
exit(1);
}
execve(BUN_PATH,
(char *[]){"bun-debug", "-e", "console.log(process.env)", NULL},
garbage_env);
// If both fail, exit with error
perror("execve");
exit(127);
} else {
// Parent process
close(stdout_pipe[1]); // Close write end
close(stderr_pipe[1]); // Close write end
// Read from stdout pipe
ssize_t stdout_bytes =
read(stdout_pipe[0], stdout_buffer, sizeof(stdout_buffer) - 1);
if (stdout_bytes > 0) {
stdout_buffer[stdout_bytes] = '\0';
}
// Read from stderr pipe
ssize_t stderr_bytes =
read(stderr_pipe[0], stderr_buffer, sizeof(stderr_buffer) - 1);
if (stderr_bytes > 0) {
stderr_buffer[stderr_bytes] = '\0';
}
close(stdout_pipe[0]);
close(stderr_pipe[0]);
// Wait for child process
waitpid(pid, &status, 0);
// Print results
printf("=== PROCESS OUTPUT ===\n");
printf("Exit code: %d\n", WEXITSTATUS(status));
printf("\n=== STDOUT ===\n");
printf("%s", stdout_buffer);
fflush(stdout);
if (stderr_bytes > 0) {
fprintf(stderr, "\n=== STDERR ===\n");
fprintf(stderr, "%s", stderr_buffer);
fflush(stderr);
}
exit(status);
}
return 0;
}
+28
View File
@@ -0,0 +1,28 @@
import { describe, expect, test } from "bun:test";
import { bunExe, isPosix, tempDir } from "harness";
import path from "path";
describe.if(isPosix)("garbage env", () => {
test("garbage env", async () => {
const cfile = path.join(import.meta.dirname, "garbage-env.c");
// Compile into a temp dir so the binary never lands in the repo root.
using dir = tempDir("garbage-env", {});
const exe = path.join(String(dir), "garbage-env");
{
const cc = Bun.which("clang") || Bun.which("gcc") || Bun.which("cc");
const { exitCode, stderr } = await Bun.$`${cc} -o ${exe} ${cfile}`;
const stderrText = stderr.toString();
if (stderrText.length > 0) {
console.error(stderrText);
}
expect(exitCode).toBe(0);
}
const { exitCode, stderr } = await Bun.$`${exe}`.env({ BUN_PATH: bunExe() });
const stderrText = stderr.toString();
if (stderrText.length > 0) {
console.error(stderrText);
}
expect(exitCode).toBe(0);
});
});
+94
View File
@@ -0,0 +1,94 @@
/**
* Test that Bun.Glob and fs.globSync work correctly on FUSE filesystems
* where d_type returns DT_UNKNOWN.
*
* Related to issue #24007 and PR #18172
*/
import { spawn, type ReadableSubprocess } from "bun";
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { isLinux, tmpdirSync } from "harness";
import fs from "node:fs";
import { join } from "node:path";
describe.skipIf(!isLinux)("glob on a FUSE mount", () => {
// Mount once for the whole describe block. The first python3/libfuse
// cold-start on Alpine CI can take several seconds when disk I/O is
// contended by background container setup, so a per-test mount with a
// short poll budget is flaky. afterAll always runs (even if beforeAll
// throws) so cleanup is guaranteed.
let mountpoint: string;
let pythonProcess: ReadableSubprocess | undefined;
beforeAll(async () => {
// Use tmpdirSync for empty mount point (tempDir requires file tree)
mountpoint = tmpdirSync();
pythonProcess = spawn({
cmd: ["python3", "fuse-fs.py", "-f", mountpoint],
cwd: __dirname,
stdout: "pipe",
stderr: "pipe",
});
// Poll until the mount is ready or the python process exits. The
// bound only guards against a true hang; normal startup is <200ms
// once the page cache is warm.
let tries = 0;
while (!fs.existsSync(join(mountpoint, "main.js")) && tries < 1600 && pythonProcess.exitCode === null) {
tries++;
await Bun.sleep(5);
}
if (pythonProcess.exitCode !== null && pythonProcess.exitCode !== 0) {
throw new Error(
`FUSE process exited early with code ${pythonProcess.exitCode}: ${await pythonProcess.stderr.text()}`,
);
}
expect(fs.existsSync(join(mountpoint, "main.js"))).toBeTrue();
}, 10000);
afterAll(async () => {
if (!pythonProcess) return;
const umount = spawn({ cmd: ["fusermount", "-u", mountpoint] });
await umount.exited;
await Promise.race([pythonProcess.exited, Bun.sleep(1000)]);
if (pythonProcess.exitCode === null) {
pythonProcess.kill("SIGKILL");
console.error("python process errored:", await pythonProcess.stderr.text());
}
});
test("Bun.Glob.scanSync finds files on FUSE mount", () => {
const glob = new Bun.Glob("*.js");
const results = Array.from(glob.scanSync({ cwd: mountpoint }));
// fuse-fs.py provides main.js and main-symlink.js
expect(results).toContain("main.js");
expect(results.length).toBeGreaterThanOrEqual(1);
});
test("fs.globSync finds files on FUSE mount", () => {
const results = fs.globSync("*.js", { cwd: mountpoint });
expect(results).toContain("main.js");
expect(results.length).toBeGreaterThanOrEqual(1);
});
test("fs.readdirSync works on FUSE mount", () => {
const results = fs.readdirSync(mountpoint);
expect(results).toContain("main.js");
expect(results).toContain("main-symlink.js");
});
test("fs.readdirSync with withFileTypes returns correct types on FUSE mount", () => {
const results = fs.readdirSync(mountpoint, { withFileTypes: true });
const mainJs = results.find(d => d.name === "main.js");
expect(mainJs).toBeDefined();
expect(mainJs!.isFile()).toBe(true);
const symlink = results.find(d => d.name === "main-symlink.js");
expect(symlink).toBeDefined();
expect(symlink!.isSymbolicLink()).toBe(true);
});
});
+119
View File
@@ -0,0 +1,119 @@
import { spawnSync } from "bun";
import { beforeAll, describe, expect, test } from "bun:test";
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
let cwd: string;
beforeAll(() => {
cwd = tempDirWithFiles("--if-present", {
"present.js": "console.log('Here!');",
"package.json": JSON.stringify({
"name": "present",
"scripts": {
"present": "echo 'Here!'",
},
}),
});
});
describe("bun", () => {
test("should error with missing script", () => {
const { exitCode, stdout, stderr } = spawnSync({
cwd,
cmd: [bunExe(), "notpresent"],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
expect(stdout.toString()).toBeEmpty();
expect(stderr.toString()).toMatch(/Script not found/);
expect(exitCode).toBe(1);
});
test("should error with missing module", () => {
const { exitCode, stdout, stderr } = spawnSync({
cwd,
cmd: [bunExe(), "./notpresent.js"],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
expect(stdout.toString()).toBeEmpty();
expect(stderr.toString()).toMatch(/Module not found/);
expect(exitCode).toBe(1);
});
test("should error with missing file", () => {
const { exitCode, stdout, stderr } = spawnSync({
cwd,
cmd: [bunExe(), "/path/to/notpresent.txt"],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
expect(stdout.toString()).toBeEmpty();
expect(stderr.toString()).toMatch(/Module not found/);
expect(exitCode).toBe(1);
});
});
describe("bun --if-present", () => {
test("should not error with missing script", () => {
const { exitCode, stdout, stderr } = spawnSync({
cwd,
cmd: [bunExe(), "--if-present", "notpresent"],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
expect(stdout.toString()).toBeEmpty();
expect(stderr.toString()).toBeEmpty();
expect(exitCode).toBe(0);
});
test("should not error with missing module", () => {
const { exitCode, stdout, stderr } = spawnSync({
cwd,
cmd: [bunExe(), "--if-present", "./notpresent.js"],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
expect(stdout.toString()).toBeEmpty();
expect(stderr.toString()).toBeEmpty();
expect(exitCode).toBe(0);
});
test("should not error with missing file", () => {
const { exitCode, stdout, stderr } = spawnSync({
cwd,
cmd: [bunExe(), "--if-present", "/path/to/notpresent.txt"],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
expect(stdout.toString()).toBeEmpty();
expect(stderr.toString()).toBeEmpty();
expect(exitCode).toBe(0);
});
test("should run present script", () => {
const { exitCode, stdout, stderr } = spawnSync({
cwd,
cmd: [bunExe(), "run", "present"],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
expect(stdout.toString()).toMatch(/Here!/);
expect(stderr.toString()).not.toBeEmpty();
expect(exitCode).toBe(0);
});
test("should run present module", () => {
const { exitCode, stdout, stderr } = spawnSync({
cwd,
cmd: [bunExe(), "run", "present.js"],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
expect(stdout.toString()).toMatch(/Here!/);
expect(stderr.toString()).toBeEmpty();
expect(exitCode).toBe(0);
});
});
+3
View File
@@ -0,0 +1,3 @@
import { Fragment } from "./fragment.tsx";
console.log(Fragment);
@@ -0,0 +1,9 @@
import { expect, it } from "bun:test";
import { nsExample1, nsExample2, nsExample3, nsExample4 } from "../../snippets/jsx-attributes.tsx";
it("parses namespaced attributes correctly", () => {
expect(nsExample1.props).toEqual({ "ns:bar": "baz", "tag": true });
expect(nsExample2.props).toEqual({ "ns:bar42": "baz", "tag": false });
expect(nsExample3.props).toEqual({ "ns:bar42bar": "baz" });
expect(nsExample4.props).toEqual({ "ns42:bar": "baz" });
});
+18
View File
@@ -0,0 +1,18 @@
import { describe, expect, it } from "bun:test";
import { bunEnv, bunExe } from "harness";
import { join } from "path";
describe.concurrent("jsx-symbol-collision", () => {
it("should not have a symbol collision with jsx imports", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--bun", join(import.meta.dir, "jsx-collision.tsx")],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("[Function: Fragment]\n");
expect(stderr).toBeEmpty();
expect(exitCode).toBe(0);
});
});
+1
View File
@@ -0,0 +1 @@
module.exports = {};
+34
View File
@@ -0,0 +1,34 @@
import { spawnSync } from "bun";
import { expect, it } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
it("should not log .env when quiet", async () => {
using dir = tempDir("log-test-silent", {
".env": "FOO=bar",
"bunfig.toml": `logLevel = "error"`,
"index.ts": "export default console.log('Here');",
});
const { stderr } = spawnSync({
cmd: [bunExe(), "index.ts"],
cwd: String(dir),
env: bunEnv,
});
expect(stderr!.toString()).toBe("");
});
it("should log .env by default", async () => {
using dir = tempDir("log-test-silent", {
".env": "FOO=bar",
"bunfig.toml": ``,
"index.ts": "export default console.log('Here');",
});
const { stderr } = spawnSync({
cmd: [bunExe(), "index.ts"],
cwd: String(dir),
env: bunEnv,
});
expect(stderr?.toString().includes(".env")).toBe(false);
});
+404
View File
@@ -0,0 +1,404 @@
import { afterEach, describe, expect, test } from "bun:test";
import { readdirSync, readFileSync, rmSync, symlinkSync } from "fs";
import { bunEnv, bunExe, isASAN, isDebug, isPosix, tempDir } from "harness";
import { join } from "path";
// Tracks exit code from the last runMd() call so individual tests can
// assert it after snapshotting stdout (giving a readable diff on failure).
let lastExitCode: number | null = null;
async function runMd(source: string, env: Record<string, string> = {}) {
using dir = tempDir("md-entry-", { "doc.md": source });
await using proc = Bun.spawn({
cmd: [bunExe(), "./doc.md"],
env: { ...bunEnv, FORCE_COLOR: "1", TERM: "xterm-256color", ...env },
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
// stderr intentionally not asserted: ASAN builds emit a warning there.
const [stdout, _stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
lastExitCode = exitCode;
return stdout;
}
describe("bun <file.md>", () => {
afterEach(() => {
// Implicit exit-code assertion for every test that relies on runMd().
if (lastExitCode !== null) {
expect(lastExitCode).toBe(0);
lastExitCode = null;
}
});
test("renders headings with underlines", async () => {
expect(
await runMd(["# Heading 1", "", "## Heading 2", "", "### Heading 3", "", "body", ""].join("\n")),
).toMatchSnapshot();
});
test("renders bold, italic, strikethrough, inline code", async () => {
expect(await runMd("**bold** *italic* ~~strike~~ `code` regular\n")).toMatchSnapshot();
});
test("renders ordered, unordered, and task lists", async () => {
expect(
await runMd(
[
"1. first",
"2. second",
"3. third",
"",
"- apple",
"- banana",
"- cherry",
"",
"- [ ] todo",
"- [x] done",
"",
].join("\n"),
),
).toMatchSnapshot();
});
test("renders blockquotes and nested blockquotes", async () => {
expect(await runMd(["> A quote", ">", "> > Nested quote", ""].join("\n"))).toMatchSnapshot();
});
test("renders horizontal rules", async () => {
expect(await runMd(["above", "", "---", "", "below", ""].join("\n"))).toMatchSnapshot();
});
test("renders fenced code block with JS syntax highlighting", async () => {
expect(
await runMd(["```js", 'const name = "world";', "console.log(`hello ${name}`);", "```", ""].join("\n")),
).toMatchSnapshot();
});
test("renders fenced code block without language", async () => {
expect(await runMd(["```", "plain text", "no highlighting", "```", ""].join("\n"))).toMatchSnapshot();
});
test("renders link text with url fallback when no TTY", async () => {
// runMd() spawns Bun with stdout:"pipe", so Output.isStdoutTTY() is
// false and hyperlinks fall back to `text (url)`. The OSC 8 path fires
// when stdout really is a TTY.
expect(await runMd("Visit [Bun](https://bun.com) today.\n")).toMatchSnapshot();
});
test("emits OSC 8 escape for links when hyperlinks are enabled", async () => {
// runMd() pipes stdout so the CLI path can't see a TTY; drive the
// JS API directly with hyperlinks:true to cover the OSC 8 branch.
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`process.stdout.write(Bun.markdown.ansi("see [Bun](https://bun.com)\\n", { hyperlinks: true }))`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, _stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// Output must contain the OSC 8 opener + closer around the link.
expect(stdout).toContain("\x1b]8;;https://bun.com\x1b\\");
expect(stdout).toContain("\x1b]8;;\x1b\\");
expect(exitCode).toBe(0);
});
test("renders link with text + url pair fallback", async () => {
expect(await runMd("see [Bun](https://bun.com)\n")).toMatchSnapshot();
});
test("renders images as alt text with link", async () => {
expect(await runMd("![an image](https://bun.com/logo.png)\n")).toMatchSnapshot();
});
test("renders wikilinks", async () => {
expect(await runMd("see [[SomePage]] for more\n")).toMatchSnapshot();
});
// Regression: the parser's `[[` fast-path used to skip emitting the
// preceding text when the wikilink failed to close. `foo [[bar baz`
// should render the full text literally, not drop the `foo ` prefix.
test("falls back to literal text when `[[` never closes", async () => {
expect(await runMd("foo [[bar baz\n")).toMatchSnapshot();
});
test("renders simple table with alignment", async () => {
expect(
await runMd(
[
"| Name | Age | City |",
"|:------|:---:|-----:|",
"| Alice | 30 | NYC |",
"| Bob | 25 | LA |",
"",
].join("\n"),
),
).toMatchSnapshot();
});
test("renders table with CJK multi-width graphemes", async () => {
expect(
await runMd(
[
"| 名前 | 言語 | 都市 |",
"|:-------|:----------:|-----------:|",
"| 山田 | 日本語 | 東京 |",
"| Alice | English | NYC |",
"",
].join("\n"),
),
).toMatchSnapshot();
});
test("renders table with emoji graphemes", async () => {
expect(
await runMd(
[
"| Status | Label |",
"|:------:|:-------|",
"| ✅ | pass |",
"| ❌ | fail |",
"| 🚀 | launch |",
"",
].join("\n"),
),
).toMatchSnapshot();
});
test("renders combining characters without breaking alignment", async () => {
// Decomposed NFD: 'e' + U+0301 combining acute, 'i' + U+0308 combining diaeresis.
// These must be zero-width in the grapheme counter for the table to line up.
const stdout = await runMd(
["| Name | Note |", "|:-------|:-----|", "| cafe\u0301 | hot |", "| nai\u0308ve | ok |", ""].join("\n"),
);
expect(stdout).toMatchSnapshot();
expect(lastExitCode).toBe(0);
});
test("renders inline formatting inside table cells", async () => {
const stdout = await runMd(
[
"| Name | Style |",
"|:------|:----------|",
"| **Alice** | *editor* |",
"| `bob` | [link](https://bun.com) |",
"",
].join("\n"),
);
expect(stdout).toMatchSnapshot();
expect(lastExitCode).toBe(0);
});
test("renders inline formatting inside headings", async () => {
const stdout = await runMd("# Hello **bold** and *italic* heading\n");
expect(stdout).toMatchSnapshot();
expect(lastExitCode).toBe(0);
});
test("nested code span inside bold keeps outer bold", async () => {
const stdout = await runMd("**before `code` after** tail\n");
expect(stdout).toMatchSnapshot();
expect(lastExitCode).toBe(0);
});
test("renders mixed inline styles with autolinks", async () => {
expect(await runMd("Check **https://bun.com** and <[email protected]>!\n")).toMatchSnapshot();
});
test("renders nested lists", async () => {
expect(
await runMd(["- outer", " - inner 1", " - inner 2", " - deep", "- second outer", ""].join("\n")),
).toMatchSnapshot();
});
test("runs without colors when NO_COLOR is set", async () => {
using dir = tempDir("md-no-color-", {
"doc.md": "# Hello\n\n**world**\n",
});
const env = { ...bunEnv, NO_COLOR: "1" };
// FORCE_COLOR set to anything (even "") forces colors on, so drop it.
delete env.FORCE_COLOR;
await using proc = Bun.spawn({
cmd: [bunExe(), "./doc.md"],
env,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, _stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// No escape characters expected
expect(stdout).not.toContain("\x1b[");
expect(stdout).toMatchSnapshot();
expect(exitCode).toBe(0);
});
test("renders via `bun run ./file.md`", async () => {
using dir = tempDir("md-run-", { "doc.md": "# Title\n\nbody\n" });
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "./doc.md"],
env: { ...bunEnv, FORCE_COLOR: "1", TERM: "xterm-256color" },
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, _stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toMatchSnapshot();
expect(exitCode).toBe(0);
});
test("renders .markdown extension too", async () => {
using dir = tempDir("md-ext-", { "doc.markdown": "# yep\n" });
await using proc = Bun.spawn({
cmd: [bunExe(), "./doc.markdown"],
env: { ...bunEnv, FORCE_COLOR: "1", TERM: "xterm-256color" },
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, _stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toMatchSnapshot();
expect(exitCode).toBe(0);
});
// Every emitted line must fit the terminal — checked via Bun.stringWidth
// (visible width, ANSI-stripped) so escape sequences don't inflate the
// count.
function maxLineWidth(out: string): number {
return Math.max(0, ...out.split("\n").map(l => Bun.stringWidth(l)));
}
test("wraps inline code spans within COLUMNS, never mid-word", async () => {
const out = await runMd(
"After modifying `coreBeeps.ts`, `controlBoops.ts`, or `toolBoops.ts`, run " +
"`bun run build:boop` to regenerate. **Do not edit `*.generated.ts` files directly.**\n",
{ COLUMNS: "45" },
);
expect(maxLineWidth(out)).toBeLessThanOrEqual(45);
expect(out).toMatchSnapshot();
});
test("hard-breaks an overlong CJK word at the visible-width boundary", async () => {
// 60 bytes = 20 CJK chars = 40 visible columns; must wrap at 30.
const out = await runMd("A " + Buffer.alloc(60, "测试").toString() + " wide.\n", { COLUMNS: "30" });
expect(out).not.toContain("\uFFFD");
expect(maxLineWidth(out)).toBeLessThanOrEqual(30);
expect(out).toMatchSnapshot();
});
test("shrinks table columns to fit COLUMNS and wraps cell content", async () => {
const out = await runMd(
[
"| Path | Scope |",
"| --- | --- |",
"| `docs/guide.md` | Top-level architecture: module map, startup flow, state management |",
"| `docs/components/design-system/guide.md` | Design system component API (Dialog, Tabs, StatusIcon, etc.) |",
"",
].join("\n"),
{ COLUMNS: "50" },
);
expect(maxLineWidth(out)).toBeLessThanOrEqual(50);
// Borders stay aligned: every non-blank line starts and ends with `│`
// (or the matching corner/junction char for the top/bottom/separator).
for (const line of out.split("\n")) {
if (line.trim().length === 0) continue;
const stripped = Bun.stripANSI(line);
expect(
stripped.startsWith("│") || stripped.startsWith("┌") || stripped.startsWith("├") || stripped.startsWith("└"),
).toBe(true);
expect(stripped.endsWith("│") || stripped.endsWith("┐") || stripped.endsWith("┤") || stripped.endsWith("┘")).toBe(
true,
);
}
expect(out).toMatchSnapshot();
});
// Regression: the row-wrapper's word-break refinement used a raw byte
// scan for the last space inside the cut, which found spaces inside an
// OSC 8 URL (e.g. `[text](<https://host/my file.png>)`) and truncated
// the escape sequence mid-URL, corrupting every subsequent row.
//
// Triggering this needs the URL space to be the ONLY space in the
// refinement window: the link is the first content in the cell and is
// followed by unbreakable text (no literal spaces) that forces a wrap
// BEFORE any external space appears — so lastIndexOfChar(' ') has
// nothing else to return.
test("table link with space in URL keeps OSC 8 sequences intact", () => {
const source = "| Col |\n" + "|---|\n" + "| [c](<https://host/my file.png>)longunbreakabletailtextxx |\n";
const out = Bun.markdown.ansi(source, { hyperlinks: true, columns: 25 });
// Full URL (including the space) must survive inside the opener.
expect(out).toContain("\x1b]8;;https://host/my file.png\x1b\\");
// Every OSC 8 opener must have its own ST before the next one starts
// — a truncation would leave an opener dangling.
let i = 0;
let openers = 0;
while (true) {
const open = out.indexOf("\x1b]8;;", i);
if (open === -1) break;
openers++;
const close = out.indexOf("\x1b\\", open);
expect(close).not.toBe(-1);
const nextOpen = out.indexOf("\x1b]8;;", open + 5);
if (nextOpen !== -1) expect(close).toBeLessThan(nextOpen);
i = close + 2;
}
expect(openers).toBeGreaterThanOrEqual(2);
});
test.skipIf(!isPosix || !(isDebug || isASAN))(
"prefetched remote images are staged into fresh temp files and never written through a pre-existing entry at the staged path",
async () => {
const payload = Buffer.alloc(256, "z");
await using server = Bun.serve({
port: 0,
fetch() {
return new Response(payload, { headers: { "content-type": "image/png" } });
},
});
using dir = tempDir("md-remote-img-", {
"doc.md": `![img](http://127.0.0.1:${server.port}/img.png)\n`,
"tmp/decoy": "keep",
});
const tmp = join(String(dir), "tmp");
const decoy = join(tmp, "decoy");
const env = {
...bunEnv,
FORCE_COLOR: "1",
TERM: "xterm-kitty",
KITTY_WINDOW_ID: "1",
BUN_DEBUG_HASH_RANDOM_SEED: "42",
BUN_TMPDIR: tmp,
TMPDIR: tmp,
};
async function runOnce() {
await using proc = Bun.spawn({
cmd: [bunExe(), "./doc.md"],
env,
cwd: String(dir),
terminal: { cols: 80, rows: 24, data() {} },
});
const exitCode = await proc.exited;
proc.terminal?.close();
return exitCode;
}
const firstExit = await runOnce();
const staged = readdirSync(tmp).filter(n => n.startsWith("bun-md-"));
expect(staged.length).toBe(1);
expect(staged[0].endsWith(".png")).toBe(true);
expect(readFileSync(join(tmp, staged[0])).equals(payload)).toBe(true);
expect(firstExit).toBe(0);
rmSync(join(tmp, staged[0]));
symlinkSync(decoy, join(tmp, staged[0]));
const secondExit = await runOnce();
expect(readFileSync(decoy, "utf8")).toBe("keep");
expect(readdirSync(tmp).sort()).toEqual([staged[0], "decoy"].sort());
expect(secondExit).toBe(0);
},
);
});
@@ -0,0 +1 @@
console.log(eval("typeof module === 'undefined'"));
@@ -0,0 +1 @@
console.log(eval("typeof module === 'undefined'"));
@@ -0,0 +1 @@
console.log(eval("typeof module === 'undefined'"));
@@ -0,0 +1 @@
console.log(eval("typeof module === 'undefined'"));
@@ -0,0 +1 @@
console.log(eval("typeof module === 'undefined'"));
@@ -0,0 +1 @@
console.log(eval("typeof module === 'undefined'"));
@@ -0,0 +1 @@
console.log(eval("typeof module === 'undefined'"));
@@ -0,0 +1 @@
console.log(eval("typeof module === 'undefined'"));
@@ -0,0 +1,3 @@
import * as fs from "node:fs";
console.log(eval("typeof module === 'undefined'"));
+fs;
@@ -0,0 +1,3 @@
{
"type": "commonjs"
}
@@ -0,0 +1 @@
console.log(eval("typeof module === 'undefined'"));
@@ -0,0 +1 @@
console.log(eval("typeof module === 'undefined'"));
@@ -0,0 +1 @@
console.log(eval("typeof module === 'undefined'"));
@@ -0,0 +1 @@
console.log(eval("typeof module === 'undefined'"));
@@ -0,0 +1 @@
console.log(eval("typeof module === 'undefined'"));
@@ -0,0 +1 @@
console.log(eval("typeof module === 'undefined'"));
@@ -0,0 +1 @@
console.log(eval("typeof module === 'undefined'"));
@@ -0,0 +1 @@
console.log(eval("typeof module === 'undefined'"));
@@ -0,0 +1,3 @@
import * as fs from "node:fs";
console.log(eval("typeof module === 'undefined'"));
+fs;
@@ -0,0 +1,3 @@
{
"type": "module"
}
File diff suppressed because it is too large Load Diff
+265
View File
@@ -0,0 +1,265 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
test("--no-env-file disables .env loading", async () => {
using dir = tempDir("no-env-file", {
".env": "FOO=bar",
"index.js": "console.log(process.env.FOO);",
});
// Without --no-env-file, .env should be loaded
{
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout.trim()).toBe("bar");
expect(exitCode).toBe(0);
}
// With --no-env-file, .env should NOT be loaded
{
await using proc = Bun.spawn({
cmd: [bunExe(), "--no-env-file", "index.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout.trim()).toBe("undefined");
expect(exitCode).toBe(0);
}
});
test("--no-env-file disables .env.local loading", async () => {
using dir = tempDir("no-env-file-local", {
".env": "FOO=bar",
".env.local": "FOO=local",
"index.js": "console.log(process.env.FOO);",
});
// Without --no-env-file, .env.local should override .env
{
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout.trim()).toBe("local");
expect(exitCode).toBe(0);
}
// With --no-env-file, neither should be loaded
{
await using proc = Bun.spawn({
cmd: [bunExe(), "--no-env-file", "index.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout.trim()).toBe("undefined");
expect(exitCode).toBe(0);
}
});
test("--no-env-file disables .env.development.local loading", async () => {
using dir = tempDir("no-env-file-dev-local", {
".env": "FOO=bar",
".env.development.local": "FOO=dev-local",
"index.js": "console.log(process.env.FOO);",
});
// Without --no-env-file, .env.development.local should be loaded
{
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout.trim()).toBe("dev-local");
expect(exitCode).toBe(0);
}
// With --no-env-file, it should NOT be loaded
{
await using proc = Bun.spawn({
cmd: [bunExe(), "--no-env-file", "index.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout.trim()).toBe("undefined");
expect(exitCode).toBe(0);
}
});
test("bunfig env.file = false disables .env loading", async () => {
using dir = tempDir("bunfig-env-file-false", {
".env": "FOO=bar",
"bunfig.toml": `
[env]
file = false
`,
"index.js": "console.log(process.env.FOO);",
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout.trim()).toBe("undefined");
expect(exitCode).toBe(0);
});
test("bunfig env = false disables .env loading", async () => {
using dir = tempDir("bunfig-env-false", {
".env": "FOO=bar",
"bunfig.toml": `
env = false
`,
"index.js": "console.log(process.env.FOO);",
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout.trim()).toBe("undefined");
expect(exitCode).toBe(0);
});
test("--no-env-file with -e flag", async () => {
using dir = tempDir("no-env-file-eval", {
".env": "FOO=bar",
});
await using proc = Bun.spawn({
cmd: [bunExe(), "--no-env-file", "-e", "console.log(process.env.FOO)"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout.trim()).toBe("undefined");
expect(exitCode).toBe(0);
});
test("--no-env-file combined with --env-file still loads explicit file", async () => {
using dir = tempDir("no-env-file-with-env-file", {
".env": "FOO=bar",
".env.custom": "FOO=custom",
"index.js": "console.log(process.env.FOO);",
});
// --no-env-file should skip .env but --env-file should load .env.custom
await using proc = Bun.spawn({
cmd: [bunExe(), "--no-env-file", "--env-file", ".env.custom", "index.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout.trim()).toBe("custom");
expect(exitCode).toBe(0);
});
test("bunfig env = true still loads .env files", async () => {
using dir = tempDir("bunfig-env-true", {
".env": "FOO=bar",
"bunfig.toml": `
env = true
`,
"index.js": "console.log(process.env.FOO);",
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout.trim()).toBe("bar");
expect(exitCode).toBe(0);
});
test("--no-env-file in production mode", async () => {
using dir = tempDir("no-env-file-production", {
".env": "FOO=bar",
".env.production": "FOO=prod",
"index.js": "console.log(process.env.FOO);",
});
await using proc = Bun.spawn({
cmd: [bunExe(), "--no-env-file", "index.js"],
env: { ...bunEnv, NODE_ENV: "production" },
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout.trim()).toBe("undefined");
expect(exitCode).toBe(0);
});
File diff suppressed because it is too large Load Diff
+213
View File
@@ -0,0 +1,213 @@
import { spawnSync } from "bun";
import { describe, expect, test } from "bun:test";
import { mkdirSync, realpathSync } from "fs";
import { bunEnv, bunExe } from "harness";
import { tmpdir } from "os";
import { join } from "path";
const preloadModule = `
import {plugin} from 'bun';
plugin({
setup(build) {
build.onResolve({ filter: /.*\.txt$/, }, async (args) => {
return {
path: args.path,
namespace: 'boop'
}
});
build.onLoad({ namespace: "boop", filter: /.*/ }, async (args) => {
return {
contents: '"hello world"',
loader: 'json'
}
});
}
});
`;
const mainModule = `import hey from './hey.txt';
if (hey !== 'hello world') {
throw new Error('preload test failed, got ' + hey);
}
console.log('Test passed');
process.exit(0);
`;
const bunfig = `preload = ["./preload.js"]`;
describe("preload", () => {
test.todo("works", async () => {
const preloadDir = join(realpathSync(tmpdir()), "bun-preload-test");
mkdirSync(preloadDir, { recursive: true });
const preloadPath = join(preloadDir, "preload.js");
const mainPath = join(preloadDir, "main.js");
const bunfigPath = join(preloadDir, "bunfig.toml");
await Bun.write(preloadPath, preloadModule);
await Bun.write(mainPath, mainModule);
await Bun.write(bunfigPath, bunfig);
const cmds = [
[bunExe(), "run", mainPath],
[bunExe(), mainPath],
];
for (let cmd of cmds) {
const { stderr, exitCode, stdout } = spawnSync({
cmd,
cwd: preloadDir,
stderr: "pipe",
stdout: "pipe",
env: bunEnv,
});
expect(stderr.toString()).toBe("");
expect(stdout.toString()).toContain("Test passed");
expect(exitCode).toBe(0);
}
});
test.todo("works from CLI", async () => {
const preloadDir = join(realpathSync(tmpdir()), "bun-preload-test4");
mkdirSync(preloadDir, { recursive: true });
const preloadPath = join(preloadDir, "preload.js");
const mainPath = join(preloadDir, "main.js");
await Bun.write(preloadPath, preloadModule);
await Bun.write(mainPath, mainModule);
const cmds = [
[bunExe(), "-r=" + preloadPath, "run", mainPath],
[bunExe(), "-r=" + preloadPath, mainPath],
];
for (let cmd of cmds) {
const { stderr, exitCode, stdout } = spawnSync({
cmd,
cwd: preloadDir,
stderr: "pipe",
stdout: "pipe",
env: bunEnv,
});
expect(stderr.toString()).toBe("");
expect(stdout.toString()).toContain("Test passed");
expect(exitCode).toBe(0);
}
});
describe("as entry point", () => {
const preloadModule = `
import {plugin} from 'bun';
console.log('preload')
plugin({
setup(build) {
build.onResolve({ filter: /.*\.txt$/, }, async (args) => {
return {
path: args.path,
namespace: 'boop'
}
});
build.onLoad({ namespace: "boop", filter: /.*/ }, async (args) => {
return {
contents: 'console.log("Test passed")',
loader: 'js'
}
});
}
});
`;
test.todo("works from CLI", async () => {
const preloadDir = join(realpathSync(tmpdir()), "bun-preload-test6");
mkdirSync(preloadDir, { recursive: true });
const preloadPath = join(preloadDir, "preload.js");
const mainPath = join(preloadDir, "boop.txt");
await Bun.write(preloadPath, preloadModule);
await Bun.write(mainPath, "beep");
const cmds = [
[bunExe(), "-r=" + preloadPath, "run", mainPath],
[bunExe(), "-r=" + preloadPath, mainPath],
];
for (let cmd of cmds) {
const { stderr, exitCode, stdout } = spawnSync({
cmd,
cwd: preloadDir,
stderr: "pipe",
stdout: "pipe",
env: bunEnv,
});
expect(stderr.toString()).toBe("");
expect(stdout.toString()).toContain("Test passed");
expect(exitCode).toBe(0);
}
});
});
test("throws an error when preloaded module fails to execute", async () => {
const preloadModule = "throw new Error('preload test failed');";
const preloadDir = join(realpathSync(tmpdir()), "bun-preload-test3");
mkdirSync(preloadDir, { recursive: true });
const preloadPath = join(preloadDir, "preload.js");
const mainPath = join(preloadDir, "main.js");
const bunfigPath = join(preloadDir, "bunfig.toml");
await Bun.write(preloadPath, preloadModule);
await Bun.write(mainPath, mainModule);
await Bun.write(bunfigPath, bunfig);
const cmds = [
[bunExe(), "run", mainPath],
[bunExe(), mainPath],
];
for (let cmd of cmds) {
const { stderr, exitCode, stdout } = spawnSync({
cmd,
cwd: preloadDir,
stderr: "pipe",
stdout: "pipe",
env: bunEnv,
});
expect(stderr.toString()).toContain("preload test failed");
expect(stdout.toString()).toBe("");
expect(exitCode).toBe(1);
}
});
test("throws an error when preloaded module not found", async () => {
const bunfig = `preload = ["./bad-file.js"]`;
const preloadDir = join(realpathSync(tmpdir()), "bun-preload-test2");
mkdirSync(preloadDir, { recursive: true });
const preloadPath = join(preloadDir, "preload.js");
const mainPath = join(preloadDir, "main.js");
const bunfigPath = join(preloadDir, "bunfig.toml");
await Bun.write(preloadPath, preloadModule);
await Bun.write(mainPath, mainModule);
await Bun.write(bunfigPath, bunfig);
const cmds = [
[bunExe(), "run", mainPath],
[bunExe(), mainPath],
];
for (let cmd of cmds) {
const { stderr, exitCode, stdout } = spawnSync({
cmd,
cwd: preloadDir,
stderr: "pipe",
stdout: "pipe",
env: bunEnv,
});
expect(stderr.toString()).toContain("preload not found ");
expect(stdout.toString()).toBe("");
expect(exitCode).toBe(1);
}
});
});
@@ -0,0 +1,28 @@
import { expect, test } from "bun:test";
import { tempDir } from "harness";
test("require() with trailing slash", () => {
using requireDir = tempDir("require-trailing", {
"package.json": `
{
// Comments!
"name": "require-and-import-trailing",
"version": "1.0.0",
},`,
});
expect(require(requireDir + "/package.json").name).toBe("require-and-import-trailing");
});
test("import() with trailing slash", async () => {
await using importDir = tempDir("import-trailing", {
"package.json": `
{
// Comments!
"name": "require-and-import-trailing",
"version": "1.0.0",
},`,
});
expect((await import(importDir + "/package.json")).default.name).toBe("require-and-import-trailing");
});
+22
View File
@@ -0,0 +1,22 @@
const { strictEqual, ok } = require("assert");
// The synthetic "manually insert an unevaluated entry via Loader.registry.set"
// case was removed: the JSC module loader is now pure C++ and no longer exposes
// a writable JS registry. The msgpackr-extract path below covers the original
// bug (#5188) end-to-end.
// hard to simplify this test case, but importing this would cause require.cache.extract to be set
require("msgpackr-extract");
strictEqual(require.cache["extract"], undefined);
ok(!("extract" in require.cache)); // https://github.com/oven-sh/bun/issues/5898
ok(!Object.hasOwnProperty.call(require.cache, "extract"));
ok(!Object.getOwnPropertyNames(require.cache).includes("extract"));
for (const key of Object.keys(require.cache)) {
if (!require.cache[key]) {
throw new Error("require.cache has an undefined value that was in it's keys");
}
}
console.log("--pass--");
File diff suppressed because it is too large Load Diff
+34
View File
@@ -0,0 +1,34 @@
const dest = require.resolve("./require-cache-bug-leak-fixture-large-ast.js");
// ASAN's quarantine retains freed allocations (default 256 MB) so RSS deltas
// run far higher under bun-asan; widen the threshold to avoid false positives.
const isASAN = process.execPath.includes("bun-asan");
const rss =
process.platform === "darwin" && typeof Bun !== "undefined" && typeof Bun.unsafe.memoryFootprint === "function"
? Bun.unsafe.memoryFootprint
: process.memoryUsage.rss;
if (typeof Bun !== "undefined") Bun.gc(true);
for (let i = 0; i < 5; i++) {
delete require.cache[dest];
require(dest);
}
if (typeof Bun !== "undefined") Bun.gc(true);
const baseline = rss();
for (let i = 0; i < 50; i++) {
delete require.cache[dest];
require(dest);
}
if (typeof Bun !== "undefined") Bun.gc(true);
setTimeout(() => {
let diff = rss() - baseline;
diff = (diff / 1024 / 1024) | 0;
console.log({ leaked: diff + " MB" });
if (diff > (isASAN ? 400 : 120)) {
console.log("\n--fail--\n");
process.exit(1);
} else {
console.log("\n--pass--\n");
}
}, 16);
+15
View File
@@ -0,0 +1,15 @@
exports.foo = 123;
exports.bar = 456;
exports.baz = 789;
if (require.main === module) {
console.error(__filename, module.id);
throw new Error("require.main === module");
}
if (module.parent == null || typeof module.parent !== "object") {
console.error(module.parent);
throw new Error("module.parent == null");
}
module.exports = { x: module.parent };
+66
View File
@@ -0,0 +1,66 @@
// This fixture is intended to be able to run in both Node.js and Bun
const Bun = (globalThis.Bun ??= { gc() {} });
const { resolve } = require("path");
if (require.main !== module) {
console.error(__filename, module.id);
throw new Error("require.main !== module");
}
if (module.parent !== null) {
console.error(module.parent);
throw new Error("module.parent !== null");
}
if (process.mainModule !== module) {
console.error(__filename, module.id);
throw new Error("process.mainModule !== module");
}
if (__filename !== resolve(module.filename)) {
console.error(__filename, module.id);
throw new Error("__filename !== module.id");
}
if (__dirname !== resolve(module.filename, "../")) {
console.error(__filename, module.id);
throw new Error("__dirname !== module.filename");
}
const foo = require("./require-cache-fixture-b.cjs");
if (foo.x !== module) {
console.error(__filename, foo);
throw new Error("foo !== module");
}
exports.foo = foo;
var res = require.resolve;
if (require.cache[res("./require-cache-fixture-b.cjs")].exports !== exports.foo) {
throw new Error("exports.foo !== require.cache[require.resolve('./require-cache-fixture-b')]");
}
Bun.gc(true);
delete require.cache[res("./require-cache-fixture-b.cjs")];
Bun.gc(true);
exports.bar = require("./require-cache-fixture-b.cjs");
Bun.gc(true);
if (require.cache[res("./require-cache-fixture-b.cjs")].exports !== exports.bar) {
throw new Error("exports.bar !== require.cache[require.resolve('./require-cache-fixture-b')]");
}
if (require.cache[require.resolve("./require-cache-fixture-b.cjs")].exports === exports.foo) {
throw new Error("exports.bar === exports.foo");
}
console.log(require.cache);
console.log("\n--pass--\n");
+345
View File
@@ -0,0 +1,345 @@
import { describe, expect, test } from "bun:test";
import {
bunEnv,
bunExe,
isArm64,
isASAN,
isBroken,
isCI,
isIntelMacOS,
isMacOS,
isMusl,
isWindows,
tempDir,
} from "harness";
import { join } from "path";
describe.concurrent("require.cache", () => {
test("require.cache is not an empty object literal when inspected", () => {
const inspected = Bun.inspect(require.cache);
expect(inspected).not.toBe("{}");
expect(inspected).toContain("Module {");
});
// This also tests __dirname and __filename
test("require.cache", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "run", join(import.meta.dir, "require-cache-fixture.cjs")],
env: bunEnv,
stderr: "inherit",
});
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
expect(stdout.trim()).toEndWith("--pass--");
expect(exitCode).toBe(0);
});
// https://github.com/oven-sh/bun/issues/5188
// msgpackr-extract has no prebuilt binary for win32-arm64, so it's unavailable there
test.skipIf(isWindows && isArm64)("require.cache does not include unevaluated modules", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "run", join(import.meta.dir, "require-cache-bug-5188.js")],
env: bunEnv,
stderr: "inherit",
});
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
expect(stdout.trim()).toEndWith("--pass--");
expect(exitCode).toBe(0);
});
describe.skipIf(isBroken && isIntelMacOS)("files transpiled and loaded don't leak the output source code", () => {
test("via require() with a lot of long export names", async () => {
let text = "";
for (let i = 0; i < 10000; i++) {
text += `exports.superDuperExtraCrazyLongNameWowSuchNameLongYouveNeverSeenANameThisLongForACommonJSModuleExport${i} = 1;\n`;
}
console.log("Text length:", text.length);
await using dir = tempDir("require-cache-bug-leak-1", {
"index.js": text,
"require-cache-bug-leak-fixture.js": `
const path = require.resolve("./index.js");
const gc = global.gc || globalThis?.Bun?.gc || (() => {});
const rss = process.platform === "darwin" && typeof Bun !== "undefined" && typeof Bun.unsafe.memoryFootprint === "function" ? Bun.unsafe.memoryFootprint : process.memoryUsage.rss;
const noChildren = module.children = { indexOf() { return 0; } }; // disable children tracking
function bust() {
const mod = require.cache[path];
if (mod) {
mod.parent = null;
mod.children = noChildren;
delete require.cache[path];
}
}
for (let i = 0; i < 50; i++) {
require(path);
bust();
}
gc(true);
const baseline = rss();
for (let i = 0; i < 500; i++) {
require(path);
bust(path);
}
gc(true);
const after = rss();
const diff = after - baseline;
console.log("RSS diff", (diff / 1024 / 1024) | 0, "MB");
console.log("RSS", (diff / 1024 / 1024) | 0, "MB");
if (diff > ${isASAN ? 400 : 100} * 1024 * 1024) {
// Bun v1.1.21 reported 844 MB here on macOS arm64.
throw new Error("Memory leak detected");
}
exports.abc = 123;
`,
});
console.log({ dir });
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--smol", join(dir, "require-cache-bug-leak-fixture.js")],
env: bunEnv,
stdio: ["inherit", "inherit", "inherit"],
});
const exitCode = await proc.exited;
expect(exitCode).toBe(0);
}, 60000);
test("via await import() with a lot of function calls", async () => {
let text = "function i() { return 1; }\n";
for (let i = 0; i < 20000; i++) {
text += `i();\n`;
}
text += "exports.forceCommonJS = true;\n";
console.log("Text length:", text.length);
await using dir = tempDir("require-cache-bug-leak-3", {
"index.js": text,
"require-cache-bug-leak-fixture.js": `
const path = require.resolve("./index.js");
const gc = global.gc || globalThis?.Bun?.gc || (() => {});
const rss = process.platform === "darwin" && typeof Bun !== "undefined" && typeof Bun.unsafe.memoryFootprint === "function" ? Bun.unsafe.memoryFootprint : process.memoryUsage.rss;
function bust() {
delete require.cache[path];
}
for (let i = 0; i < 100; i++) {
await import(path);
bust();
}
gc(true);
const baseline = rss();
for (let i = 0; i < 400; i++) {
await import(path);
bust(path);
}
gc(true);
const after = rss();
const diff = after - baseline;
console.log("RSS diff", (diff / 1024 / 1024) | 0, "MB");
console.log("RSS", (diff / 1024 / 1024) | 0, "MB");
if (diff > ${isASAN ? 320 : 64} * 1024 * 1024) {
// Bun v1.1.22 reported 1 MB here on macoS arm64.
// Bun v1.1.21 reported 257 MB here on macoS arm64.
throw new Error("Memory leak detected");
}
export default 123;
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--smol", join(dir, "require-cache-bug-leak-fixture.js")],
env: bunEnv,
stdio: ["inherit", "inherit", "inherit"],
});
const exitCode = await proc.exited;
expect(exitCode).toBe(0);
}, 60000); // takes 4s on an M1 in release build
test("via import() with a lot of long export names", async () => {
let text = "";
for (let i = 0; i < 10000; i++) {
text += `export const superDuperExtraCrazyLongNameWowSuchNameLongYouveNeverSeenANameThisLongForACommonJSModuleExport${i} = 1;\n`;
}
await using dir = tempDir("require-cache-bug-leak-4", {
"index.js": text,
"require-cache-bug-leak-fixture.js": `
const path = require.resolve("./index.js");
const gc = global.gc || globalThis?.Bun?.gc || (() => {});
const rss = process.platform === "darwin" && typeof Bun !== "undefined" && typeof Bun.unsafe.memoryFootprint === "function" ? Bun.unsafe.memoryFootprint : process.memoryUsage.rss;
function bust() {
delete require.cache[path];
}
for (let i = 0; i < 50; i++) {
await import(path);
bust();
}
gc(true);
const baseline = rss();
for (let i = 0; i < 250; i++) {
await import(path);
bust(path);
}
gc(true);
const after = rss();
const diff = after - baseline;
console.log("RSS diff", (diff / 1024 / 1024) | 0, "MB");
console.log("RSS", (diff / 1024 / 1024) | 0, "MB");
if (diff > ${isASAN ? 320 : 64} * 1024 * 1024) {
// Bun v1.1.21 reported 423 MB here on macoS arm64.
// Bun v1.1.22 reported 4 MB here on macoS arm64.
throw new Error("Memory leak detected");
}
export default 124;
`,
});
console.log({ dir });
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--smol", join(dir, "require-cache-bug-leak-fixture.js")],
env: bunEnv,
stdio: ["inherit", "inherit", "inherit"],
});
const exitCode = await proc.exited;
expect(exitCode).toBe(0);
}, 60000);
test.todoIf(
// Flaky specifically on macOS CI, and on musl-aarch64 under ThinLTO +
// -Zshare-generics where RSS reports ~280 MB for the same workload
// that measures under 64 MB elsewhere (intermittent).
isBroken && isCI && (isMacOS || (isMusl && isArm64)),
)(
"via require() with a lot of function calls",
async () => {
let text = "function i() { return 1; }\n";
for (let i = 0; i < 20000; i++) {
text += `i();\n`;
}
text += "exports.forceCommonJS = true;\n";
console.log("Text length:", text.length);
await using dir = tempDir("require-cache-bug-leak-2", {
"index.js": text,
"require-cache-bug-leak-fixture.js": `
const path = require.resolve("./index.js");
const gc = global.gc || globalThis?.Bun?.gc || (() => {});
const rss = process.platform === "darwin" && typeof Bun !== "undefined" && typeof Bun.unsafe.memoryFootprint === "function" ? Bun.unsafe.memoryFootprint : process.memoryUsage.rss;
function bust() {
const mod = require.cache[path];
if (mod) {
mod.parent = null;
mod.children = [];
delete require.cache[path];
}
}
for (let i = 0; i < 100; i++) {
require(path);
bust();
}
gc(true);
const baseline = rss();
for (let i = 0; i < 400; i++) {
require(path);
bust(path);
}
gc(true);
const after = rss();
const diff = after - baseline;
console.log("RSS diff", (diff / 1024 / 1024) | 0, "MB");
console.log("RSS", (diff / 1024 / 1024) | 0, "MB");
if (diff > ${isASAN ? 320 : 64} * 1024 * 1024) {
// Bun v1.1.22 reported 4 MB here on macoS arm64.
// Bun v1.1.21 reported 248 MB here on macoS arm64.
throw new Error("Memory leak detected");
}
exports.abc = 123;
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--smol", join(dir, "require-cache-bug-leak-fixture.js")],
env: bunEnv,
stdio: ["inherit", "inherit", "inherit"],
});
const exitCode = await proc.exited;
expect(exitCode).toBe(0);
},
60000,
); // takes 4s on an M1 in release build
});
describe("files transpiled and loaded don't leak the AST", () => {
test("via require()", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "run", join(import.meta.dir, "require-cache-bug-leak-fixture.js")],
env: bunEnv,
stderr: "inherit",
});
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
expect(stdout.trim()).toEndWith("--pass--");
expect(exitCode).toBe(0);
}, 20000);
test("via import()", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "run", join(import.meta.dir, "esm-bug-leak-fixture.mjs")],
env: bunEnv,
stderr: "inherit",
});
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
expect(stdout.trim()).toEndWith("--pass--");
expect(exitCode).toBe(0);
}, 20000);
});
// These tests are extra slow in debug builds
describe("files transpiled and loaded don't leak file paths", () => {
test("via require()", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "--smol", "run", join(import.meta.dir, "cjs-fixture-leak-small.js")],
env: bunEnv,
stderr: "inherit",
});
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
expect(stdout.trim()).toEndWith("--pass--");
expect(exitCode).toBe(0);
}, 30000);
test(
"via import()",
async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "--smol", "run", join(import.meta.dir, "esm-fixture-leak-small.mjs")],
env: bunEnv,
stderr: "inherit",
});
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
expect(stdout.trim()).toEndWith("--pass--");
expect(exitCode).toBe(0);
},
// TODO: Investigate why this is so slow on Windows
isWindows ? 60000 : 30000,
);
});
});
@@ -0,0 +1,67 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import { join } from "path";
// dirInfoForResolution stored DirInfo.abs_path as a slice into the threadlocal
// bufs(.path_in_global_disk_cache) buffer. After auto-installing a second package, that
// buffer is overwritten and the first package's cached DirInfo.abs_path points at stale
// bytes. When a module inside the first package then resolves its own name (package
// self-reference), loadNodeModules reads dir_info.abs_path directly and looks up a
// garbage path. With global_cache=.auto the auto-install fallback is skipped because
// any_node_modules_folder was set by the self-reference branch, so resolution fails with
// "Cannot find module". With the fix, abs_path is interned in DirnameStore and stays
// valid. A debug assertion in dirInfoUncached additionally guards the invariant.
//
// Kept in its own file because the sibling run-autoinstall.test.ts cases mutate the
// shared bunEnv and routinely time out on debug/ASAN builds when hitting the live
// registry, which would otherwise mask this test's pass/fail signal.
test("auto-install: DirInfo.abs_path survives threadlocal buffer reuse across resolutions", async () => {
using dir = tempDir("autoinstall-abs-path", {
// No package.json / node_modules so global_cache defaults to .auto (resolver.zig canUse).
// nanoid has `exports` with a `./non-secure` subpath, enabling the self-reference
// branch at resolver.zig:1807. left-pad's cache folder name is longer than nanoid's,
// so nanoid's cached abs_path slice becomes a truncated prefix of left-pad's path
// once left-pad is resolved. left-pad is archived so its version string is stable.
"index.js": `
const path = require("path");
const { createRequire } = require("module");
const nanoidPath = require.resolve("nanoid");
const nanoidDir = path.dirname(nanoidPath);
require.resolve("left-pad");
const innerRequire = createRequire(nanoidPath);
const nonSecure = innerRequire.resolve("nanoid/non-secure");
if (!nonSecure.startsWith(nanoidDir)) {
throw new Error("self-reference resolved outside package dir: " + nonSecure + " vs " + nanoidDir);
}
console.log("resolved");
`,
});
await using proc = Bun.spawn({
// Deliberately no -i / --install flag: default .auto prevents the auto-install
// fallback from masking the corrupted abs_path.
cmd: [bunExe(), "index.js"],
cwd: String(dir),
// Use a per-test cache dir: a shared cache can be left half-populated (extracted dir
// present, name/version symlink missing) if a prior run is killed mid-install, which
// makes pathForCachedNPMPath's readlinkat return ENOENT before auto-install kicks in.
env: {
...bunEnv,
BUN_INSTALL: undefined,
BUN_INSTALL_CACHE_DIR: join(String(dir), "cache"),
},
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).not.toContain("Cannot find module");
expect(stdout.trim()).toBe("resolved");
expect(exitCode).toBe(0);
// Cold-cache download of two packages from the live registry on a debug/ASAN build
// routinely exceeds the 5s default; other registry-hitting tests (bun-add.test.ts,
// bun-create.test.ts) use the same pattern.
}, 30_000);
+123
View File
@@ -0,0 +1,123 @@
import { describe, expect, test } from "bun:test";
import { mkdirSync } from "fs";
import { bunEnv, bunExe, tempDir, tmpdirSync } from "harness";
import { join } from "path";
// --install=<val> Configure auto-install behavior. One of "auto" (default, auto-installs when no node_modules), "fallback" (missing packages only), "force" (always).
// -i Auto-install dependencies during execution. Equivalent to --install=fallback.
describe("basic autoinstall", () => {
for (const install of ["", "-i", "--install=auto", "--install=fallback", "--install=force"]) {
for (const has_node_modules of [true, false]) {
let should_install = false;
if (has_node_modules) {
if (install === "" || install === "--install=auto") {
should_install = false;
} else {
should_install = true;
}
} else {
should_install = true;
}
test(`${install || "<no flag>"} ${has_node_modules ? "with" : "without"} node_modules ${should_install ? "should" : "should not"} autoinstall`, async () => {
const dir = tmpdirSync();
mkdirSync(dir, { recursive: true });
await Bun.write(join(dir, "index.js"), "import isEven from 'is-even'; console.log(isEven(2));");
const env = bunEnv;
env.BUN_INSTALL = install;
if (has_node_modules) {
mkdirSync(join(dir, "node_modules/abc"), { recursive: true });
}
const { stdout, stderr } = Bun.spawnSync({
cmd: [bunExe(), ...(install === "" ? [] : [install]), join(dir, "index.js")],
cwd: dir,
env,
stdout: "pipe",
stderr: "pipe",
});
if (should_install) {
expect(stderr?.toString("utf8")).not.toContain("error: Cannot find package 'is-even'");
expect(stdout?.toString("utf8")).toBe("true\n");
} else {
expect(stderr?.toString("utf8")).toContain("error: Cannot find package 'is-even'");
}
});
}
}
});
// In auto-install mode the project's own package.json is the lockfile's root
// package (resolution tag `root`, not `npm`). With a name and an exact version
// present, resolving any missing bare specifier used to read that resolution
// through the npm union accessor: "assertion failed: self.tag == Tag::Npm".
test("auto-install in a project whose package.json has a name and version", async () => {
const requests: string[] = [];
using registry = Bun.serve({
port: 0,
fetch(req) {
requests.push(new URL(req.url).pathname);
return new Response("not found", { status: 404 });
},
});
using dir = tempDir("autoinstall-root-name-version", {
"package.json": JSON.stringify({ name: "myapp", version: "1.0.0" }),
"index.js": `import "pkg-that-does-not-exist-anywhere";\n`,
"bunfig.toml": `[install]\nregistry = "http://127.0.0.1:${registry.port}/"\n`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
cwd: String(dir),
env: { ...bunEnv, BUN_INSTALL_CACHE_DIR: join(String(dir), ".bun-cache") },
stdout: "pipe",
stderr: "pipe",
});
const [, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// The resolver must get as far as asking the (local) registry for the
// missing package, then report it as missing instead of dying while
// re-parsing the project's own package.json.
expect(requests).toContain("/pkg-that-does-not-exist-anywhere");
expect(stderr).toContain("Cannot find package 'pkg-that-does-not-exist-anywhere'");
expect(exitCode).toBe(1);
});
test("--install=fallback to install missing packages", async () => {
const dir = tmpdirSync();
mkdirSync(dir, { recursive: true });
await Promise.all([
Bun.write(
join(dir, "index.js"),
"import isEven from 'is-even'; import isOdd from 'is-odd'; console.log(isEven(2), isOdd(2));",
),
Bun.write(
join(dir, "package.json"),
JSON.stringify({
name: "test",
dependencies: {
"is-odd": "1.0.0",
},
}),
),
]);
Bun.spawnSync({
cmd: [bunExe(), "install"],
cwd: dir,
env: bunEnv,
});
const { stdout, stderr } = Bun.spawnSync({
cmd: [bunExe(), "--install=fallback", join(dir, "index.js")],
cwd: dir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
expect(stderr?.toString("utf8")).not.toContain("error: Cannot find package 'is-odd'");
expect(stdout?.toString("utf8")).toBe("true false\n");
});
+47
View File
@@ -0,0 +1,47 @@
import { describe, expect, test } from "bun:test";
import { mkdirSync } from "fs";
import { bunEnv, bunExe, tempDir, tmpdirSync } from "harness";
import { join } from "path";
describe.concurrent("run-cjs", () => {
test("running a commonjs module works", async () => {
const dir = tmpdirSync();
mkdirSync(dir, { recursive: true });
await Bun.write(join(dir, "index1.js"), "module.exports = 1; console.log('hello world');");
await using proc = Bun.spawn({
cmd: [bunExe(), join(dir, "index1.js")],
cwd: dir,
env: bunEnv,
stdout: "pipe",
});
const stdout = await proc.stdout.text();
expect(stdout).toEqual("hello world\n");
});
test("a pre-bundled entry point still consults require.extensions", async () => {
// `bun build --target=bun --format=cjs` emits this header, and the `// @bun`
// pragma makes the parser hand the source straight to JSC without printing
// it. That must not change how the modules this entry loads are resolved.
using dir = tempDir("run-cjs-bundled-entry", {
"entry.cjs": `// @bun @bun-cjs
(function(exports, require, module, __filename, __dirname) {
require.extensions[".data"] = (module, filename) => {
module.exports = "custom-loader";
};
console.log(require("./asset.data"));
})`,
// If the custom loader is skipped, this is transpiled as JS/TS instead.
"asset.data": `module.exports = "default-loader";`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "entry.cjs"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stdout, stderr, exitCode }).toMatchObject({ stdout: "custom-loader\n", exitCode: 0 });
});
});
+602
View File
@@ -0,0 +1,602 @@
import { crash_handler } from "bun:internal-for-testing";
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, isASAN, isDebug, isLinux, isPosix, isWindows, mergeWindowEnvs, tempDir } from "harness";
import { rmSync } from "node:fs";
import path from "path";
const { getMachOImageZeroOffset } = crash_handler;
// CI sets BUN_CRASH_REPORT_URL so unexpected crashes are captured; these
// deliberate crashes must not upload there or the runner pins them on the
// next unrelated failing test as "crash reported" and blocks its retries.
const noReportEnv = { ...bunEnv, BUN_CRASH_REPORT_URL: "", BUN_ENABLE_CRASH_REPORTING: "0" };
// On Linux, debug builds symbolize crash traces by spawning llvm-symbolizer;
// without it the fallback printer has no Rust symbol names to assert on.
const hasSymbolizer = !!(Bun.which("llvm-symbolizer") || Bun.which("llvm-symbolizer-21"));
test.if(isDebug && isLinux && hasSymbolizer)(
"crash trace starts at the crash site, not inside the crash handler",
async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), path.join(import.meta.dir, "fixture-crash.js"), "panic"],
env: noReportEnv,
stdio: ["ignore", "pipe", "pipe"],
});
// The panic header goes to stderr; the symbolized frames are printed by
// llvm-symbolizer, which is spawned with inherited stdio, so they land on
// stdout.
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("panic(main thread): invoked crashByPanic() handler");
expect(exitCode).not.toBe(0);
// The innermost frame of the trace must be the code that crashed (the
// js_panic test hook)...
const firstFrame = stdout.split("\n").find(line => line.trim().length > 0);
expect(firstFrame ?? "<no frames printed>").toContain("js_panic");
// ...not the capture machinery. A mismatched trim anchor used to leave
// `capture_stack_trace` → `crash_handler` → `panic_impl` as the innermost
// frames of every report, burying the real crash site.
expect(stdout).not.toContain("capture_stack_trace");
},
60_000, // symbolizing the debug binary takes several seconds
);
// `crash()` resets fatal-signal dispositions to SIG_DFL before re-raising so
// that JS-registered listeners (`process.on("SIGABRT")` etc., installed by
// npm's widely-used signal-exit package) cannot swallow the termination. A
// JS listener's backing sigaction enqueues to the JS thread and returns;
// without the reset the process would survive the raise and fall through to
// the trap fallback, which on aarch64 (brk → SIGTRAP) used to spin forever.
test.if(isPosix)(
"panic terminates the process even when JS registered trap-signal listeners",
async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`process.on("SIGTRAP", () => {});
process.on("SIGILL", () => {});
process.on("SIGABRT", () => {});
require("bun:internal-for-testing").crash_handler.panic();`,
// Make debug builds take the fast trace-string path instead of
// spawning llvm-symbolizer, which can take tens of seconds.
"--debug-crash-handler-use-trace-string",
],
env: noReportEnv,
stdio: ["ignore", "pipe", "pipe"],
});
// Without the fix the child never exits — it loops SIGTRAP delivery on the
// trap instruction. Bound the wait and fail explicitly rather than hanging
// the test runner and leaking a core-pinning process.
const exited = await Promise.race([proc.exited, Bun.sleep(8_000).then(() => "spinning" as const)]);
if (exited === "spinning") {
proc.kill("SIGKILL");
}
const stderr = await proc.stderr.text();
expect(exited, `process should have died from the trap, stderr:\n${stderr}`).not.toBe("spinning");
// It went through the crash handler...
expect(stderr).toContain("invoked crashByPanic() handler");
// ...and died from the trap's default action, not a clean exit, and not a
// JS-observed SIGTRAP (the JS listener must never swallow the crash).
expect(proc.signalCode === null ? proc.exitCode : proc.signalCode).not.toBe(0);
},
20_000,
);
// After printing the crash report the handler must terminate with a signal
// that reflects the crash cause: panics abort (SIGABRT), a caught fault is
// re-raised as the original signal. Previously the handler ended in a trap
// instruction (ud2 → SIGILL on x86_64, brk → SIGTRAP on aarch64) so shells
// reported "illegal hardware instruction" for every crash and parent
// processes could not distinguish a panic from a CPU/codegen fault.
describe.if(isPosix)("terminal signal reflects the crash cause", () => {
test.each([
["panic", "SIGABRT"],
["outOfMemory", "SIGABRT"],
["segfault", "SIGSEGV"],
["abort", "SIGABRT"],
["trap", "SIGTRAP"],
] as const)("%s terminates with %s", async (approach, expectedSignal) => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
path.join(import.meta.dir, "fixture-crash.js"),
approach,
"--debug-crash-handler-use-trace-string",
],
env: noReportEnv,
stdio: ["ignore", "pipe", "pipe"],
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
if (approach === "segfault") {
expect(stderr).toContain("Segmentation fault at address");
} else if (approach === "panic") {
expect(stderr).toContain("invoked crashByPanic() handler");
} else if (approach === "abort") {
expect(stderr).toContain("abort() called");
} else if (approach === "trap") {
expect(stderr).toContain("Trap instruction");
}
expect(proc.signalCode).toBe(expectedSignal);
expect(exitCode).not.toBe(0);
void stdout;
});
});
// POSIX-only: Windows refuses to remove a directory that is any process's cwd.
describe.if(isPosix)("cwd deleted before startup", () => {
test.concurrent.each(["install", "test"])("bun %s prints the cwd-deleted hint", async cmd => {
using dir = tempDir("cwd-unlinked", {});
const gone = String(dir);
await using proc = Bun.spawn({
cmd: ["/bin/sh", "-c", `cd "${gone}" && rmdir "${gone}" && exec "${bunExe()}" '${cmd}'`],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stdout, stderr, exitCode }).toEqual({
stdout: "",
stderr: expect.stringContaining("The current working directory was deleted"),
exitCode: 1,
});
expect(stderr).not.toContain("Bun could not find a file");
});
test.concurrent("bun -e boots via the exe-dir fallback instead", async () => {
using dir = tempDir("cwd-unlinked-run", {});
const gone = String(dir);
await using proc = Bun.spawn({
cmd: ["/bin/sh", "-c", `cd "${gone}" && rmdir "${gone}" && exec "${bunExe()}" -e 'console.log(1)'`],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("1\n");
expect(stderr).toBe("");
expect(exitCode).toBe(0);
});
});
// Windows: the VEH handler must walk the stack from the fault CONTEXT record
// (RtlVirtualUnwind), not from inside the handler. When the fault is in an
// external DLL the old RtlCaptureStackBackTrace path could stop at
// KiUserExceptionDispatcher on some Windows versions, leaving only the
// handler's own frames in the trace and none of the bun callers.
test.if(isWindows && isDebug)("Windows: segfault inside a system DLL captures the bun callers", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), path.join(import.meta.dir, "fixture-crash.js"), "segfaultInDll"],
env: noReportEnv,
stdio: ["ignore", "pipe", "pipe"],
});
const [, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("Segmentation fault at address 0xDEADBEEF");
expect(exitCode).not.toBe(0);
// The debug build's fallback printer emits one `???:?:?: 0x<addr>` line per
// captured frame. A walk seeded from the fault CONTEXT reaches through the
// DLL into the bun call chain (the JS host-fn dispatch is several frames
// deep), so a short trace means the unwind stopped at the exception
// dispatcher and the handler's own frames are all that was captured.
const frameAddrs = [...stderr.matchAll(/: (0x[0-9a-f]{6,}) in /gi)].map(m => BigInt(m[1]));
expect(frameAddrs.length).toBeGreaterThanOrEqual(7);
// Frame 0 is the fault PC inside ntdll.dll; frames 1+ must be the bun call
// chain with no handler or ntdll-dispatch frames interleaved. Frames 1..6 all
// coming from one image means their address span fits inside that image's
// mapped range; the old RtlCaptureStackBackTrace path left
// [handler x3][ntdll-dispatch x3] ahead of the first real caller, so frames
// 4-6 landed in ntdll and the span covered the >10 GiB gap between the EXE
// and system-DLL HEASLR regions.
const callers = frameAddrs.slice(1, 7);
const span = callers.reduce((a, b) => (a > b ? a : b)) - callers.reduce((a, b) => (a < b ? a : b));
expect(span).toBeLessThan(2n ** 31n);
});
// The Windows crash handler is a Vectored Exception Handler, which sees every
// first-chance exception process-wide before frame-based SEH does. Third-party
// DLLs injected into the process (AV/EDR agents such as BeyondTrust's
// PGHook.dll, virtualization guest tools, shell extensions) routinely raise
// and then handle access violations under SEH as part of normal operation.
// The VEH must let those through rather than treating them as a fatal crash.
// `IsBadReadPtr` is the canonical example: it probes its argument inside a
// `__try`/`__except` in kernel32, so the AV it raises is inside a system DLL
// and is immediately swallowed by that DLL's own SEH.
//
// See https://github.com/oven-sh/bun/issues/10056 (Carbon Black),
// https://github.com/oven-sh/bun/issues/11898 (Trend Micro).
describe.if(isWindows)("Windows VEH handler and first-chance faults in external DLLs", () => {
test("SEH-guarded probe survives", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`const { dlopen } = require("bun:ffi");
const lib = dlopen("kernel32.dll", {
IsBadReadPtr: { args: ["usize", "usize"], returns: "i32" },
});
const rc = lib.symbols.IsBadReadPtr(0xE8, 8);
console.log("SURVIVED rc=" + rc);`,
],
env: noReportEnv,
stdio: ["ignore", "pipe", "pipe"],
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).not.toContain("Segmentation fault");
// rc=1: kernel32's SEH caught the AV and reported the pointer as bad.
expect(stdout.trim()).toBe("SURVIVED rc=1");
expect(exitCode).toBe(0);
});
// `RtlFillMemory` has no `__try`/`__except` around its store. With the VEH
// now returning CONTINUE_SEARCH for out-of-image PCs, the catch point is
// JSC's jscJITSEHHandler (registered for JIT frames), which routes to
// Bun__crashHandlerFromJSCFrame, or UEF. This exercises that the crash is
// still reported and the report carries the fault address.
test("unguarded fault still crash-reports", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"--debug-crash-handler-use-trace-string",
"-e",
`const { dlopen } = require("bun:ffi");
const lib = dlopen("ntdll.dll", {
RtlFillMemory: { args: ["usize", "usize", "i32"], returns: "void" },
});
lib.symbols.RtlFillMemory(0xE8, 8, 0);
console.log("SHOULD NOT REACH");`,
],
env: noReportEnv,
stdio: ["ignore", "pipe", "pipe"],
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("Segmentation fault at address 0xE8");
expect(stdout).not.toContain("SHOULD NOT REACH");
expect(exitCode).not.toBe(0);
});
// Validate WebKit's registerJITUnwindInfo against the actual unwinder:
// RtlLookupFunctionEntry must return a RUNTIME_FUNCTION for a JIT pool PC.
// This is the smoke test for the hand-encoded UNWIND_INFO / .xdata bytes.
// LLInt PCs are not covered here: LLInt lives in image .text and Windows
// only consults static .pdata for in-module PCs; that needs build-time
// .seh_* emission in offlineasm (follow-up).
test("RtlLookupFunctionEntry resolves JSC JIT pool PCs", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`const { dlopen, FFIType, ptr } = require("bun:ffi");
const { symbols } = dlopen("ntdll.dll", {
RtlLookupFunctionEntry: {
args: [FFIType.u64, FFIType.pointer, FFIType.pointer],
returns: FFIType.pointer,
},
});
const { jscInternals } = require("bun:internal-for-testing");
const pool = jscInternals.startOfFixedExecutableMemoryPool();
const imageBase = new BigUint64Array(1);
const jitEntry = symbols.RtlLookupFunctionEntry(pool + 0x100n, ptr(imageBase), null);
console.log(JSON.stringify({
pool: pool.toString(16),
jitEntry: jitEntry === null ? "null" : "ok",
}));`,
],
env: noReportEnv,
stdio: ["ignore", "pipe", "pipe"],
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
const out = JSON.parse(stdout.trim());
expect(out.jitEntry).toBe("ok");
expect(exitCode).toBe(0);
});
// End-to-end: warm a JS function into the JIT, then fault from inside it
// via FFI. The crash report must fire via jscJITSEHHandler at the JIT
// boundary. Clears the UEF backstop first so the assertion isolates the JSC
// handler (deleting setJITExceptionHandlerWin would break this test, not
// just fall through to UEF). Disables the concurrent JIT so warm-up is
// deterministic.
test("unguarded fault from inside a JIT-compiled frame still crash-reports via the JSC handler", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"--debug-crash-handler-use-trace-string",
"-e",
`const { dlopen } = require("bun:ffi");
const ntdll = dlopen("ntdll.dll", {
RtlFillMemory: { args: ["usize", "usize", "i32"], returns: "void" },
});
const k32 = dlopen("kernel32.dll", {
SetUnhandledExceptionFilter: { args: ["usize"], returns: "usize" },
});
function hot(i) {
if (i === 10000) ntdll.symbols.RtlFillMemory(0xE8, 8, 0);
return i;
}
for (let i = 0; i < 10000; i++) hot(i);
k32.symbols.SetUnhandledExceptionFilter(0);
hot(10000);
console.log("SHOULD NOT REACH");`,
],
env: { ...noReportEnv, BUN_JSC_jitPolicyScale: "0", BUN_JSC_useConcurrentJIT: "0" },
stdio: ["ignore", "pipe", "pipe"],
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("Segmentation fault at address 0xE8");
expect(stdout).not.toContain("SHOULD NOT REACH");
expect(exitCode).not.toBe(0);
});
});
test.if(process.platform === "darwin")("macOS has the assumed image offset", () => {
// If this fails, then https://bun.report will be incorrect and the stack
// trace remappings will stop working.
expect(getMachOImageZeroOffset()).toBe(0x100000000);
});
test("raise ignoring panic handler does not trigger the panic handler", async () => {
let sent = false;
const resolve_handler = Promise.withResolvers();
using server = Bun.serve({
port: 0,
fetch(request, server) {
sent = true;
resolve_handler.resolve();
return new Response("OK");
},
});
const proc = Bun.spawn({
cmd: [bunExe(), path.join(import.meta.dir, "fixture-crash.js"), "raiseIgnoringPanicHandler"],
env: mergeWindowEnvs([
bunEnv,
{
BUN_CRASH_REPORT_URL: server.url.toString(),
BUN_ENABLE_CRASH_REPORTING: "1",
},
]),
});
await proc.exited;
/// Wait two seconds for a slow http request, or continue immediately once the request is heard.
await Promise.race([resolve_handler.promise, Bun.sleep(2000)]);
expect(proc.exited).resolves.not.toBe(0);
expect(sent).toBe(false);
});
// SIGABRT (libc abort(), mimalloc/glibc heap-corruption, std::terminate) and
// SIGTRAP (WTF CRASH()/RELEASE_ASSERT, __builtin_trap() -> `brk` on aarch64)
// must route through the crash handler so they are not silently lost.
describe.if(isPosix)("SIGABRT/SIGTRAP are caught by the crash handler", () => {
test.concurrent.each([
["abort", "SIGABRT", "abort() called"],
["trap", "SIGTRAP", "Trap instruction"],
] as const)("%s produces a crash report", async (approach, expectedSignal, expectedMsg) => {
let sent = false;
const resolve_handler = Promise.withResolvers<void>();
using server = Bun.serve({
port: 0,
fetch(request) {
expect(request.url).toEndWith("/ack");
sent = true;
resolve_handler.resolve();
return new Response("OK");
},
});
await using proc = Bun.spawn({
cmd: [
bunExe(),
path.join(import.meta.dir, "fixture-crash.js"),
approach,
"--debug-crash-handler-use-trace-string",
],
env: mergeWindowEnvs([
bunEnv,
{
BUN_CRASH_REPORT_URL: server.url.toString(),
BUN_ENABLE_CRASH_REPORTING: "1",
GITHUB_ACTIONS: undefined,
CI: undefined,
},
]),
stdio: ["ignore", "pipe", "pipe"],
});
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
expect(stderr).toContain(expectedMsg);
expect(stderr).toContain("oh no");
expect(stderr).toContain(server.url.toString());
expect(proc.signalCode).toBe(expectedSignal);
expect(exitCode).not.toBe(0);
await resolve_handler.promise;
expect(sent).toBe(true);
});
// These two tests terminate via SIG_DFL (not via a test hook that calls
// suppress_core_dumps_if_necessary()), so on the --coredump-upload CI lane
// the runner would flag leaked core files as a hard failure. ulimit -c 0 in
// a shell wrapper is inherited by the bun child; the whole describe is
// isPosix-gated so /bin/sh is available.
const noCoreCmd = (argv: string[]) => ["/bin/sh", "-c", `ulimit -c 0 && exec "$@"`, "--", ...argv];
// The above goes via the internal test hook, which under ASAN calls the
// handler directly because ASAN owns the fault signals. This case raises the
// signal for real to prove the sigaction registration itself; ASAN builds
// never install those handlers so skip there.
test.skipIf(isASAN).concurrent.each(["SIGABRT", "SIGTRAP"] as const)(
"raised %s produces a crash report",
async signal => {
await using proc = Bun.spawn({
cmd: noCoreCmd([
bunExe(),
"-e",
`process.kill(process.pid, "${signal}")`,
"--debug-crash-handler-use-trace-string",
]),
env: noReportEnv,
stdio: ["ignore", "pipe", "pipe"],
});
const [stderr] = await Promise.all([proc.stderr.text(), proc.exited]);
expect(stderr).toContain("oh no");
expect(proc.signalCode).toBe(signal);
},
);
// process.abort() is a deliberate user action, not a Bun crash. It must still
// terminate with SIGABRT but must not print a crash report or upload one.
test.concurrent("process.abort() does not report a crash", async () => {
let sent = false;
using server = Bun.serve({
port: 0,
fetch() {
sent = true;
return new Response("OK");
},
});
await using proc = Bun.spawn({
cmd: noCoreCmd([bunExe(), "-e", "process.abort()"]),
env: mergeWindowEnvs([
bunEnv,
{
BUN_CRASH_REPORT_URL: server.url.toString(),
BUN_ENABLE_CRASH_REPORTING: "1",
GITHUB_ACTIONS: undefined,
CI: undefined,
},
]),
stdio: ["ignore", "pipe", "pipe"],
});
const [stderr] = await Promise.all([proc.stderr.text(), proc.exited]);
expect(stderr).not.toContain("Bun has crashed");
expect(stderr).not.toContain(server.url.toString());
expect(proc.signalCode).toBe("SIGABRT");
expect(sent).toBe(false);
});
});
describe("automatic crash reporter", () => {
for (const approach of ["panic", "segfault", "outOfMemory"]) {
test(`${approach} should report`, async () => {
let sent = false;
const resolve_handler = Promise.withResolvers();
// Self host the crash report backend.
using server = Bun.serve({
port: 0,
fetch(request, server) {
expect(request.url).toEndWith("/ack");
sent = true;
resolve_handler.resolve();
return new Response("OK");
},
});
const proc = Bun.spawn({
cmd: [bunExe(), path.join(import.meta.dir, "fixture-crash.js"), approach],
env: mergeWindowEnvs([
bunEnv,
{
BUN_CRASH_REPORT_URL: server.url.toString(),
BUN_ENABLE_CRASH_REPORTING: "1",
GITHUB_ACTIONS: undefined,
CI: undefined,
},
]),
stdio: ["ignore", "pipe", "pipe"],
});
const exitCode = await proc.exited;
const stderr = await proc.stderr.text();
console.log(stderr);
await resolve_handler.promise;
expect(exitCode).not.toBe(0);
expect(stderr).toContain(server.url.toString());
if (approach !== "outOfMemory") {
expect(stderr).toContain("oh no: Bun has crashed. This indicates a bug in Bun, not your code");
} else {
expect(stderr.toLowerCase()).toContain("out of memory");
expect(stderr.toLowerCase()).not.toContain("panic");
}
expect(sent).toBe(true);
});
}
});
test.if(isWindows)(
"Windows: crash report upload runs the system PowerShell, not a powershell.exe in the working directory",
async () => {
let sent = false;
const acked = Promise.withResolvers<void>();
using server = Bun.serve({
port: 0,
fetch(request) {
expect(request.url).toEndWith("/ack");
sent = true;
acked.resolve();
return new Response("OK");
},
});
// Not `using`: the crash reporter's PowerShell child inherits this cwd
// and can outlive the crashed process, so a scoped delete races it.
const dir = tempDir("crash-report-system-powershell", { "placeholder.js": "" });
try {
await Bun.write(path.join(String(dir), "powershell.exe"), Bun.file(bunExe()));
await using proc = Bun.spawn({
cmd: [bunExe(), path.join(import.meta.dir, "fixture-crash.js"), "panic"],
cwd: String(dir),
env: mergeWindowEnvs([
bunEnv,
{
BUN_CRASH_REPORT_URL: server.url.toString(),
BUN_ENABLE_CRASH_REPORTING: "1",
GITHUB_ACTIONS: undefined,
CI: undefined,
},
]),
stdio: ["ignore", "pipe", "pipe"],
});
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
/// Wait two seconds for a slow http request, or continue immediately once the request is heard.
await Promise.race([acked.promise, Bun.sleep(2000)]);
expect(stderr).toContain(server.url.toString());
expect(sent).toBe(true);
expect(exitCode).not.toBe(0);
} finally {
try {
rmSync(String(dir), { recursive: true, force: true, maxRetries: 20, retryDelay: 250 });
} catch {}
}
},
);
+54
View File
@@ -0,0 +1,54 @@
import { bunEnv, bunExe } from "harness";
import { join } from "path";
import { expect, test } from "bun:test";
// module type -> file extensions -> expected module type
const table = {
cjs: {
'hello.cjs': 'commonjs',
'hello.js': 'commonjs',
'hello.mjs': 'module',
'hello.ts': 'commonjs',
'hello.tsx': 'module',
'hello.cts': 'commonjs',
'hello.jsx': 'module',
'hello.mts': 'module',
// files using ES import and no exports will be detected as module
"import.cjs": "module",
},
esm: {
'hello.cjs': 'commonjs',
'hello.js': 'module',
'hello.mjs': 'module',
'hello.ts': 'module',
'hello.tsx': 'module',
'hello.cts': 'commonjs',
'hello.jsx': 'module',
'hello.mts': 'module',
// files using ES import and no exports will be detected as module
"import.cjs": "module",
},
};
test("detect module type", () => {
const expected = Object.entries(table).map(([moduleType, extensions]) => {
return Object.entries(extensions).map(([extension, expected]) => {
return `${moduleType} ${extension} -> ${expected}`;
});
}).flat();
const actual = Object.entries(table).map(([moduleType, extensions]) => {
return Object.entries(extensions).map(([extension, expected]) => {
const proc = Bun.spawnSync({
cmd: [bunExe(), "run", join(import.meta.dir, 'module-type-fixture', moduleType, extension)],
env: bunEnv,
});
if (proc.exitCode !== 0) {
throw new Error(`Failed to run ${moduleType} ${extension}: ${proc.stderr.toString('utf8').trim()}`);
}
return `${moduleType} ${extension} -> ${proc.stdout.toString('utf8').trim() === "false" ? "commonjs" : "module"}`;
});
}).flat();
expect(actual).toEqual(expected);
});
+322
View File
@@ -0,0 +1,322 @@
import { SyncSubprocess } from "bun";
import { describe, expect, test } from "bun:test";
import { rmSync, writeFileSync } from "fs";
import { bunEnv, bunExe, isWindows, tmpdirSync } from "harness";
import { tmpdir } from "os";
import { join, sep } from "path";
for (const flag of ["-e", "--print"]) {
describe(`bun ${flag}`, () => {
test("it works", async () => {
const input = flag === "--print" ? '"hello world"' : 'console.log("hello world")';
let { stdout } = Bun.spawnSync({
cmd: [bunExe(), flag, input],
env: bunEnv,
});
expect(stdout.toString("utf8")).toEqual("hello world\n");
});
test("import, tsx, require in esm, import.meta", async () => {
const ref = await import("react");
const input =
flag === "--print"
? 'import {version} from "react"; console.log(JSON.stringify({version,file:import.meta.path,require:require("react").version})); <hello>world</hello>'
: 'import {version} from "react"; console.log(JSON.stringify({version,file:import.meta.path,require:require("react").version})); console.log(<hello>world</hello>);';
let { stdout } = Bun.spawnSync({
cmd: [bunExe(), flag, input],
env: bunEnv,
});
const json = {
version: ref.version,
file: join(process.cwd(), "[eval]"),
require: ref.version,
};
expect(stdout.toString("utf8")).toEqual(JSON.stringify(json) + "\n<hello>world</hello>\n");
});
test("error has source map info 1", async () => {
let { stderr } = Bun.spawnSync({
cmd: [bunExe(), flag, '(throw new Error("hi" as 2))'],
env: bunEnv,
});
expect(stderr.toString("utf8")).toInclude('"hi" as 2');
expect(stderr.toString("utf8")).toInclude("Unexpected throw");
});
test("process.argv", async () => {
function testProcessArgv(args: string[], expected: string[]) {
const input = flag === "--print" ? "process.argv" : "console.log(process.argv)";
let { stdout, stderr, exitCode } = Bun.spawnSync({
cmd: [bunExe(), flag, input, ...args],
env: bunEnv,
});
expect(stderr.toString("utf8")).toBe("");
expect(JSON.parse(stdout.toString("utf8"))).toEqual(expected);
expect(exitCode).toBe(0);
}
// replace the trailin
const exe = isWindows ? bunExe().replaceAll("/", "\\") : bunExe();
testProcessArgv([], [exe]);
testProcessArgv(["abc", "def"], [exe, "abc", "def"]);
testProcessArgv(["--", "abc", "def"], [exe, "abc", "def"]);
// testProcessArgv(["--", "abc", "--", "def"], [exe, "abc", "--", "def"]);
});
test("process._eval", async () => {
const code = flag === "--print" ? "process._eval" : "console.log(process._eval)";
const { stdout } = Bun.spawnSync({
cmd: [bunExe(), flag, code],
env: bunEnv,
});
expect(stdout.toString("utf8")).toEqual(code + "\n");
});
// The eval source is UTF-8; reading it back as Latin-1 turns every
// multi-byte character into mojibake. The expected text is compared here
// in the parent -- comparing inside the child would pass either way, since
// a Latin-1-decoded source corrupts the literal and process._eval alike.
test("process._eval round-trips multi-byte UTF-8", async () => {
const marker = "/* 한글-🎉-café */";
const code = (flag === "--print" ? "process._eval" : "console.log(process._eval)") + ` ${marker}`;
const { stdout } = Bun.spawnSync({
cmd: [bunExe(), flag, code],
env: bunEnv,
});
expect(stdout.toString("utf8")).toEqual(code + "\n");
});
test("does not crash in non-latin1 directory", async () => {
const dir = join(tmpdirSync(), "eval-test-开始学习");
await Bun.write(join(dir, "index.js"), "console.log('hello world')");
const { stdout, stderr, exitCode } = Bun.spawnSync({
cmd: [bunExe(), flag, "import './index.js'"],
env: bunEnv,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
});
expect(stderr.toString("utf8")).toBe("");
expect(stdout.toString("utf8")).toEqual("hello world\n" + (flag === "--print" ? "undefined\n" : ""));
expect(exitCode).toBe(0);
});
});
}
describe("--print for cjs/esm", () => {
test("eval result between esm imports", async () => {
let cwd = tmpdirSync();
writeFileSync(join(cwd, "foo.js"), "'foo'");
writeFileSync(join(cwd, "bar.js"), "'bar'");
let { stdout, stderr, exitCode } = Bun.spawnSync({
cmd: [bunExe(), "--print", 'import "./foo.js"; 123; import "./bar.js"'],
cwd: cwd,
env: bunEnv,
});
expect(stderr.toString("utf8")).toBe("");
expect(stdout.toString("utf8")).toEqual("123\n");
expect(exitCode).toBe(0);
rmSync(cwd, { recursive: true, force: true });
});
// https://github.com/oven-sh/bun/issues/30207
describe.each([
{ expr: "(await 1) + 1", expected: "2" },
{ expr: 'await Promise.resolve("hello") + " world"', expected: "hello world" },
{ expr: "(await 1) + (await 2)", expected: "3" },
// no top-level await — still returns the expression value.
{ expr: "1 + 1", expected: "2" },
])("bun -p $expr", ({ expr, expected }) => {
test(`${expected}`, async () => {
const { stdout, stderr, exitCode } = Bun.spawnSync({
cmd: [bunExe(), "-p", expr],
env: bunEnv,
});
expect(stderr.toString("utf8")).toBe("");
expect(stdout.toString("utf8")).toBe(`${expected}\n`);
expect(exitCode).toBe(0);
});
});
test("forced cjs", async () => {
let { stdout, stderr, exitCode } = Bun.spawnSync({
cmd: [bunExe(), "--print", "module.exports; 123"],
env: bunEnv,
});
expect(stderr.toString("utf8")).toBe("");
expect(stdout.toString("utf8")).toEqual("123\n");
expect(exitCode).toBe(0);
});
test("module, exports, require, __filename, __dirname", async () => {
let { stdout, stderr, exitCode } = Bun.spawnSync({
cmd: [
bunExe(),
"--print",
`
console.log(typeof module, typeof exports, typeof require, typeof __filename, typeof __dirname); 123
`,
],
env: bunEnv,
});
expect(stderr.toString("utf8")).toBe("");
expect(stdout.toString("utf8")).toEqual("object object function string string\n123\n");
expect(exitCode).toBe(0);
});
test("module._compile is require('module').prototype._compile", async () => {
const { stdout, exitCode } = Bun.spawnSync({
cmd: [bunExe(), "-p", "module._compile === require('module').prototype._compile"],
env: bunEnv,
});
expect(stdout.toString()).toBe("true\n");
expect(exitCode).toBe(0);
});
});
function group(run: (code: string) => SyncSubprocess<"pipe", "inherit">) {
test("it works", async () => {
const { stdout } = run('console.log("hello world")');
expect(stdout.toString("utf8")).toEqual("hello world\n");
});
test("it gets a correct specifer", async () => {
const { stdout } = run("console.log(import.meta.path)");
expect(stdout.toString("utf8")).toEndWith(sep + "[stdin]\n");
});
test("it can require", async () => {
const { stdout } = run(`
const process = require("node:process");
console.log(process.platform);
`);
expect(stdout.toString("utf8")).toEqual(process.platform + "\n");
});
test("it can import", async () => {
const { stdout } = run(`
import * as process from "node:process";
console.log(process.platform);
`);
expect(stdout.toString("utf8")).toEqual(process.platform + "\n");
});
test("process.argv", async () => {
const { stdout } = run("console.log(process.argv)");
const exe = isWindows ? bunExe().replaceAll("/", "\\") : bunExe();
expect(JSON.parse(stdout.toString("utf8"))).toEqual([exe, "-"]);
});
test("process._eval", async () => {
const code = "console.log(process._eval)";
const { stdout } = run(code);
// the file piping one on windows can include extra carriage returns
if (isWindows) {
expect(stdout.toString("utf8")).toInclude(code);
} else {
expect(stdout.toString("utf8")).toEqual(code + "\n");
}
});
}
describe("bun run - < file-path.js", () => {
function run(code: string) {
// bash only supports / as path separator
const file = join(tmpdir(), "bun-run-eval-test.js").replaceAll("\\", "/");
require("fs").writeFileSync(file, code);
try {
let result;
if (process.platform === "win32") {
result = Bun.spawnSync(["powershell", "-c", `Get-Content ${file} | ${bunExe()} run -`], {
env: bunEnv,
stderr: "inherit",
});
} else {
result = Bun.spawnSync(["bash", "-c", `${bunExe()} run - < ${file}`], {
env: bunEnv,
stderr: "inherit",
});
}
if (!result.success) {
queueMicrotask(() => {
throw new Error("bun run - < file-path.js failed");
});
}
return result;
} finally {
try {
require("fs").unlinkSync(file);
} catch (e) {}
}
}
group(run);
});
describe("echo | bun run -", () => {
function run(code: string) {
const result = Bun.spawnSync([bunExe(), "run", "-"], {
env: bunEnv,
stdin: Buffer.from(code),
stderr: "inherit",
});
if (!result.success) {
queueMicrotask(() => {
throw new Error("bun run - failed");
});
}
return result;
}
group(run);
});
test("process._eval (undefined for normal run)", async () => {
const cwd = tmpdirSync();
const file = join(cwd, "test.js");
writeFileSync(file, "console.log(typeof process._eval)");
const { stdout } = Bun.spawnSync({
cmd: [bunExe(), "run", file],
cwd: cwd,
env: bunEnv,
});
expect(stdout.toString("utf8")).toEqual("undefined\n");
rmSync(cwd, { recursive: true, force: true });
});
test("uncaught error from a CommonJS-sniffed eval entry reports and exits 1", async () => {
// The presence of require() makes the eval source evaluate as CommonJS,
// which used to swallow a top-level throw entirely: no stderr output and
// exit code 0.
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", `require("assert"); throw new Error("eval-cjs-uncaught");`],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("eval-cjs-uncaught");
expect(stdout).toBe("");
expect(exitCode).toBe(1);
});
test("uncaught error from a CommonJS-sniffed stdin entry reports and exits 1", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "-"],
env: bunEnv,
stdin: Buffer.from(`require("assert"); throw new Error("stdin-cjs-uncaught");`),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("stdin-cjs-uncaught");
expect(stdout).toBe("");
expect(exitCode).toBe(1);
});
+35
View File
@@ -0,0 +1,35 @@
import { describe, expect, test } from "bun:test";
import { mkdirSync, writeFileSync } from "fs";
import { bunEnv, bunExe, isWindows, tmpdirSync } from "harness";
import { join } from "path";
describe.concurrent("run-extensionless", () => {
test("running extensionless file works", async () => {
const dir = tmpdirSync();
mkdirSync(dir, { recursive: true });
await Bun.write(join(dir, "cool"), "const x: Test = 2; console.log('hello world');");
await using proc = Bun.spawn({
cmd: [bunExe(), join(dir, "./cool")],
cwd: dir,
env: bunEnv,
stdout: "pipe",
});
const stdout = await proc.stdout.text();
expect(stdout).toEqual("hello world\n");
});
test.skipIf(isWindows)("running shebang typescript file works", async () => {
const dir = tmpdirSync();
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, "cool"), `#!${bunExe()}\nconst x: Test = 2; console.log('hello world');`, { mode: 0o777 });
await using proc = Bun.spawn({
cmd: [join(dir, "./cool")],
cwd: dir,
env: bunEnv,
stdout: "pipe",
});
const stdout = await proc.stdout.text();
expect(stdout).toEqual("hello world\n");
});
});
+67
View File
@@ -0,0 +1,67 @@
import { type ReadableSubprocess, spawn } from "bun";
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { bunEnv, bunExe, isLinux, tmpdirSync } from "harness";
import fs from "node:fs";
import { join } from "node:path";
describe.skipIf(!isLinux)("running files on a FUSE mount", () => {
// Mount once for the whole describe block. The first python3/libfuse
// cold-start on Alpine CI can take several seconds when disk I/O is
// contended by background container setup, so a per-test mount with a
// short poll budget is flaky. afterAll always runs (even if beforeAll
// throws) so cleanup is guaranteed.
let mountpoint: string;
let pythonProcess: ReadableSubprocess | undefined;
beforeAll(async () => {
mountpoint = tmpdirSync();
pythonProcess = spawn({
cmd: ["python3", "fuse-fs.py", "-f", mountpoint],
cwd: __dirname,
stdout: "pipe",
stderr: "pipe",
});
// Poll until the mount is ready or the python process exits. The
// bound only guards against a true hang; normal startup is <200ms
// once the page cache is warm.
let tries = 0;
while (!fs.existsSync(join(mountpoint, "main.js")) && tries < 1600 && pythonProcess.exitCode === null) {
tries++;
await Bun.sleep(5);
}
if (pythonProcess.exitCode !== null && pythonProcess.exitCode !== 0) {
throw new Error(
`FUSE process exited early with code ${pythonProcess.exitCode}: ${await pythonProcess.stderr.text()}`,
);
}
expect(fs.existsSync(join(mountpoint, "main.js"))).toBeTrue();
}, 10000);
afterAll(async () => {
if (!pythonProcess) return;
const umount = spawn({ cmd: ["fusermount", "-u", mountpoint] });
await umount.exited;
await Promise.race([pythonProcess.exited, Bun.sleep(1000)]);
if (pythonProcess.exitCode === null) {
pythonProcess.kill("SIGKILL");
console.error("python process errored:", await pythonProcess.stderr.text());
}
});
async function doTest(pathOnMount: string): Promise<void> {
const bun = spawn({
cmd: [bunExe(), join(mountpoint, pathOnMount)],
cwd: __dirname,
stdout: "pipe",
env: bunEnv,
});
await Promise.race([bun.exited, Bun.sleep(1000)]);
expect(bun.exitCode).toBe(0);
expect(await new Response(bun.stdout).text()).toBe("hello world\n");
}
test("regular file", () => doTest("main.js"));
test("symlink", () => doTest("main-symlink.js"));
});
+38
View File
@@ -0,0 +1,38 @@
import { expect, test } from "bun:test";
import { mkdirSync } from "fs";
import { bunEnv, bunExe, tmpdirSync } from "harness";
import { join } from "path";
test("import.meta.main", async () => {
const dir = tmpdirSync();
mkdirSync(dir, { recursive: true });
await Bun.write(
join(dir, "index1.js"),
`import "fs"; console.log(JSON.stringify([typeof require, import.meta.main, !import.meta.main, require.main === module, require.main !== module]));`,
);
const { stdout } = Bun.spawnSync({
cmd: [bunExe(), join(dir, "index1.js")],
cwd: dir,
env: bunEnv,
stderr: "inherit",
stdout: "pipe",
});
expect(stdout.toString("utf8").trim()).toEqual(JSON.stringify(["function", true, false, true, false]));
});
test("import.meta.main in a common.js file", async () => {
const dir = tmpdirSync();
mkdirSync(dir, { recursive: true });
await Bun.write(
join(dir, "index1.js"),
`module.exports = {}; console.log(JSON.stringify([typeof require, import.meta.main, !import.meta.main, require.main === module, require.main !== module]));`,
);
const { stdout } = Bun.spawnSync({
cmd: [bunExe(), join(dir, "index1.js")],
cwd: dir,
env: bunEnv,
stderr: "inherit",
stdout: "pipe",
});
expect(stdout.toString("utf8").trim()).toEqual(JSON.stringify(["function", true, false, true, false]));
});
+28
View File
@@ -0,0 +1,28 @@
import { describe, expect, test } from "bun:test";
import { bunExe, bunRunAsScript, tempDir } from "harness";
describe("process.env", () => {
test("npm_lifecycle_event", () => {
const scriptName = "start:dev";
using dir = tempDir("processenv", {
"package.json": JSON.stringify({ "scripts": { [`${scriptName}`]: `'${bunExe()}' run index.ts` } }),
"index.ts": "console.log(process.env.npm_lifecycle_event);",
});
const { stdout } = bunRunAsScript(dir, scriptName);
expect(stdout).toBe(scriptName);
});
// https://github.com/oven-sh/bun/issues/3589
test("npm_lifecycle_event should have the value of the last call", () => {
using dir = tempDir("processenv_ls_call", {
"package.json": JSON.stringify({ scripts: { first: `'${bunExe()}' run --cwd lsc second` } }),
"lsc": {
"package.json": JSON.stringify({ scripts: { second: `'${bunExe()}' run index.ts` } }),
"index.ts": "console.log(process.env.npm_lifecycle_event);",
},
});
const { stdout } = bunRunAsScript(dir, "first");
expect(stdout).toBe("second");
});
});
@@ -0,0 +1,33 @@
// `bun run <script>` re-raises the child's terminating signal via
// `Global.raiseIgnoringPanicHandler`, which first resets the signal's
// disposition with `bun.sys.sigaction(sig, …)`. `SIGKILL`/`SIGSTOP` can't
// have their disposition changed, so libc returns `EINVAL` there — that
// must not reach `std.posix.sigaction`'s `else => unreachable`.
import { expect, test } from "bun:test";
import { bunEnv, bunExe, isPosix, tempDir } from "harness";
test.skipIf(!isPosix)("bun run propagates SIGKILL from a child without hitting unreachable", async () => {
using dir = tempDir("run-sigkill", {
"package.json": JSON.stringify({
name: "t",
scripts: { go: `${bunExe()} -e 'process.kill(process.pid, "SIGKILL")'` },
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "go"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// The outer `bun run` must itself die by SIGKILL re-raised from the child.
// If `bun.sys.sigaction` routed through `std.posix.sigaction`'s
// `else => unreachable`, this would be SIGILL (debug) or undefined.
expect(stderr).toContain("SIGKILL");
expect(stdout).toBe("");
expect(proc.signalCode).toBe("SIGKILL");
expect(exitCode).not.toBe(0);
});
+79
View File
@@ -0,0 +1,79 @@
import { expect, it } from "bun:test";
import { bunEnv, bunExe, bunRunAsScript, tempDir } from "harness";
it("should handle quote escapes", () => {
const package_json = JSON.stringify({
scripts: {
test: `echo "test\\\\$(pwd)"`,
},
});
expect(package_json).toContain('\\"');
expect(package_json).toContain("\\\\");
using dir = tempDir("run-quote", { "package.json": package_json });
const { stdout } = bunRunAsScript(dir, "test");
expect(stdout).toBe(`test\\${dir}`);
});
it("keeps pass-through arguments containing tabs and question marks as single words", async () => {
await using dir = tempDir("run-quote-passthrough", {
"package.json": JSON.stringify({
scripts: {
args: `${bunExe()} print-args.js`,
},
}),
"print-args.js": "console.log(JSON.stringify(process.argv.slice(2)));",
"aXb": "",
});
const passthrough = ["a\tb", "a?b", "c\rd"];
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "args", "--", ...passthrough],
cwd: dir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, _stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe(JSON.stringify(passthrough) + "\n");
expect(exitCode).toBe(0);
});
// Positional arguments appended to a package.json script are joined into the
// script string, so an empty one must be emitted as a quoted empty word (`""`).
// Dropping it shifts every following positional left by one.
const argvEchoFixture = {
"package.json": JSON.stringify({ name: "run-empty-arg", version: "1.0.0", scripts: { p: "bun args.js" } }),
"args.js": "console.log(JSON.stringify(process.argv.slice(2)));",
};
it.concurrent.each([
["bun run <script>", ["run", "p"]],
["bun run <script> --", ["run", "p", "--"]],
["bun <script>", ["p"]],
])("preserves empty passthrough arguments (%s)", async (_label, prefix) => {
using dir = tempDir("run-empty-arg", argvEchoFixture);
await using proc = Bun.spawn({
cmd: [bunExe(), ...prefix, "a", "", "b"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
if (exitCode !== 0) expect(stderr).toBe("");
expect(stdout.trim()).toBe('["a","","b"]');
expect(exitCode).toBe(0);
});
it.concurrent("preserves empty passthrough arguments (bun --filter)", async () => {
using dir = tempDir("run-empty-arg-filter", argvEchoFixture);
await using proc = Bun.spawn({
cmd: [bunExe(), "--filter", "*", "p", "a", "", "b"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
if (exitCode !== 0) expect(stderr).toBe("");
// `--filter` prefixes each output line with "<package> <script>: ".
expect(stdout).toContain('["a","","b"]');
expect(exitCode).toBe(0);
});
+82
View File
@@ -0,0 +1,82 @@
import { describe, expect, test } from "bun:test";
import { chmodSync, mkdirSync } from "fs";
import { bunEnv, bunExe, isWindows, tempDir, tmpdirSync } from "harness";
import { join } from "path";
describe.concurrent("run-shell", () => {
test("running a shell script works", async () => {
const dir = tmpdirSync();
mkdirSync(dir, { recursive: true });
await Bun.write(join(dir, "something.sh"), "echo wah");
await using proc = Bun.spawn({
cmd: [bunExe(), join(dir, "something.sh")],
cwd: dir,
env: bunEnv,
stderr: "pipe",
stdout: "pipe",
});
const stdout = await proc.stdout.text();
const stderr = await proc.stderr.text();
console.log(stderr);
expect(stdout).toEqual("wah\n");
});
test("invalid syntax reports the error correctly", async () => {
const dir = tmpdirSync("bun-shell-test-error");
mkdirSync(dir, { recursive: true });
const shellScript = `-h)
echo "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"`;
await Bun.write(join(dir, "scripts", "script.sh"), shellScript);
await using proc = Bun.spawn({
cmd: [bunExe(), join(dir, "scripts", "script.sh")],
cwd: dir,
env: bunEnv,
stderr: "pipe",
stdout: "pipe",
});
const stderr = await proc.stderr.text();
expect(stderr).toBe("error: Failed to run script.sh due to error Unexpected ')'\n");
});
});
test.skipIf(isWindows)(
"package script shell interpreter is resolved from the original PATH, not node_modules/.bin",
async () => {
// A dependency can place arbitrary executables named "bash"/"sh"/"zsh" into
// node_modules/.bin via its "bin" field. The interpreter that runs
// package.json scripts must never be picked up from there.
const fakeShell = "#!/bin/sh\necho FAKE_SHELL_USED\n";
using dir = tempDir("run-shell-interpreter", {
"package.json": JSON.stringify({
name: "shell-interpreter-fixture",
version: "1.0.0",
scripts: {
"say-hi": "echo real-shell-ran",
},
}),
"node_modules/.bin/bash": fakeShell,
"node_modules/.bin/sh": fakeShell,
"node_modules/.bin/zsh": fakeShell,
});
for (const name of ["bash", "sh", "zsh"]) {
chmodSync(join(String(dir), "node_modules", ".bin", name), 0o755);
}
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "say-hi"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// The script must run under a real system shell, not the executables a
// dependency dropped into node_modules/.bin.
expect(stdout).not.toContain("FAKE_SHELL_USED");
expect(stderr).not.toContain("FAKE_SHELL_USED");
expect(stdout).toContain("real-shell-ran");
expect(exitCode).toBe(0);
},
);
+43
View File
@@ -0,0 +1,43 @@
import { describe, expect, test } from "bun:test";
import { mkdirSync, realpathSync } from "fs";
import { bunEnv, bunExe, bunRun } from "harness";
import { tmpdir } from "os";
import { join } from "path";
describe.concurrent("run-unicode", () => {
test("running a weird filename works", async () => {
const troll = process.platform == "win32" ? "💥'\\" : "💥'\"\n";
const dir = join(realpathSync(tmpdir()), "bun-run-test" + troll);
mkdirSync(dir, { recursive: true });
console.log("dir", dir);
// i this it's possible that the filesystem rejects the path
await Bun.write(join(dir, troll + ".js"), "console.log('hello world');");
await using proc = Bun.spawn({
cmd: [bunExe(), join(dir, troll + ".js")],
cwd: dir,
env: bunEnv,
stdin: "ignore",
stdout: "pipe",
stderr: "inherit",
});
const stdout = await proc.stdout.text();
expect(stdout).toEqual("hello world\n");
});
test("ts enum with utf16 works", async () => {
const result = await bunRun(join(import.meta.dir, "ts-enum-fixture.ts"));
expect(result).toSpawn(`{
"1": "aaaa\u5FEB\u00E9\u00E9",
"123": "bbb",
"\u5B89\u5168\u4E32\u884C": "\u5B89\u5168\u4E32\u884C",
aaa: "\u5E73\u8861\u4E32\u884C",
"aa\u90ED": "\u5FEB\u901F\u4E32\u884C",
"\u5B89\u5168\u5E76\u884C": "\u5B89\u5168\u5E76\u884C",
"\u5E73\u8861\u5E76\u884C": "\u5E73\u8861\u5E76\u884C",
"\u5FEB\u901F\u5E76\u884C": "\u5FEB\u901F\u5E76\u884C",
"aaaa\u5FEB\u00E9\u00E9": 1,
"Fran\u00E7ais": 123,
bbb: 123,
}`);
});
});
+52
View File
@@ -0,0 +1,52 @@
import { spawnSync } from "bun";
import { describe, expect, test } from "bun:test";
import { rmSync, writeFileSync } from "fs";
import { bunEnv, bunExe, bunRun, isWindows, tempDir } from "harness";
let cwd: string;
describe("bun", () => {
test("should error with missing script", () => {
const { exitCode, stdout, stderr } = spawnSync({
cwd,
cmd: [bunExe(), "run", "dev"],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
expect(stdout.toString()).toBeEmpty();
expect(stderr.toString()).toMatch(/Script not found/);
expect(exitCode).toBe(1);
});
test("an empty-string script value is not a runnable script", () => {
using dir = tempDir("empty-script", {
"package.json": JSON.stringify({ scripts: { build: "" } }),
});
// Zig `asPropertyStringMap` drops empty-valued script entries; an empty
// `build` must report "Script not found" and exit 1, not run an empty
// `$ ` command and exit 0. (npm runs empty scripts and exits 0 — Bun
// intentionally diverges here to match its own prior/Zig behavior.)
const { exitCode, stdout, stderr } = spawnSync({
cwd: String(dir),
cmd: [bunExe(), "run", "build"],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
expect(stdout.toString()).toBeEmpty();
expect(stderr.toString()).toMatch(/Script not found/);
expect(exitCode).toBe(1);
});
});
test.if(isWindows)("[windows] A file in drive root runs", async () => {
const path = "C:\\root-file" + Math.random().toString().slice(2) + ".js";
try {
writeFileSync(path, "console.log(`PASS`);");
const { stdout } = await bunRun("C:\\root-file.js", {});
expect(stdout).toBe("PASS");
} catch {
rmSync(path);
}
});
+68
View File
@@ -0,0 +1,68 @@
import { spawn } from "bun";
import { describe, expect, test } from "bun:test";
import { writeFile } from "fs/promises";
import { bunExe, tmpdirSync } from "harness";
import { join } from "path";
const testWord = "bunny";
const testString = `${testWord} ${testWord}`;
describe("bun", () => {
test("should resolve self-imports by name", async () => {
const tempDir = tmpdirSync();
for (const packageName of ["pkg", "@scope/pkg"]) {
// general check without exports
await writeFile(
join(tempDir, "package.json"),
JSON.stringify({
name: packageName,
}),
);
await writeFile(join(tempDir, "index.js"), `module.exports.testWord = "${testWord}";`);
await writeFile(
join(tempDir, "other.js"),
`const pkg = require("${packageName}");\nimport pkg2 from "${packageName}"\nconsole.log(pkg.testWord,pkg2.testWord);`,
);
let subprocess = spawn({
cmd: [bunExe(), "run", "other.js"],
cwd: tempDir,
stdout: "pipe",
});
let out = await subprocess.stdout.text();
expect(out).not.toContain(testString);
// should not resolve not exported files
await writeFile(
join(tempDir, "package.json"),
JSON.stringify({
name: packageName,
exports: { "./index.js": "./index.js" },
}),
);
subprocess = spawn({
cmd: [bunExe(), "run", "other.js"],
cwd: tempDir,
stdout: "pipe",
});
out = await subprocess.stdout.text();
expect(out).not.toContain(testString);
// should resolve exported files
await writeFile(
join(tempDir, "other.js"),
`const pkg = require("${packageName}/index.js");\nimport pkg2 from "${packageName}/index.js"\nconsole.log(pkg.testWord,pkg2.testWord);`,
);
subprocess = spawn({
cmd: [bunExe(), "run", "other.js"],
cwd: tempDir,
stdout: "pipe",
});
out = await subprocess.stdout.text();
expect(out).toContain(testString);
}
});
});
+8
View File
@@ -0,0 +1,8 @@
process.exitCode = 1;
(async () => {
console.log("here 1");
await Bun.$`ls .`;
console.log("here 2");
process.exit(0);
})();
+6
View File
@@ -0,0 +1,6 @@
process.exitCode = 1;
(async () => {
await Bun.$`${process.execPath} -e "console.log('hi')"`;
process.exit(0);
})();
+11
View File
@@ -0,0 +1,11 @@
import { expect, test } from "bun:test";
import { bunRun } from "harness";
import { join } from "path";
test.concurrent("shell should stay alive while a builtin command is in progress", async () => {
expect(await bunRun(join(import.meta.dir, "shell-keepalive-fixture-1.js"))).toSpawn();
});
test.concurrent("shell should stay alive while a non-builtin command is in progress", async () => {
expect(await bunRun(join(import.meta.dir, "shell-keepalive-fixture-2.js"))).toSpawn();
});
+84
View File
@@ -0,0 +1,84 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
describe("--sql-preconnect", () => {
test("should attempt to preconnect to PostgreSQL on startup", async () => {
let connectionAttempts = 0;
const { promise, resolve } = Promise.withResolvers<void>();
await using server = Bun.listen({
port: 0,
hostname: "127.0.0.1",
socket: {
open(socket) {
connectionAttempts++;
socket.end();
if (connectionAttempts >= 1) {
resolve();
}
},
data() {},
close() {},
},
});
await using testDir = tempDir("sql-preconnect-test", {
"index.js": `console.log("Script executed");`,
});
const proc = Bun.spawn({
cmd: [bunExe(), "--sql-preconnect", "index.js"],
env: {
...bunEnv,
DATABASE_URL: `postgres://127.0.0.1:${server.port}/MY_DATABASE`,
},
cwd: testDir,
});
await promise;
proc.kill();
await proc.exited;
expect(connectionAttempts).toBeGreaterThan(0);
});
test("should not connect when flag is not used", async () => {
let connectionAttempts = 0;
await using server = Bun.listen({
port: 0,
hostname: "127.0.0.1",
socket: {
open(socket) {
connectionAttempts++;
socket.end();
},
data() {},
close() {},
},
});
await using testDir = tempDir("sql-no-preconnect", {
"index.js": `console.log("Normal script executed");`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
env: {
...bunEnv,
DATABASE_URL: `postgres://127.0.0.1:${server.port}/MY_DATABASE`,
},
cwd: testDir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stdout).toContain("Normal script executed");
expect(connectionAttempts).toBe(0); // No connection should be attempted without the flag
});
});
+415
View File
@@ -0,0 +1,415 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
const exitCode0 = [
" ",
"\n",
"\t",
"\r\n",
" ",
"\n\n\n",
"export default '\\u{000C}'",
"export default '\\u{200B}'",
"export default '\\u{200C}'",
"export default '\\u{200D}'",
"export default '\\u{FEFF}'",
'"use strict";',
'"use strict";\n',
"'use strict';",
'"use strict";\n\n',
'"use asm";',
'"use asm";\n',
"'use strict'; 'use asm';",
'"use strict"; "use strict";',
"// empty comment",
"/* empty block comment */",
"/** JSDoc comment */",
"//\n//\n//",
"/* \n Multi\n line\n comment\n */",
"-->",
"// TODO: ",
"/** @type {number} */",
"const a = 123;",
"let a;",
"var a = undefined;",
"const a = null;",
"const [a] = [];",
"const {a} = {};",
"const [...a] = [];",
"const {...a} = {};",
"let [a = 1] = [];",
"let {a: b = 1} = {};",
"``",
"`template`",
"`template${123}`",
"`${null}`",
"`${'`'}`",
"`\n`",
"`\n`",
"({});",
"({ a: 1 });",
"({ ['computed']: 1 });",
"({ get x() { return 1; } });",
"({ __proto__: null });",
"({ get [1+2](){}, set [1+2](x){} });",
"({ a: 1, ...{b: 2} });",
"[];",
"[,];",
"[,,];",
"[1,];",
"[,...[1]];",
"[,,...[1]];",
"[[[[[]]]]]",
"[...[...[...[]]]]",
"[1,,2,,3];",
"Array(1_000_000).fill(1);",
"()=>{};",
"async()=>{};",
"(function(){}).bind(null);",
"()=>()=>()=>1;",
"function f(a=1,{b}={},...c){};",
"async function* f(){await 1; yield 2;};",
"(async()=>await 1)();",
"class A{}",
"class A extends null{}",
"class A{#private=1}",
"class A{static{}}",
"class A{get #a(){} set #a(x){}}",
"class A{*#gen(){yield this.#p}#p=1}",
"class A extends class B{}{};",
"export {};",
"export default 1;",
"export default function(){};",
"import.meta.url;",
"const pi = Math.PI;",
"const hello = 'world';",
"const star = '⭐';",
"'\\u0000\\uFFFF';",
"'\\x00\\xFF';",
"const \\u{61} = 1;",
"'\\0';",
"'\\v\\f\\b';",
"'\\\r\n';",
"/./;",
"/[]/;",
"/[^]/;",
"/\\u{0}/u;",
"/[\\u0000-\\u{10FFFF}]/u;",
"/./gimsuyd;",
"/\\p{Script=Latin}/u;",
"/(?<=a)b/;",
"/(?<!a)b/;",
"/\\k<name>/;",
"1n;",
"0n;",
"9007199254740991n;",
"0b1n;",
"0o7n;",
"0xFn;",
"1_2_3n;",
"BigInt(1);",
"-0n;",
"~0n;",
"0b1_0_1;",
"0o7_7_7;",
"0xF_F_F;",
"1_2_3_4;",
"try{}catch{}",
"try{}catch(e){}",
"try{}catch{}finally{}",
"try{}finally{}",
"try{throw 1}catch(e){}",
"try{throw new Error}catch{}",
"try{throw{}}catch({message}){}",
"try{throw null}catch(e){}",
"try{throw undefined}catch(e){}",
"try{throw function(){}}catch(e){}",
"function*g(){yield;}",
"function*g(){yield*g();}",
"async function*g(){yield;}",
"async function*g(){await 1;}",
"(async function*(){for await(x of y){}});",
"function*g(){try{yield}finally{}}",
"[...new Set];",
"for(x of[]){}",
"for await(x of[]){}",
"async function f(){for await(x of y){}}",
"void 0;",
"new (class{})();",
"(class extends null{});",
"(class{[Symbol.hasInstance](){}});",
"function*g(){yield yield yield;}",
"1..toString();",
"`${`${`${1}`}`}`",
"String.raw`\n`",
"String.raw`\\n`",
"`${`${function*(){yield 1}}`}`",
"`${class{}}${()=>{}}${/./}`",
"`${`${async()=>await 1}`}`",
"`${`${class{static{``}}}`}`",
"await import('bun');",
"await import('bun:ffi');",
// "await import(import.meta.path);" — the spec-compliant module loader treats
// a top-level await on its own module's load promise as an unsettleable TLA,
// so this snippet now blocks forever instead of completing synchronously.
"/(?:)/",
"/\\b\\B\\d\\D\\w\\W\\s\\S/",
"/\\cA\\cZ\\ca\\cz/",
"/\\p{General_Category=Letter}/u",
"/\\p{Script_Extensions=Latin}/u",
"/(?<=(?=a)b)c/",
"/(?<!(?!a)b)c/",
"/\\k<name>\\k<other>/",
"/\\u{10FFFF}\\u{0000}/u",
"/[\\p{ASCII}--\\p{Decimal_Number}]/v",
"const [{a: [b = 1] = []} = [{a: [2]}]] = [];",
"let {a: {b: {c = 1} = {}} = {}} = {};",
"const [a, , ...{0: b, length}] = [];",
"const {[class{}.prop]: x} = {};",
"const {[()=>{}]: x} = {};",
"let {[/./]: x} = {};",
"const {[class extends null{}]: x} = {};",
"const {[function*(){}]: x} = {};",
"function f([a] = [1], {b} = {b: 2}, ...c){}",
"(function({[key]: value} = {}){})",
"(({[this?.prop]: x} = {})=>{})",
"function f(a = () => b, b = 1){}",
"(a = class{}, b = new a)=>{}",
"function f(a = new.target){}",
"for await(const x of async function*(){yield 1}()){}",
"for(const x of function*(){yield 1}()){}",
"for(const {[key]: x} of []){}",
"for(let [{a = 1} = {}] of []){}",
"do{continue}while(false);",
"label:do{break label}while(false);",
"outer:for(;;)inner:break outer;",
"block:{break block;}",
"while(false)with(null);",
"switch(0){case 1:default:case 2:}",
"try{throw async()=>{}}catch(e){await e}",
"try{throw class{}}catch(e){new e}",
"try{throw{[Symbol.iterator]:()=>{}}}catch(e){}",
"try{throw Object.create(null)}catch(e){}",
"try{throw new Proxy({},{})}catch(e){}",
"try{throw new WeakMap}catch(e){}",
"try{throw new Int32Array}catch(e){}",
"try{throw new SharedArrayBuffer(0)}catch(e){}",
"try{throw new WebAssembly.Module(new Uint8Array)}catch(e){}",
"void void void 0;",
"typeof typeof typeof 0;",
"delete (delete (delete 0));",
"await (await (await 1));",
"--{a:1}.a;",
"(class{}).prototype.constructor;",
"(()=>{}).prototype?.call;",
"(async()=>{}).prototype?.call;",
"(function*(){}).prototype.next;",
"(async function*(){}).prototype.next;",
"this?.prop?.[key]?.();",
"0b0_1_0_1;",
"0B0_1_0_1;",
"0o0_1_2_3;",
"0O0_1_2_3;",
"0x0_1_2_3;",
"0X0_1_2_3;",
"1_2_3.4_5_6;",
"1_2_3e1_2_3;",
"1_2_3E1_2_3;",
".0_1_2_3;",
"0b1_0_1n;",
"0B1_0_1n;",
"0o1_2_3n;",
"0O1_2_3n;",
"0x1_2_3n;",
"0X1_2_3n;",
"1_2_3_4_5n;",
"BigInt(0b101);",
"BigInt(0o777);",
"BigInt(0xff);",
"for(const [,,...a] of [[]]){}",
"for(let {a: [b]} of []){}",
"for await(const [a = await 1] of []){}",
"for(const {[await 1]: x} of []){}",
"for(var {[class{}]: x} of []){}",
"for(let {[/./]: x} of []){}",
"for(const {[`${a}`]: x} of []){}",
"for await(const x of async function*(){yield*[1,2,3]}()){}",
"new class extends (await Promise.resolve(class{})){}",
"function*g(){yield yield*function*(){yield yield yield;}();}",
"async function*f(){yield await Promise.all([1,2,3])}",
"async function*f(){yield*[await 1,await 2]}",
"(async function(){await (async()=>1)()})();",
"async function*f(){for await(const x of async function*(){yield 1}())yield x}",
"async function f(){return await new Promise(r=>r(1))}",
"async function*f(){try{yield await 1}finally{await 2}}",
"(async()=>{for await(const x of[])for await(const y of[]){}})();",
"async function*f(){yield await(yield await(yield await 1))}",
"async function*f(){yield*async function*(){yield await 1}()}",
"export default await(async()=>1)();",
"function*g(){yield*{[Symbol.iterator]:function*(){yield 1}}}",
"function*g(){yield*{async*[Symbol.asyncIterator](){yield 1}}}",
"function*g(){yield*function*(){yield function*(){yield 1}}()}",
"function*g(){yield*{get[Symbol.iterator](){return function*(){yield 1}}}}",
"function*g(){yield*(class{static*[Symbol.iterator](){yield 1}});}",
"function*g(){yield*class{static*[Symbol.iterator](){yield 1}}}",
"function*g(){yield*{*[Symbol.iterator](){yield class{}}}}",
"function*g(){yield*{*[Symbol.iterator](){yield function(){}}}}",
"function*g(){yield*{*[Symbol.iterator](){yield async()=>{}}}}",
"function*g(){yield*{*[Symbol.iterator](){yield function*(){}}}}",
"({[function*(){yield 1}]:1});",
"({[async function*(){yield 1}]:1});",
"({[(()=>class{})()]:1});",
"({[new class{valueOf(){return 1}}]:2});",
"({[new class{[Symbol.toPrimitive](){return 1}}]:2});",
"({[new class{toString(){return '1'}}]:2});",
"({[new class{get [Symbol.toPrimitive](){return()=>1}}]:2});",
"({[new class{static{this.prototype.valueOf=()=>1}}]:2});",
"label:while(1)try{continue label}finally{break label}",
"new Proxy(class{},{construct(){return{}}});",
"new Proxy(function(){},{apply(){return{}}});",
"new Proxy({},{get(){return new Proxy({},{})}})",
"new Proxy([],{get(){return new Proxy({},{})}});",
"new Proxy(function(){},{construct(){return new Proxy({},{})}})",
"new Proxy(class{},{construct(){return new Proxy({},{})}})",
"new Proxy({},{get(){return function(){return new Proxy({},{})}}})",
"new Proxy(function(){},{apply(){return new Proxy(class{},{})}})",
"new Proxy(class{},{construct(){return new Proxy(function(){},{})}})",
"Reflect.get(new Proxy({},{}),'')",
"Reflect.set(new Proxy({},{}),'')",
"Reflect.has(new Proxy({},{}),'')",
"Reflect.deleteProperty(new Proxy({},{}),'')",
"Reflect.getOwnPropertyDescriptor(new Proxy({},{}),'')",
"Reflect.getPrototypeOf(new Proxy({},{}))",
"Reflect.setPrototypeOf(new Proxy({},{}),{})",
"Reflect.isExtensible(new Proxy({},{}))",
"Reflect.preventExtensions(new Proxy({},{}))",
"Promise.all([Promise.race([]),Promise.allSettled([])])",
"Promise.race([Promise.all([]),Promise.any([])])",
"Promise.allSettled([Promise.race([]),Promise.all([])])",
"Promise.any([Promise.allSettled([]),Promise.race([])])",
"Promise.resolve(Promise.reject().catch(()=>Promise.all([])))",
"new Function(`return function*(){${`yield${`yield${`yield`}`}`}}`)();",
"new Function(`return async()=>${`await${`await${`1`}`}`}`)();",
"new Function(`return class{${`*[Symbol.iterator](){${`yield 1`}}`}}`)();",
"new Function(`return class{${`async*[Symbol.asyncIterator](){${`yield 1`}}`}}`)();",
"new Function(`return class{${`get #a(){${`return 1`}}`}}`)();",
"new Function(`return class{${`set #a(v){${`this.#b=v`}}#b`}}`)();",
"new Function(`return class{${`#a;${`#b;${`#c`}`}`}}`)();",
"for await(let {...x} of async function*(){yield*[]}()){}",
"for await(const x of (async function*(){yield await 1})()){}",
"function f(){let a=b,b=1}",
"(function(){const[a=b,b=1]=[]})",
"(()=>{let{a=b,b=1}={}})",
];
// Originally each source was spawned twice (bun file + bun --eval), giving ~600
// cold process startups whose cost dominated the file's wall time under ASAN.
// The driver below parses and evaluates every fixture inside two subprocesses
// instead: one launched as a file entrypoint that imports each fixture from
// disk, and one launched via `--eval` that imports each fixture as a data: URL.
// Every source still passes through Bun's transpiler and module evaluation, and
// per-fixture failures are reported individually.
const driver = `
const count = Number(process.env.SYNTAX_FIXTURE_COUNT);
const mode = process.env.SYNTAX_FIXTURE_MODE;
let failures = 0;
for (let i = 0; i < count; i++) {
try {
if (mode === "data") {
const source = await Bun.file(\`./fixture-\${i}.js\`).text();
await import("data:text/javascript;base64," + Buffer.from(source).toString("base64"));
} else {
await import(\`./fixture-\${i}.js\`);
}
process.stdout.write(\`PASS \${i}\\n\`);
} catch (e) {
process.stdout.write(\`FAIL \${i} \${e?.message ?? e}\\n\`);
failures++;
}
}
process.stdout.write(\`DONE \${count}\\n\`);
process.exitCode = failures === 0 ? 0 : 1;
`;
const fixturePath = tempDirWithFiles("syntax-fixture", {
"package.json": `{ "name": "test" }`,
"driver.mjs": driver,
...Object.fromEntries(exitCode0.map((source, i) => [`fixture-${i}.js`, source])),
});
type RunResult = {
results: Map<number, string>;
stdout: string;
stderr: string;
exitCode: number | null;
};
async function runDriver(cmd: string[], mode: string): Promise<RunResult> {
await using proc = Bun.spawn(cmd, {
env: { ...bunEnv, SYNTAX_FIXTURE_COUNT: String(exitCode0.length), SYNTAX_FIXTURE_MODE: mode },
cwd: fixturePath,
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const results = new Map<number, string>();
for (const line of stdout.split("\n")) {
const m = /^(PASS|FAIL) (\d+)(?: (.*))?$/.exec(line);
if (m) results.set(Number(m[2]), m[1] === "PASS" ? "PASS" : `FAIL: ${m[3] ?? ""}`);
}
return { results, stdout, stderr, exitCode };
}
describe("exit code 0", () => {
let file: RunResult;
let data: RunResult;
beforeAll(async () => {
[file, data] = await Promise.all([
runDriver([bunExe(), "driver.mjs"], "file"),
runDriver([bunExe(), "--eval", driver], "data"),
]);
});
for (let i = 0; i < exitCode0.length; i++) {
const source = exitCode0[i];
test(`file #${i}: ${JSON.stringify(source)}`, () => {
const result = file.results.get(i);
if (result !== "PASS") {
// Surface stderr and exit code when the fixture failed or the driver crashed before reaching it.
expect({ result, stderr: file.stderr, exitCode: file.exitCode }).toEqual({
result: "PASS",
stderr: "",
exitCode: 0,
});
}
expect(result).toBe("PASS");
});
test(`eval #${i}: ${JSON.stringify(source)}`, () => {
const result = data.results.get(i);
if (result !== "PASS") {
expect({ result, stderr: data.stderr, exitCode: data.exitCode }).toEqual({
result: "PASS",
stderr: "",
exitCode: 0,
});
}
expect(result).toBe("PASS");
});
}
test("file driver: clean exit", () => {
expect(file.stderr).toBe("");
expect(file.stdout.trimEnd().split("\n").at(-1)).toBe(`DONE ${exitCode0.length}`);
expect(file.exitCode).toBe(0);
});
test("eval driver: clean exit", () => {
expect(data.stderr).toBe("");
expect(data.stdout.trimEnd().split("\n").at(-1)).toBe(`DONE ${exitCode0.length}`);
expect(data.exitCode).toBe(0);
});
});
@@ -0,0 +1,7 @@
import { rmSync } from "fs";
while (1) {
try {
rmSync(process.argv[2], { recursive: true, force: true });
} catch (error) {}
}
+406
View File
@@ -0,0 +1,406 @@
import { Subprocess } from "bun";
import { beforeEach, describe, expect, test } from "bun:test";
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "fs";
import { bunEnv, bunExe, bunRun, tmpdirSync } from "harness";
import { join } from "path";
function dummyFile(size: number, cache_bust: string, value: string | { code: string }) {
const data = Buffer.alloc(size);
data.write("/*" + cache_bust);
const end = `*/\nconsole.log(${(value as any).code ?? JSON.stringify(value)});`;
data.fill("*", 2 + cache_bust.length, size - end.length, "utf-8");
data.write(end, size - end.length, "utf-8");
return data;
}
let temp_dir: string = "";
let cache_dir = "";
const env = {
...bunEnv,
BUN_RUNTIME_TRANSPILER_CACHE_PATH: cache_dir,
BUN_DEBUG_ENABLE_RESTORE_FROM_TRANSPILER_CACHE: "1",
};
let prev_cache_count = 0;
function newCacheCount() {
let new_count = readdirSync(cache_dir).length;
let delta = new_count - prev_cache_count;
prev_cache_count = new_count;
return delta;
}
function removeCache() {
prev_cache_count = 0;
try {
rmSync(cache_dir, { recursive: true, force: true });
} catch (error) {
chmodSync(cache_dir, 0o777);
readdirSync(cache_dir).forEach(item => {
chmodSync(join(cache_dir, item), 0o777);
});
rmSync(cache_dir, { recursive: true, force: true });
}
}
beforeEach(() => {
if (cache_dir) {
rmSync(temp_dir, { recursive: true, force: true });
removeCache();
}
temp_dir = tmpdirSync();
mkdirSync(temp_dir, { recursive: true });
temp_dir = realpathSync(temp_dir);
cache_dir = join(temp_dir, ".cache");
env.BUN_RUNTIME_TRANSPILER_CACHE_PATH = cache_dir;
});
describe("transpiler cache", () => {
test("works", async () => {
writeFileSync(join(temp_dir, "a.js"), dummyFile((50 * 1024 * 1.5) | 0, "1", "a"));
expect(await bunRun(join(temp_dir, "a.js"), env)).toSpawn("a");
expect(existsSync(cache_dir)).toBeTrue();
expect(newCacheCount()).toBe(1);
expect(await bunRun(join(temp_dir, "a.js"), env)).toSpawn("a");
expect(newCacheCount()).toBe(0);
});
test("works with empty files", async () => {
writeFileSync(join(temp_dir, "a.js"), "//" + "a".repeat(50 * 1024 * 1.5));
expect(await bunRun(join(temp_dir, "a.js"), env)).toSpawn("");
expect(existsSync(cache_dir)).toBeTrue();
expect(newCacheCount()).toBe(1);
expect(await bunRun(join(temp_dir, "a.js"), env)).toSpawn("");
expect(newCacheCount()).toBe(0);
});
test("ignores files under the minimum cache size", async () => {
// MINIMUM_CACHE_SIZE is 4 KiB (src/jsc/RuntimeTranspilerCache.rs); files
// below it skip the cache entirely so a stat+open+read can't be slower than
// just re-transpiling.
writeFileSync(join(temp_dir, "a.js"), dummyFile(4 * 1024 - 1, "1", "a"));
expect(await bunRun(join(temp_dir, "a.js"), env)).toSpawn("a");
expect(!existsSync(cache_dir)).toBeTrue();
});
test("it is indeed content addressable", async () => {
writeFileSync(join(temp_dir, "a.js"), dummyFile(50 * 1024, "1", "b"));
expect(await bunRun(join(temp_dir, "a.js"), env)).toSpawn("b");
expect(newCacheCount()).toBe(1);
writeFileSync(join(temp_dir, "a.js"), dummyFile(50 * 1024, "1", "c"));
expect(await bunRun(join(temp_dir, "a.js"), env)).toSpawn("c");
expect(newCacheCount()).toBe(1);
writeFileSync(join(temp_dir, "b.js"), dummyFile(50 * 1024, "1", "b"));
expect(await bunRun(join(temp_dir, "b.js"), env)).toSpawn("b");
expect(newCacheCount()).toBe(0);
});
test("doing 50 buns at once does not crash", async () => {
writeFileSync(join(temp_dir, "a.js"), dummyFile(50 * 1024, "1", "b"));
writeFileSync(join(temp_dir, "b.js"), dummyFile(50 * 1024, "2", "b"));
const remover = Bun.spawn({
cmd: [bunExe(), join(import.meta.dir, "transpiler-cache-aggressive-remover.js"), cache_dir],
env,
cwd: temp_dir,
});
let processes: Subprocess<"ignore", "pipe", "inherit">[] = [];
let killing = false;
for (let i = 0; i < 50; i++) {
processes.push(
Bun.spawn({
cmd: [bunExe(), i % 2 == 0 ? "a.js" : "b.js"],
env,
cwd: temp_dir,
onExit(subprocess, exitCode, signalCode, error) {
if (exitCode != 0 && !killing) {
killing = true;
processes.forEach(x => x.kill(9));
remover.kill(9);
}
},
}),
);
}
await Promise.all(processes.map(x => x.exited));
expect(!killing).toBeTrue();
remover.kill(9);
for (const proc of processes) {
expect(proc.exitCode).toBe(0);
expect(await proc.stdout.text()).toBe("b\n");
}
}, 99999999);
test("disables the cache instead of falling back to the shared temp directory", async () => {
writeFileSync(join(temp_dir, "a.js"), dummyFile((50 * 1024 * 1.5) | 0, "1", "no-tmpdir-cache"));
// Stand-in for the shared, world-writable system temp dir. Pre-create
// bun/@t@ inside it the way another local user could on a multi-user host.
const shared_tmp = join(temp_dir, "shared-tmp");
const shared_cache = join(shared_tmp, "bun", "@t@");
mkdirSync(shared_cache, { recursive: true });
// No per-user cache location is available (no BUN_RUNTIME_TRANSPILER_CACHE_PATH,
// no XDG_CACHE_HOME, no HOME) — the only remaining candidate is the shared
// temp dir, so the cache must be disabled instead of using it.
expect(
await bunRun(join(temp_dir, "a.js"), {
...env,
BUN_RUNTIME_TRANSPILER_CACHE_PATH: undefined,
XDG_CACHE_HOME: undefined,
HOME: undefined,
USERPROFILE: undefined,
BUN_TMPDIR: undefined,
TMPDIR: shared_tmp,
TMP: shared_tmp,
TEMP: shared_tmp,
}),
).toSpawn("no-tmpdir-cache");
// No cache entry may be written into (or read back from) a directory that
// another local user could own and pre-populate.
expect(readdirSync(shared_cache)).toEqual([]);
// A per-user cache location still works.
expect(await bunRun(join(temp_dir, "a.js"), env)).toSpawn("no-tmpdir-cache");
expect(newCacheCount()).toBe(1);
});
test("works if the cache is not user-readable", async () => {
mkdirSync(cache_dir, { recursive: true });
writeFileSync(join(temp_dir, "a.js"), dummyFile((50 * 1024 * 1.5) | 0, "1", "b"));
expect(await bunRun(join(temp_dir, "a.js"), env)).toSpawn("b");
expect(newCacheCount()).toBe(1);
const cache_item = readdirSync(cache_dir)[0];
chmodSync(join(cache_dir, cache_item), 0);
expect(await bunRun(join(temp_dir, "a.js"), env)).toSpawn("b");
expect(newCacheCount()).toBe(0);
chmodSync(join(cache_dir), "0");
try {
expect(await bunRun(join(temp_dir, "a.js"), env)).toSpawn("b");
} finally {
chmodSync(join(cache_dir), "777");
}
});
test("works if the cache is not user-writable", async () => {
mkdirSync(cache_dir, { recursive: true });
writeFileSync(join(temp_dir, "a.js"), dummyFile((50 * 1024 * 1.5) | 0, "1", "b"));
try {
chmodSync(join(cache_dir), "0");
expect(await bunRun(join(temp_dir, "a.js"), env)).toSpawn("b");
} finally {
chmodSync(join(cache_dir), "777");
}
});
test("does not inline process.env", async () => {
writeFileSync(
join(temp_dir, "a.js"),
dummyFile((50 * 1024 * 1.5) | 0, "1", { code: "process.env.NODE_ENV, process.env.HELLO" }),
);
expect(await bunRun(join(temp_dir, "a.js"), { ...env, NODE_ENV: undefined, HELLO: "1" })).toSpawn("undefined 1");
expect(existsSync(cache_dir)).toBeTrue();
expect(newCacheCount()).toBe(1);
expect(await bunRun(join(temp_dir, "a.js"), { ...env, NODE_ENV: "production", HELLO: "5" })).toSpawn(
"production 5",
);
expect(newCacheCount()).toBe(0);
});
test("--feature flag invalidates cache", () => {
// feature() can only appear in an if/ternary, so wrap it
const code = `import { feature } from "bun:bundle";\nif (feature("SUPER_SECRET")) console.log("enabled"); else console.log("disabled");`;
const filler = Buffer.alloc((50 * 1024 * 1.5) | 0, "/").toString();
writeFileSync(join(temp_dir, "a.js"), code + "\n//" + filler);
const run = (extra: string[]) => {
const result = Bun.spawnSync({
cmd: [bunExe(), ...extra, "a.js"],
cwd: temp_dir,
env,
});
if (!result.success) throw new Error(result.stderr.toString());
return result.stdout.toString().trim();
};
// First run with flag: cache miss, write entry
expect(run(["--feature=SUPER_SECRET"])).toBe("enabled");
expect(newCacheCount()).toBe(1);
// Same flag: cache hit
expect(run(["--feature=SUPER_SECRET"])).toBe("enabled");
expect(newCacheCount()).toBe(0);
// No flag: features_hash differs -> old entry deleted, new entry written
expect(run([])).toBe("disabled");
expect(newCacheCount()).toBe(0); // deleted + written = net 0
// Flag again: another delete + write
expect(run(["--feature=SUPER_SECRET"])).toBe("enabled");
expect(newCacheCount()).toBe(0);
// Multiple flags, different order: same hash, cache hit
expect(run(["--feature=SUPER_SECRET", "--feature=OTHER"])).toBe("enabled");
expect(newCacheCount()).toBe(0); // delete + write
expect(run(["--feature=OTHER", "--feature=SUPER_SECRET"])).toBe("enabled");
expect(newCacheCount()).toBe(0); // cache hit, order doesn't matter
});
// Serving the entry point from the cache must not change how the modules it
// loads are resolved. Both of these are gated on the `has_loaded` flag, which
// used to be set only on the path that runs the printer.
describe("a cached entry point does not change how later modules load", () => {
// Padding so the entry point clears MINIMUM_CACHE_SIZE (4 KiB) and is
// eligible for the cache at all.
const filler = "\n//" + Buffer.alloc(5 * 1024, "f").toString();
test("require.extensions is still consulted", async () => {
writeFileSync(
join(temp_dir, "entry.js"),
`require.extensions[".data"] = (module, filename) => {
module.exports = "custom-loader";
};
console.log(require("./asset.data"));${filler}`,
);
// If the custom loader is skipped, this is transpiled as JS/TS instead.
writeFileSync(join(temp_dir, "asset.data"), `module.exports = "default-loader";`);
expect(await bunRun(join(temp_dir, "entry.js"), env)).toSpawn("custom-loader");
expect(newCacheCount()).toBe(1);
expect(await bunRun(join(temp_dir, "entry.js"), env)).toSpawn("custom-loader");
expect(newCacheCount()).toBe(0);
});
test("unknown extensions still use the file loader", async () => {
writeFileSync(
join(temp_dir, "entry.mjs"),
`import asset from "./asset.someext";
console.log(typeof asset === "string" ? "file-loader" : "???");${filler}`,
);
// Not valid JS/TS, so a non-file loader fails the run outright.
writeFileSync(join(temp_dir, "asset.someext"), `hello world contents\n`);
expect(await bunRun(join(temp_dir, "entry.mjs"), env)).toSpawn("file-loader");
expect(newCacheCount()).toBe(1);
expect(await bunRun(join(temp_dir, "entry.mjs"), env)).toSpawn("file-loader");
expect(newCacheCount()).toBe(0);
});
});
});
test("rejects cached module records containing out-of-range string indices", () => {
// When test isolation is enabled, the runtime transpiler cache stores a
// serialized ES module record ("esm_record") alongside the transpiled
// output. The string indices inside that record are used to index an
// identifier table when the record is converted back into a JSC module
// record, so any index beyond the table length (other than the reserved
// *-default / *-namespace sentinels near u32::MAX) must be rejected.
//
// Cache entry layout (src/jsc/RuntimeTranspilerCache.rs, Metadata::encode):
// 0: cache_version u32, 4: module_type u8, 5: output_encoding u8,
// then twelve u64 fields; esm_record_byte_offset @ 78,
// esm_record_byte_length @ 86, esm_record_hash @ 94. Payload follows @ 102.
// Serialized module record layout (src/bundler/analyze_transpiled_module.rs,
// serialize()):
// [record_kinds_len u32][record_kinds, 1 byte each][pad to 4]
// [buffer_len u32][buffer: u32 string index x buffer_len] ...
const ESM_RECORD_BYTE_OFFSET_AT = 78;
const ESM_RECORD_BYTE_LENGTH_AT = 86;
const ESM_RECORD_HASH_AT = 94;
const METADATA_SIZE = 102;
function corruptModuleRecordStringIndices(file: string): boolean {
const data = readFileSync(file);
if (data.length < METADATA_SIZE) return false;
const esmOff = Number(data.readBigUInt64LE(ESM_RECORD_BYTE_OFFSET_AT));
const esmLen = Number(data.readBigUInt64LE(ESM_RECORD_BYTE_LENGTH_AT));
if (esmLen === 0 || esmOff + esmLen > data.length) return false;
const recordKindsLen = data.readUInt32LE(esmOff);
const pad = (4 - (recordKindsLen % 4)) % 4;
let off = esmOff + 4 + recordKindsLen + pad;
const bufferLen = data.readUInt32LE(off);
off += 4;
if (bufferLen === 0) return false;
// Point every string index in the record buffer far beyond the identifier
// table (but below the reserved sentinel range near u32::MAX).
for (let i = 0; i < bufferLen; i++) {
data.writeUInt32LE(0x7fffffff, off + i * 4);
}
// The cache loader skips esm-record content verification when the stored
// hash field is zero, so whoever writes the cache file controls exactly
// what reaches the module record deserializer.
data.writeBigUInt64LE(0n, ESM_RECORD_HASH_AT);
writeFileSync(file, data);
return true;
}
// An ES module big enough to be eligible for the transpiler cache (>= 4 KiB)
// with imports, exports and top-level variables, so its module record
// contains string indices of every record kind.
const filler = ("// " + "x".repeat(120) + "\n").repeat(120);
writeFileSync(
join(temp_dir, "big-lib.js"),
`import { join } from "node:path";
export const value = 42;
let counter = 0;
export function next() {
counter += 1;
return join("a", String(counter));
}
${filler}`,
);
writeFileSync(
join(temp_dir, "uses-lib.test.js"),
`import { test, expect } from "bun:test";
import { value, next } from "./big-lib.js";
test("cached module still works", () => {
expect(value).toBe(42);
expect(next().length).toBeGreaterThan(0);
});`,
);
const run = () =>
Bun.spawnSync({
// --isolate enables the isolation source-provider cache, which is the
// code path that converts the cached module record back into a JSC
// module record.
cmd: [bunExe(), "test", "--isolate", "./uses-lib.test.js"],
cwd: temp_dir,
env,
});
// First run transpiles the module and writes the cache entry, including the
// serialized module record.
const first = run();
expect(first.stderr.toString() + first.stdout.toString()).toContain("1 pass");
expect(existsSync(cache_dir)).toBeTrue();
expect(first.exitCode).toBe(0);
// Second run restores from the intact cache entry: the legitimate record is
// accepted and the module still works.
const second = run();
expect(second.stderr.toString() + second.stdout.toString()).toContain("1 pass");
expect(second.exitCode).toBe(0);
// Rewrite the stored module record so every string index is out of range.
let corrupted = 0;
for (const name of readdirSync(cache_dir)) {
if (corruptModuleRecordStringIndices(join(cache_dir, name))) corrupted++;
}
expect(corrupted).toBeGreaterThanOrEqual(1);
// Third run: the corrupted record must be rejected with a clean module load
// error and a normal (non-signal) process exit.
const third = run();
expect(third.stderr.toString() + third.stdout.toString()).toContain("parseFromSourceCode failed");
expect(third.signalCode).toBeUndefined();
expect(third.exitCode).toBe(1);
});
+14
View File
@@ -0,0 +1,14 @@
// https://github.com/oven-sh/bun/issues/11963
enum Enum {
= "安全串行",
aaa = "平衡串行",
aa郭 = "快速串行",
= "安全并行",
= "平衡并行",
"快速并行" = "快速并行",
aaaa快éé = 1,
Français = 123,
bbb = Français,
}
console.log(Enum);
+305
View File
@@ -0,0 +1,305 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import path from "node:path";
describe("bun run --tsconfig-override", () => {
test("should use custom tsconfig for path resolution", async () => {
await using dir = tempDir("run-tsconfig-override", {
"index.ts": `
import { helper } from '@helpers/math';
console.log(helper());
`,
"src/math.ts": `
export function helper() {
return "success from custom tsconfig";
}
`,
"tsconfig.json": `
{
"compilerOptions": {
"paths": {
"@helpers/*": ["./wrong/*"]
}
}
}
`,
"custom-tsconfig.json": `
{
"compilerOptions": {
"paths": {
"@helpers/*": ["./src/*"]
}
}
}
`,
});
await using failProc = Bun.spawn({
cmd: [bunExe(), "run", path.join(dir, "index.ts")],
env: bunEnv,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const [failStderr, failExitCode] = await Promise.all([failProc.stderr.text(), failProc.exited]);
expect(failStderr).toContain("Cannot find module");
expect(failExitCode).not.toBe(0);
await using successProc = Bun.spawn({
cmd: [bunExe(), "run", "--tsconfig-override", path.join(dir, "custom-tsconfig.json"), path.join(dir, "index.ts")],
env: bunEnv,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const [successStdout, successStderr, successExitCode] = await Promise.all([
successProc.stdout.text(),
successProc.stderr.text(),
successProc.exited,
]);
expect(successStdout).toContain("success from custom tsconfig");
if (!successStderr.includes("Internal error: directory mismatch")) {
expect(successStderr).toBe("");
}
expect(successExitCode).toBe(0);
});
test("should work with relative tsconfig path", async () => {
await using dir = tempDir("run-tsconfig-relative", {
"src/main.ts": `
import { lib } from '@lib/util';
console.log(lib());
`,
"lib/util.ts": `
export function lib() {
return 42;
}
`,
"config/custom.json": `
{
"compilerOptions": {
"baseUrl": "../",
"paths": {
"@lib/*": ["lib/*"]
}
}
}
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--tsconfig-override", "./config/custom.json", "./src/main.ts"],
env: bunEnv,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("42");
if (!stderr.includes("Internal error: directory mismatch")) {
expect(stderr).toBe("");
}
expect(exitCode).toBe(0);
});
test("should work with monorepo-style paths", async () => {
await using dir = tempDir("run-tsconfig-monorepo", {
"apps/web/src/index.ts": `
import { Button } from '@ui/components';
import { config } from '@shared/config';
console.log('App loaded with', Button(), config);
`,
"packages/ui/components/index.ts": `
export function Button() {
return 'Button component';
}
`,
"packages/shared/config.ts": `
export const config = { name: 'monorepo-app' };
`,
"apps/web/tsconfig.json": `
{
"compilerOptions": {
"baseUrl": "../../",
"paths": {
"@ui/*": ["packages/ui/*"],
"@shared/*": ["packages/shared/*"]
}
}
}
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--tsconfig-override", "./apps/web/tsconfig.json", "./apps/web/src/index.ts"],
env: bunEnv,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("Button component");
expect(stdout).toContain("monorepo-app");
if (!stderr.includes("Internal error: directory mismatch")) {
expect(stderr).toBe("");
}
expect(exitCode).toBe(0);
});
test("should work with nested directories and complex paths", async () => {
await using dir = tempDir("run-tsconfig-nested", {
"frontend/src/pages/home.ts": `
import { api } from '~/api/client';
import { utils } from '#/utils/helpers';
console.log(api.getHome(), utils.format('test'));
`,
"frontend/src/api/client.ts": `
export const api = {
getHome: () => 'home-data'
};
`,
"frontend/src/utils/helpers.ts": `
export const utils = {
format: (str: string) => \`formatted-\${str}\`
};
`,
"frontend/tsconfig.json": `
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"~/*": ["./*"],
"#/*": ["./*"]
}
}
}
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--tsconfig-override", "./frontend/tsconfig.json", "./frontend/src/pages/home.ts"],
env: bunEnv,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("home-data");
expect(stdout).toContain("formatted-test");
if (!stderr.includes("Internal error: directory mismatch")) {
expect(stderr).toBe("");
}
expect(exitCode).toBe(0);
});
test("should handle extending tsconfig with overrides", async () => {
await using dir = tempDir("run-tsconfig-extends", {
"src/app.ts": `
import { core } from '@core/main';
import { feature } from '@features/auth';
console.log('Loaded:', core, feature);
`,
"packages/core/main.ts": `
export const core = 'core-module';
`,
"features/auth/index.ts": `
export const feature = 'auth-feature';
`,
"tsconfig.base.json": `
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@core/*": ["packages/core/*"]
}
}
}
`,
"tsconfig.dev.json": `
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@core/*": ["packages/core/*"],
"@features/*": ["features/*"]
}
}
}
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--tsconfig-override", "./tsconfig.dev.json", "./src/app.ts"],
env: bunEnv,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("core-module");
expect(stdout).toContain("auth-feature");
if (!stderr.includes("Internal error: directory mismatch")) {
expect(stderr).toBe("");
}
expect(exitCode).toBe(0);
});
test("should work from different working directories", async () => {
await using dir = tempDir("run-tsconfig-cwd", {
"project/src/main.ts": `
import { helper } from '@utils/math';
console.log('Result:', helper(5, 3));
`,
"project/utils/math.ts": `
export function helper(a: number, b: number) {
return a + b;
}
`,
"project/tsconfig.json": `
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@utils/*": ["utils/*"]
}
}
}
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--tsconfig-override", "project/tsconfig.json", "project/src/main.ts"],
env: bunEnv,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("Result: 8");
if (!stderr.includes("Internal error: directory mismatch")) {
expect(stderr).toBe("");
}
expect(exitCode).toBe(0);
});
});
+113
View File
@@ -0,0 +1,113 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
describe.concurrent("bun run --workspaces", () => {
test("runs script in all workspace packages", async () => {
using dir = tempDir("workspaces-test", {
"package.json": JSON.stringify({
name: "root",
workspaces: ["packages/*"],
scripts: {
test: "echo root test",
},
}),
"packages/a/package.json": JSON.stringify({
name: "a",
scripts: {
test: "echo package a test",
},
}),
"packages/b/package.json": JSON.stringify({
name: "b",
scripts: {
test: "echo package b test",
},
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--workspaces", "test"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
// Packages run in parallel so output order is not deterministic; compare sorted lines.
// The root package must not be included when using --workspaces, which the exact line set proves.
expect(stdout.split("\n").filter(Boolean).sort()).toEqual([
"a test: Exited with code 0",
"a test: package a test",
"b test: Exited with code 0",
"b test: package b test",
]);
expect(exitCode).toBe(0);
});
test("--if-present succeeds when script is missing", async () => {
using dir = tempDir("workspaces-if-present", {
"package.json": JSON.stringify({
name: "root",
workspaces: ["packages/*"],
}),
"packages/a/package.json": JSON.stringify({
name: "a",
scripts: {
test: "echo package a test",
},
}),
"packages/b/package.json": JSON.stringify({
name: "b",
// No test script
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--workspaces", "--if-present", "test"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout).toContain("package a test");
// Should not fail or emit anything for package b
expect(stdout).not.toContain("b test:");
expect(exitCode).toBe(0);
});
test("fails when no packages have the script", async () => {
using dir = tempDir("workspaces-no-script", {
"package.json": JSON.stringify({
name: "root",
workspaces: ["packages/*"],
}),
"packages/a/package.json": JSON.stringify({
name: "a",
}),
"packages/b/package.json": JSON.stringify({
name: "b",
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--workspaces", "nonexistent"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("");
expect(stderr).toContain('No workspace packages have script "nonexistent"');
expect(exitCode).toBe(1);
});
});