A per-test coverage API for Bun
What ubq needs from Bun for coverage of each test.
What ubq needs from Bun, what Bun 1.4.3 has, and the smallest change to Bun that closes the gap. This text is the base of a proposal to the Bun project (D10). It is issue https://github.com/oven-sh/bun/issues/48712 (2026-11-12). Work on a patch for Bun starts only if the Bun project accepts the issue (D39).
Measurements: 03-measurements.md, section 11.
What ubq does on Bun now#
Source counters (D10, 04-architecture.md, section 4). A preload
adds a counter to each function and branch of each project file.
The runner starts each Bun test process with the preload.
This is correct and fast. It has these limits:
- A plain
bun test/foo.tshas no preload, so it records no coverage. On Node, such a run records partial coverage. - A file that the parser of ubq cannot read gets no counters.
- The package carries a TypeScript parser (232 KB) for this.
What a test tool needs#
- Counts for each function and each branch since the last request, with no restart of the process. Then the tool can ask after each test.
- A start with no flag and no environment variable, from code in the process.
- Positions in the source file: offsets, or lines and columns.
What Bun 1.4.3 has#
node:inspector gives points 1 and 2:
import { Session } from 'node:inspector'
const session = new Session()
session.connect()
session.post('Profiler.enable')
session.post('Profiler.startPreciseCoverage', { callCount: true, detailed: true })
// after each test:
session.post('Profiler.takePreciseCoverage', (error, { result }) => {})
Point 3 is absent. Each range has offsets in the JavaScript that Bun made from the module, not in the source. For this file of 241 characters, the last range ends at 115:
export function pick(a: number, b: string | null): string {
if (a > 1) {
return 'big'
}
return b ?? 'none'
}
export const unused = (): number => 1
A program cannot change these offsets to source positions. It
cannot read the JavaScript of a loaded module
(Debugger.getScriptSource does not exist), and it cannot read
the source map that Bun keeps for stack traces.
The change to Bun#
One of these, in order of preference:
takePreciseCoveragegives source offsets for a module that Bun compiled, as it gives source lines in a stack trace. This is what V8 on Node does for a TypeScript file (Node removes types and keeps each position). No new API.- A function that gives the source map of a loaded module,
such as
sourceMapFor(url)inbun:jsc. The tool does the change of positions itself. Debugger.getScriptSourceon aSession, with the inline map that Bun gives to a debugger.
With one of them, the Bun collector of ubq is the V8 collector of
Node (src/coverage.ts), and a plain bun test/foo.ts records
coverage.
A check for the change#
experiments/bun/inspector.ts prints the ranges of
experiments/bun/src.ts after each call. With the change, the
range of pick starts at offset 7 and ends at 118, as on Node.
Cost#
One snapshot costs 1.33 ms on Bun with 9 scripts loaded, against 0.43 ms on Node. Counters cost microseconds. So, also with the change, the runner can keep counters for speed, and use the inspector where counters are absent.