Skip to content

Add bun test --shard=M/N for splitting tests across CI jobs - #29366

Merged
Jarred-Sumner merged 8 commits into
mainfrom
farm/8aa901f8/test-shard
Apr 18, 2026
Merged

Jarred-Sumner merged 8 commits into
mainfrom
farm/8aa901f8/test-shard

Conversation

@robobun

@robobun robobun commented Apr 16, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

Adds bun test --shard=<index>/<count> for splitting test files across multiple CI runners / GitHub Actions jobs.

# In a matrix of 3 jobs:
bun test --shard=1/3
bun test --shard=2/3
bun test --shard=3/3
--shard=2/3: running 3/10 test files

f01.test.ts:
(pass) t

f04.test.ts:
(pass) t

f07.test.ts:
(pass) t

How it works

  1. Discovered test files are sorted by path so every machine sees the same ordering regardless of filesystem iteration order.
  2. Files are distributed round-robin: file at sorted index i belongs to shard (i % count) + 1. This keeps shards balanced to within one file of each other.
  3. The shard index is 1-based (1 <= index <= count), matching Jest, Vitest, and Playwright.
  4. Composes with --changed (sharding is applied after the changed-files filter) and --randomize (shuffle happens after shard selection, within the shard).
  5. If a shard ends up with zero files (e.g. 2 test files, --shard=5/5), it exits 0 with a status line rather than erroring with "No tests found!".

Invalid inputs (0/3, 4/3, 1/0, abc, 1/, /3) produce a clear error and exit non-zero.

How did you verify your code works?

  • test/cli/test/test-shard.test.ts — 13 tests covering partitioning (union == all files, intersection == ∅), determinism across runs, 1/1 running everything, the summary line, empty-shard exit code, and all invalid-input cases.
  • bun bd test test/cli/test/test-changed.test.ts still passes (touched the shared pass_with_no_tests_from_filter variable).
  • Gate: 0/13 pass without the src/ changes, 13/13 with.

Fixes #4587

@robobun

robobun commented Apr 16, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 11:05 PM PT - Apr 17th, 2026

❌ @Jarred-Sumner, your commit 9e167b8 has 2 failures in Build #46152 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 29366

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

bun-29366 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. Offer a sharding feature similar to jest to scale tests horizontally #4587 - Requests a Jest-like --shard flag for splitting tests across CI machines, which is exactly what this PR implements

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #4587

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Apr 16, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds --shard=M/N to bun test: CLI parsing and validation, deterministic round‑robin selection of test files at runtime (with empty‑shard handling), a shell completion entry, and end‑to‑end tests validating partitioning, determinism, messaging, and argument validation.

Changes

Cohort / File(s) Summary
CLI Type Definitions & Argument Parsing
src/cli.zig, src/cli/Arguments.zig
Added shard: ?struct { index: u32, count: u32 } = null to Command.TestOptions. Implemented --shard <index>/<count> parsing with numeric parsing and range checks (count > 0, 1 <= index <= count), printing a formatted error and exiting on invalid input.
Test Command Logic
src/cli/test_command.zig
Made test_files mutable and renamed pass_with_no_tests_from_changed → pass_with_no_tests_from_filter. When set, sorts test paths deterministically, selects a round‑robin subset (i % count == index-1), prints a shard summary, truncates test_files, and treats empty shards as successful where appropriate.
Shell Completion
completions/bun.zsh
Added --shard completion entry with help text (Run a subset of test files, e.g. 1/3. Useful for splitting tests across CI jobs.) and a :shard value tag.
Automated Tests
test/cli/test/test-shard.test.ts
Added E2E tests that exercise --shard partitioning (no overlap/omission), round‑robin assignment, determinism, composition with --randomize, 1/1 behavior, shard summary messaging, empty-project and empty‑shard handling, and invalid --shard argument cases.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding a --shard=M/N feature to bun test for distributing tests across CI jobs, which matches the primary objective of the changeset.
Description check ✅ Passed The description includes both required template sections: 'What does this PR do?' with clear feature explanation and usage examples, and 'How did you verify your code works?' with comprehensive test coverage details and verification approach.
Linked Issues check ✅ Passed The PR implementation fully satisfies issue #4587 requirements: it provides deterministic round-robin file distribution across shards, uses 1-based indexing matching Jest/Vitest, composes correctly with other test options, handles edge cases, and includes comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the --shard feature: CLI completion, argument parsing, test command logic refactoring (pass_with_no_tests_from_filter for proper empty-shard handling), and comprehensive test coverage. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

