Uh oh!
There was an error while loading. Please reload this page.
Cover LibFs: path structure and what buildFileForContract actually writes - #36
Conversation
… on disk Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan includes up to 1 review per rolling hour; 0 remain after this review. WalkthroughThe PR adds property-based tests for ChangesLibFs behavior tests
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk:⚪ Minimal · up to This change only adds focused coverage for filesystem path and generated-file behavior; no actionable merge-blocking risk remains beyond normal checks and review. Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ 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 |
…stale Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
thedavidmeister
commented
Aug 16, 2026
Reviewed bd16655: ready. Tests only — 18 mutants, 17 killed. Every behaviour of both functions is probed: each segment of the path concat, the concat order, the relative-vs-absolute form, the The surviving mutant is deleting A harness caveat worth recording, since it applies to any repo asserting multi-line strings: the probe's fail-pattern cannot name killers for the seven mutants whose One test reaches beyond Four findings came out of this group and are filed separately, none of them blessed by a test here: #38, #40, #41, #42. All five checks green. CodeRabbit reports |
Uh oh!
There was an error while loading. Please reload this page.
Tests only.
src/lib/LibFs.solhad 17 lines of coverage — one assertion onpathForContract("Foo")— and nothing at all onbuildFileForContract, whichis the only thing in this repo that touches disk and the function every consumer
actually calls.
This adds 13 tests across the two functions and reports the mutation matrix that
justifies each of them.
What is covered
pathForContract(intest/lib/LibFs.t.sol, alongside the existingtestPathForContract, which is untouched):testPathForContractStructure— fuzzed over arbitrary names, asserts the pathis exactly three regions positionally:
src/generated/, the name byte forbyte,
.sol. The length equality is what makes it exhaustive — it forbids anyextra byte anywhere, so a name can never be quoted, escaped, trimmed, case
folded or truncated on the way into the path.
testPathForContractDistinctNamesDistinctPaths— distinct names never share afile, so generation cannot silently overwrite one contract with another.
testPathForContractIsRelative— the path is never absolute.buildFileForContract(newtest/lib/LibFs.buildFileForContract.t.sol), allasserting the bytes on disk after the call, for the whole file:
testBuildFileForContractExactContent— prefix, then the bytecode hashconstant, then the body, nothing between and nothing after.
testBuildFileForContractWritesToPathForContract— the file lands at theliteral
src/generated/<Name>.sol, asserted against the literal as well asagainst
pathForContract, so the two functions agreeing with each other is notwhat makes it pass.
testBuildFileForContractFreshPath— the first generation for a contract,where nothing is at the path yet.
testBuildFileForContractReplacesExistingContent— a pre-existing file morethan 1000 bytes long is fully replaced, so any surviving tail fails.
testBuildFileForContractIdempotent— a second run over the same inputsproduces the same bytes. This is the property CI relies on when it regenerates
and diffs.
testBuildFileForContractEmptyBody— the prefix and the hash constant areemitted even with no body, which is what the docstring promises.
testBuildFileForContractBodyVerbatim— no separator is inserted before thebody and nothing in it is escaped.
testBuildFileForContractLeavesSiblingsAlone— generating one contract doesnot disturb another's file in the same directory.
testBuildFileForContractUsesTheGivenInstance— the hash comes from theaddress passed in, not from the caller or a fixed address.
testBuildFileForContractCommittedArtifactIsCurrent— the committedsrc/generated/CodeGennable.solstill opens with what this function writestoday.
Each of the other tests owns a distinct name under
src/generated/, none ofthem is
CodeGennable(the committed artifact), and each removes its fileagain.
That last one closes a gap outside
LibFsitself.src/generated/CodeGennable.solis committed and
script/Build.solproduces it throughbuildFileForContract,but nothing in
forge testnoticed if it went stale — the only guard was theseparate
rainix-copy-artifactsjob regenerating and diffing, which does notrun in the test job. Verified discriminating by hand: flipping one hex digit of
BYTECODE_HASHin the committed file fails it withcommitted artifact is stale, regenerate with script/Build.sol.The expected content is rebuilt in the test from the literal header text and
from
address.codehash— deliberately not by callingLibCodeGen.filePrefixand
LibCodeGen.bytecodeHashConstantString. Calling those would assert thelibrary agrees with itself and would follow any drift in them in silence.
Consumers have the literal committed in their repos, so the literal is the
oracle.
QA
Discriminating tests: 22 tests, 5 suites, all pass;
forge fmt --checkclean.Every new test asserts exact bytes rather than absence of a revert, and each
was checked to fail under at least one mutation of the line it covers (matrix
below). Baseline before this PR was 9 tests, 4 suites, 0 failures — confirmed
green on a fresh clone of
c72eb89before any probing.Mutations applied: 18 mutants over the whole of
src/lib/LibFs.sol, one perbehaviour, applied with
mutation-probeclassifying from forge's ownN passed; N failedtally. 17 killed, 1 survived.line -> mutation -> killing test:"src/generated/"->"src/generated"->testBuildFileForContractExactContent(+4)"src/generated/"->"generated/"->testBuildFileForContractExactContent(+4)".sol"->".txt"->testPathForContract,testPathForContractStructure,testBuildFileForContractWritesToPathForContracttestBuildFileForContractFreshPath(+4)testBuildFileForContractExactContent(+4)"src/generated/"->"/src/generated/"->testPathForContract,testPathForContractIsRelative,testPathForContractStructurepathForContract(contractName)->pathForContract(contractName + "Mutant")->testBuildFileForContractExactContent(+4)if (vm.exists(path))->if (!vm.exists(path))->testBuildFileForContractFreshPath(+4)if (vm.exists(path))->if (true)->testBuildFileForContractFreshPath(+4)vm.removeFile(path);-> removed -> SURVIVED, see belowpath-> a fixed path ->testBuildFileForContractWritesToPathForContract(+4)filePrefix()dropped -> 8 of the 9buildFileForContracttestsbytecodeHashConstantString(...)dropped -> 8 of the 9bodydropped -> 6 of the 9 (the two empty-body tests correctly cannot see it)instance->address(0)-> 8 of the 9"\n"inserted before the body -> 8 of the 9The "8 of the 9" and "6 of the 9" rows were resolved by re-running each mutant
and reading which tests still passed: forge prints a multi-line
assertEqfailure for these, which the probe's single-line fail-pattern cannot name a
killer from. The KILLED verdict itself comes from the failing tally, not from
that pattern.
Oracle: the emitted file, not the library. Expected content is the literal
header a consumer has committed, plus
address.codehashread throughSolidity's own member rather than through the library's
extcodehashassembly; expected path is the literal
src/generated/<Name>.sola consumerimports. Both are checkable without reading
LibFsorLibCodeGenat all,which is the point — a test built out of the functions under test can only
prove they are self-consistent. Idempotency is asserted as a property of the
bytes on disk across two calls, which is the same property
rainix-copy-artifactsasserts when it regenerates and diffs.Category check: the ask is coverage for
LibFs, and the category is everybehaviour in the file, not the two the seed candidate named. Enumerated from
the source: 3 concatenated parts in
pathForContract, and inbuildFileForContractthe path derivation, the existence guard, the removal,the write target, and the 3 concatenated parts of the content plus their
order. All 18 probed; the surviving one is reported rather than papered over.
Nothing outside
test/changes.The surviving mutant
Deleting
vm.removeFile(path)entirely changes no observable outcome.vm.writeFiletruncates, so remove-then-write and write-alone leave identical bytes at the
path in every state reachable through cheatcodes, and no test can tell them
apart without
ffi(which this repo does not enable). It is left surviving andreported to the campaign rather than killed with a test that asserts something
the code does not actually guarantee. The docstring attributing idempotency to
the removal is a separate matter from this PR.
The existence guard around it is a different story and is genuinely load
bearing:
vm.removeFilereverts on a path that is not there, so inverting orremoving the guard breaks first generation.
testBuildFileForContractFreshPathis what proves it.