Skip to content

LibCodeGen: cover every unprobed function and pin the four wrap decisions - #39

Merged
thedavidmeister merged 2 commits into
mainfrom
2026-08-16-libcodegen-amt-coverage
Aug 16, 2026
Merged

LibCodeGen: cover every unprobed function and pin the four wrap decisions#39
thedavidmeister merged 2 commits into
mainfrom
2026-08-16-libcodegen-amt-coverage

Conversation

@thedavidmeister

@thedavidmeisterthedavidmeister commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Adversarial + mutation test pass over src/lib/LibCodeGen.sol. Tests only — no
source change.

Before this, three of the library's eleven functions had a test file
(filePrefix, bytes32ConstantString, addressConstantString) and the other
eight had nothing. None of the four wrap decisions was probed at all.

What is added

  • test/lib/LibCodeGenSlow.sol — a reference for the four *ConstantString
    declarations. LibCodeGen decides whether a declaration fits on one line by
    adding up magic numbers standing in for the literals it is about to
    concatenate (15 + name + 2 + 1 + 4 + hexData + 2, 17 + … + 8 + …). The
    reference instead concatenates the literals, builds the line, and measures it.
    The two agree only when every term of every sum is right. 120 and the
    "\n " wrap are spelled out again there rather than imported, so the
    reference does not move when the library's constants do.
  • test/concrete/ToolingMock.sol — answers all five tooling builders with
    distinct data, so a wrapper that asks the wrong builder emits the wrong hex
    rather than passing on data that happens to be identical.
  • Test files for bytecodeHashConstantString,
    describedByMetaHashConstantString, bytesConstantString,
    uint8ConstantString, opcodeFunctionPointersConstantString,
    literalParserFunctionPointersConstantString,
    operandHandlerFunctionPointersConstantString,
    subParserWordParsersConstantString,
    integrityFunctionPointersConstantString.
  • Wrap boundary tests added to the two existing constant string suites. No
    existing test was edited or deleted.

Suite goes from 9 tests / 4 suites to 51 tests / 13 suites.

What the wrap arithmetic turned out to be

Each of the four sums was re-derived from the literals the function actually
concatenates, and then checked against forge fmt itself on generated fixtures
at 116–123 characters. All four are exactly right, and > rather than >= is
also exactly right: forge fmt leaves a line of exactly 120 alone and breaks
one of 121 after the =, indenting the value by four. The tests now pin that at
120 and 121 for all four, in both directions — by name length and by value
length — so an off-by-one in any single term fails rather than producing a file
forge fmt --check rejects on some inputs and not others.

Note on describedByMetaHashConstantString

This function reads meta/<name>.rain.meta, and this repo's fs_permissions
grants read only on foundry.toml and read-write on src/generated, so the
read is refused under the repo's own config. The path it constructs is asserted
through the refusal, which quotes the path. The rest of the behaviour — the
comment, the constant name, and that the value is the keccak256 of the file's
bytes — is covered with a fixture written to the one directory the config does
grant, reached by a name that walks back out of meta/. That the name reaches
the path at all is unsanitised interpolation, which the test states.

Two flakes in the first commit, fixed in the second

Both suites below were green on the seeds and interleavings CI happened to draw,
and red on a clean tree locally. Neither was a source defect; both were the tests
sampling states they had no business sampling.

  • bytecodeHashConstantString fuzzed bytes straight into vm.etch, which
    refuses code shaped like an EIP-7702 delegation designator — leading bytes
    0xef01 — at any length other than 23. On an unlucky seed the suite failed on
    the very first fuzz run with Eip7702 is not 23 bytes long. The refused set
    was measured against the cheatcode rather than assumed: 0xef01 + 21 bytes is
    accepted, the same prefix at 22, 24 and 2 bytes is refused, and 0xef,
    0xef0000 and 0xef02… are all accepted. The fuzz domain now excludes exactly
    that shape and nothing wider.
  • describedByMetaHashConstantString had all four of its file-backed tests
    writing and then removing a single shared fixture path. That file is real state
    outliving the EVM, so one test's removeFile lands between another's
    writeFileBinary and the library's read, and that test fails on a file it had
    just written. It reproduced at a fixed --fuzz-seed, so the fuzzer was never
    involved. Each test owns a distinct path now.

Verified over 30 consecutive full-suite runs — seeds 1..20 plus ten repeats at
the seed the fixture race reproduced on — 51 tests green every time.

QA

  • Discriminating tests: every new test asserts the exact emitted string, not
    that a call did not revert. testBytesConstantStringAtMaxLength /
    …OverMaxLengthByName / …OverMaxLengthByData and their uint8, bytes32
    and address equivalents pin the wrap decision at exactly 120 and 121
    characters and assert the measured longest line, so both the constant and
    every term of the sum are constrained. The five wrapper tests set five
    distinct builder answers on one mock, so asking the wrong builder fails.
    Full suite: 51 tests, 13 suites, all pass; forge fmt --check clean.
  • Mutations applied: 125 mutants over src/lib/LibCodeGen.sol in three
    mutation-probe passes, each against a baseline proved green first — a flaky
    baseline scores mutants on the flake rather than on the mutation, which is why
    the two flakes above were fixed before any matrix was trusted.
    • Pass A — 95 semantic mutants (constants, every emitted literal, every
      builder call, each term of all four wrap sums, both wrap branches, the
      comparison operator): 95 KILLED, 0 SURVIVED, 0 NO-RUN, 0 HARNESS-ERROR.
    • Baseline attribution — the same 95 against a clone holding only the
      pre-existing 9 tests
      : 14 KILLED, 81 SURVIVED, 0 NO-RUN,
      0 HARNESS-ERROR.
      So 81 of the 95 were live coverage gaps that this PR
      closes; the 14 the old suite already caught are the filePrefix text and
      parts of the bytes32/address declarations.
    • Pass B — 30 structural/whitespace mutants (blank lines, trailing
      newlines, indentation, delimiters that pass A did not touch): 30 KILLED,
      0 SURVIVED, 0 NO-RUN, 0 HARNESS-ERROR.
    • Nothing survived in either pass against the new suite, so no mutant is
      reported as covered on the strength of a harness error, and there is no
      survivor left needing a discriminating test.
    • Killer attribution is incomplete by tooling, not by result: the probe names
      a killing test from a [FAIL …] testName( regex, and . does not cross a
      newline, so a forge assertion message that spans lines — which most of
      these are, being multi-line generated Solidity — yields no name. 8 of 95
      pass-A kills carry killer names; the other 87 are KILLED on the suite's own
      passed/failed tally, which is the probe's actual verdict source and is
      unaffected.
  • Oracle: the emitted text has to be valid Solidity and stable under
    forge fmt, so forge fmt is the oracle for the wrap decision rather than
    the code's own arithmetic. Ran it directly: generated fixtures of bytes,
    uint8, bytes32 and address declarations at every length from 116 to 123
    were passed through forge fmt and forge build, which is what fixes 120 as
    the last line that stays put and 121 as the first that breaks. The same run
    confirms forge fmt does not split a hex"…" literal, so a wrapped bytes
    line longer than 120 is stable rather than a defect, and that hex"" for
    empty data compiles.
  • Category check: the ask is coverage of src/lib/LibCodeGen.sol, and the
    category is every function in it plus both file level constants — eleven
    functions, all now with a test file, plus MAX_LINE_LENGTH and
    NEWLINE_DUE_TO_MAX_LENGTH pinned through the boundary tests rather than
    restated as equalities. Not covered, deliberately: an address with no code
    reaching bytecodeHashConstantString, which currently emits
    bytes32(0x00…00) from extcodehash with no guard. A passing test there
    would entrench that, so it is raised separately instead.

Adds test files for `bytecodeHashConstantString`,
`describedByMetaHashConstantString`, `bytesConstantString`,
`uint8ConstantString` and the five function pointer wrappers, and adds
wrap boundary coverage to the two existing constant string suites.
`test/lib/LibCodeGenSlow.sol` is a reference that builds the declaration
and measures it, instead of adding up magic numbers standing in for the
literals, so the two agree only when every term of the library's sum is
right. Boundary tests pin the decision at exactly 120 and 121 characters,
which is where `forge fmt` itself changes its mind.
`test/concrete/ToolingMock.sol` answers all five tooling builders with
distinct data, so a wrapper asking the wrong builder emits the wrong hex.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thedavidmeisterthedavidmeister self-assigned this Aug 16, 2026
@coderabbitai

coderabbitaiBot commented Aug 16, 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:37 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 @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 Plus

Run ID: e27cc59d-59c1-4bb8-9abb-9a4a8ddb4454

📥 Commits

Reviewing files that changed from the base of the PR and between c72eb89 and 2c71033.

📒 Files selected for processing (13)
  • test/concrete/ToolingMock.sol
  • test/lib/LibCodeGen.addressConstantString.t.sol
  • test/lib/LibCodeGen.bytecodeHashConstantString.t.sol
  • test/lib/LibCodeGen.bytes32ConstantString.t.sol
  • test/lib/LibCodeGen.bytesConstantString.t.sol
  • test/lib/LibCodeGen.describedByMetaHashConstantString.t.sol
  • test/lib/LibCodeGen.integrityFunctionPointersConstantString.t.sol
  • test/lib/LibCodeGen.literalParserFunctionPointersConstantString.t.sol
  • test/lib/LibCodeGen.opcodeFunctionPointersConstantString.t.sol
  • test/lib/LibCodeGen.operandHandlerFunctionPointersConstantString.t.sol
  • test/lib/LibCodeGen.subParserWordParsersConstantString.t.sol
  • test/lib/LibCodeGen.uint8ConstantString.t.sol
  • test/lib/LibCodeGenSlow.sol

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.

… other
Two of the suites added in the previous commit were green only by luck. Both
reproduce on a clean tree at HEAD and both were passing CI on a draw that
happened to miss them.
`bytecodeHashConstantString` fuzzes `bytes` straight into `vm.etch`, which
refuses code shaped like an EIP-7702 delegation designator — leading bytes
`0xef01` — at any length other than 23. The fuzzer reaches that shape on some
seeds and not others, so the suite failed on the first run with
`Eip7702 is not 23 bytes long`. The refused set was measured against the
cheatcode rather than assumed — `0xef01` + 21 bytes is accepted, the same prefix
at 22, 24 and 2 bytes is refused, and `0xef`, `0xef0000` and `0xef02…` are all
accepted — so the fuzz domain now excludes exactly that shape and nothing else.
`describedByMetaHashConstantString` had all four of its file-backed tests
writing and then removing one shared fixture path. The fixture is real state
that outlives the EVM, so one test's `removeFile` lands between another's
`writeFileBinary` and the library's read, and that test fails on a file it had
just written. It reproduced at a fixed `--fuzz-seed`, so the fuzzer was never
involved. Each test owns a distinct path now.
Verified over 30 consecutive full-suite runs — seeds 1..20 plus ten repeats at
the seed the fixture race reproduced on — all 51 tests green, `forge fmt
--check` clean. This is also a precondition for the mutation matrices: a flaky
baseline scores mutants KILLED on the flake rather than on the mutation, and
`mutation-probe` refuses to start against it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thedavidmeister

Copy link
Copy Markdown
ContributorAuthor

Reviewed 2c71033: ready. Tests only, 13 files, suite 9 tests / 4 suites to 51 / 13. Covers every function the three existing test files left unprobed: bytecodeHashConstantString, describedByMetaHashConstantString, bytesConstantString, uint8ConstantString and all five pointer wrappers.

Three matrices, and nothing survived:

passscoperesult
A95 semantic mutants, this branch95 killed, 0 survived
baselinethe same 95 against the ORIGINAL 9 tests14 killed, 81 survived
B30 structural/whitespace mutants30 killed, 0 survived

The baseline pass is what makes the first row mean anything: the pre-existing suite killed 14 of 95, so this closes 81 real gaps rather than re-pinning what was already pinned. Zero HARNESS-ERRORs, confirmed structurally rather than asserted — the branch's LibCodeGen.sol is byte-identical to c72eb89's, so the baseline validated the same 95 targets, 15 of them multi-line.

The hypothesis this group was pointed at is REFUTED. All four wrap sums are exactly right, re-derived from the literals each function concatenates and then checked against real forge fmt on generated fixtures at 116 to 123 characters: inline at 120, wrapping at 121, so > rather than >= is also correct. Three independent readings agree. forge fmt does not split hex"…", so the repo's own 138-character wrapped line is stable.

LibCodeGenSlow builds and measures the declaration instead of summing magic numbers, and re-spells 120 and "\n " rather than importing them — without that, mutants M01 to M04 survive against a co-mutated oracle.

Two defects in this branch's own tests were found and fixed before the matrices could run, and forge test on a clean tree at 7986572 was RED while CI was green on a lucky draw. Three fuzz tests piped unconstrained bytes into vm.etch, which refuses EIP-7702-designator-shaped code; four file-backed tests shared one fixture path and deleted it out from under each other, reproducing at a FIXED seed 1 run in 4. Both fixed in 2c71033 and verified over 30 consecutive runs, 51/51 green each time. mutation-probe refusing to start on a red baseline is what surfaced it.

Stated rather than smoothed over: killer attribution names a test for only 8 of 95 kills, because the probe's fail-pattern cannot cross the newlines in multi-line Solidity assertion messages. The KILLED verdicts come from the pass/fail tally and are unaffected. The PR body says so instead of implying attribution it does not have.

Findings from this group are filed separately and none is blessed by a test here: #44, #45, #46, #47, #48.

All five checks green. CodeRabbit reports Review rate limited, so the 0 unresolved threads is vacuous rather than clean.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@thedavidmeister@claude