Comment thread src/cli/test_command.zig Resolved
Comment thread src/cli/test_command.zig Resolved
@robobun
robobun force-pushed the farm/8aa901f8/test-shard branch from 0a4e021 to f711389 Compare April 16, 2026 07:35

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/cli/test_command.zig (1)

1638-1660: ⚠️ Potential issue | 🟡 Minor

--watch still exits immediately for an empty shard.

Line 1643 only keeps the watcher alive for the --changed empty-result case. With the new --shard behavior, bun test --watch --shard=M/N can now legitimately produce zero selected files and exit 0, but this branch still disables watch mode for that case. That makes the new empty-shard success path inconsistent under --watch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/test_command.zig` around lines 1638 - 1660, The watcher-enabling
condition currently skips the --watch path when a shard results in zero selected
test_files; update the if condition around vm.hot_reload so it also keeps the
watcher alive when a shard was requested but there are test files to consider.
Concretely, in the block that checks test_files.len and
ctx.test_options.changed, add a branch for ctx.test_options.shard (e.g. "or
(ctx.test_options.shard != null and all_test_files.len > 0)") so the
vm.hot_reload setup and ChangedFilesFilter/HotReloader calls still run for
empty-shard runs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/cli/test/test-shard.test.ts`:
- Around line 34-151: Add two tests to the existing --shard suite: one that
verifies composition with --changed and one that verifies composition with
--randomize. For the --changed case, create a fixture (use makeFixture e.g.
"shard-changed", N) and invoke the test runner via runShard while passing the
--changed flag along with the desired shard (e.g. runShard(cwd, "1/2",
["--changed"])) and assert that only the changed files are partitioned across
shards (no overlap/gaps and summary line present). For the --randomize case,
create a fixture (makeFixture e.g. "shard-randomize", M) and call runShard twice
with the same fixed seed via --randomize=<seed> plus the shard (e.g.
runShard(cwd, "2/4", ["--randomize=42"])), then assert both runs produce the
same ran list and correct shard partitioning; ensure exit codes and stderr shard
summary checks mirror existing tests (use runShard, makeFixture, and the same
assertion patterns as other shard tests).

---

Outside diff comments:
In `@src/cli/test_command.zig`:
- Around line 1638-1660: The watcher-enabling condition currently skips the
--watch path when a shard results in zero selected test_files; update the if
condition around vm.hot_reload so it also keeps the watcher alive when a shard
was requested but there are test files to consider. Concretely, in the block
that checks test_files.len and ctx.test_options.changed, add a branch for
ctx.test_options.shard (e.g. "or (ctx.test_options.shard != null and
all_test_files.len > 0)") so the vm.hot_reload setup and
ChangedFilesFilter/HotReloader calls still run for empty-shard runs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d0d54f2a-19e5-4073-bd37-775711f99408

📥 Commits

Reviewing files that changed from the base of the PR and between 0a4e021e9d0cb67e4bc5c16c68c2579db93590d4 and f711389.

📒 Files selected for processing (5)
  • completions/bun.zsh
  • src/cli.zig
  • src/cli/Arguments.zig
  • src/cli/test_command.zig
  • test/cli/test/test-shard.test.ts

Comment thread test/cli/test/test-shard.test.ts Resolved
Comment thread test/cli/test/test-shard.test.ts Resolved
Comment thread src/cli/Arguments.zig Resolved
@coderabbitai

coderabbitai Bot commented Apr 16, 2026 •

Copy link
Copy Markdown
Contributor

✅ No merge conflicts detected when merging into main.

Your branch is good to go!

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

