Skip to content

bun test: run no test of a later file after a preload beforeAll or aroundAll fails - #48272

Open
robobun wants to merge 6 commits into
mainfrom
robobun/fc4bc8dc/preload-beforeall-failure-guards-all-files
Open

robobun wants to merge 6 commits into
mainfrom
robobun/fc4bc8dc/preload-beforeall-failure-guards-all-files

Conversation

@robobun

@robobun robobun commented Nov 8, 2026 •

Copy link
Copy Markdown
Collaborator

Related to #12782

Problem

Fix

  • BunTestRoot records how the preload's beforeAll and aroundAll set-ups ended. enter_file fails the root scope of each later file, as for a file that fails to load.
  • A file that runs the hooks is never stopped, so --isolate and --parallel run the same tests as before.
  • Verified: test/regression/issue/12782.test.ts, test/cli/test/bun-test.test.ts (11 of 22 new tests fail on main), test/js/bun/test/vitest-flavor.test.ts (4 of 4), test/cli/test/test-shard.test.ts (1 of 1). More in Notes.
  • Self-reviewed: 21 concerns raised, 18 addressed. Not taken: stop after the first file (Notes), a JSDoc clause on suite.

Background

  • A preload's hooks live in BunTestRoot::hook_scope. Only BunTestRoot outlives a test file.
  • A failed beforeAll makes the runner jump within its file's order, never to the next file.
  • Considered: rerun the failed hook for each file (repeats user set-up), reuse hook_scope.failed (cannot name the failed stage).

Downsides

  • Default change with no flag, in a run that already fails. To run the later files, catch the error in the hook.
  • A later file is still loaded, as each file is under --isolate: its module body runs, its hooks and tests do not, and its tests are not listed.
  • BunTest grows from 440 to 456 bytes per test file.
Notes

Decisions

  1. The default changes with no flag. .claude/docs/landing-prs.md asks for a flag when a default changes. This change is the return of the throwing in a global (preload) beforeAll does not stop tests from running #12782 fix (Rewrite test/describe, add test.concurrent #22534, 1.2.23). run beforeAll before the first file and afterAll after the last file #23113 removed that fix as a side effect, and its description does not mention failed hooks. A hook that catches its own error keeps every file running.
  2. A later file is still loaded and reports nothing. Under --isolate and on a --parallel worker, the module body of each file runs too, and its hooks and tests do not. The shapes that this PR does not build are "start no later file" (Jest and Vitest globalSetup) and "report a failure for each file" (1.2.23, --isolate, --parallel). One test pins the decision: a file after the failure is still loaded, and reports nothing of its own but a failure to load.
  3. This PR needs none of the open PRs below, and none of them needs this PR. The list says what the second one to land must change.

Open PRs on the same code

What each build does

Debug builds of main (2596ac5) and of this branch. The preload has a beforeAll that throws, unless the row says otherwise.

Run main this PR
bun test --preload ./preload.ts a b c 2 pass, 1 fail, b and c run 0 pass, 1 fail, b and c print a header only
--isolate a b, --parallel=1 a b, one --parallel worker 0 pass, 2 fail the same
--rerun-each=2 a b 2 pass, 2 fail 0 pass, 2 fail
--rerun-each=2 a b, the hook fails on its first call only 3 pass, 1 fail the same
--rerun-each=2 a b, the hook passes on its first call only 3 pass, 1 fail the same
--bail=1 a b stops at the hook the same
Preload aroundAll that throws before runSuite(), 3 files b and c run, the preload afterAll runs no test runs, the afterAll does not run, the outer aroundAll finishes
Preload beforeAll with no timeout that never settles the later files run the same

The preload afterAll hooks still run with the last file after a failed beforeAll, as before.

Reference runs

  • Jest 30.5.2, a setupFilesAfterEnv file with a beforeAll that throws, 2 files, --runInBand: the hook runs for each file, Tests: 4 failed, 4 total, no test body runs. A globalSetup that throws: no test file runs.
  • Vitest 5.1.0, a setupFiles file with the same hook: Test Files 2 failed, Tests 4 skipped, no test body runs. A globalSetup that throws: no test file runs.

The rule

  • The record has three states: the hooks did not run, they passed, or one of them failed (with the stage, aroundAll or beforeAll).
  • exit_file writes it from the file that ran the hooks. A hook fails when it throws, rejects, fails an expect() or times out.
  • --rerun-each runs the preload hooks again with each repeat of the first file (main does too, bun test --rerun-each: run a preload's beforeAll, afterAll and aroundAll once around the reruns #48226 changes that). A repeat that fails them does not undo a repeat that passed: what it set up is still there. A set-up that cannot run twice, such as a server on a fixed port, keeps the result of main.
  • After a failed aroundAll set-up, the preload afterAll hooks do not run with the last file. They are inside the aroundAll, and a run of one file already skips them. The teardown of an outer aroundAll still runs.
  • With --timings and --update-timings, a file that ran no test after the failed set-up keeps the time that the table has (Timings::keep, as for a file whose worker crashed). That includes the file that ran the failed hook, with or without --isolate: main writes a time with no test in it there. The coordinator of a --parallel run still writes that time, as on main.

Not covered

Considered and not built

  • Start no later file. The preload afterAll and the aroundAll teardown are scheduled with the last file, so they need a new home first. The review sized it at about 30 lines in test_command.rs. It changes what a failing run loads, and no other mode of the runner skips the load of a file, so this PR does not do it. Only the test named under Decisions asserts that a later file is loaded.
  • Print no header for a later file. An earlier commit did that. A later file that writes with fs.writeSync or through a child process then printed under the header of the first file, and GITHUB_ACTIONS=true printed an ::endgroup:: with no ::group::. The commit is gone. A file with no tests, or with a -t that matches nothing, prints the same bare header.
  • Run the afterAll hooks of a later file, so that what its module body opens is closed. They would run without the beforeAll hooks of that file, and a hook such as afterAll(() => db.close()) then throws. The file that runs the failed hook does not run its own afterAll hooks either, on main and here. Three files that each listen on one fixed port at module scope give 0 pass, 3 fail, 2 errors on both builds.
  • A user-land guard: a flag that the preload beforeAll sets and a preload beforeEach checks. It works on main, but it reports every test as a failure, it cannot stop the hooks of the later files, and the documented default stays wrong.
  • A state for a set-up that never ran (--aot with a first file that the build refused). AOT: front end, test runs and tests for the rewritten compiler #47942 removed that path while this branch was in progress.

Measurements

Release builds of main (2596ac5) and of this branch (cdec3df, the last commit that changes code), same build directory.

  • size -A: .text is 108,257,833 bytes on main and 108,257,577 bytes on this branch (256 fewer). .rodata is 26,780,652 bytes in both. The stripped binary is 138,119,440 bytes in both.
  • llvm-nm -S and llvm-objdump -d on bun-profile, main to this PR: BunTest::run 12,402 to 12,421 bytes (2,453 to 2,474 instructions), TestCommand::run 13,490 to 13,464 bytes (3,081 to 3,071), the drop of EnteredFile 85 to 261 bytes (25 to 63), the closure of run_all_tests 1,297 to 1,164 bytes (282 to 247). When a test file exits, it runs 8 of the new instructions. The file that ran the preload's hooks runs 20, and 11 more for each hook.
  • lldb, debug builds: BunTestRoot 56 to 56 bytes (one per process), BunTest 440 to 456 bytes (one per test file).

Debug (ASAN) builds, test/js/bun/test/scheduling/multi-file (a preload and 2 files, all pass).

  • gdb, nexti count of the own frames of enter_file, BunTest::init, advance_inner, exit_file and the three new functions, for one test file: 1,763 on main, 2,208 (first file) and 2,247 (last file) on this branch. Two runs of each build gave the same counts. Nothing is added per test or per group.
  • The edited frames run once per test file per repeat: 2, 4 and 2 times (no flag, --rerun-each=2, --isolate) on both builds. JS callbacks in those runs: 8, 16 and 10 on both.
  • The group log (WANTS_LOUD=1) of the passing run with no flag, --isolate, --rerun-each=2 and --parallel=1 --no-isolate: 0 differing lines between the builds, after line numbers and addresses are removed.
  • Execution.rs, Order.rs, Collection.rs and jest.rs: 0 lines changed. test_command.rs changes only the two places that record a file's time. step_group, step_sequence_one and generate_order_test are 3,716, 14,856 and 4,799 bytes in both builds.
  • A failing run with 3 files and two preload beforeAll hooks: 21 JS callbacks on main, 3 on this branch. Test bodies after the failed hook: 4 and 0.

valgrind, perf, strace and bloaty are not installed here, so the instruction counts come from gdb and llvm-objdump.

Tests

  • 12782.test.ts: the snapshot no longer has (pass) bar > should not run, and the test now also checks that no test body logs.
  • bun-test.test.ts, a preload whose beforeAll fails: 22 tests. 11 fail on main. The other 11 pin what must not change: --isolate, --parallel workers, --bail, a hook that fails or passes on its first call only, a first file that fails or hangs in a hook of its own, and a hook that the runner gives up on.
  • vitest-flavor.test.ts: a preload aroundAll that throws, returns or times out before runSuite(), and a beforeAll that fails inside a preload aroundAll.
  • test-shard.test.ts: --update-timings keeps the time of each file after the beforeAll of a preload failed. Main writes 115, 8 and 7 ms there.
  • Each clause of the change, removed on a debug build, makes at least one new test fail: the kept pass (1 test), the rule for a file that runs the hooks (4), the aroundAll stage (3), the hook that the runner gives up on (1), the count of the leading set-up hooks (1).

Suites run with bun bd test on this branch: bun-test, vitest-flavor, bun_test, isolation, failure-skip, rerun-each, test-test, jest-hooks, expect-deferred, vi-utils, parallel, junit, node-test, test-shard, and test/internal/source-lints. The failures are timeouts of the debug build that a debug build of main has too: instances of a class nested too deeply to print are a RangeError, no other builtin module patches a global class when it loads, each worker has a unique JEST_WORKER_ID and BUN_TEST_WORKER_ID, and two source-lint tests.


no test proof · iteration 1 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/cli/test/bun-test.test.ts

…s failed

A preload `beforeAll` or `aroundAll` set-up runs with the first test
file. Its failure lived only in that file's execution order, so every
later file ran its tests.

`BunTestRoot` now records how those hooks ended. `exit_file` writes the
record from the file that ran the hooks, and `enter_file` reads it for
each file that follows:

- After a failure, the file's root scope is failed from the start, as
  for a file that fails to load. No `describe()` callback, hook or test
  of it runs.
- After a failed `aroundAll` set-up, the preload `afterAll` hooks do
  not run with the last file. They are inside the `aroundAll`.

A file that runs the hooks is never kept from running by the record:
its own order skips past its tests when they fail. So `--isolate` and
`--parallel` workers behave as before. `--rerun-each` runs the hooks
again with each repeat of the first file. A repeat that passes them is
not undone by one that fails them, because what it set up is still
there.

A hook that the runner gives up on (no timeout, and nothing left that
can settle it) is not a failed hook here. The later files run, as
before.
…r file from running

- `test/regression/issue/12782.test.ts`: the snapshot pinned `(pass) bar > should not run`. It now pins that no test of the second file runs.
- `test/cli/test/bun-test.test.ts`: each way the hook fails, each driver, a first file with no test to run, and the repeats of `--rerun-each`. One test pins that a later file is still loaded.
- `test/js/bun/test/vitest-flavor.test.ts`: a preload `aroundAll` that fails before `runSuite()`, and a `beforeAll` that fails inside one.
Say in the lifecycle page and in the JSDoc of `beforeAll` and `aroundAll`
what a hook of a `--preload` script guards when it fails, and that it
runs for each file with `--isolate` and on a `--parallel` worker.
@robobun
robobun requested a review from alii as a code owner November 8, 2026 19:28
@coderabbitai

coderabbitai Bot commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 17 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 2596ac5 and 64e0d9d.

Files selected for processing: 9

  • docs/test/lifecycle.mdx
  • packages/bun-types/test.d.ts
  • src/runtime/cli/test/Timings.rs
  • src/runtime/cli/test_command.rs
  • src/runtime/test_runner/bun_test.rs
  • test/cli/test/bun-test.test.ts
  • test/cli/test/test-shard.test.ts
  • test/js/bun/test/vitest-flavor.test.ts
  • test/regression/issue/12782.test.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@github-actions github-actions Bot added the claude label Nov 8, 2026
@robobun

robobun commented Nov 8, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 7:56 PM PT - Nov 8th, 2026

✅ @robobun, your commit 64e0d9d965b17ff39b36252a9b2e1aa36f1ccbfd passed in Build #138947! 🎉


🧪   To try this PR locally:

bunx bun-pr 48272

That installs a local version of the PR into your bun-48272 executable, so you can run:

bun-48272 --bun

@robobun

robobun commented Nov 8, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

Reproduced on a debug build of main 2596ac5 and on a 1.4.3 canary build (367d939):

cat > preload.ts <<'X'
import { beforeAll } from "bun:test";
beforeAll(() => { throw new Error("preload beforeAll failed"); });
X
for f in a b; do echo "import { it } from \"bun:test\"; it(\"in $f\", () => console.log(\"ran: $f\"));" > $f.test.ts; done
bun test --preload ./preload.ts ./a.test.ts ./b.test.ts

a.test.ts prints the error and (fail) (unnamed). b.test.ts prints ran: b and (pass) in b. The summary is 1 pass, 1 fail. With this branch no test of b.test.ts runs, and the summary is 0 pass, 1 fail.

  • Before the fix: test/regression/issue/12782.test.ts, 11 of the 22 new tests in test/cli/test/bun-test.test.ts, the 4 new tests in test/js/bun/test/vitest-flavor.test.ts and the new test in test/cli/test/test-shard.test.ts fail on a debug build of main.
  • After the fix: bun bd test passes all of them.

CI passes on 64e0d9d (build 138947). The diff is ready for a maintainer review.

PR: #48272

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline findings, I also checked the --bail path through deinit_for_exit (it now skips recording the set-up outcome, but the process exits before anything reads BunTestRoot::set_up), the set_up_outcome slice bounds (set_up_hooks stays (0, 0) until load_from_order fills the leading groups, so a failed-root or isolated file reads nothing), and the --rerun-each sticky Passed rule, which matches the PR's stated intent. The REVIEW.md checks that need a run (USE_SYSTEM_BUN=1 / bun bd test on the new tests) were not performed in this review.

Extended reasoning...

The change adds a SetUp record on BunTestRoot and a per-file set_up_hooks count in src/runtime/test_runner/bun_test.rs so a failed preload beforeAll/aroundAll fails the root scope of every later serial file, plus docs, .d.ts JSDoc and tests. It touches no security-sensitive surface. Four inline findings on the later-file behaviour (timings, never-settling hooks, unreleased module-level resources, silent empty-suite reporting) already signal that a human should look; this note only records what else was examined and ruled out.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment thread src/runtime/test_runner/bun_test.rs Resolved
Comment thread src/runtime/test_runner/bun_test.rs Resolved
Comment thread src/runtime/test_runner/bun_test.rs Resolved
Comment thread src/runtime/test_runner/bun_test.rs Resolved
… preload's set-up failed

With `--timings` and `--update-timings`, the serial run recorded how
long each file took. After a failed preload set-up no file runs a
test, so that time says nothing about the file. A time that is too
short starts the file late in the next `--parallel` or `--shard` run.

Such a file now keeps the time that the table has, like a file whose
worker ended in the middle of it.
Comment thread src/runtime/test_runner/bun_test.rs Resolved

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cdec3df covers the --timings/--update-timings point from my earlier review (later files now go through Timings::keep, and the new test-shard test pins it). Beyond the inline nit, I also checked the --parallel worker path: BunTestRoot::set_up is never reset between a worker's files without --isolate, so it can stay Failed, but every reader (failed_set_up_before, the afterAll gate in advance_inner) is keyed on first_last.first, which runner.rs:750 always sets, and set_up_failed() is only read by the serial record_timing path, so a stale value has no effect there.

Extended reasoning...

The latest push adds a CommandLineReporter::record_timing helper in src/runtime/cli/test_command.rs that routes a file blocked by a failed preload set-up through the existing Timings::keep, plus a serial test in test/cli/test/test-shard.test.ts; the runner changes in src/runtime/test_runner/bun_test.rs are otherwise unchanged since the prior review. No security-sensitive surface is touched. Earlier inline threads on later-file afterAll hooks and on silent empty-suite reporting are not changed by this push, so the PR still needs a human decision on those and is not approved.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Still open from earlier reviews (1):

If you have decided not to act on one of these findings, resolve its thread (a reply alone leaves it open) and the next review stops counting it. To review this commit again now, use Re-run on its "Claude Code Review" check.

Comment thread test/cli/test/test-shard.test.ts Resolved
…imings test concurrently

The timings test compares no durations, so it does not need the serial
block.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

A file that ran no test after the preload's set-up failed keeps its time too.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant