Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 17 additions & 11 deletions script/Build.sol
Original file line number Diff line number Diff line change
Expand Up @@ -59,28 +59,34 @@ contract Build is BuildScript, RegistryDeploySuites {
return names;
}

/// The directory every generated lib is written into.
///
/// Overridable for the reason `BuildScript.recordRoot` is: a hook that can
/// only be pointed at the committed tree can only be RUN by overwriting
/// files the rest of the suite compiles and reads, and forge runs test
/// contracts in parallel. Only the per-contract libs and the aggregate go
/// here — `regenerateSnapshots` has no equivalent, because `LibFs` confines
/// every snapshot it writes to `src/generated/`.
/// @return The lib directory.
function libDir() internal view virtual returns (string memory) {
return LibRainDeploySnapshot.LIB_DIR;
}

/// @inheritdoc BuildScript
/// @dev Every alias lib, every released-suites lib and the aggregate over
/// them.
function regenerateLibs() internal override {
GeneratedContract[] memory contracts = generatedContracts();
string memory dir = libDir();
for (uint256 i = 0; i < contracts.length; i++) {
LibRainDeploySnapshot.writeAliasLib(
vm,
LibRainDeploySnapshot.LIB_DIR,
contracts[i].contractName,
contracts[i].constantPrefix,
LibRainDeploySnapshot.CANDIDATE
vm, dir, contracts[i].contractName, contracts[i].constantPrefix, LibRainDeploySnapshot.CANDIDATE
);
LibRainDeploySnapshot.writeReleasedSuitesLib(
vm,
LibRainDeploySnapshot.LIB_DIR,
recordRoot(),
contracts[i].contractName,
contracts[i].candidate.snapshot
vm, dir, recordRoot(), contracts[i].contractName, contracts[i].candidate.snapshot
);
}
LibRainDeploySnapshot.writeReleasedSuitesAggregate(vm, LibRainDeploySnapshot.LIB_DIR, snapshotContractNames());
LibRainDeploySnapshot.writeReleasedSuitesAggregate(vm, dir, snapshotContractNames());
}

/// @inheritdoc BuildScript
Expand Down
37 changes: 36 additions & 1 deletion test/concrete/BuildHarness.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,50 @@ pragma solidity =0.8.25;
import {Build, GeneratedContract} from "../../script/Build.sol";
import {DeployCandidate} from "../../src/abstract/RainDeploySuitesBase.sol";

/// Thrown when a harness left pointed at `Build`'s own lib directory is asked
/// to run the lib half of a build. That write lands on the committed libs the
/// rest of the suite compiles and reads, which forge runs in parallel with it.
error BuildHarnessWouldWriteTheCommittedLibs();

