hardhat 3.1.11 → 3.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/src/internal/builtin-plugins/gas-analytics/function-gas-snapshots.d.ts +53 -0
- package/dist/src/internal/builtin-plugins/gas-analytics/function-gas-snapshots.d.ts.map +1 -0
- package/dist/src/internal/builtin-plugins/gas-analytics/function-gas-snapshots.js +288 -0
- package/dist/src/internal/builtin-plugins/gas-analytics/function-gas-snapshots.js.map +1 -0
- package/dist/src/internal/builtin-plugins/gas-analytics/gas-analytics-manager.d.ts +0 -1
- package/dist/src/internal/builtin-plugins/gas-analytics/gas-analytics-manager.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/gas-analytics/gas-analytics-manager.js +2 -14
- package/dist/src/internal/builtin-plugins/gas-analytics/gas-analytics-manager.js.map +1 -1
- package/dist/src/internal/builtin-plugins/gas-analytics/helpers.d.ts +5 -0
- package/dist/src/internal/builtin-plugins/gas-analytics/helpers.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/gas-analytics/helpers.js +14 -0
- package/dist/src/internal/builtin-plugins/gas-analytics/helpers.js.map +1 -1
- package/dist/src/internal/builtin-plugins/gas-analytics/index.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/gas-analytics/index.js +35 -2
- package/dist/src/internal/builtin-plugins/gas-analytics/index.js.map +1 -1
- package/dist/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.d.ts +45 -0
- package/dist/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.d.ts.map +1 -0
- package/dist/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.js +276 -0
- package/dist/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.js.map +1 -0
- package/dist/src/internal/builtin-plugins/gas-analytics/tasks/solidity-test/task-action.d.ts +22 -0
- package/dist/src/internal/builtin-plugins/gas-analytics/tasks/solidity-test/task-action.d.ts.map +1 -0
- package/dist/src/internal/builtin-plugins/gas-analytics/tasks/solidity-test/task-action.js +88 -0
- package/dist/src/internal/builtin-plugins/gas-analytics/tasks/solidity-test/task-action.js.map +1 -0
- package/dist/src/internal/builtin-plugins/index.js +1 -1
- package/dist/src/internal/builtin-plugins/solidity-test/config.d.ts +3 -1
- package/dist/src/internal/builtin-plugins/solidity-test/config.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/solidity-test/config.js +9 -0
- package/dist/src/internal/builtin-plugins/solidity-test/config.js.map +1 -1
- package/dist/src/internal/builtin-plugins/solidity-test/helpers.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/solidity-test/helpers.js +4 -10
- package/dist/src/internal/builtin-plugins/solidity-test/helpers.js.map +1 -1
- package/dist/src/internal/builtin-plugins/solidity-test/index.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/solidity-test/index.js +0 -1
- package/dist/src/internal/builtin-plugins/solidity-test/index.js.map +1 -1
- package/dist/src/internal/builtin-plugins/solidity-test/runner.d.ts +1 -1
- package/dist/src/internal/builtin-plugins/solidity-test/runner.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/solidity-test/runner.js +2 -2
- package/dist/src/internal/builtin-plugins/solidity-test/runner.js.map +1 -1
- package/dist/src/internal/builtin-plugins/solidity-test/task-action.d.ts +5 -0
- package/dist/src/internal/builtin-plugins/solidity-test/task-action.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/solidity-test/task-action.js +10 -5
- package/dist/src/internal/builtin-plugins/solidity-test/task-action.js.map +1 -1
- package/dist/src/internal/builtin-plugins/solidity-test/type-extensions.d.ts +15 -10
- package/dist/src/internal/builtin-plugins/solidity-test/type-extensions.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/test/task-action.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/test/task-action.js +34 -12
- package/dist/src/internal/builtin-plugins/test/task-action.js.map +1 -1
- package/dist/src/types/test.d.ts +7 -0
- package/dist/src/types/test.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/internal/builtin-plugins/gas-analytics/function-gas-snapshots.ts +473 -0
- package/src/internal/builtin-plugins/gas-analytics/gas-analytics-manager.ts +3 -17
- package/src/internal/builtin-plugins/gas-analytics/helpers.ts +29 -0
- package/src/internal/builtin-plugins/gas-analytics/index.ts +36 -2
- package/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.ts +454 -0
- package/src/internal/builtin-plugins/gas-analytics/tasks/solidity-test/task-action.ts +172 -0
- package/src/internal/builtin-plugins/index.ts +1 -1
- package/src/internal/builtin-plugins/solidity-test/config.ts +15 -0
- package/src/internal/builtin-plugins/solidity-test/helpers.ts +6 -14
- package/src/internal/builtin-plugins/solidity-test/index.ts +0 -1
- package/src/internal/builtin-plugins/solidity-test/runner.ts +2 -2
- package/src/internal/builtin-plugins/solidity-test/task-action.ts +17 -8
- package/src/internal/builtin-plugins/solidity-test/type-extensions.ts +17 -10
- package/src/internal/builtin-plugins/test/task-action.ts +36 -18
- package/src/types/test.ts +8 -0
- package/templates/hardhat-3/01-node-test-runner-viem/package.json +2 -2
- package/templates/hardhat-3/02-mocha-ethers/package.json +2 -2
- package/templates/hardhat-3/03-minimal/package.json +1 -1
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import type { SuiteResult } from "@nomicfoundation/edr";
|
|
2
|
+
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { HardhatError } from "@nomicfoundation/hardhat-errors";
|
|
6
|
+
import { ensureError } from "@nomicfoundation/hardhat-utils/error";
|
|
7
|
+
import {
|
|
8
|
+
FileNotFoundError,
|
|
9
|
+
readUtf8File,
|
|
10
|
+
writeUtf8File,
|
|
11
|
+
} from "@nomicfoundation/hardhat-utils/fs";
|
|
12
|
+
import { findDuplicates } from "@nomicfoundation/hardhat-utils/lang";
|
|
13
|
+
import chalk from "chalk";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
getFullyQualifiedName,
|
|
17
|
+
parseFullyQualifiedName,
|
|
18
|
+
} from "../../../utils/contract-names.js";
|
|
19
|
+
|
|
20
|
+
import { getUserFqn } from "./gas-analytics-manager.js";
|
|
21
|
+
import { formatSectionHeader } from "./helpers.js";
|
|
22
|
+
|
|
23
|
+
export const FUNCTION_GAS_SNAPSHOTS_FILE = ".gas-snapshot";
|
|
24
|
+
|
|
25
|
+
export interface FunctionGasSnapshot {
|
|
26
|
+
contractNameOrFqn: string;
|
|
27
|
+
functionSig: string;
|
|
28
|
+
gasUsage: StandardTestKindGasUsage | FuzzTestKindGasUsage;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface FunctionGasSnapshotWithMetadata extends FunctionGasSnapshot {
|
|
32
|
+
metadata: {
|
|
33
|
+
source: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface StandardTestKindGasUsage {
|
|
38
|
+
kind: "standard";
|
|
39
|
+
gas: bigint;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface FuzzTestKindGasUsage {
|
|
43
|
+
kind: "fuzz";
|
|
44
|
+
runs: bigint;
|
|
45
|
+
meanGas: bigint;
|
|
46
|
+
medianGas: bigint;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface FunctionGasSnapshotComparison {
|
|
50
|
+
added: FunctionGasSnapshot[];
|
|
51
|
+
removed: FunctionGasSnapshot[];
|
|
52
|
+
changed: FunctionGasSnapshotChange[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface FunctionGasSnapshotChange {
|
|
56
|
+
source: string;
|
|
57
|
+
contractNameOrFqn: string;
|
|
58
|
+
functionSig: string;
|
|
59
|
+
kind: "standard" | "fuzz";
|
|
60
|
+
expected: number;
|
|
61
|
+
actual: number;
|
|
62
|
+
runs?: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface FunctionGasSnapshotCheckResult {
|
|
66
|
+
passed: boolean;
|
|
67
|
+
comparison: FunctionGasSnapshotComparison;
|
|
68
|
+
written: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function getFunctionGasSnapshotsPath(basePath: string): string {
|
|
72
|
+
return path.join(basePath, FUNCTION_GAS_SNAPSHOTS_FILE);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function extractFunctionGasSnapshots(
|
|
76
|
+
suiteResults: SuiteResult[],
|
|
77
|
+
): FunctionGasSnapshotWithMetadata[] {
|
|
78
|
+
const duplicateContractNames = findDuplicates(
|
|
79
|
+
suiteResults.map(({ id }) => id.name),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const snapshots: FunctionGasSnapshotWithMetadata[] = [];
|
|
83
|
+
for (const { id: suiteId, testResults } of suiteResults) {
|
|
84
|
+
for (const { name: functionSig, kind: testKind } of testResults) {
|
|
85
|
+
if ("calls" in testKind) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const userFqn = getUserFqn(
|
|
90
|
+
getFullyQualifiedName(suiteId.source, suiteId.name),
|
|
91
|
+
);
|
|
92
|
+
const contractNameOrFqn = duplicateContractNames.has(suiteId.name)
|
|
93
|
+
? userFqn
|
|
94
|
+
: suiteId.name;
|
|
95
|
+
|
|
96
|
+
const gasUsage =
|
|
97
|
+
"consumedGas" in testKind
|
|
98
|
+
? {
|
|
99
|
+
kind: "standard" as const,
|
|
100
|
+
gas: testKind.consumedGas,
|
|
101
|
+
}
|
|
102
|
+
: {
|
|
103
|
+
kind: "fuzz" as const,
|
|
104
|
+
runs: testKind.runs,
|
|
105
|
+
meanGas: testKind.meanGas,
|
|
106
|
+
medianGas: testKind.medianGas,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
snapshots.push({
|
|
110
|
+
contractNameOrFqn,
|
|
111
|
+
functionSig,
|
|
112
|
+
gasUsage,
|
|
113
|
+
metadata: {
|
|
114
|
+
source: parseFullyQualifiedName(userFqn).sourceName,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return snapshots;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function writeFunctionGasSnapshots(
|
|
123
|
+
basePath: string,
|
|
124
|
+
snapshots: FunctionGasSnapshot[],
|
|
125
|
+
): Promise<void> {
|
|
126
|
+
const snapshotsPath = getFunctionGasSnapshotsPath(basePath);
|
|
127
|
+
try {
|
|
128
|
+
await writeUtf8File(
|
|
129
|
+
snapshotsPath,
|
|
130
|
+
stringifyFunctionGasSnapshots(snapshots),
|
|
131
|
+
);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
ensureError(error);
|
|
134
|
+
throw new HardhatError(
|
|
135
|
+
HardhatError.ERRORS.CORE.SOLIDITY_TESTS.SNAPSHOT_WRITE_ERROR,
|
|
136
|
+
{ snapshotsPath, error: error.message },
|
|
137
|
+
error,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function readFunctionGasSnapshots(
|
|
143
|
+
basePath: string,
|
|
144
|
+
): Promise<FunctionGasSnapshot[]> {
|
|
145
|
+
const snapshotsPath = getFunctionGasSnapshotsPath(basePath);
|
|
146
|
+
let stringifiedSnapshots: string;
|
|
147
|
+
try {
|
|
148
|
+
stringifiedSnapshots = await readUtf8File(snapshotsPath);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
ensureError(error);
|
|
151
|
+
|
|
152
|
+
// Re-throw as-is to allow the caller to handle this case specifically
|
|
153
|
+
if (error instanceof FileNotFoundError) {
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
throw new HardhatError(
|
|
158
|
+
HardhatError.ERRORS.CORE.SOLIDITY_TESTS.SNAPSHOT_READ_ERROR,
|
|
159
|
+
{ snapshotsPath, error: error.message },
|
|
160
|
+
error,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return parseFunctionGasSnapshots(stringifiedSnapshots);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function stringifyFunctionGasSnapshots(
|
|
168
|
+
snapshots: FunctionGasSnapshot[],
|
|
169
|
+
): string {
|
|
170
|
+
const lines: string[] = [];
|
|
171
|
+
for (const { contractNameOrFqn, functionSig, gasUsage } of snapshots) {
|
|
172
|
+
const gasDetails =
|
|
173
|
+
gasUsage.kind === "standard"
|
|
174
|
+
? `gas: ${gasUsage.gas}`
|
|
175
|
+
: `runs: ${gasUsage.runs}, μ: ${gasUsage.meanGas}, ~: ${gasUsage.medianGas}`;
|
|
176
|
+
|
|
177
|
+
lines.push(`${contractNameOrFqn}#${functionSig} (${gasDetails})`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return lines.sort((a, b) => a.localeCompare(b)).join("\n");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function parseFunctionGasSnapshots(
|
|
184
|
+
stringifiedSnapshots: string,
|
|
185
|
+
): FunctionGasSnapshot[] {
|
|
186
|
+
if (stringifiedSnapshots.trim() === "") {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const lines = stringifiedSnapshots.split("\n");
|
|
191
|
+
const snapshots: FunctionGasSnapshot[] = [];
|
|
192
|
+
|
|
193
|
+
const standardTestRegex = /^(.+)#(.+) \(gas: (\d+)\)$/;
|
|
194
|
+
const fuzzTestRegex = /^(.+)#(.+) \(runs: (\d+), μ: (\d+), ~: (\d+)\)$/;
|
|
195
|
+
|
|
196
|
+
for (const line of lines) {
|
|
197
|
+
if (line.trim() === "") {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const standardMatch = standardTestRegex.exec(line);
|
|
202
|
+
if (standardMatch !== null) {
|
|
203
|
+
const [, contractNameOrFqn, functionSig, gasValue] = standardMatch;
|
|
204
|
+
snapshots.push({
|
|
205
|
+
contractNameOrFqn,
|
|
206
|
+
functionSig,
|
|
207
|
+
gasUsage: { kind: "standard", gas: BigInt(gasValue) },
|
|
208
|
+
});
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const fuzzMatch = fuzzTestRegex.exec(line);
|
|
213
|
+
if (fuzzMatch !== null) {
|
|
214
|
+
const [, contractNameOrFqn, functionSig, runs, meanGas, medianGas] =
|
|
215
|
+
fuzzMatch;
|
|
216
|
+
snapshots.push({
|
|
217
|
+
contractNameOrFqn,
|
|
218
|
+
functionSig,
|
|
219
|
+
gasUsage: {
|
|
220
|
+
kind: "fuzz",
|
|
221
|
+
runs: BigInt(runs),
|
|
222
|
+
meanGas: BigInt(meanGas),
|
|
223
|
+
medianGas: BigInt(medianGas),
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
throw new HardhatError(
|
|
230
|
+
HardhatError.ERRORS.CORE.SOLIDITY_TESTS.INVALID_SNAPSHOT_FORMAT,
|
|
231
|
+
{
|
|
232
|
+
file: FUNCTION_GAS_SNAPSHOTS_FILE,
|
|
233
|
+
line,
|
|
234
|
+
expectedFormat:
|
|
235
|
+
"'ContractName#functionName (gas: value)' for standard tests or 'ContractName#functionName (runs: value, μ: value, ~: value)' for fuzz tests",
|
|
236
|
+
},
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return snapshots;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function compareFunctionGasSnapshots(
|
|
244
|
+
previousSnapshots: FunctionGasSnapshot[],
|
|
245
|
+
currentSnapshots: FunctionGasSnapshotWithMetadata[],
|
|
246
|
+
): FunctionGasSnapshotComparison {
|
|
247
|
+
const previousSnapshotsMap = new Map(
|
|
248
|
+
previousSnapshots.map((s) => [
|
|
249
|
+
`${s.contractNameOrFqn}#${s.functionSig}`,
|
|
250
|
+
s,
|
|
251
|
+
]),
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
const added: FunctionGasSnapshot[] = [];
|
|
255
|
+
const changed: FunctionGasSnapshotChange[] = [];
|
|
256
|
+
|
|
257
|
+
for (const current of currentSnapshots) {
|
|
258
|
+
const key = `${current.contractNameOrFqn}#${current.functionSig}`;
|
|
259
|
+
const previous = previousSnapshotsMap.get(key);
|
|
260
|
+
const currentKind = current.gasUsage.kind;
|
|
261
|
+
const previousKind = previous?.gasUsage.kind;
|
|
262
|
+
|
|
263
|
+
if (
|
|
264
|
+
previous === undefined ||
|
|
265
|
+
// If the kind doesn't match, we treat it as an addition + removal
|
|
266
|
+
previousKind !== currentKind
|
|
267
|
+
) {
|
|
268
|
+
added.push(current);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (hasGasUsageChanged(previous.gasUsage, current.gasUsage)) {
|
|
273
|
+
const expectedValue =
|
|
274
|
+
previousKind === "standard"
|
|
275
|
+
? previous.gasUsage.gas
|
|
276
|
+
: previous.gasUsage.medianGas;
|
|
277
|
+
const actualValue =
|
|
278
|
+
currentKind === "standard"
|
|
279
|
+
? current.gasUsage.gas
|
|
280
|
+
: current.gasUsage.medianGas;
|
|
281
|
+
|
|
282
|
+
changed.push({
|
|
283
|
+
contractNameOrFqn: current.contractNameOrFqn,
|
|
284
|
+
functionSig: current.functionSig,
|
|
285
|
+
kind: currentKind,
|
|
286
|
+
expected: Number(expectedValue),
|
|
287
|
+
actual: Number(actualValue),
|
|
288
|
+
runs:
|
|
289
|
+
currentKind === "fuzz" ? Number(current.gasUsage.runs) : undefined,
|
|
290
|
+
source: current.metadata.source,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
previousSnapshotsMap.delete(key);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const removed = Array.from(previousSnapshotsMap.values());
|
|
297
|
+
|
|
298
|
+
return { added, removed, changed };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function hasGasUsageChanged(
|
|
302
|
+
previous: StandardTestKindGasUsage | FuzzTestKindGasUsage,
|
|
303
|
+
current: StandardTestKindGasUsage | FuzzTestKindGasUsage,
|
|
304
|
+
): boolean {
|
|
305
|
+
if (previous.kind === "standard" && current.kind === "standard") {
|
|
306
|
+
return previous.gas !== current.gas;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (previous.kind === "fuzz" && current.kind === "fuzz") {
|
|
310
|
+
return previous.medianGas !== current.medianGas;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export async function checkFunctionGasSnapshots(
|
|
317
|
+
basePath: string,
|
|
318
|
+
suiteResults: SuiteResult[],
|
|
319
|
+
): Promise<FunctionGasSnapshotCheckResult> {
|
|
320
|
+
const functionGasSnapshots = extractFunctionGasSnapshots(suiteResults);
|
|
321
|
+
|
|
322
|
+
let previousFunctionGasSnapshots: FunctionGasSnapshot[];
|
|
323
|
+
try {
|
|
324
|
+
previousFunctionGasSnapshots = await readFunctionGasSnapshots(basePath);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (error instanceof FileNotFoundError) {
|
|
327
|
+
await writeFunctionGasSnapshots(basePath, functionGasSnapshots);
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
passed: true,
|
|
331
|
+
comparison: {
|
|
332
|
+
added: [],
|
|
333
|
+
removed: [],
|
|
334
|
+
changed: [],
|
|
335
|
+
},
|
|
336
|
+
written: true,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const comparison = compareFunctionGasSnapshots(
|
|
344
|
+
previousFunctionGasSnapshots,
|
|
345
|
+
functionGasSnapshots,
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
// Update snapshots when functions are added or removed (but not changed)
|
|
349
|
+
const hasAddedOrRemoved =
|
|
350
|
+
comparison.added.length > 0 || comparison.removed.length > 0;
|
|
351
|
+
if (comparison.changed.length === 0 && hasAddedOrRemoved) {
|
|
352
|
+
await writeFunctionGasSnapshots(basePath, functionGasSnapshots);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
passed: comparison.changed.length === 0,
|
|
357
|
+
comparison,
|
|
358
|
+
written: hasAddedOrRemoved,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function logFunctionGasSnapshotsSection(
|
|
363
|
+
result: FunctionGasSnapshotCheckResult,
|
|
364
|
+
logger: typeof console.log = console.log,
|
|
365
|
+
): void {
|
|
366
|
+
const { comparison, written } = result;
|
|
367
|
+
const changedLength = comparison.changed.length;
|
|
368
|
+
const addedLength = comparison.added.length;
|
|
369
|
+
const removedLength = comparison.removed.length;
|
|
370
|
+
const hasChanges = changedLength > 0;
|
|
371
|
+
const hasAdded = addedLength > 0;
|
|
372
|
+
const hasRemoved = removedLength > 0;
|
|
373
|
+
const hasAnyDifferences = hasChanges || hasAdded || hasRemoved;
|
|
374
|
+
const isFirstTimeWrite = written && !hasAnyDifferences;
|
|
375
|
+
|
|
376
|
+
// Nothing to report
|
|
377
|
+
if (!isFirstTimeWrite && !hasAnyDifferences) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
logger(
|
|
382
|
+
formatSectionHeader("Function gas snapshots", {
|
|
383
|
+
changedLength,
|
|
384
|
+
addedLength,
|
|
385
|
+
removedLength,
|
|
386
|
+
}),
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
if (isFirstTimeWrite) {
|
|
390
|
+
logger();
|
|
391
|
+
logger(
|
|
392
|
+
chalk.green(
|
|
393
|
+
" No existing snapshots found. Function gas snapshots written successfully",
|
|
394
|
+
),
|
|
395
|
+
);
|
|
396
|
+
logger();
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (hasChanges) {
|
|
401
|
+
logger();
|
|
402
|
+
printFunctionGasSnapshotChanges(comparison.changed, logger);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (hasAdded) {
|
|
406
|
+
logger();
|
|
407
|
+
logger(` Added ${comparison.added.length} function(s):`);
|
|
408
|
+
const addedLines = stringifyFunctionGasSnapshots(comparison.added).split(
|
|
409
|
+
"\n",
|
|
410
|
+
);
|
|
411
|
+
for (const line of addedLines) {
|
|
412
|
+
logger(chalk.green(` + ${line}`));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (hasRemoved) {
|
|
417
|
+
logger();
|
|
418
|
+
logger(` Removed ${comparison.removed.length} function(s):`);
|
|
419
|
+
const removedLines = stringifyFunctionGasSnapshots(
|
|
420
|
+
comparison.removed,
|
|
421
|
+
).split("\n");
|
|
422
|
+
for (const line of removedLines) {
|
|
423
|
+
logger(chalk.red(` - ${line}`));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
logger();
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function printFunctionGasSnapshotChanges(
|
|
431
|
+
changes: FunctionGasSnapshotChange[],
|
|
432
|
+
logger: typeof console.log = console.log,
|
|
433
|
+
): void {
|
|
434
|
+
for (let i = 0; i < changes.length; i++) {
|
|
435
|
+
const change = changes[i];
|
|
436
|
+
const isLast = i === changes.length - 1;
|
|
437
|
+
|
|
438
|
+
logger(` ${change.contractNameOrFqn}#${change.functionSig}`);
|
|
439
|
+
logger(chalk.grey(` (in ${change.source})`));
|
|
440
|
+
|
|
441
|
+
if (change.kind === "fuzz") {
|
|
442
|
+
logger(chalk.grey(` Runs: ${change.runs}`));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const diff = change.actual - change.expected;
|
|
446
|
+
const formattedDiff = diff > 0 ? `Δ+${diff}` : `Δ${diff}`;
|
|
447
|
+
|
|
448
|
+
let gasChange = `${formattedDiff}`;
|
|
449
|
+
if (change.expected > 0) {
|
|
450
|
+
const percent = (diff / change.expected) * 100;
|
|
451
|
+
const formattedPercent =
|
|
452
|
+
percent >= 0 ? `+${percent.toFixed(2)}%` : `${percent.toFixed(2)}%`;
|
|
453
|
+
gasChange = `${formattedPercent}, ${formattedDiff}`;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Color: green for decrease (improvement), red for increase (regression)
|
|
457
|
+
const formattedGasChange =
|
|
458
|
+
diff < 0 ? chalk.green(gasChange) : chalk.red(gasChange);
|
|
459
|
+
|
|
460
|
+
const label = change.kind === "fuzz" ? "~" : "gas";
|
|
461
|
+
|
|
462
|
+
logger(chalk.grey(` Expected (${label}): ${change.expected}`));
|
|
463
|
+
logger(
|
|
464
|
+
chalk.grey(` Actual (${label}): ${change.actual} (`) +
|
|
465
|
+
formattedGasChange +
|
|
466
|
+
chalk.grey(")"),
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
if (!isLast) {
|
|
470
|
+
logger();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
remove,
|
|
13
13
|
writeJsonFile,
|
|
14
14
|
} from "@nomicfoundation/hardhat-utils/fs";
|
|
15
|
+
import { findDuplicates } from "@nomicfoundation/hardhat-utils/lang";
|
|
15
16
|
import chalk from "chalk";
|
|
16
17
|
import debug from "debug";
|
|
17
18
|
|
|
@@ -131,8 +132,8 @@ export class GasAnalyticsManagerImplementation implements GasAnalyticsManager {
|
|
|
131
132
|
};
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
const overloadedFnNames =
|
|
135
|
-
|
|
135
|
+
const overloadedFnNames = findDuplicates(
|
|
136
|
+
[...measurements.functions.keys()].map(getFunctionName),
|
|
136
137
|
);
|
|
137
138
|
|
|
138
139
|
for (const [functionSig, gasValues] of measurements.functions) {
|
|
@@ -317,18 +318,3 @@ export function getUserFqn(inputFqn: string): string {
|
|
|
317
318
|
export function getFunctionName(signature: string): string {
|
|
318
319
|
return signature.split("(")[0];
|
|
319
320
|
}
|
|
320
|
-
|
|
321
|
-
export function findDuplicates<T>(arr: T[]): T[] {
|
|
322
|
-
const seen = new Set<T>();
|
|
323
|
-
const duplicates = new Set<T>();
|
|
324
|
-
|
|
325
|
-
for (const item of arr) {
|
|
326
|
-
if (seen.has(item)) {
|
|
327
|
-
duplicates.add(item);
|
|
328
|
-
} else {
|
|
329
|
-
seen.add(item);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
return [...duplicates];
|
|
334
|
-
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
testRunDone,
|
|
3
5
|
testRunStart,
|
|
@@ -24,3 +26,30 @@ export async function markTestRunDone(id: string): Promise<void> {
|
|
|
24
26
|
const { default: hre } = await import("../../../index.js");
|
|
25
27
|
await testRunDone(hre, id);
|
|
26
28
|
}
|
|
29
|
+
|
|
30
|
+
export function formatSectionHeader(
|
|
31
|
+
sectionName: string,
|
|
32
|
+
{
|
|
33
|
+
changedLength,
|
|
34
|
+
addedLength,
|
|
35
|
+
removedLength,
|
|
36
|
+
}: {
|
|
37
|
+
changedLength: number;
|
|
38
|
+
addedLength: number;
|
|
39
|
+
removedLength: number;
|
|
40
|
+
},
|
|
41
|
+
): string {
|
|
42
|
+
const parts: string[] = [];
|
|
43
|
+
|
|
44
|
+
if (changedLength > 0) {
|
|
45
|
+
parts.push(`${changedLength} changed`);
|
|
46
|
+
}
|
|
47
|
+
if (addedLength > 0) {
|
|
48
|
+
parts.push(`${addedLength} added`);
|
|
49
|
+
}
|
|
50
|
+
if (removedLength > 0) {
|
|
51
|
+
parts.push(`${removedLength} removed`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return `${sectionName}: ${chalk.gray(parts.join(", "))}`;
|
|
55
|
+
}
|
|
@@ -1,12 +1,42 @@
|
|
|
1
1
|
import type { HardhatPlugin } from "../../../types/plugins.js";
|
|
2
2
|
|
|
3
|
-
import { globalFlag } from "../../core/config.js";
|
|
3
|
+
import { globalFlag, overrideTask } from "../../core/config.js";
|
|
4
4
|
|
|
5
5
|
import "./type-extensions.js";
|
|
6
6
|
|
|
7
7
|
const hardhatPlugin: HardhatPlugin = {
|
|
8
8
|
id: "builtin:gas-analytics",
|
|
9
|
-
tasks: [
|
|
9
|
+
tasks: [
|
|
10
|
+
overrideTask("test")
|
|
11
|
+
.addFlag({
|
|
12
|
+
name: "snapshot",
|
|
13
|
+
description: "Update snapshots (Solidity tests only)",
|
|
14
|
+
})
|
|
15
|
+
.addFlag({
|
|
16
|
+
name: "snapshotCheck",
|
|
17
|
+
description:
|
|
18
|
+
"Check the snapshots match the stored values (Solidity tests only)",
|
|
19
|
+
})
|
|
20
|
+
.setAction(async () => ({
|
|
21
|
+
default: async (args, _hre, runSuper) => {
|
|
22
|
+
// We don't need to do anything here, as the test task will forward
|
|
23
|
+
// the arguments to its subtasks.
|
|
24
|
+
return runSuper(args);
|
|
25
|
+
},
|
|
26
|
+
}))
|
|
27
|
+
.build(),
|
|
28
|
+
overrideTask(["test", "solidity"])
|
|
29
|
+
.addFlag({
|
|
30
|
+
name: "snapshot",
|
|
31
|
+
description: "Update snapshots",
|
|
32
|
+
})
|
|
33
|
+
.addFlag({
|
|
34
|
+
name: "snapshotCheck",
|
|
35
|
+
description: "Check the snapshots match the stored values",
|
|
36
|
+
})
|
|
37
|
+
.setAction(async () => import("./tasks/solidity-test/task-action.js"))
|
|
38
|
+
.build(),
|
|
39
|
+
],
|
|
10
40
|
globalOptions: [
|
|
11
41
|
globalFlag({
|
|
12
42
|
name: "gasStats",
|
|
@@ -18,6 +48,10 @@ const hardhatPlugin: HardhatPlugin = {
|
|
|
18
48
|
hre: () => import("./hook-handlers/hre.js"),
|
|
19
49
|
test: () => import("./hook-handlers/test.js"),
|
|
20
50
|
},
|
|
51
|
+
dependencies: () => [
|
|
52
|
+
import("../test/index.js"),
|
|
53
|
+
import("../solidity-test/index.js"),
|
|
54
|
+
],
|
|
21
55
|
npmPackage: "hardhat",
|
|
22
56
|
};
|
|
23
57
|
|