Repository navigation
test: add seeded differential fuzz oracles for node:path, Bun.Glob, source maps and the transpiler - #39006
test: add seeded differential fuzz oracles for node:path, Bun.Glob, source maps and the transpiler#39006robobun wants to merge 7 commits into
Conversation
…z timeouts for soaks
…ze fuzz defaults per build type
|
Warning Review limit reached
Next review available in: 25 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 ignored due to path filters (2)
📒 Files selected for processing (7)
Comment |
|
Status: tests only, ready for review.
|
|
Updated 4:46 AM PT - Aug 15th, 2026
⏳ @robobun, your commit 16bf772 is still building in
|
…on; soaks pass --timeout instead of scaling it
|
71f2fc8 changes since the first revision:
|
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. It's test-only, but it's ~1,700 lines of new fuzz generators across four subsystems, each with carefully-tuned dialect exclusions and test.failing pins — a human look at the generator constraints and CI-stability tradeoffs would still be worthwhile.
What was reviewed:
_util/fuzz.tsextraction is behavior-preserving for the existing JSON fuzz (sameRng, same seed/iters defaults).- Path oracle: cwd-independence (fixed absolute bases for
resolve, device-carrying args forrelative/toNamespacedPath, drive-relative only names the base's drive),skipIfon missing/wrong-major node, subprocess pipes drained concurrently. - Glob/transpiler/sourcemap oracles: seeded determinism, non-vacuous floor assertions, each
test.failingpin sits next to the exclusion it justifies;picomatchwas already in the lockfile transitively.
Extended reasoning...
Overview
This PR adds four new seeded differential-fuzz oracles (node:path vs a spawned node, Bun.Glob vs picomatch, Bun.build source maps vs mozilla source-map, and the transpiler vs acorn + itself), extracts the shared Rng/env-seed plumbing into test/_util/fuzz.ts, refactors the existing JSON fuzz to import it, adds picomatch as a direct test dependency, and updates the glob scan.test.ts snapshot for the new file. Every change is under test/; no src/ code is touched.
Security risks
None. Test-only additions; the only external process spawned is the local node binary via nodeExe() from harness (no network, no downloads — the PR explicitly avoids nodeExeMatchingAbi() for that reason). The added dependency (picomatch@4.0.4) was already present transitively in the lockfile.
Level of scrutiny
Medium-high. Although test-only and additive, this is a large change (~1,700 lines) with a lot of encoded domain knowledge: each generator carries a header of dialect differences that justify what it deliberately does not emit, and six test.failing pins that lock in known main-branch bugs. Getting an exclusion wrong in either direction means either a flaky CI test or a silently-narrowed oracle. The repo's review guidance treats test quality as a first-class blocking category, and the intricacy here (glob brace/globstar interaction rules, transpiler scope tracking to guarantee termination and no TDZ, win32 root generation that stays cwd-independent) is well past "mechanical".
Other factors
- The PR description is unusually thorough, with soak counts and per-oracle timing on debug+ASAN vs release, and each exclusion is commented at the site — that reduces the review burden considerably.
- Debug builds get a smaller default iteration count via
isDebug; the tests set no per-test timeouts (soaks are meant to pass--timeout), which matches the guidance in CLAUDE.md. - The one CI failure noted in the timeline (
test/bake/deinitialization.test.tssegfault on Windows) is unrelated to any file this PR touches. - The
test.failingpins are the fail-before evidence for the six divergences found; each pin's comment names the generator gate to remove when it flips, so silent narrowing is guarded against — but a human should sanity-check that the pinned assertions actually reflect the intended reference behavior (e.g., the two transpiler pins go through the oracle's owncheck(), and the glob pin's expected value comes from picomatch itself).
Given the size and the amount of carefully-encoded exclusion logic, I'm deferring rather than approving so a maintainer can spot-check the dialect boundaries and the CI-time budget.
|
For whoever spot-checks the pins: this is what each one asserts and how it fails on main today (obtained by temporarily turning the
Each pin's comment names the exclusion to delete when it flips: the On time budget: on a debug+ASAN build the oracle bodies take roughly 1.6 s (source maps, 10 programs), 2.4 s (transpiler, 10 programs; almost all of it acorn and the programs themselves running under a debug JSC), 1.7 s (glob, 200 patterns) and 1.4 s (path, 100 iterations); on a release build the whole family is under 2 s with 5-30x more cases. The counts are the |
Problem
test/js/bun/jsonc/json-differential-fuzz.test.ts.node:path,Bun.Glob, source map generation and the transpiler printer were all rewritten and only have example-based tests.Fix
test/_util/fuzz.ts: the seed/iteration plumbing lifted out of the JSON fuzz (Rng,envSeed,envIters,fuzzEnv);json-differential-fuzz.test.tsimports it, same seed and counts as before.BUN_<NAME>_FUZZ_SEED/BUN_<NAME>_FUZZ_ITERS; case N is the same however many cases a run asks for, and each failure message prints the two variables that replay it. A soak is theITERSvariable plus--timeout(documented in the helper); the tests themselves set no timeouts.test/js/node/path/path-differential-fuzz.test.ts: random posix and win32 paths (./.., repeated, trailing and mixed separators, empty segments, drive letters, UNC roots, non-ASCII) throughnormalize,join,resolvefrom fixed absolute bases,relative,dirname,basenamewith and without suffix,extname,parse,format(parse()),isAbsolute,toNamespacedPath, compared call for call withnodeExe().Function#toString; skipped when node is missing or not the major Bun reports (CI images install exactly that release;nodeExeMatchingAbi()was not used because it may download a node).node:pathpredates their handling in Node (device names, CVE-2024-36139.\prefixing,\\.\roots, Unicode folding inwin32.relative, lone surrogates).node:pathand adds a recorded 1540-case node parity corpus: the two are complementary (a fixed corpus that runs without node vs a soakable live oracle), and this file passes unchanged on main and, since it only generates behaviour both implement, after node:path: rewrite on JSString storage in Rust, match Node.js v26 exactly, ~3x faster #37305. The pin described below tells node:path: rewrite on JSString storage in Rust, match Node.js v26 exactly, ~3x faster #37305 which exclusions to drop.test/js/bun/glob/glob-differential-fuzz.test.ts: patterns derived from a random path (*,?, classes and ranges, nested braces, escapes, globstar segments, negation) run throughBun.Glob#matchandpicomatchagainst the path, mutations of it and unrelated paths.picomatch4.0.4 is added totest/package.json; it was already in the lockfile as a transitive dependency, and the isolated linker only exposes direct ones.scan.test.ts's snapshot of this directory's listing gains the new file.a/**/{,b}matcha, reads a**inside or next to a brace group as slash-crossing where Bun's bash-style expansion does not, and gives.followed by*inside braces its dotfile guard).test/bundler/sourcemap-differential-fuzz.test.ts: random multi-module programs with random layout, comments, non-ASCII text and\r\n, built withBun.build({ sourcemap: "external" })as esm, iife, cjs and whitespace-minified esm/iife, decoded withsource-map0.7.4.sourcesContentmust be verbatim, and every output identifier that has a mapping must map to that same identifier in the original (identifiers are unique across the run, so the check is exact); aname, should Bun start emitting them, must be that identifier.test/bundler/transpiler/transpiler-differential-fuzz.test.ts: random programs from a small grammar (declarations, functions with default and rest parameters, arrows, classes, destructuring, loops,try,switch, literals with holes, spread, getters and computed keys, templates, optional chaining,??, every operator), with every compound expression parenthesized in the source so the printer has to decide which parentheses the output needs.__outas the source when run.test.failingnext to the exclusion; the pin starts passing when the fix lands, and its comment says which exclusion to delete, so the generators cannot stay narrowed silently:Bun.Glob:a**/**matches"ab"althougha*/**does not (src/glob/matcher.rscollapses the/**before checking that the**is a globstar).Bun.Glob:**/a**/{x,y}matches"ab/ay"(same family, different path through the matcher). One pin for both, since the gating can only go once both are fixed.Bun.build: with several entrypointsBuildArtifact.sourcemappoints at the wrong artifact; outputs are[js0, js1, map0, map1]butjs_bundle_completion_task.rsattaches the next output. The oracle pairs maps by path until the pin flips, then should readjs.sourcemap."a" != Infinityprints as"a" != 1 / 0, which reprints as1 / 0 != "a"(print_numberplus the constants-on-the-right reordering).(a, b) ? f() : 0;prints asa, b && f();and only the next pass drops thea,(simplify_unused_expr). The two transpiler pins run the excluded programs through the oracle's owncheck().node:path:basename("//", "日本")is"//"in node and""on main (suffix length compared in UTF-8 bytes rather than UTF-16 units); node:path: rewrite on JSString storage in Rust, match Node.js v26 exactly, ~3x faster #37305 fixes it, and its pin is the cue to widen the whole generator.bun bd test differential-fuzzruns the five files (15 tests) in about 17 s on this debug+ASAN build, each oracle body 1-3 s; with the release binary the set takes under 2 s. The new files typecheck undertest/tsconfig.json. CI build for the previous revision was green on every lane for these files.Background
source-mapdecoder together with the text the generator wrote, and acorn plus JavaScriptCore running the program. The seed makes any failure reproducible, and the same test doubles as a soak by raising the count.test.failing(docs/test/writing-tests.mdx) inverts a test: it passes while the body throws and fails once the body passes. It is the repo's way of tracking a known bug so that the fix is noticed; here each pin doubles as the instruction for widening the generator.