Skip to content

Cover the generated directory being created, by making the directory injectable - #112

Merged
thedavidmeister merged 6 commits into
mainfrom
2026-08-16-issue-64
Aug 17, 2026
Merged

Cover the generated directory being created, by making the directory injectable#112
thedavidmeister merged 6 commits into
mainfrom
2026-08-16-issue-64

Conversation

@thedavidmeister

@thedavidmeister thedavidmeister commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Closes #64

This branch had been reduced to nothing, and is restored here

Read this first if you looked at this PR before today. Its previous head,
30478d4, was a merge of main that resolved by taking main wholesale and
discarded both of the branch's own commits. git diff b422d97 30478d4 was
empty: the head tree was byte-identical to main at b422d97, which is
pre-#138. Against today's main that head was not a no-op but a revert — it
would have put script/Build.sol, src/generated/CodeGennable.sol and
.github/workflows/build-pointers.yaml back.

The work has been reconstructed on top of current main and the branch now
carries a real diff again.

The gap is real, measured on main today

src/lib/LibFs.sol creates the generated directory for itself:

vm.createDir(GENERATED_DIR, true);

Comment that line out on main and the suite reports 145 tests passed, 0 failed. Re-run it after rm -rf src/generated, to rule out the directory
merely being inherited from an earlier run, and it is 145 passed again. The
mutant survives, so the bootstrap behaviour — and the NatSpec claim that "the
first generation in a repo does not need it committed already" — is unprobed.

The masker is the suite's own scaffolding. Both
test/lib/LibFs.buildFileForContract.t.sol:40 and
test/src/lib/LibFs.isPresent.t.sol:26 call vm.createDir(GENERATED_DIR, true)
in setUp, so the directory is always already there by the time the library
would have created it.

Those setUp calls are not the bug and are not removed here. They exist because
every generating test writes under GENERATED_DIR and forge runs test
contracts in parallel, so a test that deletes the directory to make it missing
races all of its siblings. That is the hazard issue #64 measured directly: 1
failure in 6 full-suite runs, 0 in 8 runs under -j 1.

What ships

Issue #64 offered two ways to land the test — serialize the suite, or make the
directory injectable. This takes the second, because the first taxes every run
forever to cover one line.

src/lib/LibFs.sol:

  • pathForContractIn(string dir, string contractName), private. The name
    check is unchanged, so the path is still a direct child of dir for every
    name accepted at all.
  • buildFileForContract(Vm, address, string dir, string contractName, string body), internal overload. dir is what gets created when missing and what
    the file is written a direct child of.

The existing pathForContract and four-argument buildFileForContract are now
each the new function applied to GENERATED_DIR and are otherwise untouched, so
no consumer sees a behaviour change. dir is interpolated verbatim and is not
checked — that is why pathForContractIn is private rather than internal, and
it is stated in the NatSpec of both new functions.

One NatSpec line is corrected while it is being touched: the directory is created
"along with any missing parent of it", which vm.createDir(dir, true) has always
done and the docstring did not say.

test/lib/LibFs.buildFileForContract.t.sol:

  • testBuildFileForContractCreatesTheDirectory owns
    src/generated/LibFsBuildCreatesDir/nested and never touches GENERATED_DIR
    itself, so it does not race a single sibling. Two levels are missing, so a
    create that made the leaf but not the missing parent fails here too.

Mutant proven dead

With vm.createDir(dir, true) removed from the new overload, on this branch:

[FAIL: vm.writeFile: failed to open file ".../src/generated/LibFsBuildCreatesDir/nested/LibFsBuildCreatesDir.sol": No such file or directory (os error 2)] testBuildFileForContractCreatesTheDirectory()
Ran 19 test suites: 145 tests passed, 1 failed, 0 skipped (146 total tests)

Exactly one test fails, and it is the new one. That is the same mutant that
survives 145/145 on main.

QA

origin/main = 7394a42, merged in (merge, not rebase).

  • nix develop -c forge testRan 19 test suites: 146 tests passed, 0 failed, 0 skipped (146 total tests). main is 145; the one added test is the whole
    difference.
  • nix develop -c forge fmt --check — clean, no diff.
  • git status --porcelain — empty after a full run.

thedavidmeister and others added 2 commits August 16, 2026 18:20
Take the directory as a parameter on an overload of buildFileForContract so a
test can drive the create against a directory it owns, and make the existing
four argument function that overload applied to GENERATED_DIR.

Closes #64

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The file cannot be read back at that path unless the directory it is in was
created, so the assertion above the read does no work: removing it leaves the
suite green.

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: 8 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: d984ffb9-453d-4e86-af4b-c66246359d69

📥 Commits

Reviewing files that changed from the base of the PR and between 7394a42 and acf1bf4.

📒 Files selected for processing (2)
  • src/lib/LibFs.sol
  • test/lib/LibFs.buildFileForContract.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 17, 2026 03:59
…injectable

The library's vm.createDir(GENERATED_DIR, true) had no test: commenting
it out on main leaves 145/145 passing, twice, including after
rm -rf src/generated. Both test/lib/LibFs.buildFileForContract.t.sol
and test/src/lib/LibFs.isPresent.t.sol create GENERATED_DIR in their own
setUp, which masks the library creating it.

Deleting GENERATED_DIR to make it missing is not an option: every
generating test writes under it and forge runs them in parallel, which
is why those setUp calls exist in the first place.

So the directory becomes injectable. A private pathForContractIn(dir,
name) and an internal buildFileForContract(vm, instance, dir, name,
body) overload; the existing four-argument function is that overload
applied to GENERATED_DIR and is otherwise untouched. The new test owns
src/generated/LibFsBuildCreatesDir/nested, two missing levels deep, so
a create that did not also make the missing parent fails too.

Mutant proven dead: with vm.createDir(dir, true) removed the new test
fails with vm.writeFile: ... No such file or directory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thedavidmeister
thedavidmeister merged commit 3b4c0f2 into main Aug 17, 2026
4 checks passed
thedavidmeister added a commit that referenced this pull request Aug 17, 2026
Re-sites the content hoist into #112's five-arg overload, where the unlink and
the write now live, and takes #56's move of the test file into the test/src
mirror tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
thedavidmeister added a commit that referenced this pull request Aug 17, 2026
Re-sites the unlink loop into #112's five-arg overload, which is where the
unlink and the write now live, and keeps main's `vm.createDir(dir, true)`.
The docstring change stays on the four-arg NatSpec, which is where the unlink
paragraph lives and which the overload defers to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
thedavidmeister added a commit that referenced this pull request Aug 17, 2026
`pathForTaggedContract` is `pathForContractIn(dirForTag(tag), contractName)`
and `buildFileForTaggedContract` is the five argument `buildFileForContract`
applied to `dirForTag(tag)`, so the name check, the separator, the extension,
the directory creation, the unlink guard and the write exist once. Argument
evaluation runs `dirForTag` before the call, so the tag is still checked before
the name and before any cheatcode.

`LibFsBuildFileForTaggedContractTest` creates `GENERATED_DIR` in `setUp`:
`testBuildFileForTaggedContractLeavesTheUntaggedFileAlone` writes its sentinel
there directly, and `src/generated/` holds no committed file, so on a fresh
clone that write has no parent directory unless another test in the contract
happened to run first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
thedavidmeister added a commit that referenced this pull request Aug 17, 2026
Take #138's deletion of script/Build.sol and src/generated/CodeGennable.sol,
and drop testBuildFileForContractCommittedArtifactIsCurrent with them: it read
the deleted artifact.

Re-site the licence and copyright parameters into #112's five-arg
buildFileForContract overload, which is where the write now lives, and thread
them through the wrapper. testBuildFileForContractCreatesTheDirectory, added on
main, passes them too.

Take main's testFilePrefixExact docstring, which subsumes the
testFilePrefixNamesNoScript main deliberately dropped in 95a1e89.
thedavidmeister added a commit that referenced this pull request Aug 17, 2026
Re-sites the orphaned-artifact refusal onto main's current `LibFs`:

- `buildFileForContract` is now the six-arg call applied to `GENERATED_DIR`
  over a seven-arg `dir` overload (#112), builds the whole file content
  before touching disk (#100), and unlinks in a `while` loop (#127). The
  check goes into the shared body, after `vm.createDir` because it is a read
  of that directory, and before the unlink so a refusal leaves the existing
  artifact where it found it.
- `requireNoOrphanedArtifact(vm, contractName)` is that check applied to
  `GENERATED_DIR`, over a private `requireNoOrphanedArtifactIn`, mirroring
  `pathForContract` / `pathForContractIn`. The overload reads the directory
  it writes into rather than always `GENERATED_DIR`.
- The test moves from `test/lib/` to `test/src/lib/` (#56), and its calls
  carry the licence and copyright `filePrefix` now takes (#135).
- `InvalidContractName` / `isContractNameSlow` are `InvalidIdentifier` /
  `isIdentifierSlow`, and forge-std is 1.16.2.
- The README's "Generated paths" section anchors ahead of "Formatter
  requirements": the worked-example section it sat under is gone (#138) and
  the publish section it appended to was rewritten (#140).

Drops the hand-set `[package].version = "0.2.0"` and the README paragraph
that justified it. Autopublish owns the version.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

vm.createDir(GENERATED_DIR, true) in LibFs.buildFileForContract has no test: deleting it leaves 134/134 passing

1 participant