/// @title BuildHarness
/// @notice An external seam onto the two internal declarations `BuildTest`
/// compares, so a test can hold both at once.
/// compares, so a test can hold both at once, and onto `regenerateLibs()`
/// pointed somewhere nothing compiles, so a test can RUN it.
///
/// A harness rather than a change to `Build`: `generatedContracts()` is the
/// script's own declaration and has no caller outside it, and widening it to
/// `public` to be testable would put a second entry point on a script whose
/// whole surface is `run()` and `cutRelease()`.
contract BuildHarness is Build {
/// The lib directory the hooks write into. Empty defers to `Build`'s own,
/// which is what makes the default assertable and the write refusable.
string internal sLibDir;

/// @param libDirectory The directory `regenerateLibs` writes into, or empty
/// for `Build`'s own.
constructor(string memory libDirectory) {
sLibDir = libDirectory;
}

/// @inheritdoc Build
function libDir() internal view override returns (string memory) {
return bytes(sLibDir).length > 0 ? sLibDir : super.libDir();
}

function externalLibDir() external view returns (string memory) {
return libDir();
}

/// Runs the lib half of a build, into the fixture directory this harness
/// was handed. The directory has to exist already, as it does for every
/// writer `regenerateLibs` calls.
function externalRegenerateLibs() external {
if (bytes(sLibDir).length == 0) {
revert BuildHarnessWouldWriteTheCommittedLibs();
}
regenerateLibs();
}

/// The generator's list.
/// @return The generated contracts.
function externalGeneratedContracts() external pure returns (GeneratedContract[] memory) {
Expand Down
2 changes: 1 addition & 1 deletion test/concrete/BuildRecordRootHarness.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {BuildHarness} from "./BuildHarness.sol";
contract BuildRecordRootHarness is BuildHarness {
string internal sRoot;

constructor(string memory root) {
constructor(string memory root) BuildHarness("") {
sRoot = root;
}

Expand Down
123 changes: 117 additions & 6 deletions test/script/Build.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {DeployCandidate} from "../../src/abstract/RainDeploySuitesBase.sol";
import {LibCodeGen} from "rain-sol-codegen-0.1.37/src/lib/LibCodeGen.sol";
import {LibRainDeploySnapshot} from "../../src/lib/LibRainDeploySnapshot.sol";
import {LibReleasedSuitesAggregate} from "../lib/LibReleasedSuitesAggregate.sol";
import {BuildHarness} from "../concrete/BuildHarness.sol";
import {BuildHarness, BuildHarnessWouldWriteTheCommittedLibs} from "../concrete/BuildHarness.sol";
import {LibMemoryKV, MemoryKV, MemoryKVKey, MemoryKVVal} from "rain-lib-memkv-0.1.5/src/lib/LibMemoryKV.sol";

/// @title BuildTest
Expand Down Expand Up @@ -38,18 +38,18 @@ import {LibMemoryKV, MemoryKV, MemoryKVKey, MemoryKVVal} from "rain-lib-memkv-0.
/// contract's pins are written into.
///
/// Deliberately nothing here calls `run()` or `cutRelease()`. Both rewrite the
/// committed `src/generated/` snapshots and `src/lib/` libs that other test
/// contracts read, and forge runs test contracts in parallel — a contract
/// rewriting what another one is reading is a race, not a check. Nothing below
/// writes anything.
/// committed `src/generated/` snapshots that other test contracts read, and
/// forge runs test contracts in parallel — a contract rewriting what another
/// one is reading is a race, not a check. `regenerateLibs()` is run, and only
/// into a fixture directory nothing compiles; the one write below is that.
contract BuildTest is Test {
using LibMemoryKV for MemoryKV;

/// The harness the two declarations are read through.
BuildHarness internal sBuild;

function setUp() external {
sBuild = new BuildHarness();
sBuild = new BuildHarness("");
}

/// PROPERTY: the generator's list and the deploy declaration are the SAME
Expand Down Expand Up @@ -312,4 +312,115 @@ contract BuildTest is Test {
);
}
}

/// PROPERTY: a build with nothing overridden writes its libs into the
/// directory the committed ones are in.
///
/// Asserted against the path as text rather than against
/// `LibRainDeploySnapshot.LIB_DIR`, which is the constant the default
/// returns: a default pointed at a directory nothing compiles leaves every
/// committed lib stale forever while the regeneration reports success, and
/// the test below cannot see it, because that test overrides this.
function testTheDefaultLibDirIsWhereTheCommittedLibsAre() external view {
assertEq(sBuild.externalLibDir(), "src/lib");
}

/// Where `regenerateLibs()` is driven.
///
/// Outside `src/` and `test/`, which is everything `fs_permissions`
/// otherwise grants and both of which are compiled: a generated lib imports
/// `../generated/`, `../abstract/` and `./Lib<Contract>Released.sol`, which
/// resolve from `src/lib` and nowhere else, so a copy under either root
/// fails the build for every suite — including the copy a failing test
/// leaves behind. `foundry.toml` grants this root for exactly that, and
/// nothing compiles it.
string constant LIBS_FIXTURE_DIR = "fixture-lib/build-regenerate-libs";

/// PROPERTY: `regenerateLibs()` RUN emits exactly the committed libs — one
/// alias lib and one released lib per generated contract, one aggregate,
/// and nothing else.
///
/// Every assertion above this one is output-anchored: it compares a
/// committed file against the emitters, so it sees drift only AFTER
/// somebody re-runs the generator and commits what came out. The hook that
/// decides which emitter is called, with which arguments, how many times,
/// was executed by nothing at all — a loop bound that stopped one contract
/// short, or a `constantPrefix` taken from `contracts[0]` on every pass,
/// was invisible until the next release cut it into the record. Both hooks
/// took a `revert()` as their first statement with the whole suite still
/// green.
///
/// So this runs it, and the oracle is the committed tree rather than the
/// emitters: a regeneration of a clean checkout is a no-op, so every file
/// it writes MUST be byte-identical to the file already there. That is
/// independent of the emitters in the way the pins above are not — they say
/// the committed files are what the emitters produce, and this says the
/// hook asks the emitters for those files.
///
/// The count is asserted as well as the contents, because a loop that
/// stopped short writes nothing wrong — it writes nothing at all — and a
/// file the hook wrote that the repo does not commit is a generated file
/// nothing regenerates.
///
/// `regenerateSnapshots()` has no counterpart here and can have none:
/// `LibFs` confines every snapshot write to `src/generated/<tag>/`, the
/// record `frozenSnapshotPaths` walks, so there is no directory to drive it
/// into that is not read by the suites running beside this one.
///
/// Read, then removed, then asserted: forge-std assertions revert, so a
/// removal after them removes in every case except a failure, which is the
/// only case that leaves a directory behind. `vm.isFile` before each read
/// for the same reason — a missing file is what the loop-bound failure
/// looks like, and a cheatcode revert there would strand the fixture.
function testRegenerateLibsEmitsExactlyTheCommittedLibs() external {
GeneratedContract[] memory generated = sBuild.externalGeneratedContracts();
BuildHarness harness = new BuildHarness(LIBS_FIXTURE_DIR);

//forge-lint: disable-next-line(unsafe-cheatcode)
vm.createDir(LIBS_FIXTURE_DIR, true);
harness.externalRegenerateLibs();

string[] memory names = new string[](generated.length * 2 + 1);
for (uint256 i = 0; i < generated.length; i++) {
names[i * 2] = string.concat("Lib", generated[i].contractName, "Deploy.sol");
names[i * 2 + 1] =
string.concat(LibRainDeploySnapshot.releasedLibraryName(generated[i].contractName), ".sol");
}
names[names.length - 1] = string.concat(LibRainDeploySnapshot.RELEASED_SUITES_LIBRARY, ".sol");

uint256 written = vm.readDir(LIBS_FIXTURE_DIR).length;
bool[] memory emitted = new bool[](names.length);
string[] memory bodies = new string[](names.length);
for (uint256 i = 0; i < names.length; i++) {
string memory path = string.concat(LIBS_FIXTURE_DIR, "/", names[i]);
emitted[i] = vm.isFile(path);
bodies[i] = emitted[i] ? vm.readFile(path) : "";
}

//forge-lint: disable-next-line(unsafe-cheatcode)
vm.removeDir(LIBS_FIXTURE_DIR, true);

assertEq(written, names.length, "regenerateLibs emitted a different number of files than the repo commits");
for (uint256 i = 0; i < names.length; i++) {
assertTrue(emitted[i], string.concat("regenerateLibs emitted no ", names[i]));
assertEq(
bodies[i],
vm.readFile(string.concat("src/lib/", names[i])),
string.concat("regenerateLibs emitted something other than the committed ", names[i])
);
}
}

/// A harness left pointed at `Build`'s own lib directory MUST refuse to run
/// the lib half of a build.
///
/// `setUp` builds one, because the default is what
/// `testTheDefaultLibDirIsWhereTheCommittedLibsAre` reads, and it is shared
/// with every other test in this contract. A call that went through would
/// rewrite `src/lib/` while the suites that compile and read those files
/// are running.
function testRegenerateLibsRefusesToWriteTheCommittedLibs() external {
vm.expectRevert(BuildHarnessWouldWriteTheCommittedLibs.selector);
sBuild.externalRegenerateLibs();
}
}
Loading