test(LibCast): observe the intermediate and pin the in-place contract - #18
Conversation
The round trip tests compared the input buffer against the round trip of that same buffer. Both names address one allocation, so the assertion held for any cast that rewrote elements where they lay, and only a cast that returned a different pointer could fail it. Snapshot the expected words into an independent buffer before casting, and assert against the intermediate array as a consumer of the cast reads it, so that a cast which masks or otherwise rewrites elements is observable. Add tests for the in-place contract itself: each cast returns the buffer it was given rather than a copy, and a write through either name is visible through the other. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. WalkthroughThe ChangesLibCast array cast verification
Estimated code review effort: 2 (Simple) | ~10 minutes Merge Risk: ⚪ Minimal · up to This test-only change strengthens coverage for LibCast value preservation and in-place behavior without changing production code or runtime behavior. No actionable merge-blocking risk remains beyond normal checks and review. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
#18 landed on main and appended its in-place and element-coverage tests to the end of `LibCastTest`, which is where the containment tests for #20 also sit. Both sides are kept in full: #18's five in-place tests and strengthened round trips, and this branch's two containment tests plus the `echoFirstAddress` ABI-boundary helper. Nothing from either side is dropped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The four
LibCastround trip tests each compared the input buffer against theround trip of that same buffer. Because every cast is a pointer retype, both
sides of those assertions addressed one allocation, so the comparison held for
any cast that rewrote elements where they lay. Only a cast that returned a
different pointer could fail them, and nothing at all observed the intermediate
typed array — the value a caller of a single cast actually receives.
Two behaviours were therefore unpinned:
uint256words down to 160 bitswhile retyping them to
address[]passed the whole suite.returns a copy of the input rather than the input itself, passed the whole
suite — even though "retype the data in place" is the documented contract and
callers depend on the two names addressing one mutable structure.
The round trips are strengthened rather than duplicated: expected words are
snapshotted into an independent buffer before the cast, and the intermediate is
read directly, as a consumer of it would. Separate tests pin the in-place
contract itself, since nothing previously targeted it.
QA
Discriminating tests:
testAddressesArrayRound0,testAddressesArrayRound1,testBytes32ArrayRound0,testBytes32ArrayRound1(strengthened in place),testAsAddressesArrayRetypesInPlace,testAsUint256ArrayFromAddressesRetypesInPlace,testAsBytes32ArrayRetypesInPlace,testAsUint256ArrayFromBytes32RetypesInPlace,testAsAddressesArrayWritesAreShared- each fails on base under its mutation and passes unmutated, verified by re-runningmutation-probeover the same 13 mutants after the tests were added (13/13 killed, previously 8/13). Each compares an exact value, never a bare revert: the intermediate word against the full 256 bit input word, the returned pointer against the input pointer, andus[0]against the address written throughaddresses[0].Mutations applied: 13 against
src/LibCast.sol, one behaviour each.addresses := us->add(us, 0x20)(M01) ->testAddressesArrayRound0;addresses := us->0x60(M02) ->testAddressesArrayRound0;addresses := us-> mask each element to 160 bits (M03, survived before) ->testAddressesArrayRound0;addresses := us-> allocate and copy (M04, survived before) ->testAsAddressesArrayRetypesInPlaceandtestAsAddressesArrayWritesAreShared;us := addresses->add(addresses, 0x20)/0x60/ copy (M05, M06, M07 - M07 survived before) ->testAddressesArrayRound0,testAddressesArrayRound1,testAsUint256ArrayFromAddressesRetypesInPlace;b32s := us->add(us, 0x20)/0x60/ copy (M08, M09, M10 - M10 survived before) ->testBytes32ArrayRound0,testBytes32ArrayRound1,testAsBytes32ArrayRetypesInPlace;us := b32s->add(b32s, 0x20)/0x60/ copy (M11, M12, M13 - M13 survived before) ->testBytes32ArrayRound0,testBytes32ArrayRound1,testAsUint256ArrayFromBytes32RetypesInPlace. Baseline green at both points: 6 tests before, 11 after.Oracle: the
LibCastNatSpec and the README, not the implementation. The NatSpec states a cast "will merely retype the data in place, generally without additional checks" and that "the cast does NOT (can't) check that the input is a valid output ... It is the calling context that MUST ensure the validity of the data". The README states a cast "moves between Solidity types without changing the binary data". The in-place assertions and the assertion that upper bits survive unmasked are both derived from those statements, so the tests pin the documented contract rather than whatever the code happens to do.Category check: no issue drives this PR; it is the coverage half of an adversarial mutation test run over the whole repo, so the categories are the mutation catalog's. Exercised against this unit: side effects (delete/replace the write), constants and identifiers (pointer arithmetic on the retype), and returns/outputs (empty array, copied array). Conditionals, comparisons and filters/scopes do not occur in these four functions, which contain no branches. Test-only change; no source file is touched.
🤖 Generated with Claude Code
Summary by CodeRabbit