Files
bun-src/test/napi/napi-finalizer-delete-ref.test.ts
2026-08-27 21:09:14 +00:00

119 lines
4.5 KiB
TypeScript

import { spawn, spawnSync } from "bun";
import { beforeAll, expect, it } from "bun:test";
import { existsSync } from "fs";
import { bunEnv, bunExe, canBuildNodeAddons } from "harness";
import { join } from "path";
const napiAppDir = join(__dirname, "napi-app");
const addonName = "test_delete_ref_in_finalizer_experimental";
const addonPath = join(napiAppDir, `build/Debug/${addonName}.node`);
beforeAll(() => {
if (!canBuildNodeAddons()) return;
// Build the native addons in napi-app, but only if the one this test needs
// is missing (napi.test.ts or a previous run usually has built it already).
// The addon doesn't link against bun, so an existing binary stays valid
// across bun builds; skipping the install avoids re-running the node-gyp
// rebuild, which is slow and occasionally flaky under resource pressure.
if (existsSync(addonPath)) {
return;
}
// Fast path: `bun install` runs napi-app's install script, a full
// `node-gyp rebuild` of every addon target in binding.gyp. This test needs
// exactly one single-source-file target, so install the toolchain without
// running that script and ask node-gyp to build only this target.
if (!existsSync(join(napiAppDir, "node_modules/node-gyp"))) {
spawnSync({
cmd: [bunExe(), "install", "--ignore-scripts", "--verbose"],
cwd: napiAppDir,
stderr: "inherit",
env: bunEnv,
stdout: "inherit",
stdin: "inherit",
});
}
spawnSync({
// Same invocation as napi-app's install script, minus `clean` and scoped
// to one target (node-gyp forwards trailing args as make/msbuild targets).
cmd: [bunExe(), "--bun", "node-gyp", "configure", "build", "--debug", "-j", "max", addonName],
cwd: napiAppDir,
stderr: "inherit",
env: bunEnv,
stdout: "inherit",
stdin: "inherit",
});
if (existsSync(addonPath)) {
return;
}
// Fallback: the full install + rebuild of everything, retried once.
for (let attempt = 0; ; attempt++) {
const install = spawnSync({
cmd: [bunExe(), "install", "--verbose"],
cwd: napiAppDir,
stderr: "inherit",
env: bunEnv,
stdout: "inherit",
stdin: "inherit",
});
if (install.success && existsSync(addonPath)) {
return;
}
if (attempt >= 1) {
throw new Error("building napi-app addons failed");
}
}
}, 300_000);
it.skipIf(!canBuildNodeAddons())(
"napi_delete_reference can be called from finalizers during GC in experimental modules",
async () => {
// Finalizers in NAPI_VERSION_EXPERIMENTAL modules run synchronously while
// the garbage collector is sweeping. Unlike napi_reference_unref (which
// really is forbidden there, see "napi_reference_unref is blocked from
// finalizers in experimental modules" in napi.test.ts), Node.js still
// allows napi_delete_reference during GC: it takes node_api_basic_env,
// and deleting the reference returned by napi_wrap is documented to be
// done from the finalize callback (node-addon-api's ObjectWrap destructor
// does exactly this). Bun used to abort with a "napi_reference_unref"
// panic.
const code = `
const addon = require(${JSON.stringify(addonPath)});
function makeGarbage() {
addon.createWrapped(50);
addon.createWithFinalizer(50);
}
makeGarbage();
Bun.gc(true);
Bun.gc(true);
const stats = addon.getStats();
if (stats.finalizersCalled === 0) {
throw new Error("test bug: no finalizers ran during Bun.gc");
}
if (stats.deletesSucceeded !== stats.finalizersCalled) {
throw new Error(
\`napi_delete_reference failed in \${stats.finalizersCalled - stats.deletesSucceeded} of \${stats.finalizersCalled} finalizers\`,
);
}
console.log("SUCCESS");
`;
const { BUN_INSPECT_CONNECT_TO: _, ASAN_OPTIONS, ...rest } = bunEnv;
await using proc = spawn({
cmd: [bunExe(), "-e", code],
env: {
...rest,
// If the GC check wrongly fires, die with a plain abort instead of
// hanging in the crash reporter / ASAN symbolizer.
BUN_INTERNAL_SUPPRESS_CRASH_ON_NAPI_ABORT: "1",
ASAN_OPTIONS: "allow_user_segv_handler=1:disable_coredump=1:symbolize=0",
},
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).not.toContain("FATAL ERROR");
expect(stdout).toContain("SUCCESS");
expect(exitCode).toBe(0);
},
30_000,
);