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.
Files changed (69) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/src/internal/builtin-plugins/gas-analytics/function-gas-snapshots.d.ts +53 -0
  3. package/dist/src/internal/builtin-plugins/gas-analytics/function-gas-snapshots.d.ts.map +1 -0
  4. package/dist/src/internal/builtin-plugins/gas-analytics/function-gas-snapshots.js +288 -0
  5. package/dist/src/internal/builtin-plugins/gas-analytics/function-gas-snapshots.js.map +1 -0
  6. package/dist/src/internal/builtin-plugins/gas-analytics/gas-analytics-manager.d.ts +0 -1
  7. package/dist/src/internal/builtin-plugins/gas-analytics/gas-analytics-manager.d.ts.map +1 -1
  8. package/dist/src/internal/builtin-plugins/gas-analytics/gas-analytics-manager.js +2 -14
  9. package/dist/src/internal/builtin-plugins/gas-analytics/gas-analytics-manager.js.map +1 -1
  10. package/dist/src/internal/builtin-plugins/gas-analytics/helpers.d.ts +5 -0
  11. package/dist/src/internal/builtin-plugins/gas-analytics/helpers.d.ts.map +1 -1
  12. package/dist/src/internal/builtin-plugins/gas-analytics/helpers.js +14 -0
  13. package/dist/src/internal/builtin-plugins/gas-analytics/helpers.js.map +1 -1
  14. package/dist/src/internal/builtin-plugins/gas-analytics/index.d.ts.map +1 -1
  15. package/dist/src/internal/builtin-plugins/gas-analytics/index.js +35 -2
  16. package/dist/src/internal/builtin-plugins/gas-analytics/index.js.map +1 -1
  17. package/dist/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.d.ts +45 -0
  18. package/dist/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.d.ts.map +1 -0
  19. package/dist/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.js +276 -0
  20. package/dist/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.js.map +1 -0
  21. package/dist/src/internal/builtin-plugins/gas-analytics/tasks/solidity-test/task-action.d.ts +22 -0
  22. package/dist/src/internal/builtin-plugins/gas-analytics/tasks/solidity-test/task-action.d.ts.map +1 -0
  23. package/dist/src/internal/builtin-plugins/gas-analytics/tasks/solidity-test/task-action.js +88 -0
  24. package/dist/src/internal/builtin-plugins/gas-analytics/tasks/solidity-test/task-action.js.map +1 -0
  25. package/dist/src/internal/builtin-plugins/index.js +1 -1
  26. package/dist/src/internal/builtin-plugins/solidity-test/config.d.ts +3 -1
  27. package/dist/src/internal/builtin-plugins/solidity-test/config.d.ts.map +1 -1
  28. package/dist/src/internal/builtin-plugins/solidity-test/config.js +9 -0
  29. package/dist/src/internal/builtin-plugins/solidity-test/config.js.map +1 -1
  30. package/dist/src/internal/builtin-plugins/solidity-test/helpers.d.ts.map +1 -1
  31. package/dist/src/internal/builtin-plugins/solidity-test/helpers.js +4 -10
  32. package/dist/src/internal/builtin-plugins/solidity-test/helpers.js.map +1 -1
  33. package/dist/src/internal/builtin-plugins/solidity-test/index.d.ts.map +1 -1
  34. package/dist/src/internal/builtin-plugins/solidity-test/index.js +0 -1
  35. package/dist/src/internal/builtin-plugins/solidity-test/index.js.map +1 -1
  36. package/dist/src/internal/builtin-plugins/solidity-test/runner.d.ts +1 -1
  37. package/dist/src/internal/builtin-plugins/solidity-test/runner.d.ts.map +1 -1
  38. package/dist/src/internal/builtin-plugins/solidity-test/runner.js +2 -2
  39. package/dist/src/internal/builtin-plugins/solidity-test/runner.js.map +1 -1
  40. package/dist/src/internal/builtin-plugins/solidity-test/task-action.d.ts +5 -0
  41. package/dist/src/internal/builtin-plugins/solidity-test/task-action.d.ts.map +1 -1
  42. package/dist/src/internal/builtin-plugins/solidity-test/task-action.js +10 -5
  43. package/dist/src/internal/builtin-plugins/solidity-test/task-action.js.map +1 -1
  44. package/dist/src/internal/builtin-plugins/solidity-test/type-extensions.d.ts +15 -10
  45. package/dist/src/internal/builtin-plugins/solidity-test/type-extensions.d.ts.map +1 -1
  46. package/dist/src/internal/builtin-plugins/test/task-action.d.ts.map +1 -1
  47. package/dist/src/internal/builtin-plugins/test/task-action.js +34 -12
  48. package/dist/src/internal/builtin-plugins/test/task-action.js.map +1 -1
  49. package/dist/src/types/test.d.ts +7 -0
  50. package/dist/src/types/test.d.ts.map +1 -1
  51. package/package.json +4 -4
  52. package/src/internal/builtin-plugins/gas-analytics/function-gas-snapshots.ts +473 -0
  53. package/src/internal/builtin-plugins/gas-analytics/gas-analytics-manager.ts +3 -17
  54. package/src/internal/builtin-plugins/gas-analytics/helpers.ts +29 -0
  55. package/src/internal/builtin-plugins/gas-analytics/index.ts +36 -2
  56. package/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.ts +454 -0
  57. package/src/internal/builtin-plugins/gas-analytics/tasks/solidity-test/task-action.ts +172 -0
  58. package/src/internal/builtin-plugins/index.ts +1 -1
  59. package/src/internal/builtin-plugins/solidity-test/config.ts +15 -0
  60. package/src/internal/builtin-plugins/solidity-test/helpers.ts +6 -14
  61. package/src/internal/builtin-plugins/solidity-test/index.ts +0 -1
  62. package/src/internal/builtin-plugins/solidity-test/runner.ts +2 -2
  63. package/src/internal/builtin-plugins/solidity-test/task-action.ts +17 -8
  64. package/src/internal/builtin-plugins/solidity-test/type-extensions.ts +17 -10
  65. package/src/internal/builtin-plugins/test/task-action.ts +36 -18
  66. package/src/types/test.ts +8 -0
  67. package/templates/hardhat-3/01-node-test-runner-viem/package.json +2 -2
  68. package/templates/hardhat-3/02-mocha-ethers/package.json +2 -2
  69. 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 = new Set(
135
- findDuplicates([...measurements.functions.keys()].map(getFunctionName)),
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