Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,7 @@ pub struct ExitHandler {
pub exit_code: u8,
/// `bun test` sets this at the end of a run unless `node:test` APIs were used: jest and vitest never fire a test file's `process.on('exit')` listeners.
pub skip_exit_listeners: bool,
/// `process.exit()` or a fatal error, as opposed to the event loop running dry.
/// `process.exit()`, a fatal error or the end of a `bun test` run, as opposed to the event loop running dry.
/// See `VirtualMachine::exit_tears_down_napi_envs`.
pub requested: bool,
}
Expand Down
1 change: 1 addition & 0 deletions src/runtime/cli/test/parallel/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@ pub(crate) fn run_as_worker(
// (lastChanceToFinalize) runs; bypassing it leaks JSC-owned native state.
vm_ref.exit_handler.exit_code = 0;
vm_ref.exit_handler.skip_exit_listeners = test_command::skip_exit_listeners(wloop.reporter);
vm_ref.exit_handler.requested = test_command::exit_is_requested();
vm_ref.run_with_api_lock(|| {
// SAFETY: caller guarantees `vm` is a valid live VM pointer for the worker's lifetime.
unsafe {
Expand Down
6 changes: 6 additions & 0 deletions src/runtime/cli/test_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,11 @@ pub(crate) fn skip_exit_listeners(reporter: &CommandLineReporter) -> bool {
!(reporter.jest.node_test_used || should_drain_event_loop())
}

/// `ExitHandler::requested` at the end of a run, which does not wait for the event loop to run dry.
pub(crate) fn exit_is_requested() -> bool {
!should_drain_event_loop()
}

pub struct CommandLineReporter {
// `TestRunner<'a>` borrows `TestOptions`/regex from the CLI ctx; the
// reporter is held in a `Box` local to `TestCommand::exec` which never
Expand Down Expand Up @@ -2672,6 +2677,7 @@ impl TestCommand {
vm.exit_handler.exit_code = 1;
}
vm.exit_handler.skip_exit_listeners = skip_exit_listeners(&reporter);
vm.exit_handler.requested = exit_is_requested();
// Must precede the GC-root release below: exit listeners are user JS and may touch still-live state.
{
let vm_ptr: *mut VirtualMachine = vm;
Expand Down
62 changes: 62 additions & 0 deletions test/napi/napi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1842,6 +1842,68 @@ describe.skipIf(!canBuildNodeAddons())("cleanup hooks", () => {
expect(exitCode).toBe(1);
});

// `bun test` ends the process when the last test settles. It does not wait
// for the event loop, so what the tests started can still be in flight: that
// is process.exit(), not a loop that ran dry. node-sqlite3's Statement
// finalizer emits an 'error' event for every call still queued behind a
// running one, nothing listens, and node-addon-api turns the throw into
// "NAPI FATAL ERROR: Error::ThrowAsJavaScriptException napi_throw" (#34663).
const testFile = (name: string) => `
${setup}
test(${JSON.stringify(name)}, () => {});
`;

// stdout starts with the "bun test v1.x" banner; the rest is what the addons printed.
async function runBunTest(args: string[], files: Record<string, string>, env: Record<string, string>) {
using dir = tempDir("napi-bun-test-teardown", files);
await using proc = spawn({
cmd: [bunExe(), "test", ...args, ...Object.keys(files).map(file => `./${file}`)],
env: { ...bunEnv, ...env },
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const lines = stdout
.split(/\r?\n/)
.filter(line => line && !line.startsWith("bun test "))
.sort();
return { lines, stdout, stderr, exitCode };
}

it("the end of a `bun test` run skips it", async () => {
const files = { "addon.test.js": testFile("loads the addons") };
const { lines, stderr, exitCode } = await runBunTest([], files, noDestruct);
expect(stderr).toContain("1 pass");
expect(lines).toEqual(setupLines.toSorted());
if (exitCode !== 0) expect(stderr).toBe("");
expect(exitCode).toBe(0);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

it("the end of a `bun test --parallel` worker skips it", async () => {
const files = { "a.test.js": testFile("loads the addons in a"), "b.test.js": testFile("loads the addons in b") };
const { stdout, stderr, exitCode } = await runBunTest(["--parallel=2"], files, noDestruct);
// The coordinator relays what a worker prints on its own stderr.
const output = stdout + stderr;
expect(output).toContain("2 pass");
expect(output).not.toContain("executed at position");
expect(output).not.toContain("finalize order");
if (exitCode !== 0) expect(stderr).toBe("");
expect(exitCode).toBe(0);
});

it("a `bun test` run that drained the event loop first tears it down", async () => {
const files = { "addon.test.js": testFile("loads the addons") };
const { lines, stderr, exitCode } = await runBunTest([], files, {
...noDestruct,
BUN_TEST_DRAIN_EVENT_LOOP: "1",
});
expect(stderr).toContain("1 pass");
expect(lines).toEqual(teardownLines.toSorted());
if (exitCode !== 0) expect(stderr).toBe("");
expect(exitCode).toBe(0);
});

it("an event loop that runs dry tears it down", async () => {
const { lines, stderr, exitCode } = await run(setup, noDestruct);
expect(stderr).toBe("");
Expand Down
Loading