From 3c76adebd8290b3179ecef085a7b0b364e795cab Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Tue, 15 Sep 2026 16:10:08 +0000 Subject: [PATCH 1/4] Clear the snapshot test fixture roots at the start of every run A test that fails reverts where it fails, and a mismatched `vm.expectRevert` fires at the guarded call, upstream of every `vm.removeDir` in the contract, so the run that leaves a fixture behind is a run that already failed. The next run then read that residue as if a test had put it there: a leftover `/` makes `freeze` refuse `SnapshotAlreadyFrozen` before it reaches the ordering guard `testFreezeChecksTheRecordItIsAppendingTo` exists to observe, so one failure turned a repeatable test into a permanently red one naming a different cause. `setUp` clears the contract's fixture roots before any of its tests run, which is the only point where clearing is safe: the roots are split one per test because forge runs the tests in a contract concurrently, and forge runs `setUp` once per contract. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN --- test/src/lib/LibRainDeploySnapshot.t.sol | 137 ++++++++++++++++++++++- 1 file changed, 135 insertions(+), 2 deletions(-) diff --git a/test/src/lib/LibRainDeploySnapshot.t.sol b/test/src/lib/LibRainDeploySnapshot.t.sol index 8d4b725..e5f4619 100644 --- a/test/src/lib/LibRainDeploySnapshot.t.sol +++ b/test/src/lib/LibRainDeploySnapshot.t.sol @@ -32,6 +32,86 @@ import {LibMemoryKV, MemoryKV, MemoryKVKey, MemoryKVVal} from "rain-lib-memkv-0. contract LibRainDeploySnapshotTest is Test { using LibMemoryKV for MemoryKV; + /// A run MUST NOT inherit the fixtures of the run before it. + /// + /// Every test here reads before it removes and asserts after, so that an + /// assertion that reverts still reverts on a clean tree. A mismatched + /// `vm.expectRevert` is the case that discipline cannot cover: it fires at + /// the guarded call, which is upstream of the removal, so the run that + /// leaves a fixture behind is a run that already failed. What is left is + /// then read by the NEXT run as if the test had put it there — a leftover + /// `/` refuses the cut as `SnapshotAlreadyFrozen` before `freeze` + /// reaches the guard a test is there to observe, so one failure turns a + /// repeatable test into a permanently red one naming a cause that is not + /// its own. + /// + /// Clearing at the START of the run, rather than at the end of each test, + /// is what makes the outcome independent of the runs before it. It is also + /// the only point where clearing is safe: the roots are split one per test + /// because forge runs the tests in a contract concurrently, and forge runs + /// `setUp` once per contract before any of them, so nothing here is + /// removing a directory a test is holding. + function setUp() external { + string[] memory roots = fixtureRoots(); + for (uint256 i = 0; i < roots.length; i++) { + clearFixtureRoot(roots[i]); + } + } + + /// Removes one fixture root and everything under it, if it is there. + /// @param root The root to clear. + function clearFixtureRoot(string memory root) internal { + if (vm.exists(root)) { + //forge-lint: disable-next-line(unsafe-cheatcode) + vm.removeDir(root, true); + } + } + + /// Every directory this contract writes fixtures into. + /// + /// `FROZEN_FIXTURE_ROOT` is deliberately absent: it is a committed fixture + /// rather than one a test writes, so clearing it would delete it from the + /// tree. A root added to this contract and not added here is a root that + /// keeps the defect this list exists to close. + /// @return The fixture roots, in no particular order. + function fixtureRoots() internal pure returns (string[] memory) { + string[] memory roots = new string[](33); + roots[0] = FIXTURE_ROOT; + roots[1] = MISSING_FIXTURE_ROOT; + roots[2] = NESTED_FIXTURE_ROOT; + roots[3] = TAG_SHAPED_FIXTURE_PARENT; + roots[4] = RELEASED_FIXTURE_ROOT; + roots[5] = SELECTED_FIXTURE_ROOT; + roots[6] = EMPTY_RELEASE_FIXTURE_ROOT; + roots[7] = NOTHING_TO_FREEZE_FIXTURE_ROOT; + roots[8] = FREEZE_FIXTURE_ROOT; + roots[9] = FREEZE_MULTI_FIXTURE_ROOT; + roots[10] = RECUT_FIXTURE_ROOT; + roots[11] = RECUT_EMPTY_FIXTURE_ROOT; + roots[12] = FREEZE_GUARD_FIXTURE_ROOT; + roots[13] = STALE_CUT_FIXTURE_ROOT; + roots[14] = GUARD_ORDER_FIXTURE_ROOT; + roots[15] = REFUSED_REGENERATION_FIXTURE_ROOT; + roots[16] = REGENERATION_COUNT_FIXTURE_ROOT; + roots[17] = LATE_FAILURE_FIXTURE_ROOT; + roots[18] = BAD_NAME_FIXTURE_ROOT; + roots[19] = APPEND_FIXTURE_ROOT; + roots[20] = NEWEST_FIXTURE_ROOT; + roots[21] = UNRELEASED_FIXTURE_ROOT; + roots[22] = BELOW_FIXTURE_ROOT; + roots[23] = EQUAL_FIXTURE_ROOT; + roots[24] = GREATER_FIXTURE_ROOT; + roots[25] = FIXTURE_LIB_ROOT; + roots[26] = LibRainDeploySnapshot.dirForSnapshot(SNAPSHOT_PATH_FIXTURE_DIR); + roots[27] = LibRainDeploySnapshot.dirForSnapshot(SNAPSHOT_DEFAULTS_FIXTURE_DIR); + roots[28] = LibRainDeploySnapshot.dirForSnapshot(HEADER_FIXTURE_DIR); + roots[29] = LibRainDeploySnapshot.dirForSnapshot(RECORD_FIXTURE_DIR); + roots[30] = LibRainDeploySnapshot.dirForSnapshot(ORDER_FIXTURE_DIR); + roots[31] = LibRainDeploySnapshot.dirForSnapshot(DEPENDENCIES_FIXTURE_DIR); + roots[32] = LibRainDeploySnapshot.dirForSnapshot(CONSENSUS_FIXTURE_DIR); + return roots; + } + /// External wrapper so `vm.expectRevert` lands at the right call depth. /// @param version The version to convert. /// @return The tag. @@ -472,6 +552,10 @@ contract LibRainDeploySnapshotTest is Test { ); } + /// Where `testWriteSnapshotWritesTheSnapshotAtItsPath` points the writer. + /// Its own directory, for the reason `HEADER_FIXTURE_DIR` has one. + string constant SNAPSHOT_PATH_FIXTURE_DIR = "writeSnapshotNotATag"; + /// A snapshot MUST land at the path this library says it does, and writing /// one over a directory that is already there is the ORDINARY case: the /// rolling snapshot is regenerated into the same `candidate/` on every @@ -489,7 +573,7 @@ contract LibRainDeploySnapshotTest is Test { /// a path, and being a strict `X_Y_Z` triple on top of that is what makes it /// a release. function testWriteSnapshotWritesTheSnapshotAtItsPath() external { - string memory dir = "writeSnapshotNotATag"; + string memory dir = SNAPSHOT_PATH_FIXTURE_DIR; assertFalse(LibRainDeploySnapshot.isTag(dir)); //forge-lint: disable-next-line(unsafe-cheatcode) vm.createDir(LibRainDeploySnapshot.dirForSnapshot(dir), true); @@ -2310,6 +2394,51 @@ contract LibRainDeploySnapshotTest is Test { assertEq(record.length, 1); } + /// Where the run-start clear is driven, holding the residue a failed run of + /// `testFreezeChecksTheRecordItIsAppendingTo` leaves. Its own tree, for the + /// reason the other freeze fixtures have theirs, and because this one is + /// deliberately dirty before it is used. + string constant STALE_CUT_FIXTURE_ROOT = "test/generated-freeze-stale"; + + /// Clearing a fixture root MUST take the whole tree under it, so a cut left + /// by an earlier run cannot decide this one. + /// + /// The residue is the exact shape `freeze` leaves when its refusal does not + /// fire: a real `/` cut holding a record. That directory is what + /// `freeze` looks for FIRST, so a clear that missed it — or that removed + /// only an empty root — would answer `SnapshotAlreadyFrozen` here, and the + /// ordering guard this fixture is built to reach would never run. The + /// refusal is spelled out in full rather than asserted as "it reverted", + /// because a test that accepted any revert would accept that one. + function testClearingAFixtureRootRemovesAStaleCut() external { + string memory tag = LibRainDeploySnapshot.deployTag(vm); + writeFixture(string.concat(STALE_CUT_FIXTURE_ROOT, "/", tag, "/", FIXTURE_CONTRACT, ".sol")); + + clearFixtureRoot(STALE_CUT_FIXTURE_ROOT); + bool staleCutExists = vm.exists(LibRainDeploySnapshot.dirForSnapshot(STALE_CUT_FIXTURE_ROOT, tag)); + + // The fixture the freeze-guard test builds, on the tree the clear just + // ran over: a release newer than the tag being cut, and a rolling + // snapshot ready to freeze. + writeFixture(string.concat(STALE_CUT_FIXTURE_ROOT, "/9_9_9/", FIXTURE_CONTRACT, ".sol")); + writeRollingFixture(STALE_CUT_FIXTURE_ROOT, FIXTURE_CONTRACT); + + string[] memory contractNames = new string[](1); + contractNames[0] = FIXTURE_CONTRACT; + + vm.expectRevert(abi.encodeWithSelector(NonMonotonicRelease.selector, tag, "9_9_9")); + this.externalFreezeAt(STALE_CUT_FIXTURE_ROOT, contractNames); + + // Read while the fixture is still there, asserted once it is gone. + string[] memory record = LibRainDeploySnapshot.frozenSnapshotPaths(vm, STALE_CUT_FIXTURE_ROOT); + + //forge-lint: disable-next-line(unsafe-cheatcode) + vm.removeDir(STALE_CUT_FIXTURE_ROOT, true); + + assertFalse(staleCutExists); + assertEq(record.length, 1); + } + /// Where the guard ORDER is driven: a record that is already frozen, cut /// by a call that also names no contracts. Its own tree, for the reason /// the other freeze fixtures have theirs. @@ -2702,6 +2831,10 @@ contract LibRainDeploySnapshotTest is Test { this.externalCheckReleaseFollowsRecord(FROZEN_FIXTURE_ROOT, tag); } + /// Where `testWriteSnapshotDefaultsToTheOrgHeader` points the writer. Its + /// own directory, for the reason `HEADER_FIXTURE_DIR` has one. + string constant SNAPSHOT_DEFAULTS_FIXTURE_DIR = "writeSnapshotDefaults"; + /// The defaulting `writeSnapshot` MUST write exactly what the parameterised /// one writes when handed this org's two values, in that order. A swap or a /// wrong constant is otherwise only visible as a header nobody reads. @@ -2711,7 +2844,7 @@ contract LibRainDeploySnapshotTest is Test { /// address that already has code. The files are on disk, so they outlive /// the revert. function testWriteSnapshotDefaultsToTheOrgHeader() external { - string memory dir = "writeSnapshotDefaults"; + string memory dir = SNAPSHOT_DEFAULTS_FIXTURE_DIR; //forge-lint: disable-next-line(unsafe-cheatcode) vm.createDir(LibRainDeploySnapshot.dirForSnapshot(dir), true); From aa7db22875d00c167c50aa30ac536d093f0f3cae Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Tue, 15 Sep 2026 19:54:17 +0000 Subject: [PATCH 2/4] Clear one fixture tree at the start of every run The first pass listed every fixture root by hand in `setUp`, which is the defect again one level up: a root added to the contract and not added to the list is a root that still decides the next run. Every record fixture root is now a subdirectory of `test/generated-snapshot/`, so there is one name to clear and a new test takes a subdirectory rather than a list entry. `fixture-lib` is cleared as well, and is already one root with a subdirectory per test. It cannot move under the fixture tree: a generated lib left under a compiled root fails the next BUILD, which is upstream of anything `setUp` could do about it. `FIXTURE_ROOT` now names that one tree; the record-walk fixture that held the name is `WALK_FIXTURE_ROOT`. `test/fixture-record/` is committed and stays outside the tree, so nothing can clear it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN --- foundry.toml | 4 +- test/src/lib/LibRainDeploySnapshot.t.sol | 277 +++++++++++------------ 2 files changed, 130 insertions(+), 151 deletions(-) diff --git a/foundry.toml b/foundry.toml index 79bfbde..75ead46 100644 --- a/foundry.toml +++ b/foundry.toml @@ -43,8 +43,8 @@ fs_permissions = [ { access = "read", path = "./foundry.toml" }, { access = "read-write", path = "./src" }, # LibRainDeploySnapshotTest builds a record tree of its own under - # test/generated to drive the frozen-record walk. NOT src/generated: the - # inherited record check reads that root from contracts forge runs in + # test/generated-snapshot to drive the frozen-record walk. NOT src/generated: + # the inherited record check reads that root from contracts forge runs in # parallel, so a fixture release there would be one they have to fail on. { access = "read-write", path = "./test" }, # LibRainDeploySnapshotTest points the lib writers here — the alias writer, diff --git a/test/src/lib/LibRainDeploySnapshot.t.sol b/test/src/lib/LibRainDeploySnapshot.t.sol index e5f4619..cb75492 100644 --- a/test/src/lib/LibRainDeploySnapshot.t.sol +++ b/test/src/lib/LibRainDeploySnapshot.t.sol @@ -32,86 +32,63 @@ import {LibMemoryKV, MemoryKV, MemoryKVKey, MemoryKVVal} from "rain-lib-memkv-0. contract LibRainDeploySnapshotTest is Test { using LibMemoryKV for MemoryKV; + /// The one directory this contract writes record fixtures into. Every + /// fixture root below is a subdirectory of it, so the whole of what a run + /// can leave behind is one tree with one name, and a root added later is + /// covered by the clear in `setUp` without being added to anything. + /// + /// NOT `src/generated`: the inherited record check reads that root, in + /// other contracts, which forge runs in parallel with this one — a fixture + /// release there would be a release those contracts have to fail on, for as + /// long as it exists. A directory of its own rather than a shared + /// `test/generated-` prefix, because `BuildScript.t.sol` owns roots under + /// that prefix and forge runs it in parallel too: a tree is removed by + /// name, and a prefix is not a name. + /// + /// `FROZEN_FIXTURE_ROOT` is outside this tree, and being outside is what + /// keeps it: it is committed and read only, so a clear that reached it + /// would delete it from the repo. + string constant FIXTURE_ROOT = "test/generated-snapshot"; + /// A run MUST NOT inherit the fixtures of the run before it. /// - /// Every test here reads before it removes and asserts after, so that an - /// assertion that reverts still reverts on a clean tree. A mismatched - /// `vm.expectRevert` is the case that discipline cannot cover: it fires at - /// the guarded call, which is upstream of the removal, so the run that - /// leaves a fixture behind is a run that already failed. What is left is - /// then read by the NEXT run as if the test had put it there — a leftover - /// `/` refuses the cut as `SnapshotAlreadyFrozen` before `freeze` - /// reaches the guard a test is there to observe, so one failure turns a - /// repeatable test into a permanently red one naming a cause that is not - /// its own. - /// - /// Clearing at the START of the run, rather than at the end of each test, - /// is what makes the outcome independent of the runs before it. It is also - /// the only point where clearing is safe: the roots are split one per test - /// because forge runs the tests in a contract concurrently, and forge runs - /// `setUp` once per contract before any of them, so nothing here is - /// removing a directory a test is holding. + /// Every test here reads before it removes and asserts after, so a failed + /// ASSERTION still leaves a clean tree. A mismatched `vm.expectRevert` is + /// the case that discipline cannot cover: it fires at the guarded call, + /// which is upstream of every removal, so the run that leaves a fixture + /// behind is a run that already failed. The next run then reads that + /// residue as if a test had put it there — a leftover `/` is refused + /// as `SnapshotAlreadyFrozen` before `freeze` reaches the guard a test is + /// there to observe, so one failure turns a repeatable test into a + /// permanently red one naming a cause that is not its own. + /// + /// The START of the run is what makes the outcome independent of the runs + /// before it, and it is the only point where removing a whole tree is safe: + /// forge runs the tests in a contract concurrently, and runs `setUp` once, + /// before any of them. + /// + /// Two trees rather than one because a lib these tests emit cannot sit + /// under a compiled root — `FIXTURE_LIB_ROOT` says why, and a copy left + /// there fails the next BUILD, which is upstream of anything `setUp` could + /// do about it. The snapshots the defaulting writers put in the REAL + /// `src/generated/` are in neither tree and are not cleared: that root is + /// the writer under test, and those directories are deliberately not tag + /// shaped, so what a failure leaves there is passed over by every record + /// walk and overwritten by the test that wrote it. function setUp() external { - string[] memory roots = fixtureRoots(); - for (uint256 i = 0; i < roots.length; i++) { - clearFixtureRoot(roots[i]); - } + clearFixtureTree(FIXTURE_ROOT); + clearFixtureTree(FIXTURE_LIB_ROOT); } - /// Removes one fixture root and everything under it, if it is there. - /// @param root The root to clear. - function clearFixtureRoot(string memory root) internal { + /// Removes a fixture tree and everything under it, if it is there. + /// @param root The tree to remove. + function clearFixtureTree(string memory root) internal { if (vm.exists(root)) { //forge-lint: disable-next-line(unsafe-cheatcode) vm.removeDir(root, true); } } - /// Every directory this contract writes fixtures into. - /// - /// `FROZEN_FIXTURE_ROOT` is deliberately absent: it is a committed fixture - /// rather than one a test writes, so clearing it would delete it from the - /// tree. A root added to this contract and not added here is a root that - /// keeps the defect this list exists to close. - /// @return The fixture roots, in no particular order. - function fixtureRoots() internal pure returns (string[] memory) { - string[] memory roots = new string[](33); - roots[0] = FIXTURE_ROOT; - roots[1] = MISSING_FIXTURE_ROOT; - roots[2] = NESTED_FIXTURE_ROOT; - roots[3] = TAG_SHAPED_FIXTURE_PARENT; - roots[4] = RELEASED_FIXTURE_ROOT; - roots[5] = SELECTED_FIXTURE_ROOT; - roots[6] = EMPTY_RELEASE_FIXTURE_ROOT; - roots[7] = NOTHING_TO_FREEZE_FIXTURE_ROOT; - roots[8] = FREEZE_FIXTURE_ROOT; - roots[9] = FREEZE_MULTI_FIXTURE_ROOT; - roots[10] = RECUT_FIXTURE_ROOT; - roots[11] = RECUT_EMPTY_FIXTURE_ROOT; - roots[12] = FREEZE_GUARD_FIXTURE_ROOT; - roots[13] = STALE_CUT_FIXTURE_ROOT; - roots[14] = GUARD_ORDER_FIXTURE_ROOT; - roots[15] = REFUSED_REGENERATION_FIXTURE_ROOT; - roots[16] = REGENERATION_COUNT_FIXTURE_ROOT; - roots[17] = LATE_FAILURE_FIXTURE_ROOT; - roots[18] = BAD_NAME_FIXTURE_ROOT; - roots[19] = APPEND_FIXTURE_ROOT; - roots[20] = NEWEST_FIXTURE_ROOT; - roots[21] = UNRELEASED_FIXTURE_ROOT; - roots[22] = BELOW_FIXTURE_ROOT; - roots[23] = EQUAL_FIXTURE_ROOT; - roots[24] = GREATER_FIXTURE_ROOT; - roots[25] = FIXTURE_LIB_ROOT; - roots[26] = LibRainDeploySnapshot.dirForSnapshot(SNAPSHOT_PATH_FIXTURE_DIR); - roots[27] = LibRainDeploySnapshot.dirForSnapshot(SNAPSHOT_DEFAULTS_FIXTURE_DIR); - roots[28] = LibRainDeploySnapshot.dirForSnapshot(HEADER_FIXTURE_DIR); - roots[29] = LibRainDeploySnapshot.dirForSnapshot(RECORD_FIXTURE_DIR); - roots[30] = LibRainDeploySnapshot.dirForSnapshot(ORDER_FIXTURE_DIR); - roots[31] = LibRainDeploySnapshot.dirForSnapshot(DEPENDENCIES_FIXTURE_DIR); - roots[32] = LibRainDeploySnapshot.dirForSnapshot(CONSENSUS_FIXTURE_DIR); - return roots; - } - /// External wrapper so `vm.expectRevert` lands at the right call depth. /// @param version The version to convert. /// @return The tag. @@ -186,7 +163,7 @@ contract LibRainDeploySnapshotTest is Test { /// record check reads that root, in other contracts, which forge runs in /// parallel with this one — a fixture release there would be a release /// those contracts have to fail on, for as long as it exists. - string constant FIXTURE_ROOT = "test/generated"; + string constant WALK_FIXTURE_ROOT = "test/generated-snapshot/walk"; /// Writes one file into the fixture record. /// @@ -272,20 +249,20 @@ contract LibRainDeploySnapshotTest is Test { /// order is the filesystem's, so an assertion about position would be an /// assertion about the machine that ran it. function testFrozenSnapshotPathsFindsEveryReleaseAndNothingElse() external { - writeFixture(string.concat(FIXTURE_ROOT, "/0_0_1/MockDeployable.sol")); - writeFixture(string.concat(FIXTURE_ROOT, "/0_0_2/MockDeployableV2.sol")); - writeFixture(string.concat(FIXTURE_ROOT, "/0_0_2/Second.sol")); + writeFixture(string.concat(WALK_FIXTURE_ROOT, "/0_0_1/MockDeployable.sol")); + writeFixture(string.concat(WALK_FIXTURE_ROOT, "/0_0_2/MockDeployableV2.sol")); + writeFixture(string.concat(WALK_FIXTURE_ROOT, "/0_0_2/Second.sol")); // Not releases: the rolling snapshot, a version no freeze could have // written, a file loose in the root, and a file too deep to be a record. - writeFixture(string.concat(FIXTURE_ROOT, "/", LibRainDeploySnapshot.CANDIDATE, "/MockDeployable.sol")); - writeFixture(string.concat(FIXTURE_ROOT, "/0_0_3-rc1/MockDeployable.sol")); - writeFixture(string.concat(FIXTURE_ROOT, "/Loose.sol")); - writeFixture(string.concat(FIXTURE_ROOT, "/0_0_1/nested/TooDeep.sol")); + writeFixture(string.concat(WALK_FIXTURE_ROOT, "/", LibRainDeploySnapshot.CANDIDATE, "/MockDeployable.sol")); + writeFixture(string.concat(WALK_FIXTURE_ROOT, "/0_0_3-rc1/MockDeployable.sol")); + writeFixture(string.concat(WALK_FIXTURE_ROOT, "/Loose.sol")); + writeFixture(string.concat(WALK_FIXTURE_ROOT, "/0_0_1/nested/TooDeep.sol")); - string[] memory paths = LibRainDeploySnapshot.frozenSnapshotPaths(vm, FIXTURE_ROOT); + string[] memory paths = LibRainDeploySnapshot.frozenSnapshotPaths(vm, WALK_FIXTURE_ROOT); //forge-lint: disable-next-line(unsafe-cheatcode) - vm.removeDir(FIXTURE_ROOT, true); + vm.removeDir(WALK_FIXTURE_ROOT, true); MemoryKV pathSet = MemoryKV.wrap(0); for (uint256 i = 0; i < paths.length; i++) { @@ -293,20 +270,26 @@ contract LibRainDeploySnapshotTest is Test { } assertTrue( - pathSet.has(MemoryKVKey.wrap(keccak256(bytes(string.concat(FIXTURE_ROOT, "/0_0_1/MockDeployable.sol"))))) + pathSet.has( + MemoryKVKey.wrap(keccak256(bytes(string.concat(WALK_FIXTURE_ROOT, "/0_0_1/MockDeployable.sol")))) + ) ); assertTrue( - pathSet.has(MemoryKVKey.wrap(keccak256(bytes(string.concat(FIXTURE_ROOT, "/0_0_2/MockDeployableV2.sol"))))) + pathSet.has( + MemoryKVKey.wrap(keccak256(bytes(string.concat(WALK_FIXTURE_ROOT, "/0_0_2/MockDeployableV2.sol")))) + ) + ); + assertTrue( + pathSet.has(MemoryKVKey.wrap(keccak256(bytes(string.concat(WALK_FIXTURE_ROOT, "/0_0_2/Second.sol"))))) ); - assertTrue(pathSet.has(MemoryKVKey.wrap(keccak256(bytes(string.concat(FIXTURE_ROOT, "/0_0_2/Second.sol")))))); assertEq(paths.length, 3); } /// Where the missing-root case reads. Its own tree, for the same reason - /// `RELEASED_FIXTURE_ROOT` is not `FIXTURE_ROOT`: a root another test in - /// this contract builds and tears down is not a root this one can assert is - /// absent, and nothing writes here at all. - string constant MISSING_FIXTURE_ROOT = "test/generated-missing"; + /// `RELEASED_FIXTURE_ROOT` is not `WALK_FIXTURE_ROOT`: a root another test + /// in this contract builds and tears down is not a root this one can assert + /// is absent, and nothing writes here at all. + string constant MISSING_FIXTURE_ROOT = "test/generated-snapshot/missing"; /// A root that is not there at all MUST read as a repo that has released /// nothing, not as a failure. That is the state of every deploy repo before @@ -340,10 +323,10 @@ contract LibRainDeploySnapshotTest is Test { } /// Where the depth rule is driven. Its own tree, for the reason - /// `MISSING_FIXTURE_ROOT` is not `FIXTURE_ROOT`: forge runs the tests in a - /// contract concurrently, and a walk asserted to find exactly one file + /// `MISSING_FIXTURE_ROOT` is not `WALK_FIXTURE_ROOT`: forge runs the tests + /// in a contract concurrently, and a walk asserted to find exactly one file /// counts another test's fixture the moment it is in the tree being read. - string constant NESTED_FIXTURE_ROOT = "test/generated-nested"; + string constant NESTED_FIXTURE_ROOT = "test/generated-snapshot/nested"; /// A record file is a file DIRECTLY inside a release directory. One a level /// deeper is not in the record, even where the directory holding it is @@ -383,7 +366,7 @@ contract LibRainDeploySnapshotTest is Test { /// Where a record root whose own last segment is tag shaped is built. See /// `NESTED_FIXTURE_ROOT` for why it is a tree of its own. - string constant TAG_SHAPED_FIXTURE_PARENT = "test/generated-tag-shaped"; + string constant TAG_SHAPED_FIXTURE_PARENT = "test/generated-snapshot/tag-shaped"; /// A file loose in the root is not in the record, whatever the root is /// called. @@ -552,10 +535,6 @@ contract LibRainDeploySnapshotTest is Test { ); } - /// Where `testWriteSnapshotWritesTheSnapshotAtItsPath` points the writer. - /// Its own directory, for the reason `HEADER_FIXTURE_DIR` has one. - string constant SNAPSHOT_PATH_FIXTURE_DIR = "writeSnapshotNotATag"; - /// A snapshot MUST land at the path this library says it does, and writing /// one over a directory that is already there is the ORDINARY case: the /// rolling snapshot is regenerated into the same `candidate/` on every @@ -573,7 +552,7 @@ contract LibRainDeploySnapshotTest is Test { /// a path, and being a strict `X_Y_Z` triple on top of that is what makes it /// a release. function testWriteSnapshotWritesTheSnapshotAtItsPath() external { - string memory dir = SNAPSHOT_PATH_FIXTURE_DIR; + string memory dir = "writeSnapshotNotATag"; assertFalse(LibRainDeploySnapshot.isTag(dir)); //forge-lint: disable-next-line(unsafe-cheatcode) vm.createDir(LibRainDeploySnapshot.dirForSnapshot(dir), true); @@ -807,9 +786,9 @@ contract LibRainDeploySnapshotTest is Test { /// that, and nothing compiles it. /// /// Each writer test takes a subdirectory of its own, for the reason - /// `RELEASED_FIXTURE_ROOT` is not `FIXTURE_ROOT`: forge runs the tests in a - /// contract concurrently, and two of them writing one lib path read each - /// other's output. + /// `RELEASED_FIXTURE_ROOT` is not `WALK_FIXTURE_ROOT`: forge runs the tests + /// in a contract concurrently, and two of them writing one lib path read + /// each other's output. string constant FIXTURE_LIB_ROOT = "fixture-lib"; /// `LIB_DIR` is the directory a build points every lib writer at, so the @@ -1002,14 +981,14 @@ contract LibRainDeploySnapshotTest is Test { ); } - /// Where the released-lib record fixture is built. Its own tree rather - /// than `FIXTURE_ROOT`: forge runs the tests in a contract concurrently, - /// and two of them writing one record root see each other's releases. - string constant RELEASED_FIXTURE_ROOT = "test/generated-released"; + /// Where the released-lib record fixture is built. Its own tree rather than + /// `WALK_FIXTURE_ROOT`: forge runs the tests in a contract concurrently, and + /// two of them writing one record root see each other's releases. + string constant RELEASED_FIXTURE_ROOT = "test/generated-snapshot/released"; /// Where the selection fixture's record is built, for the same reason - /// `RELEASED_FIXTURE_ROOT` is not `FIXTURE_ROOT`. - string constant SELECTED_FIXTURE_ROOT = "test/generated-selected"; + /// `RELEASED_FIXTURE_ROOT` is not `WALK_FIXTURE_ROOT`. + string constant SELECTED_FIXTURE_ROOT = "test/generated-snapshot/selected"; /// The contract the fixture record freezes, and the one the writers are /// pointed at. NOT this repo's own `AddressRegistry`: the writers derive @@ -1951,9 +1930,9 @@ contract LibRainDeploySnapshotTest is Test { /// Where `testWriteReleasedSuitesAggregateDefaultsToTheOrgHeader` points /// the writer, for the reason `RELEASED_FIXTURE_ROOT` is not - /// `FIXTURE_ROOT`: forge runs the tests in a contract concurrently, and two - /// of them creating, writing, reading and removing one directory see each - /// other's files and each other's removals. Under `FIXTURE_LIB_ROOT`, + /// `WALK_FIXTURE_ROOT`: forge runs the tests in a contract concurrently, and + /// two of them creating, writing, reading and removing one directory see + /// each other's files and each other's removals. Under `FIXTURE_LIB_ROOT`, /// spelled out for the reason `AGGREGATE_PATH_FIXTURE_DIR` gives. string constant AGGREGATE_DEFAULTS_FIXTURE_DIR = "fixture-lib/aggregate-defaults"; @@ -2034,11 +2013,11 @@ contract LibRainDeploySnapshotTest is Test { /// checked before either guard below, so a freeze pointed at the real root /// is refused for a reason that is not the one being driven. The tag stays /// the real one; it is the ROOT that has to be a fixture's. - string constant EMPTY_RELEASE_FIXTURE_ROOT = "test/generated-freeze-empty"; + string constant EMPTY_RELEASE_FIXTURE_ROOT = "test/generated-snapshot/freeze-empty"; /// Where the nothing-to-freeze refusal is driven. Its own tree, for the /// reason `EMPTY_RELEASE_FIXTURE_ROOT` is its own tree. - string constant NOTHING_TO_FREEZE_FIXTURE_ROOT = "test/generated-freeze-nothing"; + string constant NOTHING_TO_FREEZE_FIXTURE_ROOT = "test/generated-snapshot/freeze-nothing"; /// A freeze that names no contracts MUST be refused. It would write /// nothing, report success, and leave `/` there — and an empty @@ -2093,27 +2072,27 @@ contract LibRainDeploySnapshotTest is Test { } /// Where the freeze fixture's record is built. Its own tree, for the same - /// reason `RELEASED_FIXTURE_ROOT` is not `FIXTURE_ROOT`, and NOT + /// reason `RELEASED_FIXTURE_ROOT` is not `WALK_FIXTURE_ROOT`, and NOT /// `src/generated`: every freeze here cuts a release under THIS repo's /// tag, and a transient `/` in the real record is a release the /// inherited record check has to fail on, from contracts forge runs in /// parallel with this one. - string constant FREEZE_FIXTURE_ROOT = "test/generated-freeze"; + string constant FREEZE_FIXTURE_ROOT = "test/generated-snapshot/freeze"; /// Where the multi-contract freeze fixture's record is built. Its own tree /// again, and for a sharper reason than the others: every freeze test cuts /// the SAME tag, so two of them sharing a root would have whichever ran /// second refused as a re-cut. - string constant FREEZE_MULTI_FIXTURE_ROOT = "test/generated-freeze-multi"; + string constant FREEZE_MULTI_FIXTURE_ROOT = "test/generated-snapshot/freeze-multi"; /// Where the re-cut fixture's record is built. Its own tree: this one is /// deliberately left frozen between the two calls, so no other test may /// share it. - string constant RECUT_FIXTURE_ROOT = "test/generated-recut"; + string constant RECUT_FIXTURE_ROOT = "test/generated-snapshot/recut"; /// Where the empty-tag-directory fixture's record is built. Its own tree, /// for the same reason: it is a frozen tag from the moment it is created. - string constant RECUT_EMPTY_FIXTURE_ROOT = "test/generated-recut-empty"; + string constant RECUT_EMPTY_FIXTURE_ROOT = "test/generated-snapshot/recut-empty"; /// A fixture snapshot's content: a licence header and a marker, and /// deliberately NOTHING a compiler would look at twice. @@ -2354,7 +2333,7 @@ contract LibRainDeploySnapshotTest is Test { /// Where the freeze's own ordering guard is driven. Its own tree, for the /// same reason the other freeze fixtures have theirs. - string constant FREEZE_GUARD_FIXTURE_ROOT = "test/generated-freeze-guard"; + string constant FREEZE_GUARD_FIXTURE_ROOT = "test/generated-snapshot/freeze-guard"; /// The ordering guard inside `freeze` MUST read the record the release is /// being APPENDED TO — the root `freeze` was handed — and not this repo's @@ -2394,27 +2373,31 @@ contract LibRainDeploySnapshotTest is Test { assertEq(record.length, 1); } - /// Where the run-start clear is driven, holding the residue a failed run of - /// `testFreezeChecksTheRecordItIsAppendingTo` leaves. Its own tree, for the - /// reason the other freeze fixtures have theirs, and because this one is - /// deliberately dirty before it is used. - string constant STALE_CUT_FIXTURE_ROOT = "test/generated-freeze-stale"; + /// Where the run-start clear is driven, over the residue a failed run of + /// `testFreezeChecksTheRecordItIsAppendingTo` leaves. A tree of its own for + /// the reason the other freeze fixtures have theirs, and dirty before it is + /// used on purpose. + string constant STALE_CUT_FIXTURE_ROOT = "test/generated-snapshot/freeze-stale"; - /// Clearing a fixture root MUST take the whole tree under it, so a cut left - /// by an earlier run cannot decide this one. + /// Clearing a fixture tree MUST take everything under it, so a cut an + /// earlier run left cannot decide this one. /// - /// The residue is the exact shape `freeze` leaves when its refusal does not - /// fire: a real `/` cut holding a record. That directory is what - /// `freeze` looks for FIRST, so a clear that missed it — or that removed - /// only an empty root — would answer `SnapshotAlreadyFrozen` here, and the - /// ordering guard this fixture is built to reach would never run. The + /// The residue planted here is the exact shape `freeze` leaves when its + /// refusal does not fire: a real `/` cut holding a record. That + /// directory is what `freeze` looks for FIRST, so a clear that missed it — + /// or that took only an empty root — answers `SnapshotAlreadyFrozen` here, + /// and the ordering guard this fixture is built to reach never runs. The /// refusal is spelled out in full rather than asserted as "it reverted", /// because a test that accepted any revert would accept that one. - function testClearingAFixtureRootRemovesAStaleCut() external { + /// + /// `setUp` calls the same clear on `FIXTURE_ROOT` whole; this one drives it + /// over a single tree under it, because the rest of that tree is the + /// fixtures of the tests forge is running concurrently with this one. + function testClearingAFixtureTreeRemovesAStaleCut() external { string memory tag = LibRainDeploySnapshot.deployTag(vm); writeFixture(string.concat(STALE_CUT_FIXTURE_ROOT, "/", tag, "/", FIXTURE_CONTRACT, ".sol")); - clearFixtureRoot(STALE_CUT_FIXTURE_ROOT); + clearFixtureTree(STALE_CUT_FIXTURE_ROOT); bool staleCutExists = vm.exists(LibRainDeploySnapshot.dirForSnapshot(STALE_CUT_FIXTURE_ROOT, tag)); // The fixture the freeze-guard test builds, on the tree the clear just @@ -2442,7 +2425,7 @@ contract LibRainDeploySnapshotTest is Test { /// Where the guard ORDER is driven: a record that is already frozen, cut /// by a call that also names no contracts. Its own tree, for the reason /// the other freeze fixtures have theirs. - string constant GUARD_ORDER_FIXTURE_ROOT = "test/generated-freeze-order"; + string constant GUARD_ORDER_FIXTURE_ROOT = "test/generated-snapshot/freeze-order"; /// A cut that is BOTH a re-cut and an empty release MUST be refused as the /// re-cut. @@ -2468,7 +2451,7 @@ contract LibRainDeploySnapshotTest is Test { /// Where a refused cut's regeneration would be seen. Its own tree, for the /// reason the other freeze fixtures have theirs. - string constant REFUSED_REGENERATION_FIXTURE_ROOT = "test/generated-freeze-refused"; + string constant REFUSED_REGENERATION_FIXTURE_ROOT = "test/generated-snapshot/freeze-refused"; /// The regeneration `testFreezeDoesNotRegenerateARefusedRelease` hands /// `freeze`. It writes the rolling snapshot, so a regeneration that ran @@ -2520,7 +2503,7 @@ contract LibRainDeploySnapshotTest is Test { /// Where the regeneration is counted. Its own tree, for the reason the /// other freeze fixtures have theirs. - string constant REGENERATION_COUNT_FIXTURE_ROOT = "test/generated-freeze-once"; + string constant REGENERATION_COUNT_FIXTURE_ROOT = "test/generated-snapshot/freeze-once"; /// How many times `regenerateCounted` has run. uint256 internal regenerations; @@ -2556,7 +2539,7 @@ contract LibRainDeploySnapshotTest is Test { /// Where a cut that fails on a LATER contract is driven. Its own tree, for /// the reason the other freeze fixtures have theirs. - string constant LATE_FAILURE_FIXTURE_ROOT = "test/generated-freeze-late"; + string constant LATE_FAILURE_FIXTURE_ROOT = "test/generated-snapshot/freeze-late"; /// A cut whose SECOND contract has nothing to freeze MUST leave nothing /// behind: not the tag directory, and not the first contract's file. @@ -2599,7 +2582,7 @@ contract LibRainDeploySnapshotTest is Test { /// Where a cut naming something that is not a contract is driven. Its own /// tree, for the reason the other freeze fixtures have theirs. - string constant BAD_NAME_FIXTURE_ROOT = "test/generated-freeze-badname"; + string constant BAD_NAME_FIXTURE_ROOT = "test/generated-snapshot/freeze-badname"; /// A contract name that is not a Solidity identifier MUST be refused as /// one, and nothing created. @@ -2622,7 +2605,7 @@ contract LibRainDeploySnapshotTest is Test { /// Where an append onto a record that already holds a release is driven. /// Its own tree, for the reason the other freeze fixtures have theirs. - string constant APPEND_FIXTURE_ROOT = "test/generated-freeze-append"; + string constant APPEND_FIXTURE_ROOT = "test/generated-snapshot/freeze-append"; /// A cut MUST leave every release already in the record exactly as it was. /// @@ -2668,26 +2651,26 @@ contract LibRainDeploySnapshotTest is Test { } /// Every record the ordering guard is driven against gets a root of its - /// own, for the reason `RELEASED_FIXTURE_ROOT` is not `FIXTURE_ROOT`: forge - /// runs the tests in a contract concurrently, and a guard test sees the - /// wrong newest release the moment another test's fixture is in the tree it - /// is reading. - string constant NEWEST_FIXTURE_ROOT = "test/generated-newest"; + /// own, for the reason `RELEASED_FIXTURE_ROOT` is not `WALK_FIXTURE_ROOT`: + /// forge runs the tests in a contract concurrently, and a guard test sees + /// the wrong newest release the moment another test's fixture is in the + /// tree it is reading. + string constant NEWEST_FIXTURE_ROOT = "test/generated-snapshot/newest"; /// The record with nothing released in it. See `NEWEST_FIXTURE_ROOT`. - string constant UNRELEASED_FIXTURE_ROOT = "test/generated-unreleased"; + string constant UNRELEASED_FIXTURE_ROOT = "test/generated-snapshot/unreleased"; /// The record a below-the-newest tag is refused against. See /// `NEWEST_FIXTURE_ROOT`. - string constant BELOW_FIXTURE_ROOT = "test/generated-below"; + string constant BELOW_FIXTURE_ROOT = "test/generated-snapshot/below"; /// The record the newest tag itself is refused against. See /// `NEWEST_FIXTURE_ROOT`. - string constant EQUAL_FIXTURE_ROOT = "test/generated-equal"; + string constant EQUAL_FIXTURE_ROOT = "test/generated-snapshot/equal"; /// The record strictly greater tags are accepted against. See /// `NEWEST_FIXTURE_ROOT`. - string constant GREATER_FIXTURE_ROOT = "test/generated-greater"; + string constant GREATER_FIXTURE_ROOT = "test/generated-snapshot/greater"; /// The record every fuzzed tag is put to. COMMITTED, and read only — see /// the fixture itself for why a fuzz cannot build the record it is put @@ -2831,10 +2814,6 @@ contract LibRainDeploySnapshotTest is Test { this.externalCheckReleaseFollowsRecord(FROZEN_FIXTURE_ROOT, tag); } - /// Where `testWriteSnapshotDefaultsToTheOrgHeader` points the writer. Its - /// own directory, for the reason `HEADER_FIXTURE_DIR` has one. - string constant SNAPSHOT_DEFAULTS_FIXTURE_DIR = "writeSnapshotDefaults"; - /// The defaulting `writeSnapshot` MUST write exactly what the parameterised /// one writes when handed this org's two values, in that order. A swap or a /// wrong constant is otherwise only visible as a header nobody reads. @@ -2844,7 +2823,7 @@ contract LibRainDeploySnapshotTest is Test { /// address that already has code. The files are on disk, so they outlive /// the revert. function testWriteSnapshotDefaultsToTheOrgHeader() external { - string memory dir = SNAPSHOT_DEFAULTS_FIXTURE_DIR; + string memory dir = "writeSnapshotDefaults"; //forge-lint: disable-next-line(unsafe-cheatcode) vm.createDir(LibRainDeploySnapshot.dirForSnapshot(dir), true); From fff3dbf4333887d8d4ac8235f3d91f2916ab5dd5 Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Tue, 15 Sep 2026 22:45:06 +0000 Subject: [PATCH 3/4] Cut the fixture-root, setUp and stale-cut doc blocks this branch added Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN --- foundry.toml | 4 +- test/src/lib/LibRainDeploySnapshot.t.sol | 100 ----------------------- 2 files changed, 1 insertion(+), 103 deletions(-) diff --git a/foundry.toml b/foundry.toml index 75ead46..a2d75d4 100644 --- a/foundry.toml +++ b/foundry.toml @@ -43,9 +43,7 @@ fs_permissions = [ { access = "read", path = "./foundry.toml" }, { access = "read-write", path = "./src" }, # LibRainDeploySnapshotTest builds a record tree of its own under - # test/generated-snapshot to drive the frozen-record walk. NOT src/generated: - # the inherited record check reads that root from contracts forge runs in - # parallel, so a fixture release there would be one they have to fail on. + # test/generated-snapshot to drive the frozen-record walk. { access = "read-write", path = "./test" }, # LibRainDeploySnapshotTest points the lib writers here — the alias writer, # the released-suites writer and the aggregate writer — one directory per diff --git a/test/src/lib/LibRainDeploySnapshot.t.sol b/test/src/lib/LibRainDeploySnapshot.t.sol index cb75492..45ee5be 100644 --- a/test/src/lib/LibRainDeploySnapshot.t.sol +++ b/test/src/lib/LibRainDeploySnapshot.t.sol @@ -32,56 +32,13 @@ import {LibMemoryKV, MemoryKV, MemoryKVKey, MemoryKVVal} from "rain-lib-memkv-0. contract LibRainDeploySnapshotTest is Test { using LibMemoryKV for MemoryKV; - /// The one directory this contract writes record fixtures into. Every - /// fixture root below is a subdirectory of it, so the whole of what a run - /// can leave behind is one tree with one name, and a root added later is - /// covered by the clear in `setUp` without being added to anything. - /// - /// NOT `src/generated`: the inherited record check reads that root, in - /// other contracts, which forge runs in parallel with this one — a fixture - /// release there would be a release those contracts have to fail on, for as - /// long as it exists. A directory of its own rather than a shared - /// `test/generated-` prefix, because `BuildScript.t.sol` owns roots under - /// that prefix and forge runs it in parallel too: a tree is removed by - /// name, and a prefix is not a name. - /// - /// `FROZEN_FIXTURE_ROOT` is outside this tree, and being outside is what - /// keeps it: it is committed and read only, so a clear that reached it - /// would delete it from the repo. string constant FIXTURE_ROOT = "test/generated-snapshot"; - /// A run MUST NOT inherit the fixtures of the run before it. - /// - /// Every test here reads before it removes and asserts after, so a failed - /// ASSERTION still leaves a clean tree. A mismatched `vm.expectRevert` is - /// the case that discipline cannot cover: it fires at the guarded call, - /// which is upstream of every removal, so the run that leaves a fixture - /// behind is a run that already failed. The next run then reads that - /// residue as if a test had put it there — a leftover `/` is refused - /// as `SnapshotAlreadyFrozen` before `freeze` reaches the guard a test is - /// there to observe, so one failure turns a repeatable test into a - /// permanently red one naming a cause that is not its own. - /// - /// The START of the run is what makes the outcome independent of the runs - /// before it, and it is the only point where removing a whole tree is safe: - /// forge runs the tests in a contract concurrently, and runs `setUp` once, - /// before any of them. - /// - /// Two trees rather than one because a lib these tests emit cannot sit - /// under a compiled root — `FIXTURE_LIB_ROOT` says why, and a copy left - /// there fails the next BUILD, which is upstream of anything `setUp` could - /// do about it. The snapshots the defaulting writers put in the REAL - /// `src/generated/` are in neither tree and are not cleared: that root is - /// the writer under test, and those directories are deliberately not tag - /// shaped, so what a failure leaves there is passed over by every record - /// walk and overwritten by the test that wrote it. function setUp() external { clearFixtureTree(FIXTURE_ROOT); clearFixtureTree(FIXTURE_LIB_ROOT); } - /// Removes a fixture tree and everything under it, if it is there. - /// @param root The tree to remove. function clearFixtureTree(string memory root) internal { if (vm.exists(root)) { //forge-lint: disable-next-line(unsafe-cheatcode) @@ -285,10 +242,6 @@ contract LibRainDeploySnapshotTest is Test { assertEq(paths.length, 3); } - /// Where the missing-root case reads. Its own tree, for the same reason - /// `RELEASED_FIXTURE_ROOT` is not `WALK_FIXTURE_ROOT`: a root another test - /// in this contract builds and tears down is not a root this one can assert - /// is absent, and nothing writes here at all. string constant MISSING_FIXTURE_ROOT = "test/generated-snapshot/missing"; /// A root that is not there at all MUST read as a repo that has released @@ -322,10 +275,6 @@ contract LibRainDeploySnapshotTest is Test { ); } - /// Where the depth rule is driven. Its own tree, for the reason - /// `MISSING_FIXTURE_ROOT` is not `WALK_FIXTURE_ROOT`: forge runs the tests - /// in a contract concurrently, and a walk asserted to find exactly one file - /// counts another test's fixture the moment it is in the tree being read. string constant NESTED_FIXTURE_ROOT = "test/generated-snapshot/nested"; /// A record file is a file DIRECTLY inside a release directory. One a level @@ -784,11 +733,6 @@ contract LibRainDeploySnapshotTest is Test { /// fails the build for every suite — including the copy a failing test /// deliberately leaves behind. `foundry.toml` grants this root for exactly /// that, and nothing compiles it. - /// - /// Each writer test takes a subdirectory of its own, for the reason - /// `RELEASED_FIXTURE_ROOT` is not `WALK_FIXTURE_ROOT`: forge runs the tests - /// in a contract concurrently, and two of them writing one lib path read - /// each other's output. string constant FIXTURE_LIB_ROOT = "fixture-lib"; /// `LIB_DIR` is the directory a build points every lib writer at, so the @@ -981,13 +925,8 @@ contract LibRainDeploySnapshotTest is Test { ); } - /// Where the released-lib record fixture is built. Its own tree rather than - /// `WALK_FIXTURE_ROOT`: forge runs the tests in a contract concurrently, and - /// two of them writing one record root see each other's releases. string constant RELEASED_FIXTURE_ROOT = "test/generated-snapshot/released"; - /// Where the selection fixture's record is built, for the same reason - /// `RELEASED_FIXTURE_ROOT` is not `WALK_FIXTURE_ROOT`. string constant SELECTED_FIXTURE_ROOT = "test/generated-snapshot/selected"; /// The contract the fixture record freezes, and the one the writers are @@ -1928,12 +1867,6 @@ contract LibRainDeploySnapshotTest is Test { /// `string.concat` in a constant initialiser. string constant AGGREGATE_PATH_FIXTURE_DIR = "fixture-lib/aggregate-path"; - /// Where `testWriteReleasedSuitesAggregateDefaultsToTheOrgHeader` points - /// the writer, for the reason `RELEASED_FIXTURE_ROOT` is not - /// `WALK_FIXTURE_ROOT`: forge runs the tests in a contract concurrently, and - /// two of them creating, writing, reading and removing one directory see - /// each other's files and each other's removals. Under `FIXTURE_LIB_ROOT`, - /// spelled out for the reason `AGGREGATE_PATH_FIXTURE_DIR` gives. string constant AGGREGATE_DEFAULTS_FIXTURE_DIR = "fixture-lib/aggregate-defaults"; /// The aggregate MUST land at `/LibReleasedSuites.sol`, holding @@ -2071,12 +2004,6 @@ contract LibRainDeploySnapshotTest is Test { assertFalse(cutExists); } - /// Where the freeze fixture's record is built. Its own tree, for the same - /// reason `RELEASED_FIXTURE_ROOT` is not `WALK_FIXTURE_ROOT`, and NOT - /// `src/generated`: every freeze here cuts a release under THIS repo's - /// tag, and a transient `/` in the real record is a release the - /// inherited record check has to fail on, from contracts forge runs in - /// parallel with this one. string constant FREEZE_FIXTURE_ROOT = "test/generated-snapshot/freeze"; /// Where the multi-contract freeze fixture's record is built. Its own tree @@ -2373,26 +2300,8 @@ contract LibRainDeploySnapshotTest is Test { assertEq(record.length, 1); } - /// Where the run-start clear is driven, over the residue a failed run of - /// `testFreezeChecksTheRecordItIsAppendingTo` leaves. A tree of its own for - /// the reason the other freeze fixtures have theirs, and dirty before it is - /// used on purpose. string constant STALE_CUT_FIXTURE_ROOT = "test/generated-snapshot/freeze-stale"; - /// Clearing a fixture tree MUST take everything under it, so a cut an - /// earlier run left cannot decide this one. - /// - /// The residue planted here is the exact shape `freeze` leaves when its - /// refusal does not fire: a real `/` cut holding a record. That - /// directory is what `freeze` looks for FIRST, so a clear that missed it — - /// or that took only an empty root — answers `SnapshotAlreadyFrozen` here, - /// and the ordering guard this fixture is built to reach never runs. The - /// refusal is spelled out in full rather than asserted as "it reverted", - /// because a test that accepted any revert would accept that one. - /// - /// `setUp` calls the same clear on `FIXTURE_ROOT` whole; this one drives it - /// over a single tree under it, because the rest of that tree is the - /// fixtures of the tests forge is running concurrently with this one. function testClearingAFixtureTreeRemovesAStaleCut() external { string memory tag = LibRainDeploySnapshot.deployTag(vm); writeFixture(string.concat(STALE_CUT_FIXTURE_ROOT, "/", tag, "/", FIXTURE_CONTRACT, ".sol")); @@ -2400,9 +2309,6 @@ contract LibRainDeploySnapshotTest is Test { clearFixtureTree(STALE_CUT_FIXTURE_ROOT); bool staleCutExists = vm.exists(LibRainDeploySnapshot.dirForSnapshot(STALE_CUT_FIXTURE_ROOT, tag)); - // The fixture the freeze-guard test builds, on the tree the clear just - // ran over: a release newer than the tag being cut, and a rolling - // snapshot ready to freeze. writeFixture(string.concat(STALE_CUT_FIXTURE_ROOT, "/9_9_9/", FIXTURE_CONTRACT, ".sol")); writeRollingFixture(STALE_CUT_FIXTURE_ROOT, FIXTURE_CONTRACT); @@ -2412,7 +2318,6 @@ contract LibRainDeploySnapshotTest is Test { vm.expectRevert(abi.encodeWithSelector(NonMonotonicRelease.selector, tag, "9_9_9")); this.externalFreezeAt(STALE_CUT_FIXTURE_ROOT, contractNames); - // Read while the fixture is still there, asserted once it is gone. string[] memory record = LibRainDeploySnapshot.frozenSnapshotPaths(vm, STALE_CUT_FIXTURE_ROOT); //forge-lint: disable-next-line(unsafe-cheatcode) @@ -2650,11 +2555,6 @@ contract LibRainDeploySnapshotTest is Test { assertTrue(recordSet.has(MemoryKVKey.wrap(keccak256(bytes(cutPath))))); } - /// Every record the ordering guard is driven against gets a root of its - /// own, for the reason `RELEASED_FIXTURE_ROOT` is not `WALK_FIXTURE_ROOT`: - /// forge runs the tests in a contract concurrently, and a guard test sees - /// the wrong newest release the moment another test's fixture is in the - /// tree it is reading. string constant NEWEST_FIXTURE_ROOT = "test/generated-snapshot/newest"; /// The record with nothing released in it. See `NEWEST_FIXTURE_ROOT`. From a4607e6f1756dbe23692badd2a377f1bb8c26bd9 Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Wed, 16 Sep 2026 00:20:49 +0000 Subject: [PATCH 4/4] test: put back the fixture-root and setUp rationale fff3dbf cut MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sweep's message says these blocks are what this branch added. Most of them are not: the doc blocks on `MISSING_FIXTURE_ROOT`, `NESTED_FIXTURE_ROOT`, `SELECTED_FIXTURE_ROOT`, `FREEZE_FIXTURE_ROOT`, `NEWEST_FIXTURE_ROOT`, `AGGREGATE_DEFAULTS_FIXTURE_DIR`, the `FIXTURE_LIB_ROOT` paragraph and the "Read while the fixture is still there" comment are all on main, and cutting them also left the comments that cross-reference them — "See `NEWEST_FIXTURE_ROOT`.", "for the reason `RELEASED_FIXTURE_ROOT` is not `WALK_FIXTURE_ROOT`" — pointing at nothing. What each of them carries is a constraint no line of code states: forge runs the tests in a contract concurrently, so a root shared between two of them is a test reading another test's fixtures, and a fixture release under `src/generated` is one the inherited record check fails on from the contracts running in parallel. The `setUp` block is this PR's whole thesis: why the START of the run, why a mismatched `vm.expectRevert` is the case that read-before-remove cannot cover, and why two trees rather than one. `clearFixtureTree`'s block stays cut: it restates the name and the signature. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN --- foundry.toml | 4 +- test/src/lib/LibRainDeploySnapshot.t.sol | 98 ++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/foundry.toml b/foundry.toml index a2d75d4..75ead46 100644 --- a/foundry.toml +++ b/foundry.toml @@ -43,7 +43,9 @@ fs_permissions = [ { access = "read", path = "./foundry.toml" }, { access = "read-write", path = "./src" }, # LibRainDeploySnapshotTest builds a record tree of its own under - # test/generated-snapshot to drive the frozen-record walk. + # test/generated-snapshot to drive the frozen-record walk. NOT src/generated: + # the inherited record check reads that root from contracts forge runs in + # parallel, so a fixture release there would be one they have to fail on. { access = "read-write", path = "./test" }, # LibRainDeploySnapshotTest points the lib writers here — the alias writer, # the released-suites writer and the aggregate writer — one directory per diff --git a/test/src/lib/LibRainDeploySnapshot.t.sol b/test/src/lib/LibRainDeploySnapshot.t.sol index 45ee5be..6b33941 100644 --- a/test/src/lib/LibRainDeploySnapshot.t.sol +++ b/test/src/lib/LibRainDeploySnapshot.t.sol @@ -32,8 +32,49 @@ import {LibMemoryKV, MemoryKV, MemoryKVKey, MemoryKVVal} from "rain-lib-memkv-0. contract LibRainDeploySnapshotTest is Test { using LibMemoryKV for MemoryKV; + /// The one directory this contract writes record fixtures into. Every + /// fixture root below is a subdirectory of it, so the whole of what a run + /// can leave behind is one tree with one name, and a root added later is + /// covered by the clear in `setUp` without being added to anything. + /// + /// NOT `src/generated`: the inherited record check reads that root, in + /// other contracts, which forge runs in parallel with this one — a fixture + /// release there would be a release those contracts have to fail on, for as + /// long as it exists. A directory of its own rather than a shared + /// `test/generated-` prefix, because `BuildScript.t.sol` owns roots under + /// that prefix and forge runs it in parallel too: a tree is removed by + /// name, and a prefix is not a name. + /// + /// `FROZEN_FIXTURE_ROOT` is outside this tree, and being outside is what + /// keeps it: it is committed and read only, so a clear that reached it + /// would delete it from the repo. string constant FIXTURE_ROOT = "test/generated-snapshot"; + /// A run MUST NOT inherit the fixtures of the run before it. + /// + /// Every test here reads before it removes and asserts after, so a failed + /// ASSERTION still leaves a clean tree. A mismatched `vm.expectRevert` is + /// the case that discipline cannot cover: it fires at the guarded call, + /// which is upstream of every removal, so the run that leaves a fixture + /// behind is a run that already failed. The next run then reads that + /// residue as if a test had put it there — a leftover `/` is refused + /// as `SnapshotAlreadyFrozen` before `freeze` reaches the guard a test is + /// there to observe, so one failure turns a repeatable test into a + /// permanently red one naming a cause that is not its own. + /// + /// The START of the run is what makes the outcome independent of the runs + /// before it, and it is the only point where removing a whole tree is safe: + /// forge runs the tests in a contract concurrently, and runs `setUp` once, + /// before any of them. + /// + /// Two trees rather than one because a lib these tests emit cannot sit + /// under a compiled root — `FIXTURE_LIB_ROOT` says why, and a copy left + /// there fails the next BUILD, which is upstream of anything `setUp` could + /// do about it. The snapshots the defaulting writers put in the REAL + /// `src/generated/` are in neither tree and are not cleared: that root is + /// the writer under test, and those directories are deliberately not tag + /// shaped, so what a failure leaves there is passed over by every record + /// walk and overwritten by the test that wrote it. function setUp() external { clearFixtureTree(FIXTURE_ROOT); clearFixtureTree(FIXTURE_LIB_ROOT); @@ -242,6 +283,10 @@ contract LibRainDeploySnapshotTest is Test { assertEq(paths.length, 3); } + /// Where the missing-root case reads. Its own tree, for the same reason + /// `RELEASED_FIXTURE_ROOT` is not `WALK_FIXTURE_ROOT`: a root another test + /// in this contract builds and tears down is not a root this one can assert + /// is absent, and nothing writes here at all. string constant MISSING_FIXTURE_ROOT = "test/generated-snapshot/missing"; /// A root that is not there at all MUST read as a repo that has released @@ -275,6 +320,10 @@ contract LibRainDeploySnapshotTest is Test { ); } + /// Where the depth rule is driven. Its own tree, for the reason + /// `MISSING_FIXTURE_ROOT` is not `WALK_FIXTURE_ROOT`: forge runs the tests + /// in a contract concurrently, and a walk asserted to find exactly one file + /// counts another test's fixture the moment it is in the tree being read. string constant NESTED_FIXTURE_ROOT = "test/generated-snapshot/nested"; /// A record file is a file DIRECTLY inside a release directory. One a level @@ -733,6 +782,11 @@ contract LibRainDeploySnapshotTest is Test { /// fails the build for every suite — including the copy a failing test /// deliberately leaves behind. `foundry.toml` grants this root for exactly /// that, and nothing compiles it. + /// + /// Each writer test takes a subdirectory of its own, for the reason + /// `RELEASED_FIXTURE_ROOT` is not `WALK_FIXTURE_ROOT`: forge runs the tests + /// in a contract concurrently, and two of them writing one lib path read + /// each other's output. string constant FIXTURE_LIB_ROOT = "fixture-lib"; /// `LIB_DIR` is the directory a build points every lib writer at, so the @@ -925,8 +979,13 @@ contract LibRainDeploySnapshotTest is Test { ); } + /// Where the released-lib record fixture is built. Its own tree rather than + /// `WALK_FIXTURE_ROOT`: forge runs the tests in a contract concurrently, and + /// two of them writing one record root see each other's releases. string constant RELEASED_FIXTURE_ROOT = "test/generated-snapshot/released"; + /// Where the selection fixture's record is built, for the same reason + /// `RELEASED_FIXTURE_ROOT` is not `WALK_FIXTURE_ROOT`. string constant SELECTED_FIXTURE_ROOT = "test/generated-snapshot/selected"; /// The contract the fixture record freezes, and the one the writers are @@ -1867,6 +1926,12 @@ contract LibRainDeploySnapshotTest is Test { /// `string.concat` in a constant initialiser. string constant AGGREGATE_PATH_FIXTURE_DIR = "fixture-lib/aggregate-path"; + /// Where `testWriteReleasedSuitesAggregateDefaultsToTheOrgHeader` points + /// the writer, for the reason `RELEASED_FIXTURE_ROOT` is not + /// `WALK_FIXTURE_ROOT`: forge runs the tests in a contract concurrently, and + /// two of them creating, writing, reading and removing one directory see + /// each other's files and each other's removals. Under `FIXTURE_LIB_ROOT`, + /// spelled out for the reason `AGGREGATE_PATH_FIXTURE_DIR` gives. string constant AGGREGATE_DEFAULTS_FIXTURE_DIR = "fixture-lib/aggregate-defaults"; /// The aggregate MUST land at `/LibReleasedSuites.sol`, holding @@ -2004,6 +2069,12 @@ contract LibRainDeploySnapshotTest is Test { assertFalse(cutExists); } + /// Where the freeze fixture's record is built. Its own tree, for the same + /// reason `RELEASED_FIXTURE_ROOT` is not `WALK_FIXTURE_ROOT`, and NOT + /// `src/generated`: every freeze here cuts a release under THIS repo's + /// tag, and a transient `/` in the real record is a release the + /// inherited record check has to fail on, from contracts forge runs in + /// parallel with this one. string constant FREEZE_FIXTURE_ROOT = "test/generated-snapshot/freeze"; /// Where the multi-contract freeze fixture's record is built. Its own tree @@ -2300,8 +2371,26 @@ contract LibRainDeploySnapshotTest is Test { assertEq(record.length, 1); } + /// Where the run-start clear is driven, over the residue a failed run of + /// `testFreezeChecksTheRecordItIsAppendingTo` leaves. A tree of its own for + /// the reason the other freeze fixtures have theirs, and dirty before it is + /// used on purpose. string constant STALE_CUT_FIXTURE_ROOT = "test/generated-snapshot/freeze-stale"; + /// Clearing a fixture tree MUST take everything under it, so a cut an + /// earlier run left cannot decide this one. + /// + /// The residue planted here is the exact shape `freeze` leaves when its + /// refusal does not fire: a real `/` cut holding a record. That + /// directory is what `freeze` looks for FIRST, so a clear that missed it — + /// or that took only an empty root — answers `SnapshotAlreadyFrozen` here, + /// and the ordering guard this fixture is built to reach never runs. The + /// refusal is spelled out in full rather than asserted as "it reverted", + /// because a test that accepted any revert would accept that one. + /// + /// `setUp` calls the same clear on `FIXTURE_ROOT` whole; this one drives it + /// over a single tree under it, because the rest of that tree is the + /// fixtures of the tests forge is running concurrently with this one. function testClearingAFixtureTreeRemovesAStaleCut() external { string memory tag = LibRainDeploySnapshot.deployTag(vm); writeFixture(string.concat(STALE_CUT_FIXTURE_ROOT, "/", tag, "/", FIXTURE_CONTRACT, ".sol")); @@ -2309,6 +2398,9 @@ contract LibRainDeploySnapshotTest is Test { clearFixtureTree(STALE_CUT_FIXTURE_ROOT); bool staleCutExists = vm.exists(LibRainDeploySnapshot.dirForSnapshot(STALE_CUT_FIXTURE_ROOT, tag)); + // The fixture the freeze-guard test builds, on the tree the clear just + // ran over: a release newer than the tag being cut, and a rolling + // snapshot ready to freeze. writeFixture(string.concat(STALE_CUT_FIXTURE_ROOT, "/9_9_9/", FIXTURE_CONTRACT, ".sol")); writeRollingFixture(STALE_CUT_FIXTURE_ROOT, FIXTURE_CONTRACT); @@ -2318,6 +2410,7 @@ contract LibRainDeploySnapshotTest is Test { vm.expectRevert(abi.encodeWithSelector(NonMonotonicRelease.selector, tag, "9_9_9")); this.externalFreezeAt(STALE_CUT_FIXTURE_ROOT, contractNames); + // Read while the fixture is still there, asserted once it is gone. string[] memory record = LibRainDeploySnapshot.frozenSnapshotPaths(vm, STALE_CUT_FIXTURE_ROOT); //forge-lint: disable-next-line(unsafe-cheatcode) @@ -2555,6 +2648,11 @@ contract LibRainDeploySnapshotTest is Test { assertTrue(recordSet.has(MemoryKVKey.wrap(keccak256(bytes(cutPath))))); } + /// Every record the ordering guard is driven against gets a root of its + /// own, for the reason `RELEASED_FIXTURE_ROOT` is not `WALK_FIXTURE_ROOT`: + /// forge runs the tests in a contract concurrently, and a guard test sees + /// the wrong newest release the moment another test's fixture is in the + /// tree it is reading. string constant NEWEST_FIXTURE_ROOT = "test/generated-snapshot/newest"; /// The record with nothing released in it. See `NEWEST_FIXTURE_ROOT`.