Skip to content

Deduplicate LibHexStringExternal and NonConformingVm construction in the bytesToHex suite - #105

Merged
thedavidmeister merged 5 commits into
mainfrom
2026-08-16-issue-63
Aug 17, 2026
Merged

Deduplicate LibHexStringExternal and NonConformingVm construction in the bytesToHex suite#105
thedavidmeister merged 5 commits into
mainfrom
2026-08-16-issue-63

Conversation

@thedavidmeister

@thedavidmeister thedavidmeister commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Closes #63

What changed

test/lib/LibHexString.bytesToHex.t.sol built a fresh LibHexStringExternal
12 times and wrote Vm(address(new NonConformingVm(...))) 11 times. Two of the
LibHexStringExternal sites sat inside 2048-run fuzz tests, so those were 2048
deployments per test rather than one.

  • LibHexStringExternal internal immutable iExternal, built in the constructor,
    matching the iExternal pattern already in LibFsTest and
    LibFsBuildFileForContractTest. It holds no state, so one instance serves the
    whole suite.
  • function vmReturning(string memory toStringReturn) internal returns (Vm)
    builds the stub Vm. The stub is still built per case, because the return
    string is what each case is about, but the Vm(address(new ...)) wrapping is
    written once.
  • testBytesToHexSurvivesAbiBoundary no longer deploys, so it is external view. Leaving it external produced a new Warning (2018): Function state mutability can be restricted to view.

No test guards this change, and it can regress silently

This PR adds no test for what it changes. Nothing in the suite — before or after
— asserts how many contracts the test contract deploys, so the deduplication
can be undone without any test going red
. Putting new LibHexStringExternal()
back inside testBytesToHexSurvivesAbiBoundary, or adding a second
new NonConformingVm inside vmReturning, leaves the suite fully green. Those
two edits are mutants M1 and M3 in the matrix below and both are recorded there
as SURVIVED, measured, not assumed. The only signal a regression would give is
suite runtime and gas.

An earlier revision of this PR carried testBytesToHexDeploysOnlyStubVms, a
16-line guard that counted deployments off vm.getNonce(address(this)). It has
been removed and not replaced. It asserted on the test contract's nonce and
never on bytesToHex's output, so the only mutants it killed were mutants of the
test file itself; it coupled two tests by calling one from inside the other; and
it hardcoded "one stub per revert test", so every new revert case would have had
to edit it.

The deployment count is not rehomed anywhere — there is no
rainlanguage/rainix#317 rule for it and no follow-up issue. The gap above is
the accepted cost of this PR.

Where the issue's proposed fix was wrong

The issue proposed writing each revert case as

vm.expectRevert(...);
iExternal.bytesToHex(badVm("0xaa"), hex"aabb");

Arguments are evaluated after expectRevert is armed, so the new NonConformingVm inside the helper is the next frame foundry sees and it consumes
the expectation. That form is mutant M4 below, and it is red:
[FAIL: next call did not revert as expected] testBytesToHexRevertsOnTruncatedVmOutput().

The landed form builds the stub into a local first, then arms expectRevert,
then calls — the ordering the file already used.

The helper is named vmReturning, not badVm: two of its callers
(testBytesToHexAcceptsConformingVm,
testBytesToHexAcceptsConformingVmForEmptyData) deliberately pass a
conforming return string, and badVm would misname them.

QA

Everything below was run via nix develop -c, on 2026-08-16-issue-63 with
origin/main (b422d97, i.e. including merged #110 and #124) merged in — merge
commit ee9f46d, no rebase, no force-push.

  • forge test on the merge commit, before deleting the guard:
    Ran 18 test suites: 143 tests passed, 0 failed, 0 skipped (143 total tests)
  • forge test at HEAD, after deleting the guard:
    Ran 18 test suites: 142 tests passed, 0 failed, 0 skipped (142 total tests)
    — a drop of exactly one test, the deleted guard, with nothing else changed.
  • forge fmt --check: exit 0, no diff.
  • Oracle: unchanged from main for every test in the file. The expected
    strings come from the definition of Vm.toString(bytes) ("0x" plus two hex
    characters per byte), not from LibHexString.
  • Category check: the issue asks for one shared LibHexStringExternal and
    one helper for the NonConformingVm construction, across all 12 + 11 sites;
    both are covered at every site, and the proposed call ordering that breaks
    vm.expectRevert is corrected.

Mutation matrix

Run with mutation-probe against the whole forge test suite. Baseline green at
142 passed / 0 failed. Every mutant is an exact-string replacement asserted to
match its file exactly once, applied, run, then restored and re-verified
byte-exact, so a harness error cannot pass for "survived".

# File Mutation Verdict Killed by
M1 test/lib/LibHexString.bytesToHex.t.sol testBytesToHexSurvivesAbiBoundary deploys its own LibHexStringExternal per run again SURVIVED nothing — this is the gap named above
M2 test/lib/LibHexString.bytesToHex.t.sol vmReturning returns the real vm instead of the stub it built KILLED testBytesToHexRevertsOnEmptyVmOutput, …OnEmptyVmOutputForEmptyData, …OnOneCharacterVmOutput, …OnOverlongVmOutput, …OnTruncatedVmOutput
M3 test/lib/LibHexString.bytesToHex.t.sol vmReturning deploys a second NonConformingVm before returning SURVIVED nothing — this is the gap named above
M4 test/lib/LibHexString.bytesToHex.t.sol the issue's proposed ordering: stub built inline, after vm.expectRevert KILLED testBytesToHexRevertsOnTruncatedVmOutput
M5 src/lib/LibHexString.sol:44 drop the length equality: if eq(len, expectedLength)if 1 KILLED testBytesToHexRevertsOnOverlongVmOutput, testBytesToHexRevertsOnTruncatedVmOutput
M6 src/lib/LibHexString.sol:48 drop the prefix check: if eq(shr(240, mload(add(hexString, 0x20))), 0x3078)if 1 KILLED testBytesToHexRevertsOnUnprefixedVmOutput, testBytesToHexRevertsOnWrongFirstPrefixCharacter, testBytesToHexRevertsOnWrongSecondPrefixCharacter

The "killed by" lists are the probe's reported killers and are capped at five
entries per mutant, so they name at least these tests, not necessarily only
these.

M5 and M6 mutate the library rather than the test. They are the check that the
refactor did not weaken what the suite proves about bytesToHex — the revert
cases still catch a Vm return of the wrong length and a Vm return without the
prefix, through the shared iExternal exactly as they did through their own
local one.

CodeRabbit

CodeRabbit has not reviewed this PR. reviewThreads and reviews over GraphQL
both return zero at the time of writing; its earlier check reported
pass / Review rate limited. That green is an absence of review, not a passed
one.

claude added 2 commits August 16, 2026 18:17
The wrapper that forces the ABI boundary is deployed once in the
constructor and held as an immutable. The stub `Vm` is built through a
helper that names the return string each case is about.

Closes #63

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thedavidmeister thedavidmeister self-assigned this Aug 16, 2026
@coderabbitai

coderabbitai Bot 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: 54 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: 5fbe86a0-88eb-4e35-8b12-ae64e1359d3f

📥 Commits

Reviewing files that changed from the base of the PR and between b422d97 and 551f7da.

📒 Files selected for processing (1)
  • test/lib/LibHexString.bytesToHex.t.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.

thedavidmeister and others added 3 commits August 16, 2026 19:23
It asserts on vm.getNonce(address(this)), never on bytesToHex's output, so
its only kills are mutants of the test file itself. It also couples two
tests by calling one from inside the other and hardcodes one stub per
revert test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
build-pointers failed in nix-quick-install-action before any repo step ran:
`Fetching nix archives ... nix-2.24.12-x86_64-linux.tar.zstd` then
`zstd: /*stdin*\: unexpected end of file` / `tar: Child returned status 1`.
A truncated download of the installer tarball, not a defect in this branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thedavidmeister
thedavidmeister merged commit 2b3fb07 into main Aug 17, 2026
5 checks passed
Sign up for free to 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.

LibHexStringExternal and NonConformingVm constructed inline 12 and 11 times in LibHexString.bytesToHex.t.sol

2 participants