Skip to content

test: adopt bun test --isolate --parallel for ~2.9x CI speedup#839

Merged
BYK merged 1 commit intomainfrom
byk/bun-test-parallel
Apr 24, 2026
Merged

test: adopt bun test --isolate --parallel for ~2.9x CI speedup#839
BYK merged 1 commit intomainfrom
byk/bun-test-parallel

Conversation

@BYK
Copy link
Copy Markdown
Member

@BYK BYK commented Apr 24, 2026

Summary

Bun 1.3.13 shipped --isolate (fresh global per file) and --parallel (worker process per CPU). Adopting both cuts local wall clock from ~152s to ~52s on a 4-core box and collapses the test:unit + test:isolated split that existed only as a workaround for mock.module() registry pollution (oven-sh/bun#258) — every file now gets a clean module graph.

Preload race fix

Worker processes inherit a shared TEST_TMP_DIR = /tmp/sentry-cli-test from test/constants.ts. Each worker runs the preload on startup, which includes rmSync(TEST_TMP_DIR, { recursive, force }) to clear stale state from crashes. Under --parallel, workers would wipe each other's DB files mid-run, surfacing as SQLITE_IOERR_DELETE_NOENT flakes.

Fix: namespace TEST_TMP_DIR by BUN_TEST_WORKER_ID — each worker gets /tmp/sentry-cli-test-w${id} and the wipe stays worker-local. Serial runs (no worker ID) behave exactly as before. Tests with fixed-name subdirs (upgrade-lock-test, etc.) are automatically unique per worker because the parent is worker-scoped.

Stability

Stress-tested with 5 consecutive full runs of the merged suite (test/lib + test/commands + test/types + test/isolated) under --isolate --parallel: 5920/5920 pass each time, ~52-55s wall clock. The subprocess-heavy test/lib/db/concurrent.test.ts also passes cleanly — SQLite's WAL + busy_timeout pragmas handle the contention.

CI changes

  • Drop the separate Isolated Tests step and Merge Coverage Reports step — everything runs through test:unit now
  • Single coverage/lcov.info file
  • Remove the now-unused script/merge-lcov.ts

Script changes

  • test: alias for test:unit (was test:unit && test:isolated)
  • test:unit: now runs the merged directory tree with --isolate --parallel
  • test:isolated: removed (folder still exists; files run under --isolate via test:unit)
  • test:changed: new dev-only script using bun test --isolate --changed

Deferred to follow-ups

  • Collapse test/isolated/ folder into test/lib/test/commands (churn-heavy)
  • Remove scopedFetchMock helper (test/lib/sentry-client.test.ts) — still correct under --isolate, just no longer strictly needed
  • Add test:shard for CI matrix splitting — current wall clock is fine

Verification

  • bun run test:unit — 5920 pass / 0 fail, coverage/lcov.info emitted
  • ✅ 5 consecutive clean stress runs
  • bun run typecheck
  • bun run lint (1 pre-existing warning, unrelated)

Relying on the CI matrix for macOS and Windows coverage of the new flags.

Bun 1.3.13 shipped `--isolate` (fresh global per file) and `--parallel`
(worker process per CPU). Adopting both reduces local wall clock from
~152s to ~52s on a 4-core box and collapses the test:unit + test:isolated
split that existed only to work around mock.module() registry pollution
(bun:test#258) — every file now gets a clean module graph.

## Preload race fix

Worker processes inherit a shared `TEST_TMP_DIR = /tmp/sentry-cli-test`
from `test/constants.ts`. Each worker runs the preload on startup, which
includes `rmSync(TEST_TMP_DIR, { recursive, force })` to clear stale
state from crashes. Under `--parallel`, workers would wipe each other's
DB files mid-run, surfacing as `SQLITE_IOERR_DELETE_NOENT` flakes.

Fix: namespace `TEST_TMP_DIR` by `BUN_TEST_WORKER_ID` — each worker gets
`/tmp/sentry-cli-test-w${id}` and the wipe stays worker-local. Serial
runs (no worker ID) behave exactly as before. Tests with fixed-name
subdirs (`upgrade-lock-test`, etc.) are automatically unique per worker
because the parent path is worker-scoped.

## Stability

Stress-tested with 5 consecutive full runs of `--isolate --parallel` on
the merged suite (test/lib + test/commands + test/types + test/isolated):
5920/5920 pass each time, ~52-55s wall clock. The subprocess-heavy
`test/lib/db/concurrent.test.ts` also passes cleanly under parallel —
SQLite's WAL + busy_timeout pragmas handle the contention.

## CI changes

Drops the separate `Isolated Tests` step and `Merge Coverage Reports`
step — everything now runs through `test:unit` with a single coverage
file. Removes the now-unused `script/merge-lcov.ts`.

## Not doing here (deferred)

- Collapse `test/isolated/` folder into `test/lib`/`test/commands`
- Remove `scopedFetchMock` helper (the defensive marker-based filter in
  `test/lib/sentry-client.test.ts`) — still correct under --isolate,
  not worth churning in this PR
- Add `test:shard` for CI matrix splitting — current wall clock is fine

## Verification

- `bun run test:unit` → 5920 pass, 0 fail, coverage/lcov.info emitted
- Stress loop: 5 consecutive clean runs
- `bun run typecheck` passes
- `bun run lint` passes (1 pre-existing warning, unrelated)
@github-actions
Copy link
Copy Markdown
Contributor

Codecov Results 📊

5920 passed | Total: 5920 | Pass Rate: 100% | Execution Time: 0ms

📊 Comparison with Base Branch

Metric Change
Total Tests 📈 +5782
Passed Tests 📈 +5782
Failed Tests
Skipped Tests

All tests are passing successfully.

❌ Patch coverage is 75.00%. Project has 13391 uncovered lines.
❌ Project coverage is 74.64%. Comparing base (base) to head (head).

Files with missing lines (1)
File Patch % Lines
test/constants.ts 75.00% ⚠️ 1 Missing
Coverage diff
@@            Coverage Diff             @@
##          main       #PR       +/-##
==========================================
- Coverage    95.32%    74.64%   -20.68%
==========================================
  Files          284       284         —
  Lines        41349     52805    +11456
  Branches         0         0         —
==========================================
+ Hits         39415     39414        -1
- Misses        1934     13391    +11457
- Partials         0         0         —

Generated by Codecov Action

@BYK BYK merged commit 75dc93b into main Apr 24, 2026
23 checks passed
@BYK BYK deleted the byk/bun-test-parallel branch April 24, 2026 10:02
BYK added a commit that referenced this pull request Apr 24, 2026
Follow-up to #839. With `bun test --isolate` giving each file a fresh
module graph, the `test/isolated/` split that existed to work around
`mock.module()` registry pollution (oven-sh/cli#258) is no longer
needed.

## Merged into existing files

Per-feature tests now live with their natural counterparts:

| From | To |
|------|-----|
| `isolated/brew-upgrade.test.ts` | `test/lib/upgrade.test.ts` |
| `isolated/log-view-prompt.test.ts` | `test/commands/log/view.test.ts`
|
| `isolated/login-reauth.test.ts` | `test/commands/auth/login.test.ts` |
| `isolated/project-delete-confirm.test.ts` |
`test/commands/project/delete.test.ts` |
| `isolated/set-commits-auto.test.ts` | `test/lib/api/releases.test.ts`
|
| `isolated/set-commits-auto-no-remote.test.ts` |
`test/lib/api/releases.test.ts` (merged together — `getRepositoryName`
is now a controllable `mock()`) |
| `isolated/dsn/project-root.test.ts` |
`test/lib/dsn/project-root.test.ts` |

Pattern: `mock.module()` at the top of the merged file, followed by a
dynamic `await import()` for the code under test so the mocked bindings
win. Other static imports stay as-is.

Also unblocks the previously-deferred stub in
`test/lib/api/releases.test.ts` that said _"setCommitsAuto tests live in
test/isolated/ because they require mock.module() for git helpers"_.

## Moved as sibling files

Two isolated files tested the same subjects as existing files but with
incompatible mocking strategies. Under `--isolate` they coexist safely
as siblings:

| From | To |
|------|-----|
| `isolated/resolve-target.test.ts` |
`test/lib/resolve-target.mocked.test.ts` |
| `isolated/delta-upgrade.test.ts` |
`test/lib/delta-upgrade.mocked.test.ts` |

The existing `resolve-target.test.ts` and `delta-upgrade.test.ts` use
real DB + mocked fetch; the `.mocked.` siblings stub every collaborator
via `mock.module()`.

## New files (no prior counterpart)

Three isolated DSN helpers had no target file in `test/lib/dsn/`:

- `isolated/dsn/errors.test.ts` → `test/lib/dsn/errors.test.ts`
- `isolated/dsn/fs-utils.test.ts` → `test/lib/dsn/fs-utils.test.ts`
- `isolated/dsn/resolver.test.ts` → `test/lib/dsn/resolver.test.ts`

## Removed `scopedFetchMock` helper

`test/lib/sentry-client.test.ts` previously wrapped all its fetch mocks
with `scopedFetchMock(marker, handler)` to filter out foreign fetch
calls that leaked from other test files (CI run 24835339085). Under `bun
test --isolate` each file gets a fresh `globalThis.fetch`, so cross-file
leakage is eliminated. The 6 call sites now use `mockFetch()` directly;
the helper and its `urlOf()` dependency are gone (~30 lines).

## Other cleanups

- Removed `test/isolated/` directory
- Updated `package.json` `test:unit` to drop `test/isolated` from the
argument list
- `test/lib/dsn/project-root.test.ts`: `noopSpan` stub now includes
`setAttributes` (production code calls both `setAttribute` and
`setAttributes`)
- Replaced `() => {}` test stubs with named `noop` functions to satisfy
`lint/suspicious/noEmptyBlockStatements` without per-line biome-ignore
comments

## Verification

- ✅ `bun run test:unit` — 5920 pass / 0 fail across 253 files (was 260 —
merges consolidated 12 files into 5 new + 7 existing)
- ✅ 3 consecutive stress runs clean
- ✅ `bun run typecheck`, `bun run lint`

Net diff: **+1219 / −3830** (most "deletions" are file moves; real code
reduction is the `scopedFetchMock` removal plus deduplication from
merging).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant