Skip to content

test: mutation-harden LibFixedPointDecimalParse coverage#22

Open
thedavidmeister wants to merge 3 commits into
mainfrom
2026-06-15-fixedpoint-coverage
Open

test: mutation-harden LibFixedPointDecimalParse coverage#22
thedavidmeister wants to merge 3 commits into
mainfrom
2026-06-15-fixedpoint-coverage

Conversation

@thedavidmeister

@thedavidmeister thedavidmeister commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

What

Scoped adversarial-mutation-test coverage pass over the core string→fixed-point
parser LibFixedPointDecimalParse.decimalStringTofixedPoint. This was the only
module 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 empty
fraction. 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.sol
now requires at least one digit after the point, returning
ParseDecimalInvalidString.selector for 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 are
kept 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-trip
tests already kill most mutants. Two genuine survivors were found and killed.

Mutation matrix (behaviour → mutation → killer)

Behaviour Mutation vs existing suite Disposition
Integer overflow guard !=== killed existing overflow test ✓
Decimal-point gate (L44) == 0false SURVIVED GAP → new testDecimalStringToFixedPointInvalidAfterInteger
Garbage-after-frac gate cursor<endfalse killed existing ✓
Strip lower-bound (L60) >=> fracStart survived (both) equivalent mutant — a '0' at fracStart contributes 0 whether stripped or parsed
Strip mask flag == 1== 0 killed existing ✓
Strip recombine +1+0 killed existing ✓
Strip init cursor-1cursor-0 killed existing ✓
Strip decrement --++ killed existing ✓
cursor > fracStart gate true killed existing ✓
Frac-error propagate (L71) (errorSelector,0)(0,0) SURVIVED GAP → new testDecimalStringToFixedPointFailureFractionalPartOverflow
Precision-loss boundary >18>19 killed existing precision-loss test ✓
Precision-loss boundary >18>=18 killed existing ✓
ooms = 18 - digits 17 - digits killed existing ✓
Frac-add overflow guard (L84) <<= survived (both) equivalent mutantscaledFrac is always strictly > 0 (frac > 0 after strip, frac*10**ooms < 2^256), so value == preValue is impossible
Frac-add overflow guard if (value<preValue)if (false) killed existing overflow test ✓

The two gaps filled

  • Fractional-part overflow (line 71 — previously the one uncovered line). A
    fraction with more than 77 digits overflows uint256 inside the inner
    unsafeDecimalStringToInt, before the digits > 18 precision-loss check is
    reached, and the resulting ParseDecimalOverflow selector is returned from the
    fractional error branch. No existing test passed an overflowing fraction. New
    test pins a 78-nine fraction → ParseDecimalOverflow.
  • Decimal-point gate (line 44). A non-point character directly after the
    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-existing
    corrupt-integer tests only reached this code via inputs ("1a1.1") whose garbage
    is 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 empty
    fraction 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 → 1e18 and
    was replaced per the rework note).

Parser fix carried from #24 (PR #25 merged in)

Rejected now (ParseDecimalInvalidString, selector 0x3e8e62d6):

  • "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
  • Fuzz: every non-overflowing integer with "." appended

Unchanged:

  • "1", "1.0", "0", "0.0", "123", "123.0", and the uint256-max string still parse to the same values as before (pinned)
  • "." and ".5" still reject with ParseEmptyDecimalString (empty integer part, rejected before the fraction is considered; their status is pinned, not changed)
  • Sweep for reliance on the permissive acceptance: the only occurrence in the repo was the "1." pin in this suite, replaced here. LibFixedPointDecimalFormat.fixedPointToDecimalString only emits a . when the fraction is nonzero with at least one nonzero digit after it, so the testStringRoundTripFuzz round-trip is unaffected.

This repo has no generated pointer/deploy-constant files and nothing deployed
(pure internal library), so no pin regeneration and no redeploy is required.

Remaining-gaps checklist

  • All 15 enumerated behaviours of decimalStringTofixedPoint probed.
  • Both genuine surviving mutants (decimal-point gate, frac-error propagation) killed.
  • Two residual survivors confirmed equivalent mutants (strip lower-bound >=/>, 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 --check and forge lint clean.
  • Other modules (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 ParseDecimalOverflow rather than
ParseDecimalPrecisionLoss, because the inner-parse overflow check runs before the
digits > 18 check. Both outcomes reject the (invalid) string; only the selector
differs from a "precision-loss-first" reading. The new test pins the current
(Overflow) behaviour.

Closes #24

QA

Test summary: merged head 83 passed / 0 failed; forge fmt --check and forge lint clean.

🤖 Generated with Claude Code

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>
@thedavidmeister thedavidmeister self-assigned this Jun 15, 2026
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@thedavidmeister, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 58 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4234cf81-9d61-46e3-8c5a-e0fe3af9e962

📥 Commits

Reviewing files that changed from the base of the PR and between dc85846 and 0e5cfe9.

📒 Files selected for processing (3)
  • audit/mutation-test-scans.json
  • src/lib/parse/LibFixedPointDecimalParse.sol
  • test/src/lib/parse/LibFixedPointDecimalParse.decimalStringToFixedPoint.t.sol
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 2026-06-15-fixedpoint-coverage

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thedavidmeister

Copy link
Copy Markdown
Contributor Author

Rework note (human reject, 2026-07-04): design ruling — an EMPTY FRACTION IS INVALID. The checkDecimalStringToFixedPoint("1.", 1e18) pin enshrines behavior ruled wrong; the parser itself must change (tracked with the ruling in the new parser issue: bare trailing point → ParseDecimalInvalidString). Rework: land the parser fix + this suite together — keep both overflow tests and the "1x"/"10x" gate tests as-is (unaffected), replace the "1." acceptance pin with a revert pin on the exact selector, and Closes the new issue.

…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>
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

🤖 ai:producer
Producer note: executed the 2026-07-04 rework note — merged the #25 parser fix into this branch per the land-together instruction (fast-forward to 0e5cfe9; #25 was based on this branch's head, so its diff is fully absorbed and the two branches now point at the same commit), adapted the "1." pin to expect revert on ParseDecimalInvalidString (0x3e8e62d6), kept both fractional-overflow tests and the "1x"/"10x" decimal-point-gate tests as-is. Test evidence: full suite green on the merged head (83 passed / 0 failed), forge fmt --check and forge lint clean; mutation validation — deleting the empty-fraction guard from decimalStringTofixedPoint (i.e. restoring the pre-fix parser) fails exactly the 3 discriminating tests (testDecimalStringToFixedPointEmptyFraction, testDecimalStringToFixedPointEmptyFractionFuzz, testDecimalStringToFixedPointInvalidAfterInteger) with 0x00000000… != 0x3e8e62d6…, guard restored → 83/83 green. PR body updated: Closes #24, QA evidence block appended.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

human:reject Human maintainer: rework required (sacred)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Parser accepts a bare trailing decimal point ('1.') as a valid empty fraction — ruled invalid

1 participant