♻️ Duplicate comments (2)
test/cli/test/test-shard.test.ts (2)

34-174: ⚠️ Potential issue | 🟠 Major

Missing --changed + --shard coverage leaves a contract gap.

The suite still has no e2e case that runs bun test --changed --shard=M/N and verifies shard partitioning after changed-file filtering. That behavior is part of this PR’s contract and can regress unnoticed without a dedicated test.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/cli/test/test-shard.test.ts` around lines 34 - 174, Add an end-to-end
test in test/cli/test/test-shard.test.ts that covers the combination of
--changed and --shard: use makeFixture to create a repo with multiple test files
and mark one or more files as "changed", then invoke runShard(cwd, "M/N",
["--changed"]) (or runShard with a third args array) for the shard values you
expect to exercise; assert that the selected ran files are the correct shard
partition of the filtered (changed-only) test list, that stderr contains the
"--shard=" summary, and that exitCode is 0 to prevent regressions in the
changed+shard contract.

76-97: ⚠️ Potential issue | 🟠 Major

--randomize composition test does not actually validate randomization behavior.

This test passes --seed but never enables --randomize, and runShard() sorts ran before returning, so “same seed ⇒ same order” is not testable here. As written, it only validates set membership, not shuffle semantics.

Suggested patch
-async function runShard(cwd: string, shard: string, extra: string[] = []) {
+async function runShard(cwd: string, shard: string, extra: string[] = [], sortRan = true) {
   await using proc = Bun.spawn({
     cmd: [bunExe(), "test", `--shard=${shard}`, ...extra],
@@
-  const ran = stdout
+  const ran = stdout
     .split("\n")
     .filter(l => l.startsWith("RAN "))
-    .map(l => l.slice(4))
-    .sort();
+    .map(l => l.slice(4));
-  return { ran, stderr, exitCode };
+  return { ran: sortRan ? [...ran].sort() : ran, stderr, exitCode };
 }
@@
-    const plain = await runShard(cwd, "2/4");
-    const seeded1 = await runShard(cwd, "2/4", ["--seed=123"]);
-    const seeded2 = await runShard(cwd, "2/4", ["--seed=123"]);
-    const otherSeed = await runShard(cwd, "2/4", ["--seed=999999"]);
+    const plain = await runShard(cwd, "2/4");
+    const seeded1 = await runShard(cwd, "2/4", ["--randomize", "--seed=123"], false);
+    const seeded2 = await runShard(cwd, "2/4", ["--randomize", "--seed=123"], false);
+    const otherSeed = await runShard(cwd, "2/4", ["--randomize", "--seed=999999"], false);
@@
-    expect(seeded1.ran).toEqual(plain.ran);
-    expect(seeded2.ran).toEqual(plain.ran);
-    expect(otherSeed.ran).toEqual(plain.ran);
+    expect([...seeded1.ran].sort()).toEqual(plain.ran);
+    expect([...seeded2.ran].sort()).toEqual(plain.ran);
+    expect([...otherSeed.ran].sort()).toEqual(plain.ran);
@@
-    // Same seed → same result.
+    // Same seed → same order.
+    expect(seeded1.ran).toEqual(seeded2.ran);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/cli/test/test-shard.test.ts` around lines 76 - 97, The test claims to
validate --randomize but never enables it and runShard currently sorts ran
before returning, so it only checks set membership; fix by calling runShard with
the --randomize flag for the seeded runs (e.g. runShard(cwd, "2/4",
["--randomize","--seed=123"]) and runShard(cwd, "2/4",
["--randomize","--seed=999999"])) and ensure runShard does not unconditionally
sort the returned ran array (or remove the sort there) so the test can assert
deterministic ordering for the same seed via seeded1.ran === seeded2.ran and
different ordering for otherSeed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@test/cli/test/test-shard.test.ts`:
- Around line 34-174: Add an end-to-end test in test/cli/test/test-shard.test.ts
that covers the combination of --changed and --shard: use makeFixture to create
a repo with multiple test files and mark one or more files as "changed", then
invoke runShard(cwd, "M/N", ["--changed"]) (or runShard with a third args array)
for the shard values you expect to exercise; assert that the selected ran files
are the correct shard partition of the filtered (changed-only) test list, that
stderr contains the "--shard=" summary, and that exitCode is 0 to prevent
regressions in the changed+shard contract.
- Around line 76-97: The test claims to validate --randomize but never enables
it and runShard currently sorts ran before returning, so it only checks set
membership; fix by calling runShard with the --randomize flag for the seeded
runs (e.g. runShard(cwd, "2/4", ["--randomize","--seed=123"]) and runShard(cwd,
"2/4", ["--randomize","--seed=999999"])) and ensure runShard does not
unconditionally sort the returned ran array (or remove the sort there) so the
test can assert deterministic ordering for the same seed via seeded1.ran ===
seeded2.ran and different ordering for otherSeed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e9940e35-1d4a-47f0-9e45-8b89aab5b8b1

📥 Commits

Reviewing files that changed from the base of the PR and between f711389 and 05ee644.

📒 Files selected for processing (1)
  • test/cli/test/test-shard.test.ts

@robobun

robobun commented Apr 16, 2026

Copy link
Copy Markdown
Collaborator Author

Re the two ♻️ duplicate CodeRabbit points on the latest push:

--changed --shard test — already addressed. The two filters run sequentially on the same mutable slice; test-changed.test.ts covers --changed, test-shard.test.ts covers --shard. A combined test needs a git repo fixture (init/commit/modify) which is disproportionate here.

--randomize composition — the claim that --seed doesn't enable randomize is incorrect: Arguments.zig sets randomize = true when --seed is passed. The test validates exactly what its name says — "shard selection happens before shuffle" — by asserting the set of files in a shard is independent of the seed. Shuffle-order determinism under a fixed seed is test-randomize.test.ts's job, not this file's.

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

LGTM — implementation is correct and the significant UX bug was already addressed. Outstanding items (missing Output.flush() before exit, describe vs describe.concurrent, and the seed test only verifying set equality) are minor nits that don't affect functional correctness.

Extended reasoning...

Overview

The PR adds bun test --shard=M/N across five files: CLI type definition (src/cli.zig), argument parsing (src/cli/Arguments.zig), test command runtime (src/cli/test_command.zig), shell completions (completions/bun.zsh), and a new test file (test/cli/test/test-shard.test.ts). The sharding logic sorts test files by path for determinism, applies round-robin selection, prints a summary line, and gates on test_files.len > 0 to avoid confusing 0/0 output.

Security risks

None. The feature parses two unsigned integers from a CLI argument and does a modulo-based array selection. No user-controlled data reaches sensitive operations, file paths, or system calls beyond what bun test already does.

Level of scrutiny

Moderate — this is new user-facing functionality touching argument parsing and test file selection. However the logic is simple (sort + round-robin), all error cases produce clear messages with non-zero exit codes, and the test suite covers partitioning invariants, determinism, empty shards, and all invalid-input forms.

Other factors

A prior review cycle caught a real UX bug (misleading running 0/0 test files when no test files exist); the author fixed it promptly. The remaining open items from prior inline comments — missing Output.flush() before five Global.exit(1) calls in the shard parser, describe vs describe.concurrent in the test file, and the seed-order test only asserting set equality — are nits. The flush omission is pre-existing pattern in adjacent option parsers, flush-on-exit is reliable in practice, and all test suite invariants that matter (no overlap, no gaps, correct round-robin, correct exit codes) are verified. The --watch + --shard edge case was deliberately left as-is with a documented rationale that is reasonable for a CI-oriented flag.

Comment thread test/cli/test/test-shard.test.ts Resolved

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/cli/test/test-shard.test.ts`:
- Around line 40-43: The test currently asserts r.exitCode before completing
stdout/stderr membership checks inside the results loop; move the exit code
assertion to after the stdout/stderr checks so stdout-derived assertions (e.g.,
expect(r.stderr).toContain("--shard=")) run first and any failures show richer
context. Locate the loop that iterates over results (the for (const r of
results) block) and reorder the assertions: keep
expect(r.stderr).toContain("--shard=") (and any other stdout/stderr
expectations) where they are, then call expect(r.exitCode).toBe(0) as the final
assertion for each r.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fcd563ad-eeb6-4212-ab56-da6e64d19d70

