Skip to content

bun test: run the files of each scan in path order and shuffle from that order - #45790

Open
robobun wants to merge 2 commits into
mainfrom
robobun/dce2e1d5/test-file-path-order
Open

robobun wants to merge 2 commits into
mainfrom
robobun/dce2e1d5/test-file-path-order

Conversation

@robobun

@robobun robobun commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #18987

Problem

  • bun test runs test files in each directory's listing order, which differs per file system and checkout. --seed=N shuffles that list, so a seed does not replay elsewhere. The docs promise: "The same seed always produces the same test execution order."
  • Scanner::next (src/runtime/cli/test/Scanner.rs) appends files as listed. Only --shard and --parallel sort.

Fix

  • Commit 1: the shuffle starts from path order. scanner::sort_test_files sorts for it, --shard and --parallel.
  • Commit 2: Scanner::scan sorts the files it appended. Path arguments keep their order.
  • Commit 2 changes the default order and needs a maintainer's decision. Commit 1 stands alone.
  • Verified: test/cli/test/test-file-order.test.ts (main fails 11 of 11) and test-changed.test.ts. Also test-shard, isolation, 12782, 26851. Self-reviewed: 27 concerns raised, 24 addressed.

Background

Downsides

Notes

The decision. Commit 2 makes a run with no flag use flat path order. Today that run uses the cwd's files first (by lowercased name), then breadth-first, each directory in listing order. 7a5af8b sorted the whole list this way "so run order (and --bail, JUnit output) is deterministic". 36fabf3 dropped it 8 hours later "to preserve explicit file order". Commit 2 sorts per scan() call, so it keeps that: test/regression/issue/12782.test.ts passes unchanged.

If flat path order is not wanted, drop commit 2. Then only seeded runs change, and they run what --shard=1/1 --seed=N already runs on main. #18987 and #36636 stay open in that case.

