initial commit
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
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,
|
||||
);
|
||||
Reference in New Issue
Block a user