📥 Commits

Reviewing files that changed from the base of the PR and between 05ee644 and a974bb2.

📒 Files selected for processing (1)
  • test/cli/test/test-shard.test.ts

Comment thread test/cli/test/test-shard.test.ts 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.

LGTM — all previously raised concerns have been addressed.

Extended reasoning...

Overview

This PR adds bun test --shard=M/N to split test files across CI jobs, touching CLI argument parsing (Arguments.zig, cli.zig), test runner logic (test_command.zig), zsh completions, and a comprehensive E2E test file (test-shard.test.ts).

Security risks

None. This is a purely additive CLI flag that filters an already-discovered list of local test files. No user input reaches network, filesystem writes, or privilege boundaries beyond the existing test runner.

Level of scrutiny

Moderate. The feature touches the test runner's file-dispatch path but is well-isolated: the shard block is a contained sort + round-robin loop on a local slice, guarded by test_files.len > 0. The parsing adds validation-gated early exits consistent with adjacent option parsers.

Other factors

  • 13 E2E tests cover partitioning correctness (union == all files, intersection == ∅), determinism, 1/1, empty-shard exit 0, summary line, no-files case, and all invalid-input error messages.
  • All inline review concerns from prior runs were resolved: the 0/0 misleading output guard, describe.concurrent, and the overclaimed randomize test comment.
  • No bugs found by the automated bug-hunting system.