Why the run list and not the resolver (the #36636 review).

  • After this PR bun test needs no sorted directory listing. Every push to the run list happens during Scanner::walk, which only Scanner::scan calls.
  • A walk fed from the resolver's entry map loses files: src/resolver/fs.rs:565 keys the map on the lowercased name, so it holds one of two names that differ only by case.
  • A sort per directory cannot make --seed with several path arguments independent of the argument order.
  • Other producers sort their finished list the same way: src/router/lib.rs:492, src/runtime/cli/multi_run.rs:977, src/lint/driver/fmt/files.rs:830 (per argument, keeps argument order). bun check sorts per directory (src/sema/driver/host.rs:219).
  • node --test sorts its file list (createTestFileList ends with ArrayPrototypeSort(results)). Deno sorts before it shards and before it shuffles (cli/tools/test/mod.rs: specifiers.sort(); specifiers.shuffle(&mut rng);).

Relation to open PRs.

Snapshots. Snapshot numbers depend on the files that ran before in the same process (#38874). A .snap written under the old file order can miss under the new one. Example: z.test.ts takes one file snapshot, and sub/b.test.ts takes an inline snapshot and then a file snapshot in test b. 1.4.3 runs z.test.ts first and writes exports[`b 1`]. This branch runs sub/b.test.ts first and looks up b 2. With CI=true that run fails: Snapshot creation is disabled in CI environments unless --update-snapshots is used. #38874 numbers snapshots per file, which removes the dependency. Merge it before this PR or in the same release.

What moves, once.

  • Default order in this repo (2,345 test files under test/): 2,345 positions change. Of 1,602,326 pairs of files at different depths, 755,205 (47%) reverse. The model for today is breadth-first with each directory in name order.
  • For one seed, the file order differs from 1.4.x in every run whose discovery order was not path order. That includes a run with no argument and a seed in bunfig.toml. The order of tests inside a file does not change.
  • --shard and --parallel membership does not move on any platform. The comparator is the one they had.

Not fixed.

Measurements (linux x64, release builds of main 017b243 and this PR, same build directory).

  • Index sorts per bun test process (plain / --seed / --shard / --parallel / both), 12 files: from the cwd main 1/1/2/2/3, PR 1/1/1/1/1. With a directory argument main 0/0/1/1/2, PR 1/1/1/1/1. Counted with a gdb breakpoint on index_sort::sort_stable.
  • Allocations in discovery over 2,345 files: plain +2 blocks (18,760 B, freed after the sort), --shard 0, one file argument 0. Counted with gdb inside the sort. valgrind is not installed here.
  • Path comparisons over 2,345 files: plain +28,358, --shard +2,347 (the check that the list is in order), one file argument 0.
  • Instructions: not measured. perf and valgrind are not installed here. Interleaved CPU time, 60 rounds, median main to PR: plain 68.3 to 68.0 ms, --shard=5000/5000 29.1 to 28.9 ms, one file argument 48.6 to 49.0 ms. Two runs of main differ by 1.2 ms.
  • Release binary: .text 81,368,037 to 81,367,525 B (-512 B), .rodata equal (size -A). bloaty is not installed here.
  • openat / getdents64 / close in discovery (bun test --shard=5000/5000): 1,489 / 2,992 / 1,488 on both. Counted with gdb catch syscall. strace is not installed here.

Tests. Stock 1.4.3 and a release build of main fail 11 of 11 in test-file-order.test.ts on ext4. The debug build of this branch passes 11. Rows that also pass on main guard the sorts that moved into the helper: --shard=1/1 and --shard=1/2 --timings with directory arguments out of order, and --parallel=2 ./z.test.ts ./apple.test.ts (read from the JUnit report). The tests are in a new file because they cover one property across a plain run, --seed, --shard, --parallel and bunfig.toml with one fixture helper. Also run: test-changed, test-shard, isolation, pass-with-no-tests, path-ignore-patterns, test-randomize, coverage, junit, mock-module, 12782, 26851. parallel.test.ts passes on the release build except --parallel --dots prints one status character per test. Its check fails in 18 of 30 runs on a release build of main and in 22 of 30 on this branch, so it does not come from this change.

Self-review. Not taken: folding this into one PR with #42921 and #43361 (this sort runs after the walk and holds under each of them), a sort key relative to the project root (the key stays the one --shard has), and a note about bun run --filter (a probe here did not show an order difference).


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

@robobun

robobun commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 7:20 AM PT - Oct 19th, 2026

✅ @robobun, your commit 3e2996e5d393086af7f4a3f7f44320d9dfc574cd passed in Build #128841! 🎉


🧪   To try this PR locally:

bunx bun-pr 45790

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

bun-45790 --bun

@robobun

robobun commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

Reproduced on tmpfs with two copies of one tree of 12 test files, created in opposite orders. bun 1.4.3 and main run bun test --seed=1 as 02 05 09 12 10 07 08 01 06 11 04 03 in one copy and as 11 08 04 01 03 06 05 12 07 02 09 10 in the other. With no seed they run 12..01 and 01..12. This branch runs 11 08 04 01 03 06 05 12 07 02 09 10 in both copies, and 01..12 with no seed.

On ext4 the two copies list in the same hash order, so the tests assert the literal order. test/cli/test/test-file-order.test.ts fails 11 of 11 on main and passes on this branch.

CI is green on 3e2996e (181 of 181 jobs).

One decision is open for a maintainer: commit 2 changes the default file order of a run with no flag. Commit 1 (the seeded shuffle) stands alone without it. If commit 2 stays, merge #38874 first or in the same release, because snapshot numbers depend on the file order until then.

PR: #45790

@coderabbitai

coderabbitai Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: oven-sh/bun/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: bef5d073-587f-413e-ae23-6a2429cf3706
📥 Commits

Reviewing files that changed from the base of the PR and between b211038 and 3e2996e.

📒 Files selected for processing: 3
  • docs/test/discovery.mdx
  • test/cli/test/test-changed.test.ts
  • test/cli/test/test-file-order.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.

Walkthrough

The change defines test-file ordering by absolute-path bytes and applies it to discovery, sharding, seeded execution, and non-randomized parallel execution. CLI tests cover ordering behavior, and documentation describes ordering and seed guarantees.

Changes

Test-file ordering

Layer / File(s) Summary
Define scanner file ordering
src/runtime/cli/test/Scanner.rs
Scanner traversal is separated from scan result handling. Newly discovered test files are sorted by absolute-path bytes.
Apply ordering to execution paths
src/runtime/cli/test_command.rs, src/runtime/cli/test/Timings.rs, src/runtime/cli/test/parallel/runner.rs
Sharding and non-randomized parallel execution use scanner ordering. Seeded randomization sorts files before applying the existing shuffle.
Verify and document ordering
test/cli/test/test-file-order.test.ts, test/cli/test/test-changed.test.ts, docs/test/discovery.mdx, docs/test/index.mdx
CLI tests cover path order, argument order, seeded order, and shard assignment. Documentation describes file ordering and seed behavior.

Suggested reviewers: jarred-sumner

Priority: ➖ Normal

Severity of issue fixed: Low

Merge Risk: ⚪ Minimal · up to 3e299

The documented file-order behavior has no identified merge-blocking issue. The unrun changed-files test still needs the normal CI check.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly summarizes the main change: test files use path order as the basis for execution and seeded shuffling.
Linked Issues check Passed Active linked issue #18987 requires sequentially named test files to run in filename order. Scanner::scan sorts files appended by each scan in absolute-path byte order. sort_test_files also establ…
Out of Scope Changes check Passed The changes stay within #18987. The scanner sort, shared ordering helper, command integrations, documentation, and regression tests all support deterministic test-file ordering or its required schedul…
Description check Passed The description clearly explains the problem, implementation, scope, limitations, performance impact, and verification. It does not use the exact template headings, but it provides the required inform…
Full details: Linked Issues check

Explanation

Active linked issue #18987 requires sequentially named test files to run in filename order. Scanner::scan sorts files appended by each scan in absolute-path byte order. sort_test_files also establishes this order before seeded randomization, sharding, and parallel scheduling. The new regression tests cover 01_seller, 02_product, 03_user, and 04_order, plus directory discovery and seeded runs.

Full details: Out of Scope Changes check

Explanation

The changes stay within #18987. The scanner sort, shared ordering helper, command integrations, documentation, and regression tests all support deterministic test-file ordering or its required scheduling paths. No unrelated behavior is demonstrated.

Full details: Description check

Explanation

The description clearly explains the problem, implementation, scope, limitations, performance impact, and verification. It does not use the exact template headings, but it provides the required information in equivalent sections.

  • 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

Comment @coderabbitai help to get the list of available commands.

@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.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline findings, I also checked two things: the comparator swap in parallel/runner.rs (bun_core::order to strings::order via sort_test_files) is behavior-preserving since both are plain byte order and the is_sorted_by fast path uses the same ordering, so --shard and --parallel membership does not move on a given platform; and dropping the lowercased-basename sort that regression/issue/26851 relied on is covered, because the whole appended slice is now byte-sorted and a_* still precedes b_* under --bail.

Extended reasoning...

The change splits Scanner::scan into a walk plus a per-argument byte-order sort, removes the cached-EntryMap lowercased-basename sort, centralizes the three existing sorts into sort_test_files, and sorts before the seeded shuffle; it touches no security-sensitive surface. Not approving because inline findings on multi-argument discovery (duplicate and missing files) and Windows separator ordering remain, the default file order for every user changes, and the PR itself says commit 2 needs a maintainer decision.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟣 src/runtime/cli/test/Scanner.rs — Users who name a file and a directory that contains it run that file twice, and the new docs now promise per-argument ordering without saying so. bun test ./a.test.ts ./ pushes a.test.ts at src/runtime/cli/test/Scanner.rs:158 from the ENOTDIR branch without setting entry.abs_path, so the cwd walk pushes it again via the File arm, and each scan() sorts its own slice at Scanner.rs:132, so the duplicate survives sorting, sharding and seeding. The PR's new directories and files cases in test/cli/test/test-file-order.test.ts:89-90 never overlap arguments. Fix: a path must appear once in the run list; dedupe by path bytes after the per-scan sort (adjacent duplicates are cheap to drop there) or mark the entry's abs_path in the ENOTDIR branch.

    Why this was flagged

    Trigger: bun test <file> <dir-containing-file>, through TestCommand::exec calling Scanner::scan once per argument. The ENOTDIR branch at src/runtime/cli/test/Scanner.rs:150-159 appends the interned path to test_files but does not touch the resolver's fs::Entry, so the 'already seen it!' guard in next() (Scanner.rs:~356, !entry.abs_path.is_empty()) does not fire when the directory walk reaches the same file and pushes it again. The base has the same two pushes, so the duplicate run is pre-existing. What the diff changes: sort_test_files runs per scan() slice, the seeded path at src/runtime/cli/test_command.rs:2352 sorts and shuffles the list including the duplicate, and the round-robin at test_command.rs:2251-2258 can place the two copies in different shards. The new doc at docs/test/discovery.mdx:93 states Bun 'runs the files of each argument in turn' with no mention of the double run, and the new test file adds ordering cases that never overlap arguments. Remedy: dedupe the run list by path bytes once after scanning, which also fixes the --shard split.

    Verification: pre-existing. Trigger: bun test ./a.test.ts ./. The ENOTDIR branch at src/runtime/cli/test/Scanner.rs:150-158 appends the interned path without touching any fs::Entry, so the only dedup guard (Scanner.rs:380-383) sees an empty abs_path and pushes the path again. The base commit has the same pushes and no dedup either, so merging this PR neither introduces nor widens the duplicate.

  • 🟣 src/runtime/cli/test/Scanner.rs — Users who run bun test ./test ./ get none of the cwd's own test files, although the new docs at docs/test/discovery.mdx:92 now promise that Bun runs the files of each argument in turn. The second scan reaches src/runtime/cli/test/Scanner.rs:173 with has_iterated already true from the first argument, and the cwd listing is served from the resolver cache, so no entry is visited and nothing is appended. Fix: make the per-argument promise hold by walking a cached listing per scan (reset has_iterated in scan() or dedupe by entry.abs_path), or drop the sentence from the docs until #42921 lands.

    Why this was flagged

    Trigger: two or more path arguments where a later one is a directory the resolver already listed (the cwd is always cached before the scanner runs, see the comment at Scanner.rs:177-180), e.g. bun test ./test ./ through the loop at src/runtime/cli/test_command.rs:2044-2046. The first scan sets has_iterated = true in next() at Scanner.rs:331. The second scan gets EntriesOption::Entries for the cwd and skips the whole block at Scanner.rs:173-188, so the cwd's test files never reach test_files; the sort at Scanner.rs:132 then sorts an empty slice. The base branch drops the same files, but the PR adds the sentence at docs/test/discovery.mdx:92 that each argument's files run in turn and the --shard/--seed docs say they start from all the files, which is now a false claim users will rely on. The dismissal relied on the PR naming #42921/#43361 as follow-ups; neither is in this checkout. Remedy: reset or scope has_iterated per scan() and dedupe via entry.abs_path, or remove the docs promise.

    Verification: Pre-existing, acknowledged in diff. Trigger: two or more path arguments where a later one is a directory the resolver already listed (e.g. bun test ./test ./). The block at Scanner.rs:173-188, gated by if !self.has_iterated, is skipped, so nothing from the cwd listing is appended to test_files. This guard is identical on the base commit, so merging does not make the base worse.

Comment thread test/cli/test/test-file-order.test.ts Resolved
Comment thread test/cli/test/test-file-order.test.ts Resolved
Comment thread src/runtime/cli/test/Scanner.rs Resolved
The seeded shuffle started from the list in the order the scanner
found the files: the order in which each directory lists its entries,
and the order of the path arguments. The listing order differs between
file systems and between two checkouts of one tree, so one seed gave
another file order on another machine.

The shuffle now sorts the list by path first, as --shard and
--parallel already do. The four places that need path order call one
helper, scanner::sort_test_files. It does nothing for a list that is
already in path order.
The scanner appended test files in the order each directory listed
them, so a run with no flag had another file order on another file
system or checkout. --bail then stopped after another set of files.

Scanner::scan now sorts the files it appended by path. A run keeps the
order of the path arguments, as before. The sort of the cached cwd
listing is gone: the sort of the found files replaces it.
@robobun
robobun force-pushed the robobun/dce2e1d5/test-file-path-order branch from b211038 to 3e2996e Compare October 19, 2026 11:02

@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.

Still open from earlier reviews (2):

  • Unresolved: 2 minor or pre-existing.

@robobun

robobun commented Oct 19, 2026

Copy link
Copy Markdown
Collaborator Author

On the two findings outside the diff (both exist on main, as the review says):

The helper nit is fixed in aa5d1b3. The branch is also rebased on main. The description has one new line under Downsides: a .snap whose numbers depend on the file order (#38874) can fail in CI when the default order moves. The Notes carry the repro.

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.

Bun test doesn't run test files in correct sequence

2 participants