bun test: report a test file whose entry resolve throws instead of exiting the run - #38258
bun test: report a test file whose entry resolve throws instead of exiting the run#38258robobun wants to merge 1 commit into
Conversation
…iting the run The test runner passes each test file path straight to the module loader as the entry specifier. When that specifier cannot be resolved (a file name that is not valid UTF-8 reaches the loader with U+FFFD substituted in, so it no longer names the file the scanner found) the loader throws synchronously rather than rejecting the load promise. The runner turned that into Err(JSError) and exited with code 1 before printing the error, a summary, or the junit report, and before running any remaining files. Hand the thrown exception back as an already-handled rejected load promise so the per-file rejection path reports it against that file and the run continues. This also keeps --parallel workers alive on such a file instead of reporting "worker crashed".
|
Warning Review limit reached
Next review available in: 21 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 selected for processing (2)
Comment |
|
Updated 6:05 PM PT - Aug 13th, 2026
❌ @robobun, your commit b4b5c0c has some failures in 🧪 To try this PR locally: bunx bun-pr 38258That installs a local version of the PR into your bun-38258 --bun |
|
Status: reproduced on 1.4.0-canary (da3851e) with the repro in the description (a The automated review raised nothing to change. Waiting on CI and a maintainer review. |
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. The change is small and follows the established set_handled() → reject(Err(JsError::Thrown)) pattern, but since it touches JSC exception handling in VirtualMachine.rs, a human look would still be worthwhile.
What was reviewed:
- The
Nonearm:JSC__JSPromise__createandsetHandledarenothrowand don't declare a scope, so allocating/marking the promise while the resolve exception is still pending is safe;reject(.., Err(JsError::Thrown))then takes that exception before calling into C++. set_handled()beforereject()matchesreject_as_handled(JSPromise.rs:427-429), so the rejection tracker won't double-report.?onrejectonly propagatesJsTerminated, which correctly still routes tohandle_top_level_test_error_before_javascript_start.- Tests: Linux-only gate is justified, pipes drained concurrently,
usingcleanup, both serial and--parallelcovered.
Extended reasoning...
Overview
The PR changes one match site in VirtualMachine::reload_entry_point_for_test_runner (src/jsc/VirtualMachine.rs:4828-4846): when load_and_evaluate_module_ptr returns None (synchronous throw during entry-specifier resolve), instead of returning Err(JSError) — which the test runner's callers turn into Global::exit(1) via handle_top_level_test_error_before_javascript_start — it wraps the pending exception in a pre-handled rejected JSInternalPromise and returns that. The runner already handles a rejected load promise (bad-import path today), so the file is reported as failed and the run continues. Two Linux-only tests in test/cli/test/bun-test.test.ts cover serial and --parallel behavior with a Latin-1-byte filename and directory name.
Security risks
None. This is error-path routing inside the test runner; no untrusted input parsing, auth, or resource limits are touched.
Level of scrutiny
Medium-high. VirtualMachine.rs is core runtime code and the change sits at the JSC exception boundary. I traced the specifics that could go wrong here:
JSInternalPromiseis a Rust alias forJSPromise(src/jsc/lib.rs:206), socreatecallsJSC__JSPromise__create, which is a bareJSPromise::create(vm, structure)allocation with no ThrowScope — safe to call with an exception already pending.set_handled()is[[ZIG_EXPORT(nothrow)]]and just callsmarkAsHandled(); also safe with a pending exception.reject(.., Err(JsError::Thrown))(JSPromise.rs:402-414) takes the pending exception viatry_take_exception()before callingJSC__JSPromise__reject, so the C++ reject path sees a clean scope. If the taken exception is a termination it returnsErr(JsTerminated), which?propagates and the caller still hitsGlobal::exit(1)— same as before for that case.- Marking handled before rejecting mirrors
reject_as_handled(JSPromise.rs:427-429) and prevents the rejection tracker from queueing a duplicate report; the runner reads the promise state directly. - GC: the fresh promise is on the stack until stored in
pending_internal_promiseandensure_still_alive'd immediately after;rejecton a handler-less promise runs no reactions.
The author reports running the repro under BUN_JSC_validateExceptionChecks=1, which is the right check for the "allocate while exception pending" concern.
Other factors
The tests are well-constructed per REVIEW.md conventions: using tempDir, concurrent pipe drains, exit-code asserted last, describe.concurrent, and a comment justifying the skipIf(!isLinux) gate. They assert the positive contract (other file runs, summary printed, junit written, exit 1) and the negative one (RAN unloadable never appears, --parallel worker doesn't print worker crashed). The two other load_and_evaluate_module_ptr call sites at lines 2730 and 2753 are for bun run's single-entry path, where Err(JSError) → exit is still the intended behavior (and the synthetic-main path there makes this failure mode a rejected promise anyway, per the PR description).
Given that this is a targeted change in a critical file at the JSC exception boundary, I'm deferring rather than approving so a maintainer can confirm the exception-handling shape.
|
Heads-up: #38273 fixes the same bug (a test file whose entry specifier fails to resolve ending the run) one level down, in |
Problem
bun testrun: banner only, exit 1, no error, no summary, no--reporter-outfilejunit file, and the remaining files never run. Under--parallelthe file shows up as(worker crashed: exit code 1).bun testloads each file by handing its path to the module loader as the entry specifier (reload_entry_point_for_test_runner,src/jsc/VirtualMachine.rs:4828). The path becomes a JS string on the way, so the undecodable byte turns into U+FFFD and the entry no longer names the file the scanner found.JSC::loadAndEvaluateModule, so that failure is a thrown exception andload_and_evaluate_module_ptrreturnsNone, which the runner mapped toErr(JSError).TestCommand::run_all_tests(and the--parallelworker loop) send anyErrtohandle_top_level_test_error_before_javascript_start, whose release body isGlobal::exit(1). The pending exception is never printed.bun rundoes not hit this: it loads a synthetic main module that imports the real file, so the same resolve failure arrives as a rejected promise and is reported.Fix
load_and_evaluate_module_ptrreturnsNone, take the pending exception and return it as an already-rejected load promise, marked handled first so the promise rejection tracker does not report it a second time.TestCommand::runalready handles a rejected load promise (a test file with a badimporttakes that path today): it reports the error under the file's header, counts the file as failed, honors--bail, and moves on. The unloadable file now printserror: Cannot find module '/path/z\uFFFD.test.ts' from '', the other files run, the summary and junit report are written, and the exit code is 1 because a file failed. The--parallelworker survives the file for the same reason.test/cli/test/bun-test.test.ts("a test file whose name is not valid UTF-8", Linux only since that is the only CI platform whose file systems accept such names): both tests fail on the unfixed binary (first one never runs the other file or writes the report, second one seesworker crashed) and pass with this change.test/cli/test/bun-test.test.tsin full,test/cli/test/isolation.test.ts,test/cli/test/parallel.test.ts(one pre-existing timing-dependent failure in this container,--parallel lazily scales workers based on file duration, unrelated to this path),test/js/junit-reporter/junit.test.js, andtest/regression/issue/26851.test.ts; and the repro underBUN_JSC_validateExceptionChecks=1.Background
loadAndEvaluateModule. JSC resolves it first (calling back into Bun's resolver) and only then creates the promise that tracks fetching and evaluating the module graph. A resolve failure therefore surfaces as a synchronous throw, while anything that goes wrong later (a missing import inside the file, a top-level throw) rejects the returned promise.load_and_evaluate_module_ptrreturningNonemeans the C++ side threw and the exception is still pending on the VM.JSPromise::reject(global, Err(JsError::Thrown))is the existing helper that takes that pending exception and rejects a promise with it.Repro
Before:
After:
Same result for a Latin-1 directory name (
caf\xe9/b.test.js), with--isolate, and with--parallel(the worker reports the file instead of exiting).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/bun-test.test.ts