Conversation
…ts .snap opens The per-test snapshot counters were cleared only when a different test file's .snap was opened. Counts a file accumulated before that point (inline snapshots, or every count of a file that never opens a .snap) were either thrown away or carried into the next file, so a file's snapshot keys depended on which files ran before it in the process. Clear the counters at the start of every file run in TestCommand::run (which also covers --rerun-each) and leave them alone when the .snap file switches.
|
Warning Review limit reached
Next review available in: 3 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (6)
Comment |
|
Status: reproduced on the current release with two test files in one directory ( |
|
Updated 7:05 AM PT - Aug 15th, 2026
❌ @robobun, your commit c51d4da has some failures in 🧪 To try this PR locally: bunx bun-pr 38874That installs a local version of the PR into your bun-38874 --bun |
There was a problem hiding this comment.
LGTM — moves the snapshot counter reset to the test-file boundary where it belongs.
What was reviewed:
- Traced both failure modes: inline-before-first-file-snapshot (counts wiped by
write_snapshot_fileon.snapswitch) and inline-only-file (counts never cleared) — the newreset_counts()at the top of the per-file loop fixes both, and removing the clear fromwrite_snapshot_fileis safe because.snapswitches only at file boundaries. reset_countschanging from zero-in-place to.clear()is equivalent for the retry/repeat caller inExecution::reset_sequence(add_count's get-or-put treats absent and zero identically).- Checked the other
write_snapshot_filecallers (end-of-run intest_command.rs:2729, parallel runner) — neither needs the counts afterward. - Tests cover all three described scenarios with exact
.snapcontents, usedescribe.concurrent, drain pipes, and setCIexplicitly.
Extended reasoning...
Overview
This PR fixes snapshot key numbering so it is deterministic per test file, independent of which files ran earlier in the same bun test process. The change touches four files: test_command.rs (moves reset_counts() from the repeat_index > 0 branch to run unconditionally at the top of every per-file iteration), snapshot.rs (removes counts.clear() from write_snapshot_file, changes reset_counts to .clear() the map, adds a doc comment on the counts field), Execution.rs (comment-only wording tweak), and three new subprocess tests in snapshot.test.ts.
Security risks
None. This is test-runner bookkeeping state (a StringHashMap<usize> of test-name → snapshot-matcher count) with no user-controlled input beyond test names, which were already handled.
Level of scrutiny
Medium-low. The mechanism is well-explained in the PR description and matches the code: counts was previously cleared only inside write_snapshot_file, which runs lazily on .snap file switch, so inline snapshots taken before a file's first file-snapshot were either wiped (if a previous file had a .snap open) or leaked into the next file (if no .snap was ever opened). Moving the reset to TestCommand::run's per-file loop is the correct layer — it's the test-file boundary that both serial and --parallel workers go through.
I verified that removing counts.clear() from write_snapshot_file cannot lose a needed reset: within a single test file the file_id never changes, so get_snapshot_file only calls write_snapshot_file at file boundaries, exactly where the new reset_counts() now runs. The two other write_snapshot_file callers (end-of-run flush and parallel-runner flush) don't need counts afterward.
The reset_counts change from zero-in-place to .clear() is behaviorally equivalent for its other caller (Execution::reset_sequence on retry/repeat): add_count uses get_or_put and treats a missing key the same as a zero value. Clearing also stops the map from accumulating every test name across the run, which is a small win.
Other factors
The three new tests directly encode the failure scenarios from the PR description, assert exact .snap file bytes (not just "passes"), spread bunEnv while overriding CI, drain stdout/stderr/exited concurrently, and run under describe.concurrent since they're independent subprocess spawns. The PR description lists the existing test files that were re-run (rerun-each.test.ts, ci-restrictions.test.ts, parallel.test.ts, etc.) covering the paths whose behavior could have shifted. The --rerun-each reset that was previously guarded by repeat_index > 0 is subsumed by the unconditional call, so no coverage is lost.
Snapshots.counts was process-wide and had to be cleared by hand at every file boundary. Store the counters on BunTest instead: enter_file builds a fresh one for every file run (including each --rerun-each pass), so no reset call is needed there, and the retry/repeat reset clears the map of the file it belongs to. Adds a --parallel test where workers run several files each.
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. The fix is well-reasoned — moving the counters onto BunTest gives them the right per-file lifetime by construction — but it touches test-runner state management with new unsafe derefs (reset_sequence, add_count), so a human look at the aliasing reasoning and the CI status on the latest commit would be worthwhile.
Checked: add_count's buntest_strong.get() doesn't overlap the earlier bun_test borrow in get_or_put (last use is file_id, dead under NLL); reset_sequence's raw deref of snapshot_counts is disjoint from the live sequence borrow into execution.sequences, matching the discard_junit_failure pattern just above it. The removed --rerun-each reset is covered because enter_file builds a fresh BunTest per pass. The comment-cop note on bun_test.rs looks addressed by c51d4da (field doc is now one line).
Extended reasoning...
Overview
This PR fixes snapshot key numbering when multiple test files run in one bun test process. It moves the per-snapshot-name counter map from the process-wide Snapshots struct to the per-test-file BunTest struct, deleting three hand-placed reset calls (reset_counts in --rerun-each, the clear in write_snapshot_file, and the Jest::runner() reach-through in reset_sequence) in favour of the map dropping with BunTest on exit_file. Four files touched in src/runtime/ plus four new tests across two test files.
Security risks
None. This is internal test-runner bookkeeping; no user input parsing, no I/O path changes, no auth/crypto.
Level of scrutiny
Medium-high. The change is architecturally the right one (REVIEW.md: "Store state on the object whose lifetime matches it"), and the diff is small, but it introduces a new unsafe { (*buntest.as_ptr()).snapshot_counts.clear() } in reset_sequence and a new BunTestCell::get() call inside Snapshots::add_count while the caller get_or_put already holds an Rc to the same cell. I traced both: the get_or_put borrow's last use is bun_test.file_id before add_count is called, so no aliased &mut; the reset_sequence deref touches a field disjoint from execution.sequences (where sequence points), and mirrors the existing discard_junit_failure pattern immediately above it. Still, this is exactly the class of change where a maintainer should confirm the Stacked-Borrows reasoning.
Other factors
- The PR description is thorough and the four new tests each pin a distinct failure mode (inline-before-file-snapshot, CI read-back, inline-only carryover,
--parallel); the author confirmed each fails on the release build. - The
--rerun-eachand retry/repeat guards from #23705 are covered byrerun-each.test.tsper the description. - robobun reported CI failures on commit 978474f; two commits landed after that (7a1a268, c51d4da) and the current CI status isn't visible in the timeline.
- The github-actions comment-cop flagged a long comment on
bun_test.rs; c51d4da shortened the field doc to one line, so that looks addressed.
Given the unsafe-code surface and unclear CI status on HEAD, deferring rather than approving.
…e snapshot hunks other PRs own afterEach/afterAll/onTestFinished called by a test body the runner has already given up on were added to whichever test was running by then. They now fail with an error, through the same ref expect() uses. The snapshot.rs reordering and the get_snapshot_name reclassification are not needed once the abandoned check happens before the matchers touch anything, and they overlap with #38799 and #38874, so they are gone. The two cases that fail the running test with an unhandled error now wait through done() instead of a returned promise, so they describe the situation the runner gives up on whether or not #36719 has landed.
|
Re-verified against current main (367d939). This branch is 1,013 commits behind it and merges without a conflict. All results below are from a debug ASAN build of the merge result.
|
Problem
toMatchSnapshot()writes or looks up depends on which test files ran earlier in the samebun testprocess. Witha.test.tstaking one file snapshot andb.test.tstaking an inline snapshot followed by a file snapshot in testb:bun test ./b.test.tswritesexports[`b 2`](the inline snapshot is number 1, as in Jest)bun test ./a.test.ts ./b.test.tswritesexports[`b 1`].snapcommitted from one shape fails under the other; in CI the second shape fails withSnapshot creation is disabled in CI environments ... Snapshot name: "b 1".same name 2instead ofsame name 1).--parallelhas the same problem for every file after the first one a worker runs (6 identical files on 2 workers end up as 2 xt 2and 4 xt 1).Snapshots.counts, src/runtime/test_runner/snapshot.rs) lived on the process-wideSnapshotsand were only cleared insidewrite_snapshot_file, which runs when a file snapshot is taken for a different test file than the.snapcurrently open.Expect::inline_snapshot(src/runtime/test_runner/expect.rs) bumps the counters without opening a.snap, so the bump either landed before the switch and was wiped, or (for a file that never opens a.snap) was never cleared at all. The--rerun-eachfix for Snapshots are broken with --rerun-each #23705 had already needed a second hand-placed reset of the same map.Fix
BunTest::snapshot_counts(src/runtime/test_runner/bun_test.rs).enter_filebuilds a freshBunTestfor every file run, including each--rerun-eachpass, so the per-file lifetime comes from the object itself:Snapshots::reset_counts, the--rerun-eachreset inTestCommand::run, and the clear inwrite_snapshot_fileare all deleted.Snapshots::add_countbumps the counter on theBunTesttheexpect()belongs to, which is the sameBunTestwhosefile_idalready selects the.snapfile, so the numbering and the file it applies to can no longer disagree.Execution::reset_sequence, same behaviour as before, matching Jest'sSnapshotState.clear()on retry) now takes theBunTestand clears that file's map instead of reaching the process-wide one throughJest::runner().SnapshotState, counters included, per test file), andBunTestis bun's per-test-file object; the.snapfile is opened lazily by the first file snapshot, so its lifetime was never a usable proxy for the test file's. Single-file runs already numbered the way Jest does; this makes multi-file and--parallelruns agree with them.test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts(snapshot numbering across test files): inline snapshot before the file's first file snapshot,.snapfiles written by single-file runs passing underCI=truewhen the files run together, and an inline-only file followed by a file with the same test name; plus one intest/cli/test/parallel.test.tswhere two workers run six files each committed from a single-file run. All four fail on the current release (b 1,Snapshot name: "b 1",same name 2, and 4 of 6 files failing with"t 1") and pass with this change.test/js/bun/test/snapshot-tests/(only the pre-existing colour-dependenterror snapshotsfailure, which test: make the error snapshots test pass with colors disabled #38833 addresses),test/cli/test/rerun-each.test.ts(the Snapshots are broken with --rerun-each #23705--rerun-eachand retry/repeat numbering guards, which now exercise the fresh-BunTest-per-pass path and the newreset_sequence),test/cli/test/retry-flag.test.ts,test/js/bun/test/test-retry-repeats-basic.test.ts,test/js/bun/test/ci-restrictions.test.ts,test/cli/test/test-filter-lifecycle-snapshot.test.ts,test/js/bun/test/bun_test.test.ts,test/js/bun/test/test-test.test.ts, andtest/cli/test/parallel.test.ts.--bailexits without flushing the open.snap(with-uit leaves the existing file truncated to 0 bytes). That is the lazy flush of the.snapcontents, independent of the counters, and is being tracked separately alongside bun test: flush .snap file before afterAll runs #36679 and bun test: detect obsolete snapshots and fix -u "added" mislabel #34042.Background
.snapfile,exports[`<describe names> <test name> <N>`].Ncounts the snapshot matchers the test has run so far, and inline snapshots consume a number too (Jest behaviour, which bun already matched within a single file), which is why the example above isb 2.Snapshots(snapshot.rs) is one object for the whole process, owned by theTestRunner. It holds the summary counters (total/added/passed/failed) and the currently open.snapfile (_current_file, its parsed entries invalues, the bytes to write back infile_buf). The.snapis opened lazily by the first file snapshot in a test file and is flushed and swapped when a file snapshot from a different test file arrives, or at the end of the run.BunTest(bun_test.rs) is the per-test-file object:BunTestRoot::enter_filecreates one when a file starts running andexit_filedrops it when the file is done. Eachexpect()holds a weak reference to theBunTestit was created under, which is howadd_countand the.snapselection inget_or_putfind the file a snapshot belongs to.TestCommand::run(test_command.rs) runs one test file (looping for--rerun-each, callingenter_fileon every pass); the serial runner calls it per file, and each--parallelworker process calls it for every file it is handed.First revision of this PR
The first push kept the map on
Snapshotsand cleared it at the top of every file pass inTestCommand::run(deleting the clear inwrite_snapshot_file). That fixed the same cases, but it was the second time the reset point of this map had to be moved by hand (#29375 added the first two for #23705), so the current revision moves the map onto the per-file object instead, which removes the reset calls rather than relocating them.