From 79865726afe8cda539ddbb97abcd226c88599564 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 16 Aug 2026 13:50:56 +0000 Subject: [PATCH 1/2] test(LibCodeGen): cover every unprobed function and the wrap arithmetic Adds test files for `bytecodeHashConstantString`, `describedByMetaHashConstantString`, `bytesConstantString`, `uint8ConstantString` and the five function pointer wrappers, and adds wrap boundary coverage to the two existing constant string suites. `test/lib/LibCodeGenSlow.sol` is a reference that builds the declaration and measures it, instead of adding up magic numbers standing in for the literals, so the two agree only when every term of the library's sum is right. Boundary tests pin the decision at exactly 120 and 121 characters, which is where `forge fmt` itself changes its mind. `test/concrete/ToolingMock.sol` answers all five tooling builders with distinct data, so a wrapper asking the wrong builder emits the wrong hex. Co-Authored-By: Claude Opus 5 (1M context) --- test/concrete/ToolingMock.sol | 56 +++++++ .../LibCodeGen.addressConstantString.t.sol | 47 +++++- ...ibCodeGen.bytecodeHashConstantString.t.sol | 97 ++++++++++++ .../LibCodeGen.bytes32ConstantString.t.sol | 45 +++++- test/lib/LibCodeGen.bytesConstantString.t.sol | 107 +++++++++++++ ...en.describedByMetaHashConstantString.t.sol | 100 ++++++++++++ ...egrityFunctionPointersConstantString.t.sol | 51 +++++++ ...ParserFunctionPointersConstantString.t.sol | 59 ++++++++ ...opcodeFunctionPointersConstantString.t.sol | 56 +++++++ ...andlerFunctionPointersConstantString.t.sol | 56 +++++++ ...n.subParserWordParsersConstantString.t.sol | 59 ++++++++ test/lib/LibCodeGen.uint8ConstantString.t.sol | 100 ++++++++++++ test/lib/LibCodeGenSlow.sol | 142 ++++++++++++++++++ 13 files changed, 973 insertions(+), 2 deletions(-) create mode 100644 test/concrete/ToolingMock.sol create mode 100644 test/lib/LibCodeGen.bytecodeHashConstantString.t.sol create mode 100644 test/lib/LibCodeGen.bytesConstantString.t.sol create mode 100644 test/lib/LibCodeGen.describedByMetaHashConstantString.t.sol create mode 100644 test/lib/LibCodeGen.integrityFunctionPointersConstantString.t.sol create mode 100644 test/lib/LibCodeGen.literalParserFunctionPointersConstantString.t.sol create mode 100644 test/lib/LibCodeGen.opcodeFunctionPointersConstantString.t.sol create mode 100644 test/lib/LibCodeGen.operandHandlerFunctionPointersConstantString.t.sol create mode 100644 test/lib/LibCodeGen.subParserWordParsersConstantString.t.sol create mode 100644 test/lib/LibCodeGen.uint8ConstantString.t.sol create mode 100644 test/lib/LibCodeGenSlow.sol diff --git a/test/concrete/ToolingMock.sol b/test/concrete/ToolingMock.sol new file mode 100644 index 0000000..ca42233 --- /dev/null +++ b/test/concrete/ToolingMock.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +/// @title ToolingMock +/// Answers all five tooling builders that `LibCodeGen` dispatches to, each with +/// its own byte string, so a test can tell which builder a `LibCodeGen` wrapper +/// actually called rather than only that it called something. +/// @dev `IParserToolingV1` and `ISubParserToolingV1` declare their builders +/// `pure`, which no implementation can satisfy while returning data set per +/// instance, so this mock declares all five `view` and does not inherit the +/// interfaces. Tests cast its address to the interface at the call site, so the +/// selectors are still the interfaces' own. +contract ToolingMock { + bytes internal sOpcodeFunctionPointers; + bytes internal sLiteralParserFunctionPointers; + bytes internal sOperandHandlerFunctionPointers; + bytes internal sSubParserWordParsers; + bytes internal sIntegrityFunctionPointers; + + /// Sets every builder's answer at once. Tests pass five distinct values so + /// that a wrapper calling the wrong builder emits the wrong hex. + function setAll( + bytes memory opcodeFunctionPointers, + bytes memory literalParserFunctionPointers, + bytes memory operandHandlerFunctionPointers, + bytes memory subParserWordParsers, + bytes memory integrityFunctionPointers + ) external { + sOpcodeFunctionPointers = opcodeFunctionPointers; + sLiteralParserFunctionPointers = literalParserFunctionPointers; + sOperandHandlerFunctionPointers = operandHandlerFunctionPointers; + sSubParserWordParsers = subParserWordParsers; + sIntegrityFunctionPointers = integrityFunctionPointers; + } + + function buildOpcodeFunctionPointers() external view returns (bytes memory) { + return sOpcodeFunctionPointers; + } + + function buildLiteralParserFunctionPointers() external view returns (bytes memory) { + return sLiteralParserFunctionPointers; + } + + function buildOperandHandlerFunctionPointers() external view returns (bytes memory) { + return sOperandHandlerFunctionPointers; + } + + function buildSubParserWordParsers() external view returns (bytes memory) { + return sSubParserWordParsers; + } + + function buildIntegrityFunctionPointers() external view returns (bytes memory) { + return sIntegrityFunctionPointers; + } +} diff --git a/test/lib/LibCodeGen.addressConstantString.t.sol b/test/lib/LibCodeGen.addressConstantString.t.sol index 138ff1f..65016e2 100644 --- a/test/lib/LibCodeGen.addressConstantString.t.sol +++ b/test/lib/LibCodeGen.addressConstantString.t.sol @@ -3,7 +3,13 @@ pragma solidity =0.8.25; import {Test} from "forge-std-1.16.1/src/Test.sol"; -import {LibCodeGen} from "src/lib/LibCodeGen.sol"; +import {LibCodeGen, MAX_LINE_LENGTH} from "src/lib/LibCodeGen.sol"; +import {LibCodeGenSlow} from "./LibCodeGenSlow.sol"; + +/// @dev A checksummed address literal, 42 characters like every other, so the +/// declaration is `72 + name.length` characters long on one line. +address constant SOME_ADDRESS = address(0xc51a14251b0dcF0ae24A96b7153991378938f5F5); +string constant SOME_ADDRESS_STRING = "0xc51a14251b0dcF0ae24A96b7153991378938f5F5"; /// @title LibCodeGenAddressConstantStringTest /// @notice `addressConstantString` emits a Solidity `address constant` @@ -36,4 +42,43 @@ contract LibCodeGenAddressConstantStringTest is Test { ); assertEq(vm.parseAddress(vm.toString(data)), data); } + + /// A declaration of exactly the maximum length stays on one line. `forge fmt` + /// leaves a line of exactly `line_length` alone, so wrapping here would be a + /// reflow the formatter immediately undoes. + function testAddressConstantStringAtMaxLength() external view { + string memory name = LibCodeGenSlow.nameOfLengthSlow(48); + string memory emitted = LibCodeGen.addressConstantString(vm, "/// @dev At max.", name, SOME_ADDRESS); + assertEq( + emitted, + string.concat("\n/// @dev At max.\naddress constant ", name, " = address(", SOME_ADDRESS_STRING, ");\n") + ); + assertEq(LibCodeGenSlow.longestLineSlow(emitted), MAX_LINE_LENGTH); + } + + /// One character past the maximum wraps after the `=`, with the value + /// indented by one tab width on the next line. + function testAddressConstantStringOverMaxLength() external view { + string memory name = LibCodeGenSlow.nameOfLengthSlow(49); + assertEq( + LibCodeGen.addressConstantString(vm, "/// @dev Over max.", name, SOME_ADDRESS), + string.concat( + "\n/// @dev Over max.\naddress constant ", name, " =\n address(", SOME_ADDRESS_STRING, ");\n" + ) + ); + } + + /// Whatever the comment, name and address, the emitted text is the + /// declaration built from those literals, wrapped exactly when measuring the + /// one line form says it does not fit. Fuzzed over every input because each + /// term of the library's hand computed sum has to be right for this to hold. + function testAddressConstantStringMatchesMeasuredLine(string memory comment, string memory name, address data) + external + view + { + assertEq( + LibCodeGen.addressConstantString(vm, comment, name, data), + LibCodeGenSlow.addressConstantStringSlow(vm, comment, name, data) + ); + } } diff --git a/test/lib/LibCodeGen.bytecodeHashConstantString.t.sol b/test/lib/LibCodeGen.bytecodeHashConstantString.t.sol new file mode 100644 index 0000000..03ac19d --- /dev/null +++ b/test/lib/LibCodeGen.bytecodeHashConstantString.t.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {LibCodeGen, MAX_LINE_LENGTH} from "src/lib/LibCodeGen.sol"; +import {LibCodeGenSlow} from "./LibCodeGenSlow.sol"; +import {CodeGennable} from "../concrete/CodeGennable.sol"; + +/// @title LibCodeGenBytecodeHashConstantStringTest +/// @notice `bytecodeHashConstantString` emits the one constant that every +/// generated file carries, under a name and comment it chooses itself. What it +/// must emit is the hash of the runtime code actually at the instance address, +/// because a consumer compares it against `addr.codehash`, so these derive the +/// expectation by hashing that code rather than by reading it back off the same +/// account. +contract LibCodeGenBytecodeHashConstantStringTest is Test { + address internal constant INSTANCE = address(uint160(uint256(keccak256("instance")))); + + /// The whole emitted declaration, for code put at the address by hand. The + /// name and comment are the library's, not the caller's, so they are pinned + /// exactly: every consumer's generated file and every consumer's assertion + /// against it is written against this spelling. + function testBytecodeHashConstantString() external { + vm.etch(INSTANCE, hex"6001"); + assertEq( + LibCodeGen.bytecodeHashConstantString(vm, INSTANCE), + string.concat( + "\n/// @dev Hash of the known bytecode.\nbytes32 constant BYTECODE_HASH = bytes32(", + vm.toString(keccak256(hex"6001")), + ");\n" + ) + ); + } + + /// The hash is of the runtime code of a really deployed contract, which is + /// what `addr.codehash` returns for it. + function testBytecodeHashConstantStringDeployed() external { + CodeGennable codeGennable = new CodeGennable(); + assertEq( + LibCodeGen.bytecodeHashConstantString(vm, address(codeGennable)), + string.concat( + "\n/// @dev Hash of the known bytecode.\nbytes32 constant BYTECODE_HASH = bytes32(", + vm.toString(keccak256(address(codeGennable).code)), + ");\n" + ) + ); + } + + /// The name is fixed and so is the length of a `bytes32` literal, so this + /// declaration can never need wrapping. Asserted rather than assumed, + /// because this function does its own concatenation instead of going through + /// `bytes32ConstantString` and so has no wrap decision at all. + function testBytecodeHashConstantStringFitsMaxLength() external { + vm.etch(INSTANCE, hex"6001"); + assertLe(LibCodeGenSlow.longestLineSlow(LibCodeGen.bytecodeHashConstantString(vm, INSTANCE)), MAX_LINE_LENGTH); + } + + /// Whatever the runtime code, the constant carries its keccak256 hash. + function testBytecodeHashConstantStringHashesCode(bytes memory code) external { + vm.assume(code.length > 0); + vm.etch(INSTANCE, code); + assertEq( + LibCodeGen.bytecodeHashConstantString(vm, INSTANCE), + string.concat( + "\n/// @dev Hash of the known bytecode.\nbytes32 constant BYTECODE_HASH = bytes32(", + vm.toString(keccak256(code)), + ");\n" + ) + ); + } + + /// Two instances with different code get different constants, so the + /// constant is a fingerprint of the code rather than of the address or of + /// anything else about the account. + function testBytecodeHashConstantStringDiscriminatesCode(bytes memory codeA, bytes memory codeB) external { + vm.assume(codeA.length > 0 && codeB.length > 0); + vm.assume(keccak256(codeA) != keccak256(codeB)); + + vm.etch(INSTANCE, codeA); + string memory emittedA = LibCodeGen.bytecodeHashConstantString(vm, INSTANCE); + vm.etch(INSTANCE, codeB); + string memory emittedB = LibCodeGen.bytecodeHashConstantString(vm, INSTANCE); + + assertNotEq(emittedA, emittedB); + } + + /// The same instance generates the same text every time it is asked, so + /// regenerating a file twice does not produce a diff. + function testBytecodeHashConstantStringIdempotent(bytes memory code) external { + vm.assume(code.length > 0); + vm.etch(INSTANCE, code); + assertEq( + LibCodeGen.bytecodeHashConstantString(vm, INSTANCE), LibCodeGen.bytecodeHashConstantString(vm, INSTANCE) + ); + } +} diff --git a/test/lib/LibCodeGen.bytes32ConstantString.t.sol b/test/lib/LibCodeGen.bytes32ConstantString.t.sol index 4d4e72d..484a0d1 100644 --- a/test/lib/LibCodeGen.bytes32ConstantString.t.sol +++ b/test/lib/LibCodeGen.bytes32ConstantString.t.sol @@ -3,7 +3,13 @@ pragma solidity =0.8.25; import {Test} from "forge-std-1.16.1/src/Test.sol"; -import {LibCodeGen} from "src/lib/LibCodeGen.sol"; +import {LibCodeGen, MAX_LINE_LENGTH} from "src/lib/LibCodeGen.sol"; +import {LibCodeGenSlow} from "./LibCodeGenSlow.sol"; + +/// @dev A `bytes32` literal is 66 characters whatever the value, so the +/// declaration is `96 + name.length` characters long on one line. +bytes32 constant SOME_HASH = 0x2573004ac3a9ee7fc8d73654d76386f1b6b99e34cdf86a689c4691e47143420f; +string constant SOME_HASH_STRING = "0x2573004ac3a9ee7fc8d73654d76386f1b6b99e34cdf86a689c4691e47143420f"; /// @title LibCodeGenBytes32ConstantStringTest /// @notice `bytes32ConstantString` emits a Solidity `bytes32 constant` @@ -43,4 +49,41 @@ contract LibCodeGenBytes32ConstantStringTest is Test { ); assertEq(vm.parseBytes32(vm.toString(data)), data); } + + /// A declaration of exactly the maximum length stays on one line. `forge fmt` + /// leaves a line of exactly `line_length` alone, so wrapping here would be a + /// reflow the formatter immediately undoes. + function testBytes32ConstantStringAtMaxLength() external view { + string memory name = LibCodeGenSlow.nameOfLengthSlow(24); + string memory emitted = LibCodeGen.bytes32ConstantString(vm, "/// @dev At max.", name, SOME_HASH); + assertEq( + emitted, + string.concat("\n/// @dev At max.\nbytes32 constant ", name, " = bytes32(", SOME_HASH_STRING, ");\n") + ); + assertEq(LibCodeGenSlow.longestLineSlow(emitted), MAX_LINE_LENGTH); + } + + /// One character past the maximum wraps after the `=`, with the value + /// indented by one tab width on the next line. + function testBytes32ConstantStringOverMaxLength() external view { + string memory name = LibCodeGenSlow.nameOfLengthSlow(25); + assertEq( + LibCodeGen.bytes32ConstantString(vm, "/// @dev Over max.", name, SOME_HASH), + string.concat("\n/// @dev Over max.\nbytes32 constant ", name, " =\n bytes32(", SOME_HASH_STRING, ");\n") + ); + } + + /// Whatever the comment, name and value, the emitted text is the declaration + /// built from those literals, wrapped exactly when measuring the one line + /// form says it does not fit. Fuzzed over every input because each term of + /// the library's hand computed sum has to be right for this to hold. + function testBytes32ConstantStringMatchesMeasuredLine(string memory comment, string memory name, bytes32 data) + external + view + { + assertEq( + LibCodeGen.bytes32ConstantString(vm, comment, name, data), + LibCodeGenSlow.bytes32ConstantStringSlow(vm, comment, name, data) + ); + } } diff --git a/test/lib/LibCodeGen.bytesConstantString.t.sol b/test/lib/LibCodeGen.bytesConstantString.t.sol new file mode 100644 index 0000000..7809082 --- /dev/null +++ b/test/lib/LibCodeGen.bytesConstantString.t.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {LibCodeGen, MAX_LINE_LENGTH} from "src/lib/LibCodeGen.sol"; +import {LibCodeGenSlow} from "./LibCodeGenSlow.sol"; + +/// @dev 32 bytes, so the hex literal is 64 characters and the whole declaration +/// is `24 + name.length + 64` characters on one line. +bytes constant DATA_32 = hex"2573004ac3a9ee7fc8d73654d76386f1b6b99e34cdf86a689c4691e47143420f"; +string constant HEX_32 = "2573004ac3a9ee7fc8d73654d76386f1b6b99e34cdf86a689c4691e47143420f"; + +/// @dev One byte more than `DATA_32`, so two characters more of hex. +bytes constant DATA_33 = hex"2573004ac3a9ee7fc8d73654d76386f1b6b99e34cdf86a689c4691e47143420faa"; +string constant HEX_33 = "2573004ac3a9ee7fc8d73654d76386f1b6b99e34cdf86a689c4691e47143420faa"; + +/// @dev Two bytes more than `DATA_32`, so four characters more of hex. +bytes constant DATA_34 = hex"2573004ac3a9ee7fc8d73654d76386f1b6b99e34cdf86a689c4691e47143420faabb"; +string constant HEX_34 = "2573004ac3a9ee7fc8d73654d76386f1b6b99e34cdf86a689c4691e47143420faabb"; + +/// @title LibCodeGenBytesConstantStringTest +/// @notice `bytesConstantString` emits a Solidity `bytes constant` declaration +/// and decides for itself whether that declaration fits on one line. The +/// decision is made by adding up magic numbers standing in for the literals it +/// is about to concatenate, so these assert the emitted text against a reference +/// that measures the line instead, and pin the decision either side of the +/// maximum. +contract LibCodeGenBytesConstantStringTest is Test { + /// The short case: blank line, comment, then the whole declaration on one + /// line. The blank line is what separates this constant from whatever the + /// caller concatenated before it. + function testBytesConstantString() external pure { + assertEq( + LibCodeGen.bytesConstantString(vm, "/// @dev Some bytes comment.", "SOME_BYTES_CONSTANT", hex"12345678"), + "\n/// @dev Some bytes comment.\nbytes constant SOME_BYTES_CONSTANT = hex\"12345678\";\n" + ); + } + + /// Empty data is a real value, not a reason to skip the constant. `hex""` is + /// valid Solidity for empty bytes, so the declaration is still emitted whole + /// rather than collapsing to a bare `hex` or an unterminated literal. + function testBytesConstantStringEmptyData() external pure { + assertEq( + LibCodeGen.bytesConstantString(vm, "/// @dev Nothing.", "NOTHING", ""), + "\n/// @dev Nothing.\nbytes constant NOTHING = hex\"\";\n" + ); + } + + /// A declaration of exactly the maximum length stays on one line. `forge fmt` + /// leaves a line of exactly `line_length` alone, so wrapping here would be a + /// reflow the formatter immediately undoes. + function testBytesConstantStringAtMaxLength() external pure { + string memory name = LibCodeGenSlow.nameOfLengthSlow(32); + string memory emitted = LibCodeGen.bytesConstantString(vm, "/// @dev At max.", name, DATA_32); + assertEq(emitted, string.concat("\n/// @dev At max.\nbytes constant ", name, " = hex\"", HEX_32, "\";\n")); + assertEq(LibCodeGenSlow.longestLineSlow(emitted), MAX_LINE_LENGTH); + } + + /// One character past the maximum wraps after the `=`, with the value + /// indented by one tab width on the next line. + function testBytesConstantStringOverMaxLengthByName() external pure { + string memory name = LibCodeGenSlow.nameOfLengthSlow(33); + assertEq( + LibCodeGen.bytesConstantString(vm, "/// @dev Over max.", name, DATA_32), + string.concat("\n/// @dev Over max.\nbytes constant ", name, " =\n hex\"", HEX_32, "\";\n") + ); + } + + /// The data's own length counts toward the decision, not just the name's. + /// Same name either side, one byte of data apart. + function testBytesConstantStringOverMaxLengthByData() external pure { + string memory name = LibCodeGenSlow.nameOfLengthSlow(30); + + string memory under = LibCodeGen.bytesConstantString(vm, "/// @dev Under.", name, DATA_33); + assertEq(under, string.concat("\n/// @dev Under.\nbytes constant ", name, " = hex\"", HEX_33, "\";\n")); + assertEq(LibCodeGenSlow.longestLineSlow(under), MAX_LINE_LENGTH); + + assertEq( + LibCodeGen.bytesConstantString(vm, "/// @dev Over.", name, DATA_34), + string.concat("\n/// @dev Over.\nbytes constant ", name, " =\n hex\"", HEX_34, "\";\n") + ); + } + + /// Whatever the comment, name and data, the emitted text is the declaration + /// built from those literals, wrapped exactly when measuring the one line + /// form says it does not fit. Fuzzed over every input because each term of + /// the library's hand computed sum has to be right for this to hold. + function testBytesConstantStringMatchesMeasuredLine(string memory comment, string memory name, bytes memory data) + external + pure + { + assertEq( + LibCodeGen.bytesConstantString(vm, comment, name, data), + LibCodeGenSlow.bytesConstantStringSlow(vm, comment, name, data) + ); + } + + /// The hex the declaration carries is the data, unchanged and unprefixed, so + /// the constant compiles back to the bytes it was generated from. A `0x` + /// inside a `hex"..."` literal does not compile at all. + function testBytesConstantStringCarriesData(bytes memory data) external pure { + string memory emitted = LibCodeGen.bytesConstantString(vm, "/// @dev Fuzz.", "FUZZ", data); + assertTrue(vm.contains(emitted, string.concat("hex\"", LibCodeGenSlow.hexOfSlow(vm, data), "\";\n"))); + assertFalse(vm.contains(emitted, "hex\"0x"), "hex literal carries a 0x prefix"); + } +} diff --git a/test/lib/LibCodeGen.describedByMetaHashConstantString.t.sol b/test/lib/LibCodeGen.describedByMetaHashConstantString.t.sol new file mode 100644 index 0000000..0c6c2bf --- /dev/null +++ b/test/lib/LibCodeGen.describedByMetaHashConstantString.t.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {LibCodeGen, MAX_LINE_LENGTH} from "src/lib/LibCodeGen.sol"; +import {LibCodeGenSlow} from "./LibCodeGenSlow.sol"; + +/// @dev `describedByMetaHashConstantString` reads `meta/.rain.meta`, and +/// this repo's `fs_permissions` grants no read under `meta`. `src/generated` is +/// the one directory it grants read-write, so a fixture goes there and the name +/// walks back out of `meta` to reach it. That the name reaches the path at all +/// is itself asserted below. +string constant FIXTURE_NAME = "../src/generated/LibCodeGenDescribedByMetaHashFixture"; +string constant FIXTURE_PATH = "src/generated/LibCodeGenDescribedByMetaHashFixture.rain.meta"; + +/// @dev The fixed part of the declaration. The name and comment are the +/// library's own choice rather than the caller's, so both are pinned exactly: +/// every consumer's generated file is written against this spelling. +string constant DESCRIBED_BY_META_HASH_PREFIX = + "\n/// @dev The hash of the meta that describes the contract.\nbytes32 constant DESCRIBED_BY_META_HASH = bytes32("; + +/// @title LibCodeGenDescribedByMetaHashConstantStringTest +/// @notice `describedByMetaHashConstantString` builds a path out of the name it +/// is given, reads that file, and puts the hash of its contents into a constant. +contract LibCodeGenDescribedByMetaHashConstantStringTest is Test { + /// Reachable only through an external call so that a refused read can be + /// caught and inspected rather than aborting the test. + function callDescribedByMetaHash(string memory name) external view returns (string memory) { + return LibCodeGen.describedByMetaHashConstantString(vm, name); + } + + /// The path is `meta/.rain.meta`. The name is not a file that exists + /// here, so the path is observed through the access refusal, which quotes + /// the path that was asked for. Two names, because a hard coded path would + /// satisfy one of them. + function testDescribedByMetaHashConstantStringPath() external { + assertRequestsPath("CodeGennable", "meta/CodeGennable.rain.meta"); + assertRequestsPath("SomeOtherContract", "meta/SomeOtherContract.rain.meta"); + } + + function assertRequestsPath(string memory name, string memory expectedPath) internal { + try this.callDescribedByMetaHash(name) returns (string memory) { + fail(); + } catch (bytes memory err) { + assertTrue(vm.contains(string(err), expectedPath), "did not ask for the expected path"); + } + } + + /// The constant carries the keccak256 of the file's bytes, and nothing about + /// the file's name or path. + function testDescribedByMetaHashConstantString() external { + bytes memory meta = hex"1234"; + vm.writeFileBinary(FIXTURE_PATH, meta); + string memory emitted = LibCodeGen.describedByMetaHashConstantString(vm, FIXTURE_NAME); + vm.removeFile(FIXTURE_PATH); + + assertEq(emitted, string.concat(DESCRIBED_BY_META_HASH_PREFIX, vm.toString(keccak256(meta)), ");\n")); + } + + /// Different meta gives a different constant, so the hash is of the contents + /// rather than of anything fixed. + function testDescribedByMetaHashConstantStringHashesContents() external { + bytes memory metaA = hex"1234"; + bytes memory metaB = hex"5678"; + + vm.writeFileBinary(FIXTURE_PATH, metaA); + string memory emittedA = LibCodeGen.describedByMetaHashConstantString(vm, FIXTURE_NAME); + vm.writeFileBinary(FIXTURE_PATH, metaB); + string memory emittedB = LibCodeGen.describedByMetaHashConstantString(vm, FIXTURE_NAME); + vm.removeFile(FIXTURE_PATH); + + assertEq(emittedA, string.concat(DESCRIBED_BY_META_HASH_PREFIX, vm.toString(keccak256(metaA)), ");\n")); + assertEq(emittedB, string.concat(DESCRIBED_BY_META_HASH_PREFIX, vm.toString(keccak256(metaB)), ");\n")); + assertNotEq(emittedA, emittedB); + } + + /// Reading the same file twice gives the same text, so regenerating a file + /// does not produce a diff. + function testDescribedByMetaHashConstantStringIdempotent() external { + vm.writeFileBinary(FIXTURE_PATH, hex"1234"); + string memory first = LibCodeGen.describedByMetaHashConstantString(vm, FIXTURE_NAME); + string memory second = LibCodeGen.describedByMetaHashConstantString(vm, FIXTURE_NAME); + vm.removeFile(FIXTURE_PATH); + + assertEq(first, second); + } + + /// The name is fixed and so is the length of a `bytes32` literal, so this + /// declaration can never need wrapping. Asserted rather than assumed, + /// because this function does its own concatenation instead of going through + /// `bytes32ConstantString` and so has no wrap decision at all. + function testDescribedByMetaHashConstantStringFitsMaxLength() external { + vm.writeFileBinary(FIXTURE_PATH, hex"1234"); + string memory emitted = LibCodeGen.describedByMetaHashConstantString(vm, FIXTURE_NAME); + vm.removeFile(FIXTURE_PATH); + + assertLe(LibCodeGenSlow.longestLineSlow(emitted), MAX_LINE_LENGTH); + } +} diff --git a/test/lib/LibCodeGen.integrityFunctionPointersConstantString.t.sol b/test/lib/LibCodeGen.integrityFunctionPointersConstantString.t.sol new file mode 100644 index 0000000..4e14431 --- /dev/null +++ b/test/lib/LibCodeGen.integrityFunctionPointersConstantString.t.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {LibCodeGen} from "src/lib/LibCodeGen.sol"; +import {IIntegrityToolingV1} from "src/interface/IIntegrityToolingV1.sol"; +import {LibCodeGenSlow} from "./LibCodeGenSlow.sol"; +import {ToolingMock} from "../concrete/ToolingMock.sol"; + +/// @dev The comment the library puts above this constant, spelled out here so +/// that a change to it fails rather than moving both sides at once. This is the +/// only one of the five that is a single line, so it is also the only one whose +/// comment is passed as a plain literal rather than a concatenation. +string constant INTEGRITY_COMMENT = "/// @dev The function pointers for the integrity check fns."; + +/// @title LibCodeGenIntegrityFunctionPointersConstantStringTest +/// @notice `integrityFunctionPointersConstantString` names the constant, writes +/// the comment and picks which of the tooling instance's builders to ask. All +/// three are the library's own choice rather than the caller's, so all three are +/// pinned here. +contract LibCodeGenIntegrityFunctionPointersConstantStringTest is Test { + ToolingMock internal sMock; + + function setUp() external { + sMock = new ToolingMock(); + } + + /// The whole emitted declaration for a short pointer string. + function testIntegrityFunctionPointersConstantString() external { + sMock.setAll(hex"aaaa", hex"bbbb", hex"cccc", hex"dddd", hex"1234"); + assertEq( + LibCodeGen.integrityFunctionPointersConstantString(vm, IIntegrityToolingV1(address(sMock))), + "\n/// @dev The function pointers for the integrity check fns.\n" + "bytes constant INTEGRITY_FUNCTION_POINTERS = hex\"1234\";\n" + ); + } + + /// The pointers come from `buildIntegrityFunctionPointers` and from no other + /// builder on the same instance. + function testIntegrityFunctionPointersConstantStringUsesItsOwnBuilder(bytes memory pointers, bytes memory other) + external + { + vm.assume(keccak256(pointers) != keccak256(other)); + sMock.setAll(other, other, other, other, pointers); + assertEq( + LibCodeGen.integrityFunctionPointersConstantString(vm, IIntegrityToolingV1(address(sMock))), + LibCodeGenSlow.bytesConstantStringSlow(vm, INTEGRITY_COMMENT, "INTEGRITY_FUNCTION_POINTERS", pointers) + ); + } +} diff --git a/test/lib/LibCodeGen.literalParserFunctionPointersConstantString.t.sol b/test/lib/LibCodeGen.literalParserFunctionPointersConstantString.t.sol new file mode 100644 index 0000000..872e5e7 --- /dev/null +++ b/test/lib/LibCodeGen.literalParserFunctionPointersConstantString.t.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {LibCodeGen} from "src/lib/LibCodeGen.sol"; +import {IParserToolingV1} from "src/interface/IParserToolingV1.sol"; +import {LibCodeGenSlow} from "./LibCodeGenSlow.sol"; +import {ToolingMock} from "../concrete/ToolingMock.sol"; + +/// @dev The comment the library puts above this constant, spelled out here so +/// that a change to it fails rather than moving both sides at once. +string constant LITERAL_PARSER_COMMENT = "/// @dev Every two bytes is a function pointer for a literal parser.\n" + "/// Literal dispatches are determined by the first byte(s) of the literal\n" + "/// rather than a full word lookup, and are done with simple conditional\n" + "/// jumps as the possibilities are limited compared to the number of words we\n" "/// have."; + +/// @title LibCodeGenLiteralParserFunctionPointersConstantStringTest +/// @notice `literalParserFunctionPointersConstantString` names the constant, +/// writes the comment and picks which of the tooling instance's builders to ask. +/// All three are the library's own choice rather than the caller's, so all three +/// are pinned here. +contract LibCodeGenLiteralParserFunctionPointersConstantStringTest is Test { + ToolingMock internal sMock; + + function setUp() external { + sMock = new ToolingMock(); + } + + /// The whole emitted declaration for a short pointer string. + function testLiteralParserFunctionPointersConstantString() external { + sMock.setAll(hex"aaaa", hex"1234", hex"bbbb", hex"cccc", hex"dddd"); + assertEq( + LibCodeGen.literalParserFunctionPointersConstantString(vm, IParserToolingV1(address(sMock))), + "\n/// @dev Every two bytes is a function pointer for a literal parser.\n" + "/// Literal dispatches are determined by the first byte(s) of the literal\n" + "/// rather than a full word lookup, and are done with simple conditional\n" + "/// jumps as the possibilities are limited compared to the number of words we\n" "/// have.\n" + "bytes constant LITERAL_PARSER_FUNCTION_POINTERS = hex\"1234\";\n" + ); + } + + /// The pointers come from `buildLiteralParserFunctionPointers` and from no + /// other builder on the same instance. `IParserToolingV1` carries two + /// builders, so the sibling on the same interface is the one most easily + /// asked by mistake. + function testLiteralParserFunctionPointersConstantStringUsesItsOwnBuilder(bytes memory pointers, bytes memory other) + external + { + vm.assume(keccak256(pointers) != keccak256(other)); + sMock.setAll(other, pointers, other, other, other); + assertEq( + LibCodeGen.literalParserFunctionPointersConstantString(vm, IParserToolingV1(address(sMock))), + LibCodeGenSlow.bytesConstantStringSlow( + vm, LITERAL_PARSER_COMMENT, "LITERAL_PARSER_FUNCTION_POINTERS", pointers + ) + ); + } +} diff --git a/test/lib/LibCodeGen.opcodeFunctionPointersConstantString.t.sol b/test/lib/LibCodeGen.opcodeFunctionPointersConstantString.t.sol new file mode 100644 index 0000000..e99c5a2 --- /dev/null +++ b/test/lib/LibCodeGen.opcodeFunctionPointersConstantString.t.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {LibCodeGen} from "src/lib/LibCodeGen.sol"; +import {IOpcodeToolingV1} from "src/interface/IOpcodeToolingV1.sol"; +import {LibCodeGenSlow} from "./LibCodeGenSlow.sol"; +import {ToolingMock} from "../concrete/ToolingMock.sol"; + +/// @dev The comment the library puts above this constant, spelled out here so +/// that a change to it fails rather than moving both sides at once. +string constant OPCODE_COMMENT = "/// @dev The function pointers known to the interpreter for dynamic dispatch.\n" + "/// By setting these as a constant they can be inlined into the interpreter\n" + "/// and loaded at eval time for very low gas (~100) due to the compiler\n" + "/// optimising it to a single `codecopy` to build the in memory bytes array."; + +/// @title LibCodeGenOpcodeFunctionPointersConstantStringTest +/// @notice `opcodeFunctionPointersConstantString` names the constant, writes the +/// comment and picks which of the tooling instance's builders to ask. All three +/// are the library's own choice rather than the caller's, so all three are +/// pinned here. +contract LibCodeGenOpcodeFunctionPointersConstantStringTest is Test { + ToolingMock internal sMock; + + function setUp() external { + sMock = new ToolingMock(); + } + + /// The whole emitted declaration for a short pointer string. + function testOpcodeFunctionPointersConstantString() external { + sMock.setAll(hex"1234", hex"aaaa", hex"bbbb", hex"cccc", hex"dddd"); + assertEq( + LibCodeGen.opcodeFunctionPointersConstantString(vm, IOpcodeToolingV1(address(sMock))), + "\n/// @dev The function pointers known to the interpreter for dynamic dispatch.\n" + "/// By setting these as a constant they can be inlined into the interpreter\n" + "/// and loaded at eval time for very low gas (~100) due to the compiler\n" + "/// optimising it to a single `codecopy` to build the in memory bytes array.\n" + "bytes constant OPCODE_FUNCTION_POINTERS = hex\"1234\";\n" + ); + } + + /// The pointers come from `buildOpcodeFunctionPointers` and from no other + /// builder on the same instance. The mock answers each builder differently, + /// so asking the wrong one emits the wrong hex. + function testOpcodeFunctionPointersConstantStringUsesItsOwnBuilder(bytes memory pointers, bytes memory other) + external + { + vm.assume(keccak256(pointers) != keccak256(other)); + sMock.setAll(pointers, other, other, other, other); + assertEq( + LibCodeGen.opcodeFunctionPointersConstantString(vm, IOpcodeToolingV1(address(sMock))), + LibCodeGenSlow.bytesConstantStringSlow(vm, OPCODE_COMMENT, "OPCODE_FUNCTION_POINTERS", pointers) + ); + } +} diff --git a/test/lib/LibCodeGen.operandHandlerFunctionPointersConstantString.t.sol b/test/lib/LibCodeGen.operandHandlerFunctionPointersConstantString.t.sol new file mode 100644 index 0000000..53e437f --- /dev/null +++ b/test/lib/LibCodeGen.operandHandlerFunctionPointersConstantString.t.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {LibCodeGen} from "src/lib/LibCodeGen.sol"; +import {IParserToolingV1} from "src/interface/IParserToolingV1.sol"; +import {LibCodeGenSlow} from "./LibCodeGenSlow.sol"; +import {ToolingMock} from "../concrete/ToolingMock.sol"; + +/// @dev The comment the library puts above this constant, spelled out here so +/// that a change to it fails rather than moving both sides at once. +string constant OPERAND_HANDLER_COMMENT = "/// @dev Every two bytes is a function pointer for an operand handler.\n" + "/// These positional indexes all map to the same indexes looked up in the parse\n" "/// meta."; + +/// @title LibCodeGenOperandHandlerFunctionPointersConstantStringTest +/// @notice `operandHandlerFunctionPointersConstantString` names the constant, +/// writes the comment and picks which of the tooling instance's builders to ask. +/// All three are the library's own choice rather than the caller's, so all three +/// are pinned here. +contract LibCodeGenOperandHandlerFunctionPointersConstantStringTest is Test { + ToolingMock internal sMock; + + function setUp() external { + sMock = new ToolingMock(); + } + + /// The whole emitted declaration for a short pointer string. + function testOperandHandlerFunctionPointersConstantString() external { + sMock.setAll(hex"aaaa", hex"bbbb", hex"1234", hex"cccc", hex"dddd"); + assertEq( + LibCodeGen.operandHandlerFunctionPointersConstantString(vm, IParserToolingV1(address(sMock))), + "\n/// @dev Every two bytes is a function pointer for an operand handler.\n" + "/// These positional indexes all map to the same indexes looked up in the parse\n" "/// meta.\n" + "bytes constant OPERAND_HANDLER_FUNCTION_POINTERS = hex\"1234\";\n" + ); + } + + /// The pointers come from `buildOperandHandlerFunctionPointers` and from no + /// other builder on the same instance. `IParserToolingV1` carries two + /// builders, so the sibling on the same interface is the one most easily + /// asked by mistake. + function testOperandHandlerFunctionPointersConstantStringUsesItsOwnBuilder( + bytes memory pointers, + bytes memory other + ) external { + vm.assume(keccak256(pointers) != keccak256(other)); + sMock.setAll(other, other, pointers, other, other); + assertEq( + LibCodeGen.operandHandlerFunctionPointersConstantString(vm, IParserToolingV1(address(sMock))), + LibCodeGenSlow.bytesConstantStringSlow( + vm, OPERAND_HANDLER_COMMENT, "OPERAND_HANDLER_FUNCTION_POINTERS", pointers + ) + ); + } +} diff --git a/test/lib/LibCodeGen.subParserWordParsersConstantString.t.sol b/test/lib/LibCodeGen.subParserWordParsersConstantString.t.sol new file mode 100644 index 0000000..73f4af8 --- /dev/null +++ b/test/lib/LibCodeGen.subParserWordParsersConstantString.t.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {LibCodeGen} from "src/lib/LibCodeGen.sol"; +import {ISubParserToolingV1} from "src/interface/ISubParserToolingV1.sol"; +import {LibCodeGenSlow} from "./LibCodeGenSlow.sol"; +import {ToolingMock} from "../concrete/ToolingMock.sol"; + +/// @dev The comment the library puts above this constant, spelled out here so +/// that a change to it fails rather than moving both sides at once. +string constant SUB_PARSER_WORD_PARSERS_COMMENT = "/// @dev The function pointers for the sub parser functions that produce the\n" + "/// bytecode that this contract knows about. This is both constructing the subParser\n" + "/// bytecode that dials back into this contract at eval time, and mapping\n" + "/// to things that happen entirely on the interpreter such as well known\n" + "/// constants and references to the context grid."; + +/// @title LibCodeGenSubParserWordParsersConstantStringTest +/// @notice `subParserWordParsersConstantString` names the constant, writes the +/// comment and picks which of the tooling instance's builders to ask. All three +/// are the library's own choice rather than the caller's, so all three are +/// pinned here. +contract LibCodeGenSubParserWordParsersConstantStringTest is Test { + ToolingMock internal sMock; + + function setUp() external { + sMock = new ToolingMock(); + } + + /// The whole emitted declaration for a short parser string. + function testSubParserWordParsersConstantString() external { + sMock.setAll(hex"aaaa", hex"bbbb", hex"cccc", hex"1234", hex"dddd"); + assertEq( + LibCodeGen.subParserWordParsersConstantString(vm, ISubParserToolingV1(address(sMock))), + "\n/// @dev The function pointers for the sub parser functions that produce the\n" + "/// bytecode that this contract knows about. This is both constructing the subParser\n" + "/// bytecode that dials back into this contract at eval time, and mapping\n" + "/// to things that happen entirely on the interpreter such as well known\n" + "/// constants and references to the context grid.\n" + "bytes constant SUB_PARSER_WORD_PARSERS = hex\"1234\";\n" + ); + } + + /// The parsers come from `buildSubParserWordParsers` and from no other + /// builder on the same instance. + function testSubParserWordParsersConstantStringUsesItsOwnBuilder(bytes memory parsers, bytes memory other) + external + { + vm.assume(keccak256(parsers) != keccak256(other)); + sMock.setAll(other, other, other, parsers, other); + assertEq( + LibCodeGen.subParserWordParsersConstantString(vm, ISubParserToolingV1(address(sMock))), + LibCodeGenSlow.bytesConstantStringSlow( + vm, SUB_PARSER_WORD_PARSERS_COMMENT, "SUB_PARSER_WORD_PARSERS", parsers + ) + ); + } +} diff --git a/test/lib/LibCodeGen.uint8ConstantString.t.sol b/test/lib/LibCodeGen.uint8ConstantString.t.sol new file mode 100644 index 0000000..7bebae5 --- /dev/null +++ b/test/lib/LibCodeGen.uint8ConstantString.t.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {LibCodeGen, MAX_LINE_LENGTH} from "src/lib/LibCodeGen.sol"; +import {LibCodeGenSlow} from "./LibCodeGenSlow.sol"; + +/// @title LibCodeGenUint8ConstantStringTest +/// @notice `uint8ConstantString` emits a Solidity `uint8 constant` declaration +/// and decides for itself whether that declaration fits on one line. The +/// decision is made by adding up magic numbers standing in for the literals it +/// is about to concatenate, so these assert the emitted text against a reference +/// that measures the line instead, and pin the decision either side of the +/// maximum. +contract LibCodeGenUint8ConstantStringTest is Test { + /// The short case: blank line, comment, then the whole declaration on one + /// line, decimal rather than hex. + function testUint8ConstantString() external pure { + assertEq( + LibCodeGen.uint8ConstantString(vm, "/// @dev Some count.", "SOME_COUNT", 42), + "\n/// @dev Some count.\nuint8 constant SOME_COUNT = 42;\n" + ); + } + + /// Zero is a real value, not a sentinel to special-case. + function testUint8ConstantStringZero() external pure { + assertEq( + LibCodeGen.uint8ConstantString(vm, "/// @dev Zero.", "ZERO", 0), + "\n/// @dev Zero.\nuint8 constant ZERO = 0;\n" + ); + } + + /// The maximum a `uint8` holds still emits as a plain decimal that fits the + /// declared type, so the generated constant compiles. + function testUint8ConstantStringMax() external pure { + assertEq( + LibCodeGen.uint8ConstantString(vm, "/// @dev Max.", "MAX", type(uint8).max), + "\n/// @dev Max.\nuint8 constant MAX = 255;\n" + ); + } + + /// A declaration of exactly the maximum length stays on one line. `forge fmt` + /// leaves a line of exactly `line_length` alone, so wrapping here would be a + /// reflow the formatter immediately undoes. + function testUint8ConstantStringAtMaxLength() external pure { + string memory name = LibCodeGenSlow.nameOfLengthSlow(98); + string memory emitted = LibCodeGen.uint8ConstantString(vm, "/// @dev At max.", name, 255); + assertEq(emitted, string.concat("\n/// @dev At max.\nuint8 constant ", name, " = 255;\n")); + assertEq(LibCodeGenSlow.longestLineSlow(emitted), MAX_LINE_LENGTH); + } + + /// One character past the maximum wraps after the `=`, with the value + /// indented by one tab width on the next line. + function testUint8ConstantStringOverMaxLengthByName() external pure { + string memory name = LibCodeGenSlow.nameOfLengthSlow(99); + assertEq( + LibCodeGen.uint8ConstantString(vm, "/// @dev Over max.", name, 255), + string.concat("\n/// @dev Over max.\nuint8 constant ", name, " =\n 255;\n") + ); + } + + /// The decimal's own width counts toward the decision, not just the name's. + /// Same name either side, one digit apart. + function testUint8ConstantStringOverMaxLengthByDigits() external pure { + string memory name = LibCodeGenSlow.nameOfLengthSlow(100); + + string memory under = LibCodeGen.uint8ConstantString(vm, "/// @dev Under.", name, 9); + assertEq(under, string.concat("\n/// @dev Under.\nuint8 constant ", name, " = 9;\n")); + assertEq(LibCodeGenSlow.longestLineSlow(under), MAX_LINE_LENGTH); + + assertEq( + LibCodeGen.uint8ConstantString(vm, "/// @dev Over.", name, 10), + string.concat("\n/// @dev Over.\nuint8 constant ", name, " =\n 10;\n") + ); + } + + /// Whatever the comment, name and value, the emitted text is the declaration + /// built from those literals, wrapped exactly when measuring the one line + /// form says it does not fit. + function testUint8ConstantStringMatchesMeasuredLine(string memory comment, string memory name, uint8 data) + external + pure + { + assertEq( + LibCodeGen.uint8ConstantString(vm, comment, name, data), + LibCodeGenSlow.uint8ConstantStringSlow(vm, comment, name, data) + ); + } + + /// The emitted literal parses back to the value it was generated from, so + /// the constant is not silently truncated or reformatted. + function testUint8ConstantStringRoundTrips(uint8 data) external pure { + assertEq( + LibCodeGen.uint8ConstantString(vm, "/// @dev Fuzz.", "FUZZ", data), + string.concat("\n/// @dev Fuzz.\nuint8 constant FUZZ = ", vm.toString(uint256(data)), ";\n") + ); + assertEq(vm.parseUint(vm.toString(uint256(data))), uint256(data)); + } +} diff --git a/test/lib/LibCodeGenSlow.sol b/test/lib/LibCodeGenSlow.sol new file mode 100644 index 0000000..05c2f2b --- /dev/null +++ b/test/lib/LibCodeGenSlow.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Vm} from "forge-std-1.16.1/src/Vm.sol"; + +/// @dev `forge fmt`'s own `line_length` default. Written out here rather than +/// imported from `LibCodeGen` so that this reference does not move when the +/// library's constant does. +uint256 constant SLOW_LINE_LENGTH = 120; + +/// @dev `forge fmt` breaks a too long constant declaration after the `=` and +/// indents the value by one `tab_width`, which also defaults to 4. +string constant SLOW_WRAP = "\n "; + +/// @title LibCodeGenSlow +/// @notice A deliberately naive reference for the constant declarations +/// `LibCodeGen` emits. +/// +/// `LibCodeGen` decides whether to wrap by adding up magic numbers that stand in +/// for the literals it is about to concatenate. This reference instead builds +/// the unwrapped line and measures it, so the two agree only when every one of +/// those magic numbers is right. Every string here is spelled out again rather +/// than imported, so a change to a literal in `LibCodeGen` shows up as a +/// disagreement instead of moving both sides at once. +library LibCodeGenSlow { + /// `vm.toString` on a `bytes` always prefixes `0x`, which a `hex"..."` + /// literal must not carry. Dropped by copying the tail one byte at a time so + /// the expectation owes nothing to `LibHexString`. + function hexOfSlow(Vm vm, bytes memory data) internal pure returns (string memory) { + bytes memory prefixed = bytes(vm.toString(data)); + bytes memory stripped = new bytes(prefixed.length - 2); + for (uint256 i = 2; i < prefixed.length; i++) { + stripped[i - 2] = prefixed[i]; + } + return string(stripped); + } + + /// Joins the declaration onto one line when it fits, and onto two when it + /// does not. `declaration` is everything up to and including the `=`, and + /// `value` is everything after it. + function joinSlow(string memory declaration, string memory value) internal pure returns (string memory) { + string memory oneLine = string.concat(declaration, " ", value); + if (bytes(oneLine).length > SLOW_LINE_LENGTH) { + return string.concat(declaration, SLOW_WRAP, value); + } + return oneLine; + } + + function bytesConstantStringSlow(Vm vm, string memory comment, string memory name, bytes memory data) + internal + pure + returns (string memory) + { + return string.concat( + "\n", + comment, + "\n", + joinSlow(string.concat("bytes constant ", name, " ="), string.concat("hex\"", hexOfSlow(vm, data), "\";")), + "\n" + ); + } + + function uint8ConstantStringSlow(Vm vm, string memory comment, string memory name, uint8 data) + internal + pure + returns (string memory) + { + return string.concat( + "\n", + comment, + "\n", + joinSlow(string.concat("uint8 constant ", name, " ="), string.concat(vm.toString(uint256(data)), ";")), + "\n" + ); + } + + function bytes32ConstantStringSlow(Vm vm, string memory comment, string memory name, bytes32 data) + internal + pure + returns (string memory) + { + return string.concat( + "\n", + comment, + "\n", + joinSlow( + string.concat("bytes32 constant ", name, " ="), string.concat("bytes32(", vm.toString(data), ");") + ), + "\n" + ); + } + + function addressConstantStringSlow(Vm vm, string memory comment, string memory name, address data) + internal + pure + returns (string memory) + { + return string.concat( + "\n", + comment, + "\n", + joinSlow( + string.concat("address constant ", name, " ="), string.concat("address(", vm.toString(data), ");") + ), + "\n" + ); + } + + /// The length of the longest line in `text`, so a test can assert what + /// `forge fmt` would measure rather than what the library predicted. + function longestLineSlow(string memory text) internal pure returns (uint256) { + bytes memory data = bytes(text); + uint256 longest = 0; + uint256 current = 0; + for (uint256 i = 0; i < data.length; i++) { + if (data[i] == 0x0a) { + if (current > longest) { + longest = current; + } + current = 0; + } else { + current++; + } + } + if (current > longest) { + longest = current; + } + return longest; + } + + /// A name of `length` repeated `A` characters, for pinning the wrap decision + /// either side of the maximum line length without spelling out a name that + /// nobody can count by eye. + function nameOfLengthSlow(uint256 length) internal pure returns (string memory) { + bytes memory name = new bytes(length); + for (uint256 i = 0; i < length; i++) { + name[i] = "A"; + } + return string(name); + } +} From 2c710331bfae1c8baeae6e055c6f42e42173e6f6 Mon Sep 17 00:00:00 2001 From: David Meister Date: Sun, 16 Aug 2026 14:08:03 +0000 Subject: [PATCH 2/2] test(LibCodeGen): make the new suites independent of seed and of each other MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two of the suites added in the previous commit were green only by luck. Both reproduce on a clean tree at HEAD and both were passing CI on a draw that happened to miss them. `bytecodeHashConstantString` fuzzes `bytes` straight into `vm.etch`, which refuses code shaped like an EIP-7702 delegation designator — leading bytes `0xef01` — at any length other than 23. The fuzzer reaches that shape on some seeds and not others, so the suite failed on the first run with `Eip7702 is not 23 bytes long`. The refused set was measured against the cheatcode rather than assumed — `0xef01` + 21 bytes is accepted, the same prefix at 22, 24 and 2 bytes is refused, and `0xef`, `0xef0000` and `0xef02…` are all accepted — so the fuzz domain now excludes exactly that shape and nothing else. `describedByMetaHashConstantString` had all four of its file-backed tests writing and then removing one shared fixture path. The fixture is real state that outlives the EVM, so one test's `removeFile` lands between another's `writeFileBinary` and the library's read, and that test fails on a file it had just written. It reproduced at a fixed `--fuzz-seed`, so the fuzzer was never involved. Each test owns a distinct path now. Verified over 30 consecutive full-suite runs — seeds 1..20 plus ten repeats at the seed the fixture race reproduced on — all 51 tests green, `forge fmt --check` clean. This is also a precondition for the mutation matrices: a flaky baseline scores mutants KILLED on the flake rather than on the mutation, and `mutation-probe` refuses to start against it. Co-Authored-By: Claude Opus 5 (1M context) --- ...ibCodeGen.bytecodeHashConstantString.t.sol | 25 +++++++-- ...en.describedByMetaHashConstantString.t.sol | 54 +++++++++++++------ 2 files changed, 59 insertions(+), 20 deletions(-) diff --git a/test/lib/LibCodeGen.bytecodeHashConstantString.t.sol b/test/lib/LibCodeGen.bytecodeHashConstantString.t.sol index 03ac19d..9177377 100644 --- a/test/lib/LibCodeGen.bytecodeHashConstantString.t.sol +++ b/test/lib/LibCodeGen.bytecodeHashConstantString.t.sol @@ -17,6 +17,24 @@ import {CodeGennable} from "../concrete/CodeGennable.sol"; contract LibCodeGenBytecodeHashConstantStringTest is Test { address internal constant INSTANCE = address(uint160(uint256(keccak256("instance")))); + /// `vm.etch` refuses code shaped like an EIP-7702 delegation designator — + /// leading bytes `0xef01` — unless it is exactly the 23 bytes such a + /// designator has to be. Measured against the cheatcode rather than assumed: + /// `0xef01` + 21 bytes is accepted, `0xef01` at 22, 24 and 2 bytes is + /// refused, and `0xef`, `0xef0000` and `0xef02…` are all accepted, so the + /// refusal is exactly this two byte prefix at a length other than 23. + /// + /// That is a restriction on what the cheatcode will install at an address, + /// not a property of `bytecodeHashConstantString`, so the fuzzer's domain + /// excludes it. Without this the suite passes or fails on the luck of the + /// fuzz seed: the seeds CI happened to draw never reached `0xef01…`, and + /// seeds drawn locally did, failing on the first run with + /// `vm.etch: failed to create bytecode: Eip7702 is not 23 bytes long`. + function assumeEtchableCode(bytes memory code) internal pure { + vm.assume(code.length > 0); + vm.assume(!(code.length >= 2 && code[0] == 0xef && code[1] == 0x01 && code.length != 23)); + } + /// The whole emitted declaration, for code put at the address by hand. The /// name and comment are the library's, not the caller's, so they are pinned /// exactly: every consumer's generated file and every consumer's assertion @@ -58,7 +76,7 @@ contract LibCodeGenBytecodeHashConstantStringTest is Test { /// Whatever the runtime code, the constant carries its keccak256 hash. function testBytecodeHashConstantStringHashesCode(bytes memory code) external { - vm.assume(code.length > 0); + assumeEtchableCode(code); vm.etch(INSTANCE, code); assertEq( LibCodeGen.bytecodeHashConstantString(vm, INSTANCE), @@ -74,7 +92,8 @@ contract LibCodeGenBytecodeHashConstantStringTest is Test { /// constant is a fingerprint of the code rather than of the address or of /// anything else about the account. function testBytecodeHashConstantStringDiscriminatesCode(bytes memory codeA, bytes memory codeB) external { - vm.assume(codeA.length > 0 && codeB.length > 0); + assumeEtchableCode(codeA); + assumeEtchableCode(codeB); vm.assume(keccak256(codeA) != keccak256(codeB)); vm.etch(INSTANCE, codeA); @@ -88,7 +107,7 @@ contract LibCodeGenBytecodeHashConstantStringTest is Test { /// The same instance generates the same text every time it is asked, so /// regenerating a file twice does not produce a diff. function testBytecodeHashConstantStringIdempotent(bytes memory code) external { - vm.assume(code.length > 0); + assumeEtchableCode(code); vm.etch(INSTANCE, code); assertEq( LibCodeGen.bytecodeHashConstantString(vm, INSTANCE), LibCodeGen.bytecodeHashConstantString(vm, INSTANCE) diff --git a/test/lib/LibCodeGen.describedByMetaHashConstantString.t.sol b/test/lib/LibCodeGen.describedByMetaHashConstantString.t.sol index 0c6c2bf..f284779 100644 --- a/test/lib/LibCodeGen.describedByMetaHashConstantString.t.sol +++ b/test/lib/LibCodeGen.describedByMetaHashConstantString.t.sol @@ -11,8 +11,16 @@ import {LibCodeGenSlow} from "./LibCodeGenSlow.sol"; /// the one directory it grants read-write, so a fixture goes there and the name /// walks back out of `meta` to reach it. That the name reaches the path at all /// is itself asserted below. -string constant FIXTURE_NAME = "../src/generated/LibCodeGenDescribedByMetaHashFixture"; -string constant FIXTURE_PATH = "src/generated/LibCodeGenDescribedByMetaHashFixture.rain.meta"; +/// +/// The fixture file is real, shared, mutable state that outlives the EVM: every +/// test here writes it and then removes it. A single path shared across the +/// tests is therefore a race — one test's `removeFile` lands between another's +/// `writeFileBinary` and the library's read, and that test fails with +/// `vm.readFileBinary: ... No such file or directory` on a file it had just +/// written. It reproduced as an intermittent 2-of-5 failure in this suite at a +/// fixed `--fuzz-seed`, so the fuzzer was never involved. Each test owns a +/// distinct path instead, which is what makes them independent of each other. +string constant FIXTURE_STEM = "LibCodeGenDescribedByMetaHashFixture"; /// @dev The fixed part of the declaration. The name and comment are the /// library's own choice rather than the caller's, so both are pinned exactly: @@ -30,6 +38,18 @@ contract LibCodeGenDescribedByMetaHashConstantStringTest is Test { return LibCodeGen.describedByMetaHashConstantString(vm, name); } + /// The path this test's own fixture is written to, relative to the repo root. + function fixturePath(string memory owner) internal pure returns (string memory) { + return string.concat("src/generated/", FIXTURE_STEM, owner, ".rain.meta"); + } + + /// The name that makes the library build `fixturePath(owner)`: it walks back + /// out of the `meta/` directory the library prepends, and stops short of the + /// `.rain.meta` the library appends. + function fixtureName(string memory owner) internal pure returns (string memory) { + return string.concat("../src/generated/", FIXTURE_STEM, owner); + } + /// The path is `meta/.rain.meta`. The name is not a file that exists /// here, so the path is observed through the access refusal, which quotes /// the path that was asked for. Two names, because a hard coded path would @@ -51,9 +71,9 @@ contract LibCodeGenDescribedByMetaHashConstantStringTest is Test { /// the file's name or path. function testDescribedByMetaHashConstantString() external { bytes memory meta = hex"1234"; - vm.writeFileBinary(FIXTURE_PATH, meta); - string memory emitted = LibCodeGen.describedByMetaHashConstantString(vm, FIXTURE_NAME); - vm.removeFile(FIXTURE_PATH); + vm.writeFileBinary(fixturePath("Basic"), meta); + string memory emitted = LibCodeGen.describedByMetaHashConstantString(vm, fixtureName("Basic")); + vm.removeFile(fixturePath("Basic")); assertEq(emitted, string.concat(DESCRIBED_BY_META_HASH_PREFIX, vm.toString(keccak256(meta)), ");\n")); } @@ -64,11 +84,11 @@ contract LibCodeGenDescribedByMetaHashConstantStringTest is Test { bytes memory metaA = hex"1234"; bytes memory metaB = hex"5678"; - vm.writeFileBinary(FIXTURE_PATH, metaA); - string memory emittedA = LibCodeGen.describedByMetaHashConstantString(vm, FIXTURE_NAME); - vm.writeFileBinary(FIXTURE_PATH, metaB); - string memory emittedB = LibCodeGen.describedByMetaHashConstantString(vm, FIXTURE_NAME); - vm.removeFile(FIXTURE_PATH); + vm.writeFileBinary(fixturePath("HashesContents"), metaA); + string memory emittedA = LibCodeGen.describedByMetaHashConstantString(vm, fixtureName("HashesContents")); + vm.writeFileBinary(fixturePath("HashesContents"), metaB); + string memory emittedB = LibCodeGen.describedByMetaHashConstantString(vm, fixtureName("HashesContents")); + vm.removeFile(fixturePath("HashesContents")); assertEq(emittedA, string.concat(DESCRIBED_BY_META_HASH_PREFIX, vm.toString(keccak256(metaA)), ");\n")); assertEq(emittedB, string.concat(DESCRIBED_BY_META_HASH_PREFIX, vm.toString(keccak256(metaB)), ");\n")); @@ -78,10 +98,10 @@ contract LibCodeGenDescribedByMetaHashConstantStringTest is Test { /// Reading the same file twice gives the same text, so regenerating a file /// does not produce a diff. function testDescribedByMetaHashConstantStringIdempotent() external { - vm.writeFileBinary(FIXTURE_PATH, hex"1234"); - string memory first = LibCodeGen.describedByMetaHashConstantString(vm, FIXTURE_NAME); - string memory second = LibCodeGen.describedByMetaHashConstantString(vm, FIXTURE_NAME); - vm.removeFile(FIXTURE_PATH); + vm.writeFileBinary(fixturePath("Idempotent"), hex"1234"); + string memory first = LibCodeGen.describedByMetaHashConstantString(vm, fixtureName("Idempotent")); + string memory second = LibCodeGen.describedByMetaHashConstantString(vm, fixtureName("Idempotent")); + vm.removeFile(fixturePath("Idempotent")); assertEq(first, second); } @@ -91,9 +111,9 @@ contract LibCodeGenDescribedByMetaHashConstantStringTest is Test { /// because this function does its own concatenation instead of going through /// `bytes32ConstantString` and so has no wrap decision at all. function testDescribedByMetaHashConstantStringFitsMaxLength() external { - vm.writeFileBinary(FIXTURE_PATH, hex"1234"); - string memory emitted = LibCodeGen.describedByMetaHashConstantString(vm, FIXTURE_NAME); - vm.removeFile(FIXTURE_PATH); + vm.writeFileBinary(fixturePath("FitsMaxLength"), hex"1234"); + string memory emitted = LibCodeGen.describedByMetaHashConstantString(vm, fixtureName("FitsMaxLength")); + vm.removeFile(fixturePath("FitsMaxLength")); assertLe(LibCodeGenSlow.longestLineSlow(emitted), MAX_LINE_LENGTH); }