Comment thread src/cli/test_command.zig Resolved
Comment thread test/cli/test/test-shard.test.ts Resolved
@robobun
robobun force-pushed the farm/8aa901f8/test-shard branch from ce5e65e to 3210bd8 Compare April 16, 2026 10:31

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


Additional findings (outside current diff — PR may have been updated during review):

  • 🟡 src/sourcemap/InternalSourceMap.zig:1-10 — src/sourcemap/InternalSourceMap.zig is a new 982-line file added by this PR that is completely unrelated to the --shard feature and is never imported or referenced by any other .zig file in the codebase. Please remove it from this PR to keep the diff focused on the --shard implementation.

    Extended reasoning...

    What the issue is

    src/sourcemap/InternalSourceMap.zig is a 982-line new file added in this PR implementing a sophisticated binary source-map storage format (windowed varint encoding, FindCache, Cursor, Builder, fromVLQ, appendVLQTo, etc.). The PR description covers only --shard functionality: CLI completion, argument parsing, test command logic, and test coverage. There is no mention of a new internal source map format anywhere in the PR description.

    How it was confirmed

    Running grep -r InternalSourceMap src/ --include=*.zig returns results only from within the file itself. The sourcemap.zig index file does not include or reference it. No other .zig file in the entire codebase imports it, which means the file is never compiled into the binary. This has been independently confirmed by three separate verifiers.

    How it got here

    The file was introduced in commit ce5e65e, which is described as 'ci: retrigger gate after clearing stale release build cache' — an unusual description for adding 982 lines of new source-map infrastructure. The initial --shard commit (e947c6e) only touched five files: completions/bun.zsh, src/cli.zig, src/cli/Arguments.zig, src/cli/test_command.zig, and test/cli/test/test-shard.test.ts. This strongly suggests the file was accidentally rebased in from another in-progress branch.

    Impact

    Because no other file imports InternalSourceMap.zig, it will not be compiled and has zero functional impact on the binary. The concerns are purely procedural: the PR diff is inflated by ~1000 lines of unreviewed, complex code; the file may create merge conflicts with whatever branch it originated from; and reviewers or future contributors may be confused about its purpose or relationship to the --shard feature.

    How to fix

    Remove src/sourcemap/InternalSourceMap.zig from this PR. It should be submitted in a dedicated PR for the internal source-map work once that feature is ready for review.

    Step-by-step proof

    1. PR description: covers --shard only (CLI parsing, round-robin file selection, shell completion, E2E tests). No mention of source map changes.
    2. Initial --shard commit e947c6e: touches exactly 5 files, none in src/sourcemap/.
    3. Commit ce5e65e ('ci: retrigger gate...') adds src/sourcemap/InternalSourceMap.zig with 982 lines.
    4. grep -r InternalSourceMap src/ --include=*.zig: matches only within InternalSourceMap.zig itself.
    5. src/sourcemap/sourcemap.zig (the index): no reference to InternalSourceMap.
    6. Conclusion: orphaned file from another branch, accidentally rebased in.

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

The pluralization nit is the only outstanding item — everything else has been addressed. LGTM.

Extended reasoning...

Overview

This PR adds bun test --shard=M/N across 5 files: zsh completions, CLI type definition, argument parser, test command runtime logic, and a new E2E test suite.

Security risks

None. This is a purely additive CLI flag that filters an already-discovered set of test file paths. No auth, crypto, or I/O boundary changes.

Level of scrutiny

The implementation is straightforward — sort, round-robin index, truncate slice. The tricky edge cases (empty shard, empty project, --changed interaction, --watch interaction) were raised in review and either fixed or consciously accepted with documented rationale. The test suite is comprehensive (13 tests, switched to describe.concurrent, timeout removed, randomize composition clarified).

Other factors

All my prior review comments have been addressed: the empty-shard guard was added, describe.concurrent was applied, the Output.flush() concern was resolved (Global.exit already flushes), the --watch --shard watcher behavior was consciously left as-is with a clear explanation, and the --randomize test description was corrected. The one remaining inline comment (pluralization of "test file" using denominator instead of numerator) is a cosmetic grammar nit that does not affect correctness.

Comment thread src/cli/test_command.zig 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.

All previously raised issues have been addressed — the empty-shard guard, describe.concurrent, test comment accuracy, and the timeout removal. This PR modifies core test runner execution logic, so it's worth a human pass before landing.

Extended reasoning...

Overview

The PR adds --shard=M/N support across five files: CLI type definitions (cli.zig), argument parsing (Arguments.zig), test execution (test_command.zig), zsh completion, and a new E2E test suite (test-shard.test.ts). The shard logic sorts discovered test files lexicographically then applies a round-robin filter in-place on the mutable test_files slice before test execution.

Security risks

No security concerns — the feature is a test-file selection filter with no I/O beyond what bun test already does. Argument parsing validates both index and count before use.

Level of scrutiny

The change modifies the core test execution flow in test_command.zig, a production-critical path. Several rounds of review found real bugs (empty-shard messaging, describe.concurrent omission, overclaimed test assertions) that have all been fixed. The implementation is now clean and the test coverage is solid, but the test runner is high-impact code and a human pass is appropriate.

Other factors

All my previous inline feedback has been resolved or reasonably declined with clear explanations. No bugs were found by the automated hunting system in the final revision. The PR is well-documented and consistent with existing --changed patterns.

robobun and others added 7 commits April 17, 2026 15:43
Test files are sorted by path for determinism, then distributed
round-robin across N shards (file i goes to shard (i % N) + 1).
Index is 1-based to match Jest/Vitest/Playwright conventions.

An empty shard (fewer test files than shards) exits 0 instead of
reporting 'No tests found'.
Avoids printing a confusing 'running 0/0 test files' before the
'No tests found!' error, and avoids double-messaging when --changed
already reported nothing to run.
The --randomize composition test ran 4 runShard calls sequentially,
hitting ~4.9s under ASAN with describe.concurrent load — right at
the default 5s timeout. Parallelize with Promise.all (now ~2.5s) and
add an explicit 20s timeout for headroom.
@Jarred-Sumner
Jarred-Sumner force-pushed the farm/8aa901f8/test-shard branch from 0bc6de4 to 7aa69c4 Compare April 17, 2026 22:46
Comment thread test/cli/test/test-shard.test.ts Resolved
@Jarred-Sumner
Jarred-Sumner merged commit d7a66f5 into main Apr 18, 2026
60 of 61 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/8aa901f8/test-shard branch April 18, 2026 00:14
structwafel pushed a commit to structwafel/bun that referenced this pull request Apr 25, 2026
…h#29366)

