Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/warm-groups-knock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hardhat": patch
---

Fix inline config reading for large buildinfo files
1 change: 1 addition & 0 deletions cspell.dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ onuncaughtexception
perché
perché
popd
popescuoctavian
PREVRANDAO
pushd
randao
Expand Down
24 changes: 24 additions & 0 deletions end-to-end/base-contracts/preinstall.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env bash

~/.foundry/bin/forge install --no-git \
github.com/foundry-rs/forge-std@6853b9ec7df5dc0c213b05ae67785ad4f4baa0ea \
github.com/runtimeverification/kontrol-cheatcodes@2c48ae1ab44228c199dca29414c0b4b18a3434e6 \
github.com/ethereum-optimism/lib-keccak@3b1e7bbb4cc23e9228097cfebe42aedaf3b8f2b9 \
github.com/OpenZeppelin/openzeppelin-contracts@ecd2ca2cd7cac116f7a37d0e474bbb3d7d5e1c4d \
github.com/OpenZeppelin/openzeppelin-contracts-upgradeable@0a2cb9a445c365870ed7a8ab461b12acf3e27d63 \
github.com/transmissions11/solmate@8f9b23f8838670afda0fd8983f2c41e8037ae6bc \
github.com/safe-global/safe-contracts@bf943f80fec5ac647159d26161446ac5d716a294 \
github.com/Vectorized/solady@502cc1ea718e6fa73b380635ee0868b0740595f0 \
github.com/base/nitro-validator@0f006d2075637dd9640e530c4a7065f5c8bb2132 \
github.com/base/op-enclave@a2d5398f04c3a8e4df929d58ee638ba4a037bfec \
github.com/risc0/risc0-ethereum@a78ac4a52fe9cfa14120c3b496430f0d42e1d8d3 \
github.com/succinctlabs/sp1-contracts@22c4a47cd0a388cb4e25b4f2513954e4275c74ca

~/.foundry/bin/forge install --no-git \
github.com/ethereum-optimism/superchain-registry@84bce73573f130008d84bae6e924163bab589a11

git clone --no-checkout https://github.com/OpenZeppelin/openzeppelin-contracts.git lib/openzeppelin-contracts-v5
git -C lib/openzeppelin-contracts-v5 checkout dbb6104ce834628e473d2173bbc9d47f81a9eec3

git clone --no-checkout https://github.com/Vectorized/solady.git lib/solady-v0.0.245
git -C lib/solady-v0.0.245 checkout 8583a6e386b897f3db142a541f86d5953eccd835
11 changes: 11 additions & 0 deletions end-to-end/base-contracts/scenario.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "../../scripts/end-to-end/schema/scenario.schema.json",
"description": "Base contracts main branch (Foundry Solidity test suite)",
"tags": ["external-repo"],
"repo": "popescuoctavian/base-contracts",
"commit": "8f365822e412f90a57b80ba6571815bb1152fe79",
"packageManager": "pnpm",
"submodules": true,
"preinstall": "./preinstall.sh",
"defaultCommand": "pnpm hardhat test solidity"
Comment thread
kanej marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { readBinaryFile } from "@nomicfoundation/hardhat-utils/fs";

export interface BuildInfoAndOutput extends EdrBuildInfoAndOutput {
buildInfoId: string;
buildInfoOutputPath: string;
}

export interface EdrArtifactWithMetadata {
Expand Down Expand Up @@ -58,6 +59,7 @@ export async function getBuildInfosAndOutputs(
buildInfoId,
buildInfo,
output,
buildInfoOutputPath,
};
}),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
HardhatError,
assertHardhatInvariant,
} from "@nomicfoundation/hardhat-errors";
import { bytesToUtf8String } from "@nomicfoundation/hardhat-utils/bytes";
import { readJsonFileAsStream } from "@nomicfoundation/hardhat-utils/fs";

import { getFullyQualifiedName } from "../../../../utils/contract-names.js";

Expand Down Expand Up @@ -48,11 +48,11 @@ interface CollectedOverrides {
* in the solc AST. It only extracts them from the build info where each
* test artifact's file was compiled as a root file.
*/
export function getTestFunctionOverrides(
export async function getTestFunctionOverrides(
testSuiteArtifacts: EdrArtifactWithMetadata[],
buildInfosAndOutputs: BuildInfoAndOutput[],
): TestFunctionOverride[] {
const allRawOverrides = collectRawOverrides(
): Promise<TestFunctionOverride[]> {
const allRawOverrides = await collectRawOverrides(
testSuiteArtifacts,
buildInfosAndOutputs,
);
Expand All @@ -62,10 +62,10 @@ export function getTestFunctionOverrides(
return buildTestFunctionOverrides(allRawOverrides);
}

function collectRawOverrides(
async function collectRawOverrides(
testSuiteArtifacts: EdrArtifactWithMetadata[],
buildInfosAndOutputs: BuildInfoAndOutput[],
): CollectedOverrides {
): Promise<CollectedOverrides> {
const overrides: RawInlineOverride[] = [];
const methodIdentifiersByContract = new Map<string, Record<string, string>>();

Expand Down Expand Up @@ -153,8 +153,8 @@ function collectRawOverrides(
continue;
}

const buildInfoOutput: SolidityBuildInfoOutput = JSON.parse(
bytesToUtf8String(buildInfoAndOutput.output),
const buildInfoOutput = await readJsonFileAsStream<SolidityBuildInfoOutput>(
buildInfoAndOutput.buildInfoOutputPath,
);
Comment on lines +156 to 158
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

collectRawOverrides re-reads buildInfoOutputPath from disk even though getBuildInfosAndOutputs already loaded output into memory. For large build-info outputs this doubles I/O and can noticeably slow down test startup. Consider parsing from the already-loaded Uint8Array using a streaming JSON parser (e.g., piping a Readable.from(buildInfoAndOutput.output) into the same JSONParser used by readJsonFileAsStream), or refactoring getBuildInfosAndOutputs to avoid eagerly reading output when only inline-config extraction needs it.

Copilot uses AI. Check for mistakes.

for (const [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ const runSolidityTests: NewTaskActionFunction<TestActionArguments> = async (
}
}

const testFunctionOverrides = getTestFunctionOverrides(
const testFunctionOverrides = await getTestFunctionOverrides(
testSuiteArtifacts,
allBuildInfosAndOutputs,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import assert from "node:assert/strict";
import { describe, it } from "node:test";

import { HardhatError } from "@nomicfoundation/hardhat-errors";
import { assertThrowsHardhatError } from "@nomicfoundation/hardhat-test-utils";
import { assertRejectsWithHardhatError } from "@nomicfoundation/hardhat-test-utils";

import {
getFunctionFqn,
Expand All @@ -14,7 +14,7 @@ import { makeBuildInfo, makeTestSuiteArtifact } from "./mocks.js";

describe("inline-config", () => {
describe("getTestFunctionOverrides", () => {
it("should return empty array when no build infos contain inline config", () => {
it("should return empty array when no build infos contain inline config", async () => {
const bi = makeBuildInfo(
"test/MyTest.sol",
"// just a comment\ncontract MyTest {}",
Expand All @@ -26,10 +26,10 @@ describe("inline-config", () => {
},
);
const artifacts = [makeTestSuiteArtifact("test/MyTest.sol", "MyTest")];
assert.deepEqual(getTestFunctionOverrides(artifacts, [bi]), []);
assert.deepEqual(await getTestFunctionOverrides(artifacts, [bi]), []);
});

it("should deduplicate when same source appears in multiple build infos", () => {
it("should deduplicate when same source appears in multiple build infos", async () => {
const bi1 = makeBuildInfo(
"test/MyTest.sol",
"/// hardhat-config: fuzz.runs = 10",
Expand Down Expand Up @@ -61,11 +61,11 @@ describe("inline-config", () => {
},
);
const artifacts = [makeTestSuiteArtifact("test/MyTest.sol", "MyTest")];
const overrides = getTestFunctionOverrides(artifacts, [bi1, bi2]);
const overrides = await getTestFunctionOverrides(artifacts, [bi1, bi2]);
assert.equal(overrides.length, 1);
});

it("should produce correct ArtifactId with solcVersion", () => {
it("should produce correct ArtifactId with solcVersion", async () => {
const bi = makeBuildInfo(
"test/MyTest.sol",
"/// hardhat-config: fuzz.runs = 10",
Expand All @@ -85,15 +85,15 @@ describe("inline-config", () => {
const artifacts = [
makeTestSuiteArtifact("test/MyTest.sol", "MyTest", "0.8.20"),
];
const overrides = getTestFunctionOverrides(artifacts, [bi]);
const overrides = await getTestFunctionOverrides(artifacts, [bi]);
assert.deepEqual(overrides[0].identifier.contractArtifact, {
name: "MyTest",
source: "test/MyTest.sol",
solcVersion: "0.8.20",
});
});

it("should throw UNRESOLVED_SELECTOR when function has no ABI entry", () => {
it("should throw UNRESOLVED_SELECTOR when function has no ABI entry", async () => {
const bi = makeBuildInfo(
"test/MyTest.sol",
"/// hardhat-config: fuzz.runs = 10",
Expand All @@ -110,8 +110,8 @@ describe("inline-config", () => {
},
);
const artifacts = [makeTestSuiteArtifact("test/MyTest.sol", "MyTest")];
assertThrowsHardhatError(
() => getTestFunctionOverrides(artifacts, [bi]),
await assertRejectsWithHardhatError(
async () => getTestFunctionOverrides(artifacts, [bi]),
HardhatError.ERRORS.CORE.SOLIDITY_TESTS
.INLINE_CONFIG_UNRESOLVED_SELECTOR,
{
Expand All @@ -124,7 +124,7 @@ describe("inline-config", () => {
);
});

it("should process a basic end-to-end case", () => {
it("should process a basic end-to-end case", async () => {
const bi = makeBuildInfo(
"test/MyTest.sol",
"/// hardhat-config: fuzz.runs = 50",
Expand All @@ -141,13 +141,13 @@ describe("inline-config", () => {
},
);
const artifacts = [makeTestSuiteArtifact("test/MyTest.sol", "MyTest")];
const overrides = getTestFunctionOverrides(artifacts, [bi]);
const overrides = await getTestFunctionOverrides(artifacts, [bi]);
assert.equal(overrides.length, 1);
assert.equal(overrides[0].identifier.functionSelector, "0xaabbccdd");
assert.deepEqual(overrides[0].config, { fuzz: { runs: 50 } });
});

it("should merge overrides from mixed hardhat-config: and forge-config: prefixes", () => {
it("should merge overrides from mixed hardhat-config: and forge-config: prefixes", async () => {
const bi = makeBuildInfo(
"test/MyTest.sol",
"/// hardhat-config: fuzz.runs = 10\n/// forge-config: fuzz.max-test-rejects = 500",
Expand All @@ -165,14 +165,14 @@ describe("inline-config", () => {
},
);
const artifacts = [makeTestSuiteArtifact("test/MyTest.sol", "MyTest")];
const overrides = getTestFunctionOverrides(artifacts, [bi]);
const overrides = await getTestFunctionOverrides(artifacts, [bi]);
assert.equal(overrides.length, 1);
assert.deepEqual(overrides[0].config, {
fuzz: { runs: 10, maxTestRejects: 500 },
});
});

it("should throw BUILD_INFO_NOT_FOUND when artifact references missing build info", () => {
it("should throw BUILD_INFO_NOT_FOUND when artifact references missing build info", async () => {
const bi = makeBuildInfo(
"test/MyTest.sol",
"/// hardhat-config: fuzz.runs = 10",
Expand All @@ -192,8 +192,8 @@ describe("inline-config", () => {
);

const artifacts = [makeTestSuiteArtifact("test/MyTest.sol", "MyTest")];
assertThrowsHardhatError(
() => getTestFunctionOverrides(artifacts, [bi]),
await assertRejectsWithHardhatError(
async () => getTestFunctionOverrides(artifacts, [bi]),
HardhatError.ERRORS.CORE.SOLIDITY_TESTS
.BUILD_INFO_NOT_FOUND_FOR_CONTRACT,
{
Expand All @@ -202,7 +202,7 @@ describe("inline-config", () => {
);
});

it("should produce separate overrides for overloaded functions", () => {
it("should produce separate overrides for overloaded functions", async () => {
const bi = makeBuildInfo(
"test/MyTest.sol",
"/// hardhat-config: fuzz.runs = 10\n/// hardhat-config: fuzz.runs = 20",
Expand All @@ -228,7 +228,7 @@ describe("inline-config", () => {
},
);
const artifacts = [makeTestSuiteArtifact("test/MyTest.sol", "MyTest")];
const result = getTestFunctionOverrides(artifacts, [bi]);
const result = await getTestFunctionOverrides(artifacts, [bi]);
assert.equal(result.length, 2);

const selectors = result.map((r) => r.identifier.functionSelector).sort();
Expand All @@ -241,7 +241,7 @@ describe("inline-config", () => {
assert.deepEqual(bySelector.get("0x11223344"), { fuzz: { runs: 20 } });
});

it("should not duplicate overrides when same source is a root in multiple build infos", () => {
it("should not duplicate overrides when same source is a root in multiple build infos", async () => {
const bi1 = makeBuildInfo(
"test/MyTest.sol",
"/// hardhat-config: fuzz.runs = 10",
Expand Down Expand Up @@ -280,12 +280,12 @@ describe("inline-config", () => {
const artifacts = [
makeTestSuiteArtifact("test/MyTest.sol", "MyTest", "0.8.23", "bi-1"),
];
const overrides = getTestFunctionOverrides(artifacts, [bi1, bi2]);
const overrides = await getTestFunctionOverrides(artifacts, [bi1, bi2]);
assert.equal(overrides.length, 1);
assert.deepEqual(overrides[0].config, { fuzz: { runs: 10 } });
});

it("should handle multiple contracts in a single source file", () => {
it("should handle multiple contracts in a single source file", async () => {
const bi = makeBuildInfo(
"test/Multi.sol",
"/// hardhat-config: fuzz.runs = 10\n/// hardhat-config: fuzz.runs = 20",
Expand Down Expand Up @@ -314,7 +314,7 @@ describe("inline-config", () => {
makeTestSuiteArtifact("test/Multi.sol", "ContractA"),
makeTestSuiteArtifact("test/Multi.sol", "ContractB"),
];
const result = getTestFunctionOverrides(artifacts, [bi]);
const result = await getTestFunctionOverrides(artifacts, [bi]);
assert.equal(result.length, 2);

const bySelector = new Map(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { EdrArtifactWithMetadata } from "../../../../../src/internal/builtin-plugins/solidity-test/edr-artifacts.js";
import type { RawInlineOverride } from "../../../../../src/internal/builtin-plugins/solidity-test/inline-config/index.js";

import { randomUUID } from "node:crypto";
import { writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

import { utf8StringToBytes } from "@nomicfoundation/hardhat-utils/bytes";

export function makeRawOverride(
Expand Down Expand Up @@ -57,7 +62,12 @@ export function makeBuildInfo(
>,
solcVersion = "0.8.23",
buildInfoId = "test-build-info-id",
): { buildInfo: Uint8Array; output: Uint8Array; buildInfoId: string } {
): {
buildInfo: Uint8Array;
output: Uint8Array;
buildInfoId: string;
buildInfoOutputPath: string;
} {
const buildInfoJson = {
_format: "hh3-sol-build-info-1",
id: "test-build-info",
Expand Down Expand Up @@ -121,9 +131,14 @@ export function makeBuildInfo(
},
};

const outputJsonString = JSON.stringify(outputJson);
const buildInfoOutputPath = join(tmpdir(), `${randomUUID()}.json`);
writeFileSync(buildInfoOutputPath, outputJsonString);

Comment on lines +134 to +137
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makeBuildInfo writes a JSON file into the OS temp directory but nothing deletes it after the test run. Over time (or with repeated CI retries) this can accumulate temp files and waste disk space; consider deleting these files in the tests (e.g., track returned buildInfoOutputPaths and remove them in an after/afterEach hook) or generate them in a per-test temp dir that is cleaned up automatically.

Copilot uses AI. Check for mistakes.
return {
buildInfo: utf8StringToBytes(JSON.stringify(buildInfoJson)),
output: utf8StringToBytes(JSON.stringify(outputJson)),
output: utf8StringToBytes(outputJsonString),
buildInfoId,
buildInfoOutputPath,
};
}
15 changes: 14 additions & 1 deletion scripts/end-to-end/schema/scenario-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,19 @@ describe("isScenarioDefinition", () => {
assert.equal(isScenarioDefinition(value), true);
});

it("accepts pnpm as a package manager", () => {
const value = {
description: "Base contracts with pnpm",
repo: "popescuoctavian/base-contracts",
commit: "abc123",
packageManager: "pnpm",
defaultCommand: "pnpm exec hardhat test solidity",
tags: ["external-repo"],
};

assert.equal(isScenarioDefinition(value), true);
});

it("accepts a scenario with disabled: true", () => {
const value = {
description: "A disabled scenario",
Expand Down Expand Up @@ -131,7 +144,7 @@ describe("isScenarioDefinition", () => {
isScenarioDefinition({
repo: "org/repo",
commit: "abc",
packageManager: "pnpm",
packageManager: "pip",
tags: [],
description: "a scenario",
defaultCommand: "npx hardhat test",
Expand Down
1 change: 1 addition & 0 deletions scripts/end-to-end/schema/scenario-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export function isScenarioDefinition(
typeof obj.commit === "string" &&
(obj.packageManager === "npm" ||
obj.packageManager === "bun" ||
obj.packageManager === "pnpm" ||
obj.packageManager === "yarn") &&
typeof obj.defaultCommand === "string" &&
Array.isArray(obj.tags) &&
Expand Down
2 changes: 1 addition & 1 deletion scripts/end-to-end/schema/scenario.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
},
"packageManager": {
"type": "string",
"enum": ["npm", "bun", "yarn"],
"enum": ["npm", "bun", "pnpm", "yarn"],
"description": "Package manager used by the repo"
},
"defaultCommand": {
Expand Down
2 changes: 1 addition & 1 deletion scripts/end-to-end/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface ScenarioDefinition {
description: string;
repo: string;
commit: string;
packageManager: "npm" | "bun" | "yarn";
packageManager: "npm" | "bun" | "pnpm" | "yarn";
defaultCommand: string;
preinstall?: string;
install?: string;
Expand Down
Loading
Loading