fix: record a call as having thrown regardless of the thrown value - #2763
Open
mrpmohiburrahman wants to merge 1 commit into
Open
fix: record a call as having thrown regardless of the thrown value#2763mrpmohiburrahman wants to merge 1 commit into
mrpmohiburrahman wants to merge 1 commit into
Conversation
The invoke path recorded what was thrown but never whether anything was thrown, so both consumers re-derived "did it throw" from the value. `proxy-invoke.js` gated the rethrow on `exception !== undefined`, but `exception` is only assigned inside the catch, so `throw undefined` is indistinguishable from a call that never threw. The throw was swallowed and the caller saw a normal return. `proxy-call.js`'s `threw()` used `Boolean(this.exception)` and `!this.exception`, which are truthiness tests rather than presence tests. Every falsy thrown value reported `threw() === false`, so `throw null` propagated correctly while still being reported as not having thrown. Record the fact instead of inferring it from the value: a `didThrow` boolean is set where the catch runs and carried to the call object in a parallel array, leaving `spy.exceptions` and its contents untouched. Fixes sinonjs#2471.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Purpose (TL;DR)
Fixes #2471. A spy over a function that throws
undefinedswallowed the throw, andspy.threw()then reported that the call never threw.Background (Problem in detail)
The invoke path records what was thrown, never whether anything was thrown, so both readers infer it from the value. They infer it differently, so the two symptoms have different scope.
proxy-invoke.jsgated the rethrow onexception !== undefined. Sinceexceptionis only assigned in thecatch, a thrownundefinedlooks exactly like a call that never threw, so the throw was dropped and the caller got a normal return. That part is specific toundefined.proxy-call.js'sthrew()usedBoolean(this.exception)and!this.exception, which test truthiness rather than presence. Every falsy thrown value reportedthrew() === false:undefined,null,0,"",false,NaN,0n. Sothrow nullalready propagated correctly but was still reported as not having thrown, andspy.threw(null)returned false with it. Fixing the recording rather than the two readers closes both cases at once.Solution
A
didThrowboolean is set where thecatchruns and carried to the call object ascall.didThrow. The rethrow andthrew()both read it.The flag goes in its own parallel array (
didThrowValues) besideexceptions, deliberately:spy.exceptionsis public, so writing a sentinel into it to mark a real throw would be visible to anyone reading that array. The thrown value is left alone, and the regression test assertsspy.exceptions[0]is stillundefined.threw()is the single root for the assertion surface, so one edit there coverscall.threw(),spy.threw(),spy.alwaysThrew()and the twosinon.assertmirrors. The array is also initialised in the proxy template, cleared inresetHistory, and copied inwithArgs's back-fill.One subtlety for review: the old falsy short-circuit was incidentally guarding the
this.exception.nameread in the argument form. That read is now reachable with a nullish exception, so it isthis.exception?.name, with a test for it.Worth flagging for the changelog:
threw()now returns true for every falsy thrown value, not justundefined. That follows from fixing the root cause rather than the reported symptom, but it is a behaviour change.How to verify
npm installnpm run test-node1561 passing, 12 pending, 0 failing, against 1556 passing before this change. Five new tests, no existing test affected.
npm run lintandnpm run prettier:checkare clean.Reverting only
src/turns three of the five red: the#2471test and the two falsy-value cases. The other two should pass either way, since they pin the?.guard and the case where a spy simply returnsundefined.The new tests capture throws with a plain
try/catchrather thanassert.exception, because@sinonjs/referee'sassert.exceptiondoesif (!err)on the caught value and cannot see a falsy throw either. That may be worth a separate look in referee.On the
wontfixlabel:stale[bot]applied it in December 2023, right after its own staleness comment, whileHelp wantedandDifficulty: Easycame from @fatso83. I read it as bot noise rather than a decision. The approach here follows @fatso83's framing in the thread, which the reporter confirmed.Checklist for author
npm run lintpasses