initial commit
This commit is contained in:
@@ -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
|
||||
"
|
||||
`;
|
||||
@@ -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
@@ -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,
|
||||
});
|
||||
Generated
+7
@@ -0,0 +1,7 @@
|
||||
// @bun @bun-cjs
|
||||
|
||||
function f() {
|
||||
console.log("This module has been evaluated!");
|
||||
}
|
||||
|
||||
f();
|
||||
Generated
+43
@@ -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);
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -0,0 +1,7 @@
|
||||
// see https://www.npmjs.com/package/checkpoint-client
|
||||
require("node:fs");
|
||||
|
||||
eval("__dirname");
|
||||
eval("__filename");
|
||||
|
||||
console.log("--pass--");
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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
Generated
+36
@@ -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);
|
||||
@@ -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],
|
||||
},
|
||||
}`);
|
||||
});
|
||||
Generated
+48
@@ -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);
|
||||
+1307
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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>",
|
||||
);
|
||||
}
|
||||
@@ -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"));
|
||||
@@ -0,0 +1 @@
|
||||
export const Fragment = () => {};
|
||||
@@ -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()
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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" });
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Generated
+1
@@ -0,0 +1 @@
|
||||
module.exports = {};
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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("\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": `\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
@@ -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
@@ -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");
|
||||
});
|
||||
@@ -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--");
|
||||
+1308
File diff suppressed because it is too large
Load Diff
+34
@@ -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);
|
||||
Generated
+15
@@ -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 };
|
||||
Generated
+66
@@ -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");
|
||||
@@ -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);
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
@@ -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 {}
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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"));
|
||||
});
|
||||
@@ -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]));
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
@@ -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,
|
||||
}`);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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
@@ -0,0 +1,8 @@
|
||||
process.exitCode = 1;
|
||||
|
||||
(async () => {
|
||||
console.log("here 1");
|
||||
await Bun.$`ls .`;
|
||||
console.log("here 2");
|
||||
process.exit(0);
|
||||
})();
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
process.exitCode = 1;
|
||||
|
||||
(async () => {
|
||||
await Bun.$`${process.execPath} -e "console.log('hi')"`;
|
||||
process.exit(0);
|
||||
})();
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
Generated
+14
@@ -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);
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user