Skip to content

Commit ad17527

Browse files
committed
Gas snapshots - Show a link to the test file on failures (#7883)
* chore: update function gas snapshots file to test duplicate contracts * feat: show source path in failed snapshot check for function gas snapshots * feat: show source path in failed snapshot check for snapshot cheatcodes * Create fuzzy-ravens-promise.md * Update fuzzy-ravens-promise.md
1 parent 927547e commit ad17527

File tree

9 files changed

+423
-79
lines changed

9 files changed

+423
-79
lines changed

.changeset/fuzzy-ravens-promise.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"hardhat": patch
3+
---
4+
5+
Show a link to the test file on failures in the gas snapshots output message ([#7864](https://github.com/NomicFoundation/hardhat/issues/7864))

v-next/example-project/.gas-snapshot

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ CalculatorTest#testSnapshotGasSubtraction() (gas: 57557)
1717
CalculatorTest#testSnapshotValues() (gas: 82076)
1818
CalculatorTest#testSubtraction() (gas: 59962)
1919
CalculatorTest#testSubtractionUnderflow() (gas: 9920)
20-
CounterTest#testFuzzInc(uint8) (runs: 256, μ: 109380, ~: 61119)
21-
CounterTest#testInitialValue() (gas: 11173)
22-
CounterTest#testSnapshotGasLastCall() (gas: 31032)
23-
CounterTest#testSnapshotGasRegion() (gas: 41130)
24-
CounterTest#testSnapshotValue() (gas: 38393)
20+
contracts/Counter.t.sol:CounterTest#testFuzzInc(uint8) (runs: 256, μ: 109380, ~: 61119)
21+
contracts/Counter.t.sol:CounterTest#testInitialValue() (gas: 11173)
22+
contracts/Counter.t.sol:CounterTest#testSnapshotGasLastCall() (gas: 31032)
23+
contracts/Counter.t.sol:CounterTest#testSnapshotGasRegion() (gas: 41130)
24+
contracts/Counter.t.sol:CounterTest#testSnapshotValue() (gas: 38393)
25+
test/contracts/Counter.t.sol:CounterTest#testFuzzInc(uint8) (runs: 256, μ: 98211, ~: 59474)
26+
test/contracts/Counter.t.sol:CounterTest#testInitialValue() (gas: 11151)
2527
TestContract#testExpectArithmetic() (gas: 9899)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
import "../../contracts/Counter.sol";
5+
import "forge-std/Test.sol";
6+
7+
/**
8+
* This test contract is intentionally duplicated from
9+
* v-next/example-project/contracts/Counter.t.sol to test gas
10+
* snapshot functionality in the hardhat gas analytics plugin.
11+
*/
12+
contract CounterTest is Test {
13+
Counter counter;
14+
15+
function setUp() public {
16+
console.log("Setting up");
17+
counter = new Counter();
18+
console.log("Counter set up");
19+
}
20+
21+
function testInitialValue() public view {
22+
console.log("Testing initial value");
23+
require(counter.x() == 0, "Initial value should be 0");
24+
}
25+
26+
function testFuzzInc(uint8 x) public {
27+
console.log("Fuzz testing inc");
28+
for (uint8 i = 0; i < x; i++) {
29+
counter.inc();
30+
}
31+
require(counter.x() == x, "Value after calling inc x times should be x");
32+
}
33+
}

v-next/hardhat/src/internal/builtin-plugins/gas-analytics/function-gas-snapshots.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@ import {
1212
import { findDuplicates } from "@nomicfoundation/hardhat-utils/lang";
1313
import chalk from "chalk";
1414

15-
import { getFullyQualifiedName } from "../../../utils/contract-names.js";
15+
import {
16+
getFullyQualifiedName,
17+
parseFullyQualifiedName,
18+
} from "../../../utils/contract-names.js";
1619

20+
import { getUserFqn } from "./gas-analytics-manager.js";
1721
import { formatSectionHeader } from "./helpers.js";
1822

1923
export const FUNCTION_GAS_SNAPSHOTS_FILE = ".gas-snapshot";
@@ -24,6 +28,12 @@ export interface FunctionGasSnapshot {
2428
gasUsage: StandardTestKindGasUsage | FuzzTestKindGasUsage;
2529
}
2630

31+
export interface FunctionGasSnapshotWithMetadata extends FunctionGasSnapshot {
32+
metadata: {
33+
source: string;
34+
};
35+
}
36+
2737
export interface StandardTestKindGasUsage {
2838
kind: "standard";
2939
gas: bigint;
@@ -43,6 +53,7 @@ export interface FunctionGasSnapshotComparison {
4353
}
4454

4555
export interface FunctionGasSnapshotChange {
56+
source: string;
4657
contractNameOrFqn: string;
4758
functionSig: string;
4859
kind: "standard" | "fuzz";
@@ -63,20 +74,23 @@ export function getFunctionGasSnapshotsPath(basePath: string): string {
6374

6475
export function extractFunctionGasSnapshots(
6576
suiteResults: SuiteResult[],
66-
): FunctionGasSnapshot[] {
77+
): FunctionGasSnapshotWithMetadata[] {
6778
const duplicateContractNames = findDuplicates(
6879
suiteResults.map(({ id }) => id.name),
6980
);
7081

71-
const snapshots: FunctionGasSnapshot[] = [];
82+
const snapshots: FunctionGasSnapshotWithMetadata[] = [];
7283
for (const { id: suiteId, testResults } of suiteResults) {
7384
for (const { name: functionSig, kind: testKind } of testResults) {
7485
if ("calls" in testKind) {
7586
continue;
7687
}
7788

89+
const userFqn = getUserFqn(
90+
getFullyQualifiedName(suiteId.source, suiteId.name),
91+
);
7892
const contractNameOrFqn = duplicateContractNames.has(suiteId.name)
79-
? getFullyQualifiedName(suiteId.source, suiteId.name)
93+
? userFqn
8094
: suiteId.name;
8195

8296
const gasUsage =
@@ -96,6 +110,9 @@ export function extractFunctionGasSnapshots(
96110
contractNameOrFqn,
97111
functionSig,
98112
gasUsage,
113+
metadata: {
114+
source: parseFullyQualifiedName(userFqn).sourceName,
115+
},
99116
});
100117
}
101118
}
@@ -225,7 +242,7 @@ export function parseFunctionGasSnapshots(
225242

226243
export function compareFunctionGasSnapshots(
227244
previousSnapshots: FunctionGasSnapshot[],
228-
currentSnapshots: FunctionGasSnapshot[],
245+
currentSnapshots: FunctionGasSnapshotWithMetadata[],
229246
): FunctionGasSnapshotComparison {
230247
const previousSnapshotsMap = new Map(
231248
previousSnapshots.map((s) => [
@@ -270,6 +287,7 @@ export function compareFunctionGasSnapshots(
270287
actual: Number(actualValue),
271288
runs:
272289
currentKind === "fuzz" ? Number(current.gasUsage.runs) : undefined,
290+
source: current.metadata.source,
273291
});
274292
}
275293
previousSnapshotsMap.delete(key);
@@ -301,7 +319,7 @@ export async function checkFunctionGasSnapshots(
301319
): Promise<FunctionGasSnapshotCheckResult> {
302320
const functionGasSnapshots = extractFunctionGasSnapshots(suiteResults);
303321

304-
let previousFunctionGasSnapshots;
322+
let previousFunctionGasSnapshots: FunctionGasSnapshot[];
305323
try {
306324
previousFunctionGasSnapshots = await readFunctionGasSnapshots(basePath);
307325
} catch (error) {
@@ -418,6 +436,7 @@ export function printFunctionGasSnapshotChanges(
418436
const isLast = i === changes.length - 1;
419437

420438
logger(` ${change.contractNameOrFqn}#${change.functionSig}`);
439+
logger(chalk.grey(` (in ${change.source})`));
421440

422441
if (change.kind === "fuzz") {
423442
logger(chalk.grey(` Runs: ${change.runs}`));

v-next/hardhat/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.ts

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ import {
1212
} from "@nomicfoundation/hardhat-utils/fs";
1313
import chalk from "chalk";
1414

15+
import {
16+
getFullyQualifiedName,
17+
parseFullyQualifiedName,
18+
} from "../../../utils/contract-names.js";
19+
20+
import { getUserFqn } from "./gas-analytics-manager.js";
1521
import { formatSectionHeader } from "./helpers.js";
1622

1723
export const SNAPSHOT_CHEATCODES_DIR = "snapshots";
@@ -24,6 +30,19 @@ export type SnapshotCheatcodesMap = Map<
2430
>
2531
>;
2632

33+
export type SnapshotCheatcodesWithMetadataMap = Map<
34+
string, // group
35+
Record<
36+
string, // name
37+
{
38+
value: string;
39+
metadata: {
40+
source: string;
41+
};
42+
}
43+
>
44+
>;
45+
2746
export interface SnapshotCheatcode {
2847
group: string;
2948
name: string;
@@ -35,6 +54,7 @@ export interface SnapshotCheatcodeChange {
3554
name: string;
3655
expected: number;
3756
actual: number;
57+
source: string;
3858
}
3959

4060
export interface SnapshotCheatcodesComparison {
@@ -58,14 +78,18 @@ export function getSnapshotCheatcodesPath(
5878

5979
export function extractSnapshotCheatcodes(
6080
suiteResults: SuiteResult[],
61-
): SnapshotCheatcodesMap {
62-
const snapshots: SnapshotCheatcodesMap = new Map();
63-
for (const { testResults } of suiteResults) {
81+
): SnapshotCheatcodesWithMetadataMap {
82+
const snapshots: SnapshotCheatcodesWithMetadataMap = new Map();
83+
for (const { id: suiteId, testResults } of suiteResults) {
6484
for (const { valueSnapshotGroups: snapshotGroups } of testResults) {
6585
if (snapshotGroups === undefined) {
6686
continue;
6787
}
6888

89+
const userFqn = getUserFqn(
90+
getFullyQualifiedName(suiteId.source, suiteId.name),
91+
);
92+
6993
for (const group of snapshotGroups) {
7094
let snapshot = snapshots.get(group.name);
7195
if (snapshot === undefined) {
@@ -74,7 +98,12 @@ export function extractSnapshotCheatcodes(
7498
}
7599

76100
for (const entry of group.entries) {
77-
snapshot[entry.name] = entry.value;
101+
snapshot[entry.name] = {
102+
value: entry.value,
103+
metadata: {
104+
source: parseFullyQualifiedName(userFqn).sourceName,
105+
},
106+
};
78107
}
79108
}
80109
}
@@ -85,16 +114,21 @@ export function extractSnapshotCheatcodes(
85114

86115
export async function writeSnapshotCheatcodes(
87116
basePath: string,
88-
snapshotCheatcodes: SnapshotCheatcodesMap,
117+
snapshotCheatcodes: SnapshotCheatcodesWithMetadataMap,
89118
): Promise<void> {
90119
for (const [snapshotGroup, snapshot] of snapshotCheatcodes) {
91120
const snapshotCheatcodesPath = getSnapshotCheatcodesPath(
92121
basePath,
93122
`${snapshotGroup}.json`,
94123
);
95124

125+
const snapshotWithoutMetadata: Record<string, string> = {};
126+
for (const [name, entry] of Object.entries(snapshot)) {
127+
snapshotWithoutMetadata[name] = entry.value;
128+
}
129+
96130
try {
97-
await writeJsonFile(snapshotCheatcodesPath, snapshot);
131+
await writeJsonFile(snapshotCheatcodesPath, snapshotWithoutMetadata);
98132
} catch (error) {
99133
ensureError(error);
100134
throw new HardhatError(
@@ -167,7 +201,7 @@ export function stringifySnapshotCheatcodes(
167201

168202
export function compareSnapshotCheatcodes(
169203
previousSnapshotsMap: SnapshotCheatcodesMap,
170-
currentSnapshotsMap: SnapshotCheatcodesMap,
204+
currentSnapshotsMap: SnapshotCheatcodesWithMetadataMap,
171205
): SnapshotCheatcodesComparison {
172206
const added: SnapshotCheatcode[] = [];
173207
const removed: SnapshotCheatcode[] = [];
@@ -177,23 +211,24 @@ export function compareSnapshotCheatcodes(
177211
for (const [group, currentSnapshots] of currentSnapshotsMap) {
178212
const previousSnapshots = previousSnapshotsMap.get(group);
179213

180-
for (const [name, currentValue] of Object.entries(currentSnapshots)) {
214+
for (const [name, currentEntry] of Object.entries(currentSnapshots)) {
181215
const key = `${group}#${name}`;
182216

183217
if (
184218
previousSnapshots === undefined ||
185219
!Object.hasOwn(previousSnapshots, name)
186220
) {
187-
added.push({ group, name, value: currentValue });
221+
added.push({ group, name, value: currentEntry.value });
188222
} else {
189223
seenPreviousEntries.add(key);
190224
const previousValue = previousSnapshots[name];
191-
if (previousValue !== currentValue) {
225+
if (previousValue !== currentEntry.value) {
192226
changed.push({
193227
group,
194228
name,
195229
expected: Number(previousValue),
196-
actual: Number(currentValue),
230+
actual: Number(currentEntry.value),
231+
source: currentEntry.metadata.source,
197232
});
198233
}
199234
}
@@ -345,6 +380,7 @@ export function printSnapshotCheatcodeChanges(
345380
const isLast = i === changes.length - 1;
346381

347382
logger(` ${change.group}#${change.name}`);
383+
logger(chalk.grey(` (in ${change.source})`));
348384

349385
const diff = change.actual - change.expected;
350386
const formattedDiff = diff > 0 ? `Δ+${diff}` : ${diff}`;

0 commit comments

Comments
 (0)