initial commit
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDir } from "harness";
|
||||
import { join } from "path";
|
||||
|
||||
// Regression: the C++ synchronous-exception fallback in JSBundlerPlugin__matchOnLoad /
|
||||
// JSBundlerPlugin__matchOnResolve passed the wrong `which` value to JSBundlerPlugin__addError,
|
||||
// so Zig would reinterpret a *Load as a *Resolve (and vice versa). matchOnLoad additionally
|
||||
// passed plugin.config instead of the JSBundlerPlugin* as the plugin argument, so
|
||||
// plugin.globalObject() in Zig dereferenced the wrong pointer.
|
||||
//
|
||||
// The builtin calls the public `.then` on its pending async-IIFE promise, so we can force a
|
||||
// synchronous throw there to reach that fallback deterministically. Before the fix:
|
||||
// onLoad crashes (null deref in JSGlobalObject under ASAN, SIGSEGV on release) and
|
||||
// onResolve hangs (wrong completion handler leaves the resolve counter un-decremented).
|
||||
describe.each(["onLoad", "onResolve"] as const)("Bun.build plugin %s builtin throws synchronously", hook => {
|
||||
test.concurrent("addError receives the correct context type", async () => {
|
||||
const fixture = /* js */ `
|
||||
const originalThen = Promise.prototype.then;
|
||||
let armed = false;
|
||||
Promise.prototype.then = function (...args) {
|
||||
if (armed) {
|
||||
armed = false;
|
||||
Promise.prototype.then = originalThen;
|
||||
throw new Error("synchronous throw from ${hook} builtin");
|
||||
}
|
||||
return originalThen.apply(this, args);
|
||||
};
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: [Bun.fileURLToPath(new URL("./entry.ts", import.meta.url))],
|
||||
throw: false,
|
||||
plugins: [
|
||||
{
|
||||
name: "sync-throw",
|
||||
setup(build) {
|
||||
build.${hook}(
|
||||
{ filter: ${hook === "onLoad" ? "/entry\\.ts$/" : "/^sync-throw:thing$/"} },
|
||||
() => {
|
||||
armed = true;
|
||||
// Pending promise: the outer async IIFE in the builtin suspends on it,
|
||||
// so the builtin falls through to the public .then() call which throws
|
||||
// synchronously and surfaces to the C++ TOP_EXCEPTION_SCOPE fallback.
|
||||
return new Promise(() => {});
|
||||
},
|
||||
);
|
||||
${hook === "onResolve" ? 'build.onLoad({ filter: /.*/, namespace: "sync-throw" }, () => ({ contents: "", loader: "js" }));' : ""}
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
Promise.prototype.then = originalThen;
|
||||
console.log(JSON.stringify({
|
||||
success: result.success,
|
||||
logs: result.logs.map(l => String(l.message ?? l)),
|
||||
}));
|
||||
`;
|
||||
|
||||
using dir = tempDir(`plugin-sync-exception-${hook}`, {
|
||||
"entry.ts": hook === "onLoad" ? `export const x = 1;` : `import "sync-throw:thing"; export const x = 1;`,
|
||||
"build.ts": fixture,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "run", join(String(dir), "build.ts")],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
// If the build hangs (the onResolve bug), kill it so the assertion below reports
|
||||
// a useful diff instead of the test runner timing out.
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
// Receiver includes stdout/stderr so the diff on failure shows the crash output
|
||||
// (UBSan/SIGSEGV under the old code) instead of a bare "expected 0, got 1".
|
||||
expect({
|
||||
stdout: stdout.trim(),
|
||||
stderr: stderr.trim(),
|
||||
exitCode,
|
||||
signalCode: proc.signalCode ?? null,
|
||||
}).toMatchObject({
|
||||
exitCode: 0,
|
||||
signalCode: null,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(stdout.trim());
|
||||
expect(parsed.success).toBe(false);
|
||||
expect(parsed.logs.join("\n")).toContain(`synchronous throw from ${hook} builtin`);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user