test: mutation-harden LibFixedPointDecimalParse coverage#22
test: mutation-harden LibFixedPointDecimalParse coverage#22thedavidmeister wants to merge 3 commits into
Conversation
Adversarial mutation pass over decimalStringTofixedPoint. Existing tests
already kill most mutants; two genuine surviving mutants were found and
killed with new discriminating tests:
- Fractional-part overflow propagation (line 71, previously uncovered): a
>77-digit fraction overflows uint256 in the inner integer parse and the
resulting ParseDecimalOverflow selector is returned from the fractional
error branch. Pinned with a 78-nine fraction.
- Decimal-point gate (line 44): a non-point character directly after the
integer with no further input ("1x") must be rejected as an invalid
string. Without the gate the parse wrongly succeeds as 1e18. The
pre-existing corrupt-integer tests only reached this path via inputs whose
garbage is caught by a later gate, so they did not discriminate it.
Two remaining survivors are equivalent mutants (the strip-loop lower bound
>= -> > at fracStart, and the frac-add overflow guard < -> <=) that cannot
change any output; documented as such, no test added.
Tests only: src/ is unchanged. Adds an audit/mutation-test-scans.json scan
record for org-wide health tracking.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 58 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: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
✨ 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 |
|
Rework note (human reject, 2026-07-04): design ruling — an EMPTY FRACTION IS INVALID. The |
…tring parse An empty fraction is invalid per the org design ruling in #24: the decimal point MUST be followed by at least one digit, so "1." returns ParseDecimalInvalidString instead of parsing as 1e18. Pins the revert on the exact selector for the whole category and pins adjacent valid forms unchanged. Closes #24 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🤖 ai:producer |
What
Scoped adversarial-mutation-test coverage pass over the core string→fixed-point
parser
LibFixedPointDecimalParse.decimalStringTofixedPoint. This was the onlymodule with a real coverage gap (97.5% lines / 88.9% branches; every other module
is at 100%).
Update (2026-07-04 rework note): per the human design ruling recorded in #24
— an EMPTY FRACTION IS INVALID — this PR no longer pins
"1."as a valid emptyfraction. The parser fix for #24 (branch
fix-24-trailing-decimal-revert,PR #25) is merged into this branch so the parser fix and the hardened suite land
together: the decimal-point gate in
src/lib/parse/LibFixedPointDecimalParse.solnow requires at least one digit after the point, returning
ParseDecimalInvalidString.selectorfor a bare trailing point, and the"1."acceptance pin is replaced with a revert pin on that exact selector. The two
fractional-overflow tests and the
"1x"/"10x"decimal-point-gate tests arekept as-is.
The discipline: for each behaviour, break the exact line and run the whole suite.
Mutants the existing tests already kill are credited (no new test). Only mutants
that survive the existing suite (real gaps) get a new discriminating test that
passes clean and fails under the mutation.
Group hardened
decimalStringTofixedPoint— 15 behaviours probed. The existing example + round-triptests already kill most mutants. Two genuine survivors were found and killed.
Mutation matrix (behaviour → mutation → killer)
!=→==== 0→falsetestDecimalStringToFixedPointInvalidAfterIntegercursor<end→false>=→>fracStart'0'at fracStart contributes 0 whether stripped or parsed== 1→== 0+1→+0cursor-1→cursor-0--→++cursor > fracStartgatetrue(errorSelector,0)→(0,0)testDecimalStringToFixedPointFailureFractionalPartOverflow>18→>19>18→>=18ooms = 18 - digits17 - digits<→<=scaledFracis always strictly > 0 (frac > 0 after strip,frac*10**ooms < 2^256), sovalue == preValueis impossibleif (value<preValue)→if (false)The two gaps filled
fraction with more than 77 digits overflows
uint256inside the innerunsafeDecimalStringToInt, before thedigits > 18precision-loss check isreached, and the resulting
ParseDecimalOverflowselector is returned from thefractional error branch. No existing test passed an overflowing fraction. New
test pins a 78-nine fraction →
ParseDecimalOverflow.integer with nothing after it (
"1x") must be rejected as an invalid string.Bypassing the gate makes the parse wrongly succeed as
1e18. The pre-existingcorrupt-integer tests only reached this code via inputs (
"1a1.1") whose garbageis caught by a later gate, so they did not discriminate the decimal-point gate
itself. New test pins
"1x"/"10x"→ParseDecimalInvalidString, plus"1."→
ParseDecimalInvalidString(a bare trailing point is a malformed emptyfraction per the ruling in Parser accepts a bare trailing decimal point ('1.') as a valid empty fraction — ruled invalid #24; this pin was originally acceptance →
1e18andwas replaced per the rework note).
Parser fix carried from #24 (PR #25 merged in)
Rejected now (
ParseDecimalInvalidString, selector0x3e8e62d6):"1.","0.","123.","00.","115792089237316195423570985008687907853269984665640564039457."— the whole category: any integer digits followed by a bare trailing point"1.e5"— scientific notation is unsupported here, so nothing after the bare point can rescue the empty fraction"."appendedUnchanged:
"1","1.0","0","0.0","123","123.0", and theuint256-max string still parse to the same values as before (pinned)"."and".5"still reject withParseEmptyDecimalString(empty integer part, rejected before the fraction is considered; their status is pinned, not changed)"1."pin in this suite, replaced here.LibFixedPointDecimalFormat.fixedPointToDecimalStringonly emits a.when the fraction is nonzero with at least one nonzero digit after it, so thetestStringRoundTripFuzzround-trip is unaffected.This repo has no generated pointer/deploy-constant files and nothing deployed
(pure
internallibrary), so no pin regeneration and no redeploy is required.Remaining-gaps checklist
decimalStringTofixedPointprobed.>=/>, frac-add guard</<=) — no behaviour can distinguish them, no test added.src/change is exactly the single empty-fraction guard from the Parser accepts a bare trailing decimal point ('1.') as a valid empty fraction — ruled invalid #24 fix; full suite green (83 tests, was 78);forge fmt --checkandforge lintclean.LibFixedPointDecimalScale,LibWillOverflow,LibFixedPointDecimalArithmeticOpenZeppelin,LibFixedPointDecimalFormat) are at 100% line/branch and heavily fuzzed against a slow reference implementation; not re-probed in this scoped pass (candidates for a follow-up scoped pass if desired).Triage note (not a bug, not in this PR's scope to change)
A fraction of >77 digits reports
ParseDecimalOverflowrather thanParseDecimalPrecisionLoss, because the inner-parse overflow check runs before thedigits > 18check. Both outcomes reject the (invalid) string; only the selectordiffers from a "precision-loss-first" reading. The new test pins the current
(Overflow) behaviour.
Closes #24
QA
testDecimalStringToFixedPointEmptyFraction,testDecimalStringToFixedPointEmptyFractionFuzz(uint256),testDecimalStringToFixedPointInvalidAfterInteger(carries the replaced"1."revert pin) — each fails on base (verified by deleting the empty-fraction guard fromdecimalStringTofixedPoint, i.e. restoring the pre-Parser accepts a bare trailing decimal point ('1.') as a valid empty fraction — ruled invalid #24-fix parser, and running the parse suite: exactly these 3 failed with0x00000000… != 0x3e8e62d6…— success whereParseDecimalInvalidStringis required — while both fractional-overflow tests and the pre-existing tests stayed green; guard restored, working tree byte-identical to the pushed commit, full suite re-run green 83/83)src/lib/parse/LibFixedPointDecimalParse.solempty-fraction guardif (cursor == fracStart) { return (ParseDecimalInvalidString.selector, 0); }→ deleted (re-allow the empty fraction) → killed by all three discriminating tests above; the test-suite side is the 15-behaviour matrix in this body1e18,123e18,type(uint256).max) derived from decimal semantics, never recomputed through the parser; the expected selector is the pre-existingParseDecimalInvalidStringerror named by the ruling in Parser accepts a bare trailing decimal point ('1.') as a valid empty fraction — ruled invalid #24"0.","1.","123.","00.", 60-digit integer +".","1.e5", and a fuzz over all non-overflowing integers +"."), pinned selector0x3e8e62d6, sweep found only this suite's former"1."pin (replaced). Parser accepts a bare trailing decimal point ('1.') as a valid empty fraction — ruled invalid #24's category note about sibling parsers in OTHER repos (rain.math.float, rainlang literal parsing) is outside this repo and stays with Parser accepts a bare trailing decimal point ('1.') as a valid empty fraction — ruled invalid #24's note; this PR closes theLibFixedPointDecimalParsefix it names.Test summary: merged head 83 passed / 0 failed;
forge fmt --checkandforge lintclean.🤖 Generated with Claude Code