test: parse the literal the library emitted, not one the test formatted - #111
Conversation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 57 minutes Limit details: You’ve used all 1 included review currently available under your plan. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. 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: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
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 |
|
@coderabbitai review |
|
Closes #62
What changed
testAddressConstantStringRoundTrips,testBytes32ConstantStringRoundTripsandtestUint8ConstantStringRoundTripseach ended in an assertion overvm.toString(data)— a value the test formatted for itself. Those assertionsstated a property of foundry's own
toString/parse*pair and never read thelibrary's output at all. Each now slices the literal out of the text the library
emitted and parses that.
The slicing is
LibCodeGenSlow.betweenSlow, withindexOfSlowunder it: thetext between the first
openand the firstcloseafter it, revertingNoSlicewhen either delimiter is missing, so text that does not carry theliteral fails loudly rather than yielding an empty slice that compares equal to
nothing.
The checksum claim
The address docstring claimed the emitted literal is checksummed "so it
round-trips back to the same address rather than silently relying on an
all-lowercase form".
vm.parseAddressaccepts either form, so no round trip canstate that. The claim itself is true and load bearing, so it is proven rather
than dropped:
LibCodeGenSlow.checksumAddressSlowis an EIP-55 reference builtfrom the address's own bits rather than from
vm.toString, andtestAddressConstantStringChecksummedfuzzes the emitted literal against it.Measured rather than assumed.
solc 0.8.25onaddress constant LOWER = address(0xc51a14251b0dcf0ae24a96b7153991378938f5f5);:An all-lowercase address in a generated file is a compile error in the consumer,
which is exactly what this library's output has to survive.
Decision and ledger
The issue offered two routes: slice the literal out of
emittedand parse that,or drop the assertion and cut the unsupported half of each docstring. Sliced,
and the checksum claim proven.
test/lib/LibCodeGenSlow.sol(
indexOfSlow,betweenSlow,checksumAddressSlow), oneNoSliceerror andone new fuzz test. 85 added lines, all under
test/.checksumAddressSlowis a second EIP-55 implementation thathas to stay right; rows B, C and D of the matrix below are what holds it and
the slicing.
betweenSlowis delimiter based, so a change to the emittedwrapper (
address(,bytes32() surfaces asNoSlicerather than as a silentpass.
Nothing in
src/depends on any of it.Dropping instead would have been cheaper to carry and would have left the
library's most consequential output property — that the generated file compiles
— stated nowhere in the suite.
Not a stopgap, so no removal issue is filed.
QA
testAddressConstantStringRoundTrips,testBytes32ConstantStringRoundTrips,testUint8ConstantStringRoundTrips(final assertion rewritten) and
testAddressConstantStringChecksummed(new) -the finding is a dead assertion rather than a wrong result, so "fails on base"
is verified against a gutted base: with the three emitters in
src/lib/LibCodeGen.solchanged toreturn "";, the three assertions beingreplaced all PASS and all four replacements FAIL with
NoSlice("", ...)(probe output in section 1 below).
checksumAddressSlownibble > 7->nibble > 6->killed by
testAddressConstantStringChecksummed(134/1 of 135);betweenSlowstart = openIndex + openBytes.length->start = openIndex->killed by all four (131/4 of 135);
indexOfSlowreturn i;->return i + 1;-> killed by all four (131/4 of 135);addressConstantStringvm.toString(data)->vm.toString(address(uint160(data) ^ 1))-> killed by both address tests(127/8);
bytes32ConstantStringvm.toString(data)->vm.toString(data ^ bytes32(uint256(1)))-> killed bytestBytes32ConstantStringRoundTrips(111/24);uint8ConstantStringvm.toString(data)->vm.toString(uint256(data) ^ 1)-> killed bytestUint8ConstantStringRoundTrips(126/9). Full table in section 4.vm.parseAddress/vm.parseBytes32/vm.parseUintread back theliteral the library emitted, which is the inverse direction from the
vm.toStringthe library used to write it; andLibCodeGenSlow. checksumAddressSlow, an EIP-55 reference built from the address's own bitsand keccak256, never calling
vm.toString. The EIP-55 rule itself isanchored to
solc 0.8.25, which named0xc51a14251b0dcF0ae24A96b7153991378938f5F5as the correct checksum for thelowercase literal it rejected (error quoted above).
library's output at all three call sites, and either prove the checksum claim
with an EIP-55 reference in
LibCodeGenSlowor stop claiming it. Covered allthree call sites plus the checksum, proven rather than dropped. No
Refs.All runs are
nix develop -c ...from the flake.1. Red — the three assertions pass with the library gutted
The finding is a dead assertion, not a wrong result, so the reproduction has to
isolate the assertion from the exact-text assertion that sits above it in the
same test. A throwaway
test/lib/DeadAssertionProbe.t.solheld the threeoriginal assertions verbatim in one contract and the four replacements in
another, and the three emitters in
src/lib/LibCodeGen.solwere changed toreturn "";.nix develop -c forge test --match-path 'test/lib/DeadAssertionProbe.t.sol' --fuzz-runs 64That is the issue's claim, measured: with
LibCodeGenemitting nothing, thethree assertions being replaced still pass and all four replacements fail. The
library was restored with
git checkoutand the probe file deleted; neither isin this diff.
2. Green — full suite after the change
nix develop -c forge testorigin/mainat clone time ran 134 tests in 16 suites, sotestAddressConstantStringChecksummedis the one added and nothing regressed.3.
forge fmt --checknix develop -c forge fmt --checkexits 0 with no diff.4. Mutation matrix
Each row: break exactly one line, run the whole suite at
--fuzz-runs 256, restore. Every row reports its own... (135 total tests)line, so a harness error or a zero-match filter cannotbe read as "survived".
checksumAddressSlow:nibble > 7→nibble > 6testAddressConstantStringChecksummedbetweenSlow:start = openIndex + openBytes.length→start = openIndextestAddressConstantStringChecksummed,testAddressConstantStringRoundTrips,testBytes32ConstantStringRoundTrips,testUint8ConstantStringRoundTripsindexOfSlow:return i;→return i + 1;LibCodeGen.addressConstantString:vm.toString(data)→vm.toString(address(uint160(data) ^ 1))testAddressConstantStringChecksummedand...RoundTripsamong themLibCodeGen.bytes32ConstantString:vm.toString(data)→vm.toString(data ^ bytes32(uint256(1)))testBytes32ConstantStringRoundTripsamong themLibCodeGen.uint8ConstantString:vm.toString(data)→vm.toString(uint256(data) ^ 1)testUint8ConstantStringRoundTripsamong themRows B, C and D are the ones that matter for this change: they break exactly the
lines this PR adds, and every test they kill is one of the four new or rewritten
assertions. Nothing else in the suite touches
checksumAddressSlow,betweenSloworindexOfSlow, so those rows isolate the new assertions the waythe probe in section 1 does.
Rows E, F and G break the library itself. They are killed by the pre-existing
exact-text assertions as well, so they do not isolate anything — they are here to
show the new assertions are not vacuous when the emitted value is wrong.
Each row re-applied the mutation with
sedand re-checked it withgrepbeforerunning; the applied line is recorded per row. Every row reports
(135 total tests), so neither a harness error nor a zero-match filter can beread here as "survived".