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
56 changes: 56 additions & 0 deletions test/concrete/ToolingMock.sol
Original file line numberDiff line numberDiff line change
@@ -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;
}
}
47 changes: 46 additions & 1 deletion test/lib/LibCodeGen.addressConstantString.t.sol
Original file line numberDiff line numberDiff line change
Expand Up@@ -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`
Expand DownExpand Up@@ -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)
);
}
}
116 changes: 116 additions & 0 deletions test/lib/LibCodeGen.bytecodeHashConstantString.t.sol
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
// 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"))));

/// `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
/// 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 {
assumeEtchableCode(code);
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 {
assumeEtchableCode(codeA);
assumeEtchableCode(codeB);
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 {
assumeEtchableCode(code);
vm.etch(INSTANCE, code);
assertEq(
LibCodeGen.bytecodeHashConstantString(vm, INSTANCE), LibCodeGen.bytecodeHashConstantString(vm, INSTANCE)
);
}
}
45 changes: 44 additions & 1 deletion test/lib/LibCodeGen.bytes32ConstantString.t.sol
Original file line numberDiff line numberDiff line change
Expand Up@@ -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`
Expand DownExpand Up@@ -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)
);
}
}
107 changes: 107 additions & 0 deletions test/lib/LibCodeGen.bytesConstantString.t.sol
Original file line numberDiff line numberDiff line change
@@ -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");
}
}
Loading
Loading