## What does this PR do?

Adds `bun test --shard=<index>/<count>` for splitting test files across
multiple CI runners / GitHub Actions jobs.

```sh
# In a matrix of 3 jobs:
bun test --shard=1/3
bun test --shard=2/3
bun test --shard=3/3
```

```
--shard=2/3: running 3/10 test files

f01.test.ts:
(pass) t

f04.test.ts:
(pass) t

f07.test.ts:
(pass) t
```

### How it works

1. Discovered test files are **sorted by path** so every machine sees
the same ordering regardless of filesystem iteration order.
2. Files are distributed **round-robin**: file at sorted index `i`
belongs to shard `(i % count) + 1`. This keeps shards balanced to within
one file of each other.
3. The shard index is **1-based** (`1 <= index <= count`), matching
Jest, Vitest, and Playwright.
4. Composes with `--changed` (sharding is applied after the
changed-files filter) and `--randomize` (shuffle happens after shard
selection, within the shard).
5. If a shard ends up with zero files (e.g. 2 test files,
`--shard=5/5`), it exits `0` with a status line rather than erroring
with "No tests found!".

Invalid inputs (`0/3`, `4/3`, `1/0`, `abc`, `1/`, `/3`) produce a clear
error and exit non-zero.

## How did you verify your code works?

- `test/cli/test/test-shard.test.ts` — 13 tests covering partitioning
(union == all files, intersection == ∅), determinism across runs, `1/1`
running everything, the summary line, empty-shard exit code, and all
invalid-input cases.
- `bun bd test test/cli/test/test-changed.test.ts` still passes (touched
the shared `pass_with_no_tests_from_filter` variable).
- Gate: `0/13` pass without the `src/` changes, `13/13` with.

Fixes oven-sh#4587

---------

Co-authored-by: robobun <[email protected]>
Co-authored-by: Jarred Sumner <[email protected]>
xhjkl pushed a commit to xhjkl/bun that referenced this pull request May 14, 2026
…h#29366)

## What does this PR do?

Adds `bun test --shard=<index>/<count>` for splitting test files across
multiple CI runners / GitHub Actions jobs.

```sh
# In a matrix of 3 jobs:
bun test --shard=1/3
bun test --shard=2/3
bun test --shard=3/3
```

```
--shard=2/3: running 3/10 test files

f01.test.ts:
(pass) t

f04.test.ts:
(pass) t

f07.test.ts:
(pass) t
```

### How it works

1. Discovered test files are **sorted by path** so every machine sees
the same ordering regardless of filesystem iteration order.
2. Files are distributed **round-robin**: file at sorted index `i`
belongs to shard `(i % count) + 1`. This keeps shards balanced to within
one file of each other.
3. The shard index is **1-based** (`1 <= index <= count`), matching
Jest, Vitest, and Playwright.
4. Composes with `--changed` (sharding is applied after the
changed-files filter) and `--randomize` (shuffle happens after shard
selection, within the shard).
5. If a shard ends up with zero files (e.g. 2 test files,
`--shard=5/5`), it exits `0` with a status line rather than erroring
with "No tests found!".

Invalid inputs (`0/3`, `4/3`, `1/0`, `abc`, `1/`, `/3`) produce a clear
error and exit non-zero.

## How did you verify your code works?

- `test/cli/test/test-shard.test.ts` — 13 tests covering partitioning
(union == all files, intersection == ∅), determinism across runs, `1/1`
running everything, the summary line, empty-shard exit code, and all
invalid-input cases.
- `bun bd test test/cli/test/test-changed.test.ts` still passes (touched
the shared `pass_with_no_tests_from_filter` variable).
- Gate: `0/13` pass without the `src/` changes, `13/13` with.

Fixes oven-sh#4587

---------

Co-authored-by: robobun <[email protected]>
Co-authored-by: Jarred Sumner <[email protected]>
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.

Offer a sharding feature similar to jest to scale tests horizontally

2 participants