hardhat 3.12.0 → 3.13.0
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 +16 -0
- package/dist/src/internal/builtin-plugins/gas-analytics/function-gas-snapshots.d.ts +3 -2
- package/dist/src/internal/builtin-plugins/gas-analytics/function-gas-snapshots.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/gas-analytics/function-gas-snapshots.js +16 -7
- package/dist/src/internal/builtin-plugins/gas-analytics/function-gas-snapshots.js.map +1 -1
- package/dist/src/internal/builtin-plugins/gas-analytics/helpers/utils.d.ts +7 -0
- package/dist/src/internal/builtin-plugins/gas-analytics/helpers/utils.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/gas-analytics/helpers/utils.js +10 -0
- package/dist/src/internal/builtin-plugins/gas-analytics/helpers/utils.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 +12 -0
- 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 +3 -2
- package/dist/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.js +27 -6
- package/dist/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.js.map +1 -1
- package/dist/src/internal/builtin-plugins/gas-analytics/tasks/solidity-test/task-action.d.ts +2 -1
- package/dist/src/internal/builtin-plugins/gas-analytics/tasks/solidity-test/task-action.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/gas-analytics/tasks/solidity-test/task-action.js +18 -7
- package/dist/src/internal/builtin-plugins/gas-analytics/tasks/solidity-test/task-action.js.map +1 -1
- package/dist/src/internal/builtin-plugins/network-manager/type-validation.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/network-manager/type-validation.js +3 -1
- package/dist/src/internal/builtin-plugins/network-manager/type-validation.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 +2 -2
- package/dist/src/internal/builtin-plugins/solidity-test/helpers.js.map +1 -1
- package/dist/src/internal/builtin-plugins/solidity-test/reporter.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/solidity-test/reporter.js +4 -2
- package/dist/src/internal/builtin-plugins/solidity-test/reporter.js.map +1 -1
- package/dist/src/internal/cli/error-handling/error-handler.d.ts.map +1 -0
- package/dist/src/internal/cli/{error-handler.js → error-handling/error-handler.js} +13 -2
- package/dist/src/internal/cli/error-handling/error-handler.js.map +1 -0
- package/dist/src/internal/cli/error-handling/native-binding-error.d.ts +18 -0
- package/dist/src/internal/cli/error-handling/native-binding-error.d.ts.map +1 -0
- package/dist/src/internal/cli/error-handling/native-binding-error.js +57 -0
- package/dist/src/internal/cli/error-handling/native-binding-error.js.map +1 -0
- package/dist/src/internal/cli/hhu.js +1 -1
- package/dist/src/internal/cli/hhu.js.map +1 -1
- package/dist/src/internal/cli/main.js +1 -1
- package/dist/src/internal/cli/main.js.map +1 -1
- package/dist/src/internal/constants.d.ts +10 -0
- package/dist/src/internal/constants.d.ts.map +1 -1
- package/dist/src/internal/constants.js +10 -0
- package/dist/src/internal/constants.js.map +1 -1
- package/dist/src/internal/core/config.d.ts +14 -1
- package/dist/src/internal/core/config.d.ts.map +1 -1
- package/dist/src/internal/core/config.js +13 -5
- package/dist/src/internal/core/config.js.map +1 -1
- package/dist/src/internal/core/configuration-variables.d.ts.map +1 -1
- package/dist/src/internal/core/configuration-variables.js +3 -1
- package/dist/src/internal/core/configuration-variables.js.map +1 -1
- package/dist/src/types/config.d.ts +11 -0
- package/dist/src/types/config.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/internal/builtin-plugins/gas-analytics/function-gas-snapshots.ts +26 -4
- package/src/internal/builtin-plugins/gas-analytics/helpers/utils.ts +15 -0
- package/src/internal/builtin-plugins/gas-analytics/index.ts +13 -0
- package/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.ts +38 -3
- package/src/internal/builtin-plugins/gas-analytics/tasks/solidity-test/task-action.ts +29 -6
- package/src/internal/builtin-plugins/network-manager/type-validation.ts +5 -1
- package/src/internal/builtin-plugins/solidity-test/helpers.ts +6 -2
- package/src/internal/builtin-plugins/solidity-test/reporter.ts +5 -6
- package/src/internal/cli/{error-handler.ts → error-handling/error-handler.ts} +20 -3
- package/src/internal/cli/error-handling/native-binding-error.ts +78 -0
- package/src/internal/cli/hhu.ts +1 -1
- package/src/internal/cli/main.ts +1 -1
- package/src/internal/constants.ts +11 -0
- package/src/internal/core/config.ts +35 -2
- package/src/internal/core/configuration-variables.ts +3 -1
- package/src/types/config.ts +12 -0
- package/templates/01-node-test-runner-viem/package.json +1 -1
- package/templates/02-mocha-ethers/package.json +1 -1
- package/templates/03-minimal/package.json +1 -1
- package/dist/src/internal/cli/error-handler.d.ts.map +0 -1
- package/dist/src/internal/cli/error-handler.js.map +0 -1
- /package/dist/src/internal/cli/{error-handler.d.ts → error-handling/error-handler.d.ts} +0 -0
|
@@ -7,6 +7,17 @@ export interface ConfigurationVariable {
|
|
|
7
7
|
_type: "ConfigurationVariable";
|
|
8
8
|
name: string;
|
|
9
9
|
format?: string;
|
|
10
|
+
/**
|
|
11
|
+
* An optional fallback value, used only when the variable can't be resolved
|
|
12
|
+
* from any other source (e.g. an environment variable or the keystore).
|
|
13
|
+
*
|
|
14
|
+
* By default, the value is resolved from the environment variable first, then
|
|
15
|
+
* any plugin source (like hardhat-keystore), and finally this default. Plugins
|
|
16
|
+
* can customize this, but the built-in resolver and bundled plugins follow this
|
|
17
|
+
* order. If none of them provide a value and no default is set, resolving the
|
|
18
|
+
* variable throws.
|
|
19
|
+
*/
|
|
20
|
+
default?: string;
|
|
10
21
|
}
|
|
11
22
|
/**
|
|
12
23
|
* A resolved configuration variable.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/types/config.ts"],"names":[],"mappings":"AAAA,OAAO,qCAAqC,CAAC;AAE7C;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,uBAAuB,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/types/config.ts"],"names":[],"mappings":"AAAA,OAAO,qCAAqC,CAAC;AAE7C;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,uBAAuB,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,6BAA6B;IAC5C,KAAK,EAAE,+BAA+B,CAAC;IACvC,MAAM,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAEvB;;;;;OAKG;IACH,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAE1B;;;;;OAKG;IACH,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAE7B;;;;OAIG;IACH,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,MAAM,6BAA6B,GAAG,CAC1C,gBAAgB,EAAE,qBAAqB,GAAG,MAAM,KAC7C,6BAA6B,CAAC;AAEnC;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,qBAAqB,CAAC;AAE7D;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,sBAAsB,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,GAAG,mBAAmB,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,qBAAqB,CAAC;CACrD;AAED;;GAEG;AAGH,MAAM,WAAW,mBAAmB;CAAG;AAEvC;;GAEG;AAGH,MAAM,WAAW,qBAAqB;CAAG;AAEzC;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,kBAAkB,CAAC;CAC3B;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IAEb;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,eAAe,CAAC;IACvB,OAAO,EAAE,iBAAiB,CAAC;CAC5B;AAED;;GAEG;AAGH,MAAM,WAAW,eAAe;CAAG;AAEnC;;GAEG;AAGH,MAAM,WAAW,iBAAiB;CAAG"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hardhat",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.13.0",
|
|
4
4
|
"description": "Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.",
|
|
5
5
|
"homepage": "https://hardhat.org",
|
|
6
6
|
"repository": {
|
|
@@ -17,7 +17,11 @@ import {
|
|
|
17
17
|
parseFullyQualifiedName,
|
|
18
18
|
} from "../../../utils/contract-names.js";
|
|
19
19
|
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
formatSectionHeader,
|
|
22
|
+
getUserFqn,
|
|
23
|
+
isWithinTolerance,
|
|
24
|
+
} from "./helpers/utils.js";
|
|
21
25
|
|
|
22
26
|
export const FUNCTION_GAS_SNAPSHOTS_FILE = ".gas-snapshot";
|
|
23
27
|
|
|
@@ -49,6 +53,10 @@ export interface FunctionGasSnapshotComparison {
|
|
|
49
53
|
added: FunctionGasSnapshot[];
|
|
50
54
|
removed: FunctionGasSnapshot[];
|
|
51
55
|
changed: FunctionGasSnapshotChange[];
|
|
56
|
+
// Diffs within the allowed tolerance: they don't fail the check. They are
|
|
57
|
+
// included in the result for programmatic inspection, but are not shown in
|
|
58
|
+
// the default console output.
|
|
59
|
+
tolerated: FunctionGasSnapshotChange[];
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
export interface FunctionGasSnapshotChange {
|
|
@@ -242,6 +250,7 @@ export function parseFunctionGasSnapshots(
|
|
|
242
250
|
export function compareFunctionGasSnapshots(
|
|
243
251
|
previousSnapshots: FunctionGasSnapshot[],
|
|
244
252
|
currentSnapshots: FunctionGasSnapshotWithMetadata[],
|
|
253
|
+
tolerance: number,
|
|
245
254
|
): FunctionGasSnapshotComparison {
|
|
246
255
|
const previousSnapshotsMap = new Map(
|
|
247
256
|
previousSnapshots.map((s) => [
|
|
@@ -252,6 +261,7 @@ export function compareFunctionGasSnapshots(
|
|
|
252
261
|
|
|
253
262
|
const added: FunctionGasSnapshot[] = [];
|
|
254
263
|
const changed: FunctionGasSnapshotChange[] = [];
|
|
264
|
+
const tolerated: FunctionGasSnapshotChange[] = [];
|
|
255
265
|
|
|
256
266
|
for (const current of currentSnapshots) {
|
|
257
267
|
const key = `${current.contractNameOrFqn}#${current.functionSig}`;
|
|
@@ -278,7 +288,7 @@ export function compareFunctionGasSnapshots(
|
|
|
278
288
|
? current.gasUsage.gas
|
|
279
289
|
: current.gasUsage.medianGas;
|
|
280
290
|
|
|
281
|
-
|
|
291
|
+
const change: FunctionGasSnapshotChange = {
|
|
282
292
|
contractNameOrFqn: current.contractNameOrFqn,
|
|
283
293
|
functionSig: current.functionSig,
|
|
284
294
|
kind: currentKind,
|
|
@@ -287,14 +297,23 @@ export function compareFunctionGasSnapshots(
|
|
|
287
297
|
runs:
|
|
288
298
|
currentKind === "fuzz" ? Number(current.gasUsage.runs) : undefined,
|
|
289
299
|
source: current.metadata.source,
|
|
290
|
-
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
if (
|
|
303
|
+
tolerance > 0 &&
|
|
304
|
+
isWithinTolerance(change.expected, change.actual, tolerance)
|
|
305
|
+
) {
|
|
306
|
+
tolerated.push(change);
|
|
307
|
+
} else {
|
|
308
|
+
changed.push(change);
|
|
309
|
+
}
|
|
291
310
|
}
|
|
292
311
|
previousSnapshotsMap.delete(key);
|
|
293
312
|
}
|
|
294
313
|
|
|
295
314
|
const removed = Array.from(previousSnapshotsMap.values());
|
|
296
315
|
|
|
297
|
-
return { added, removed, changed };
|
|
316
|
+
return { added, removed, changed, tolerated };
|
|
298
317
|
}
|
|
299
318
|
|
|
300
319
|
export function hasGasUsageChanged(
|
|
@@ -315,6 +334,7 @@ export function hasGasUsageChanged(
|
|
|
315
334
|
export async function checkFunctionGasSnapshots(
|
|
316
335
|
basePath: string,
|
|
317
336
|
suiteResults: SuiteResult[],
|
|
337
|
+
tolerance: number,
|
|
318
338
|
): Promise<FunctionGasSnapshotCheckResult> {
|
|
319
339
|
const functionGasSnapshots = extractFunctionGasSnapshots(suiteResults);
|
|
320
340
|
|
|
@@ -332,6 +352,7 @@ export async function checkFunctionGasSnapshots(
|
|
|
332
352
|
added: [],
|
|
333
353
|
removed: [],
|
|
334
354
|
changed: [],
|
|
355
|
+
tolerated: [],
|
|
335
356
|
},
|
|
336
357
|
noBaseline,
|
|
337
358
|
};
|
|
@@ -343,6 +364,7 @@ export async function checkFunctionGasSnapshots(
|
|
|
343
364
|
const comparison = compareFunctionGasSnapshots(
|
|
344
365
|
previousFunctionGasSnapshots,
|
|
345
366
|
functionGasSnapshots,
|
|
367
|
+
tolerance,
|
|
346
368
|
);
|
|
347
369
|
|
|
348
370
|
return {
|
|
@@ -84,6 +84,21 @@ export function median(values: number[]): number {
|
|
|
84
84
|
: (sorted[mid - 1] + sorted[mid]) / 2;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Whether `actual` drifted from `expected` by at most `tolerance` percent.
|
|
89
|
+
* Cross-multiplied instead of `(diff / expected) * 100 <= tolerance` to
|
|
90
|
+
* avoid floating-point errors from the division (e.g. `(49 / 700) * 100`
|
|
91
|
+
* is `7.000000000000001`, which would fail a tolerance of exactly 7).
|
|
92
|
+
*/
|
|
93
|
+
export function isWithinTolerance(
|
|
94
|
+
expected: number,
|
|
95
|
+
actual: number,
|
|
96
|
+
tolerance: number,
|
|
97
|
+
): boolean {
|
|
98
|
+
const diff = Math.abs(actual - expected);
|
|
99
|
+
return diff * 100 <= tolerance * Math.abs(expected);
|
|
100
|
+
}
|
|
101
|
+
|
|
87
102
|
export function formatSectionHeader(
|
|
88
103
|
sectionName: string,
|
|
89
104
|
{
|
|
@@ -19,6 +19,13 @@ const hardhatPlugin: HardhatPlugin = definePlugin({
|
|
|
19
19
|
description:
|
|
20
20
|
"Check the snapshots match the stored values (Solidity tests only)",
|
|
21
21
|
})
|
|
22
|
+
.addOption({
|
|
23
|
+
name: "tolerance",
|
|
24
|
+
description:
|
|
25
|
+
"Allowed drift percentage for --snapshot-check (Solidity tests only)",
|
|
26
|
+
type: ArgumentType.FLOAT,
|
|
27
|
+
defaultValue: 0,
|
|
28
|
+
})
|
|
22
29
|
.setAction(async () => ({
|
|
23
30
|
default: async (args, _hre, runSuper) => {
|
|
24
31
|
// We don't need to do anything here, as the test task will forward
|
|
@@ -36,6 +43,12 @@ const hardhatPlugin: HardhatPlugin = definePlugin({
|
|
|
36
43
|
name: "snapshotCheck",
|
|
37
44
|
description: "Check the snapshots match the stored values",
|
|
38
45
|
})
|
|
46
|
+
.addOption({
|
|
47
|
+
name: "tolerance",
|
|
48
|
+
description: "Allowed drift percentage for --snapshot-check",
|
|
49
|
+
type: ArgumentType.FLOAT,
|
|
50
|
+
defaultValue: 0,
|
|
51
|
+
})
|
|
39
52
|
.setAction(
|
|
40
53
|
async () => await import("./tasks/solidity-test/task-action.js"),
|
|
41
54
|
)
|
|
@@ -20,7 +20,11 @@ import {
|
|
|
20
20
|
parseFullyQualifiedName,
|
|
21
21
|
} from "../../../utils/contract-names.js";
|
|
22
22
|
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
formatSectionHeader,
|
|
25
|
+
getUserFqn,
|
|
26
|
+
isWithinTolerance,
|
|
27
|
+
} from "./helpers/utils.js";
|
|
24
28
|
|
|
25
29
|
export const SNAPSHOT_CHEATCODES_DIR = "snapshots";
|
|
26
30
|
|
|
@@ -73,6 +77,10 @@ export interface SnapshotCheatcodesComparison {
|
|
|
73
77
|
added: SnapshotCheatcode[];
|
|
74
78
|
removed: SnapshotCheatcode[];
|
|
75
79
|
changed: SnapshotCheatcodeChange[];
|
|
80
|
+
// Diffs within the allowed tolerance: they don't fail the check. They are
|
|
81
|
+
// included in the result for programmatic inspection, but are not shown in
|
|
82
|
+
// the default console output.
|
|
83
|
+
tolerated: SnapshotCheatcodeChange[];
|
|
76
84
|
}
|
|
77
85
|
|
|
78
86
|
export interface SnapshotCheatcodesCheckResult {
|
|
@@ -295,10 +303,12 @@ export function stringifySnapshotCheatcodes(
|
|
|
295
303
|
export function compareSnapshotCheatcodes(
|
|
296
304
|
previousSnapshotsMap: SnapshotCheatcodesMap,
|
|
297
305
|
currentSnapshotsMap: SnapshotCheatcodesWithMetadataMap,
|
|
306
|
+
tolerance: number,
|
|
298
307
|
): SnapshotCheatcodesComparison {
|
|
299
308
|
const added: SnapshotCheatcode[] = [];
|
|
300
309
|
const removed: SnapshotCheatcode[] = [];
|
|
301
310
|
const changed: SnapshotCheatcodeChange[] = [];
|
|
311
|
+
const tolerated: SnapshotCheatcodeChange[] = [];
|
|
302
312
|
const seenPreviousEntries = new Set<string>();
|
|
303
313
|
|
|
304
314
|
for (const [group, currentSnapshots] of currentSnapshotsMap) {
|
|
@@ -316,13 +326,27 @@ export function compareSnapshotCheatcodes(
|
|
|
316
326
|
seenPreviousEntries.add(key);
|
|
317
327
|
const previousValue = previousSnapshots[name];
|
|
318
328
|
if (previousValue !== currentEntry.value) {
|
|
319
|
-
|
|
329
|
+
const change: SnapshotCheatcodeChange = {
|
|
320
330
|
group,
|
|
321
331
|
name,
|
|
322
332
|
expected: Number(previousValue),
|
|
323
333
|
actual: Number(currentEntry.value),
|
|
324
334
|
source: currentEntry.metadata.source,
|
|
325
|
-
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// The tolerance only applies when both values are strictly numeric;
|
|
338
|
+
// otherwise the exact string comparison stands. Snapshot cheatcode
|
|
339
|
+
// values can be arbitrary strings, unlike function gas snapshots.
|
|
340
|
+
if (
|
|
341
|
+
tolerance > 0 &&
|
|
342
|
+
isStrictlyNumeric(previousValue) &&
|
|
343
|
+
isStrictlyNumeric(currentEntry.value) &&
|
|
344
|
+
isWithinTolerance(change.expected, change.actual, tolerance)
|
|
345
|
+
) {
|
|
346
|
+
tolerated.push(change);
|
|
347
|
+
} else {
|
|
348
|
+
changed.push(change);
|
|
349
|
+
}
|
|
326
350
|
}
|
|
327
351
|
}
|
|
328
352
|
}
|
|
@@ -347,12 +371,21 @@ export function compareSnapshotCheatcodes(
|
|
|
347
371
|
added: added.sort(sortByKey),
|
|
348
372
|
removed: removed.sort(sortByKey),
|
|
349
373
|
changed: changed.sort(sortByKey),
|
|
374
|
+
tolerated: tolerated.sort(sortByKey),
|
|
350
375
|
};
|
|
351
376
|
}
|
|
352
377
|
|
|
378
|
+
// The emptiness check is needed because `Number("")` is `0`, so a bare
|
|
379
|
+
// `Number.isFinite(Number(value))` would treat empty/whitespace values as
|
|
380
|
+
// numeric.
|
|
381
|
+
function isStrictlyNumeric(value: string): boolean {
|
|
382
|
+
return value.trim() !== "" && Number.isFinite(Number(value));
|
|
383
|
+
}
|
|
384
|
+
|
|
353
385
|
export async function checkSnapshotCheatcodes(
|
|
354
386
|
basePath: string,
|
|
355
387
|
suiteResults: SuiteResult[],
|
|
388
|
+
tolerance: number,
|
|
356
389
|
): Promise<SnapshotCheatcodesCheckResult> {
|
|
357
390
|
const { snapshotCheatcodes, renamedGroups } = sanitizeSnapshotCheatcodes(
|
|
358
391
|
extractSnapshotCheatcodes(suiteResults),
|
|
@@ -372,6 +405,7 @@ export async function checkSnapshotCheatcodes(
|
|
|
372
405
|
added: [],
|
|
373
406
|
removed: [],
|
|
374
407
|
changed: [],
|
|
408
|
+
tolerated: [],
|
|
375
409
|
},
|
|
376
410
|
noBaseline,
|
|
377
411
|
renamedGroups,
|
|
@@ -384,6 +418,7 @@ export async function checkSnapshotCheatcodes(
|
|
|
384
418
|
const comparison = compareSnapshotCheatcodes(
|
|
385
419
|
previousSnapshotCheatcodes,
|
|
386
420
|
snapshotCheatcodes,
|
|
421
|
+
tolerance,
|
|
387
422
|
);
|
|
388
423
|
|
|
389
424
|
return {
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
interface GasAnalyticsTestActionArguments {
|
|
32
32
|
snapshot: boolean;
|
|
33
33
|
snapshotCheck: boolean;
|
|
34
|
+
tolerance: number;
|
|
34
35
|
// Forwarded from the base `test solidity` task; used to detect scoped runs.
|
|
35
36
|
grep?: string;
|
|
36
37
|
grepExclude?: string;
|
|
@@ -50,6 +51,30 @@ export interface SnapshotCheckResult {
|
|
|
50
51
|
const runSolidityTests: TaskOverrideActionFunction<
|
|
51
52
|
GasAnalyticsTestActionArguments
|
|
52
53
|
> = async (args, hre, runSuper) => {
|
|
54
|
+
// Validate the flags before runSuper: it runs the entire test suite, and a
|
|
55
|
+
// usage error should fail immediately, not after a potentially long run.
|
|
56
|
+
if (args.snapshot && args.snapshotCheck) {
|
|
57
|
+
throw new HardhatError(
|
|
58
|
+
HardhatError.ERRORS.CORE.SOLIDITY_TESTS.MUTUALLY_EXCLUSIVE_SNAPSHOT_FLAGS,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Unreachable from the CLI (the FLOAT parser rejects negative/non-numeric
|
|
63
|
+
// values), but programmatic `task.run({ tolerance })` only validates
|
|
64
|
+
// `typeof === "number"`.
|
|
65
|
+
if (args.tolerance < 0 || !Number.isFinite(args.tolerance)) {
|
|
66
|
+
throw new HardhatError(
|
|
67
|
+
HardhatError.ERRORS.CORE.SOLIDITY_TESTS.INVALID_SNAPSHOT_TOLERANCE,
|
|
68
|
+
{ value: args.tolerance },
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (args.tolerance !== 0 && !args.snapshotCheck) {
|
|
73
|
+
throw new HardhatError(
|
|
74
|
+
HardhatError.ERRORS.CORE.SOLIDITY_TESTS.SNAPSHOT_TOLERANCE_REQUIRES_CHECK,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
53
78
|
const superResult: Result<SolidityTestRunResult, SolidityTestRunResult> =
|
|
54
79
|
await runSuper(args);
|
|
55
80
|
const testsPassed = superResult.success;
|
|
@@ -59,12 +84,6 @@ const runSolidityTests: TaskOverrideActionFunction<
|
|
|
59
84
|
const suiteResults = solidityTestRunResult.suiteResults;
|
|
60
85
|
const rootPath = hre.config.paths.root;
|
|
61
86
|
|
|
62
|
-
if (args.snapshot && args.snapshotCheck) {
|
|
63
|
-
throw new HardhatError(
|
|
64
|
-
HardhatError.ERRORS.CORE.SOLIDITY_TESTS.MUTUALLY_EXCLUSIVE_SNAPSHOT_FLAGS,
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
87
|
let snapshotCheckPassed = true;
|
|
69
88
|
if (args.snapshot) {
|
|
70
89
|
const snapshotResult = await handleSnapshot(
|
|
@@ -77,6 +96,7 @@ const runSolidityTests: TaskOverrideActionFunction<
|
|
|
77
96
|
const snapshotCheckResult = await handleSnapshotCheck(
|
|
78
97
|
rootPath,
|
|
79
98
|
suiteResults,
|
|
99
|
+
args.tolerance,
|
|
80
100
|
);
|
|
81
101
|
logSnapshotCheckResult(
|
|
82
102
|
snapshotCheckResult,
|
|
@@ -156,14 +176,17 @@ export function logSnapshotResult(
|
|
|
156
176
|
export async function handleSnapshotCheck(
|
|
157
177
|
basePath: string,
|
|
158
178
|
suiteResults: SuiteResult[],
|
|
179
|
+
tolerance: number,
|
|
159
180
|
): Promise<SnapshotCheckResult> {
|
|
160
181
|
const functionGasSnapshotsCheck = await checkFunctionGasSnapshots(
|
|
161
182
|
basePath,
|
|
162
183
|
suiteResults,
|
|
184
|
+
tolerance,
|
|
163
185
|
);
|
|
164
186
|
const snapshotCheatcodesCheck = await checkSnapshotCheatcodes(
|
|
165
187
|
basePath,
|
|
166
188
|
suiteResults,
|
|
189
|
+
tolerance,
|
|
167
190
|
);
|
|
168
191
|
|
|
169
192
|
return {
|
|
@@ -426,7 +426,11 @@ function refineEdrNetworkUserConfig(
|
|
|
426
426
|
}
|
|
427
427
|
|
|
428
428
|
const interval = network.mining?.interval;
|
|
429
|
-
|
|
429
|
+
// A scalar `interval: 0` disables interval mining entirely, so this validation is not appropriate
|
|
430
|
+
if (
|
|
431
|
+
(typeof interval === "number" && interval !== 0) ||
|
|
432
|
+
Array.isArray(interval)
|
|
433
|
+
) {
|
|
430
434
|
const minInterval =
|
|
431
435
|
typeof interval === "number" ? interval : Math.min(...interval);
|
|
432
436
|
if (
|
|
@@ -22,7 +22,10 @@ import {
|
|
|
22
22
|
import { toBigInt } from "@nomicfoundation/hardhat-utils/bigint";
|
|
23
23
|
import { hexStringToBytes } from "@nomicfoundation/hardhat-utils/hex";
|
|
24
24
|
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
ALWAYS_COLLECT_STACK_TRACES_VERBOSITY,
|
|
27
|
+
OPTIMISM_CHAIN_TYPE,
|
|
28
|
+
} from "../../constants.js";
|
|
26
29
|
import { resolveHardfork } from "../network-manager/config-resolution.js";
|
|
27
30
|
import { hardhatHardforkToEdrSpecId } from "../network-manager/edr/utils/convert-to-edr.js";
|
|
28
31
|
import { warnIfExperimentalHardfork } from "../network-manager/edr/utils/hardfork.js";
|
|
@@ -139,7 +142,8 @@ export async function solidityTestConfigToSolidityTestRunnerConfigArgs({
|
|
|
139
142
|
}
|
|
140
143
|
}
|
|
141
144
|
|
|
142
|
-
const shouldAlwaysCollectStackTraces =
|
|
145
|
+
const shouldAlwaysCollectStackTraces =
|
|
146
|
+
verbosity >= ALWAYS_COLLECT_STACK_TRACES_VERBOSITY;
|
|
143
147
|
|
|
144
148
|
return {
|
|
145
149
|
projectRoot,
|
|
@@ -10,6 +10,7 @@ import { styleText } from "node:util";
|
|
|
10
10
|
import { bytesToHexString } from "@nomicfoundation/hardhat-utils/hex";
|
|
11
11
|
|
|
12
12
|
import { sendErrorTelemetry } from "../../cli/telemetry/error-reporter/reporter.js";
|
|
13
|
+
import { ALWAYS_COLLECT_STACK_TRACES_VERBOSITY } from "../../constants.js";
|
|
13
14
|
import { SolidityTestStackTraceGenerationError } from "../network-manager/edr/stack-traces/stack-trace-generation-errors.js";
|
|
14
15
|
import { encodeStackTraceEntry } from "../network-manager/edr/stack-traces/stack-trace-solidity-errors.js";
|
|
15
16
|
import { formatTraces } from "../network-manager/edr/utils/trace-formatters.js";
|
|
@@ -17,6 +18,8 @@ import { formatTraces } from "../network-manager/edr/utils/trace-formatters.js";
|
|
|
17
18
|
import { formatArtifactId } from "./formatters.js";
|
|
18
19
|
import { getMessageFromLastStackTraceEntry } from "./stack-trace-solidity-errors.js";
|
|
19
20
|
|
|
21
|
+
const RERUN_VERBOSITY_HINT = `Try rerunning your tests with -${"v".repeat(ALWAYS_COLLECT_STACK_TRACES_VERBOSITY)} or above.`;
|
|
22
|
+
|
|
20
23
|
class Indenter {
|
|
21
24
|
#indentation: number;
|
|
22
25
|
|
|
@@ -305,17 +308,13 @@ export async function* testReporter(
|
|
|
305
308
|
yield* output(
|
|
306
309
|
indenter.t`Stack Trace Warning: ${colorize("grey", "The test is not safe to replay because a fork url without a fork block number was provided.")}\n`,
|
|
307
310
|
);
|
|
308
|
-
yield* output(
|
|
309
|
-
indenter.t`Try rerunning your tests with -vvv or above.\n`,
|
|
310
|
-
);
|
|
311
|
+
yield* output(indenter.t`${RERUN_VERBOSITY_HINT}\n`);
|
|
311
312
|
}
|
|
312
313
|
if (stackTrace.impureCheatcodes.length > 0) {
|
|
313
314
|
yield* output(
|
|
314
315
|
indenter.t`Stack Trace Warning: ${colorize("grey", `The test is not safe to replay because it uses impure cheatcodes: ${stackTrace.impureCheatcodes.join(", ")}`)}\n`,
|
|
315
316
|
);
|
|
316
|
-
yield* output(
|
|
317
|
-
indenter.t`Try rerunning your tests with -vvv or above.\n`,
|
|
318
|
-
);
|
|
317
|
+
yield* output(indenter.t`${RERUN_VERBOSITY_HINT}\n`);
|
|
319
318
|
}
|
|
320
319
|
break;
|
|
321
320
|
case "HeuristicFailed":
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type * as ClassifierT from "
|
|
1
|
+
import type * as ClassifierT from "../telemetry/error-classification/classifier.js";
|
|
2
2
|
|
|
3
3
|
import { styleText } from "node:util";
|
|
4
4
|
|
|
@@ -7,7 +7,9 @@ import {
|
|
|
7
7
|
HardhatPluginError,
|
|
8
8
|
} from "@nomicfoundation/hardhat-errors";
|
|
9
9
|
|
|
10
|
-
import { HARDHAT_NAME, HARDHAT_WEBSITE_URL } from "
|
|
10
|
+
import { HARDHAT_NAME, HARDHAT_WEBSITE_URL } from "../../constants.js";
|
|
11
|
+
|
|
12
|
+
import { detectNativeBindingFailure } from "./native-binding-error.js";
|
|
11
13
|
|
|
12
14
|
// The classifier may import many unrelated things top-level to do its job, so
|
|
13
15
|
// we load it lazily.
|
|
@@ -165,9 +167,24 @@ async function getErrorWithCategory(error: Error): Promise<ErrorWithCategory> {
|
|
|
165
167
|
};
|
|
166
168
|
}
|
|
167
169
|
|
|
170
|
+
const nativeBindingFailure = detectNativeBindingFailure(error);
|
|
171
|
+
if (nativeBindingFailure !== undefined) {
|
|
172
|
+
return {
|
|
173
|
+
category: ErrorCategory.HARDHAT,
|
|
174
|
+
categorizedError: new HardhatError(
|
|
175
|
+
HardhatError.ERRORS.CORE.GENERAL.NATIVE_BINDING_LOAD_FAILED,
|
|
176
|
+
{
|
|
177
|
+
parentPackage: nativeBindingFailure.parentPackage,
|
|
178
|
+
missingPackage: nativeBindingFailure.missingPackage,
|
|
179
|
+
},
|
|
180
|
+
error,
|
|
181
|
+
),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
168
185
|
if (classifierModule === undefined) {
|
|
169
186
|
classifierModule =
|
|
170
|
-
await import("
|
|
187
|
+
await import("../telemetry/error-classification/classifier.js");
|
|
171
188
|
}
|
|
172
189
|
|
|
173
190
|
// Pass `ignoreDevelopmentTimeFilter=true` so the migration footer also shows
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The platform triple that napi-rs appends to the platform package name, e.g.
|
|
3
|
+
* `@nomicfoundation/edr-linux-x64-gnu`, `@scope/pkg-darwin-arm64`.
|
|
4
|
+
*/
|
|
5
|
+
const PLATFORM_SUFFIX_REGEX =
|
|
6
|
+
/-(?:android|darwin|freebsd|linux|win32)-(?:arm|arm64|ia32|x64|riscv64|ppc64|s390x)(?:-(?:gnu|musl|msvc|eabi|gnueabihf))?$/;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Matches both the CJS (`Cannot find module 'x'`) and ESM
|
|
10
|
+
* (`Cannot find package 'x' imported from y`) forms.
|
|
11
|
+
*/
|
|
12
|
+
const MISSING_MODULE_REGEX = /Cannot find (?:module|package) '([^']+)'/g;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The packages Hardhat itself ships a native binding for.
|
|
16
|
+
*/
|
|
17
|
+
const HARDHAT_NATIVE_BINDING_PACKAGES = [
|
|
18
|
+
"@nomicfoundation/edr",
|
|
19
|
+
"@nomicfoundation/solidity-analyzer",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export interface NativeBindingFailure {
|
|
23
|
+
/** The package whose loader failed, e.g. `@nomicfoundation/edr`. */
|
|
24
|
+
parentPackage: string;
|
|
25
|
+
/** The platform package that couldn't be found. */
|
|
26
|
+
missingPackage: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Detect whether a given error is a napi-rs native binding load failure.
|
|
31
|
+
*
|
|
32
|
+
* Packages like `@nomicfoundation/edr` and `@nomicfoundation/solidity-analyzer`
|
|
33
|
+
* ship one prebuilt binary per platform in a separate package, and their
|
|
34
|
+
* generated loader `require`s the one matching the current platform. When that
|
|
35
|
+
* package is missing from the installation the loader throws at import time, so
|
|
36
|
+
* the failure surfaces as an opaque `MODULE_NOT_FOUND` from deep inside
|
|
37
|
+
* `node_modules`.
|
|
38
|
+
*/
|
|
39
|
+
export function detectNativeBindingFailure(
|
|
40
|
+
error: Error,
|
|
41
|
+
): NativeBindingFailure | undefined {
|
|
42
|
+
const chain = errorChain(error);
|
|
43
|
+
|
|
44
|
+
// Some link in the chain names the missing platform package.
|
|
45
|
+
for (const chainedError of chain) {
|
|
46
|
+
for (const match of chainedError.message.matchAll(MISSING_MODULE_REGEX)) {
|
|
47
|
+
const moduleName = match[1];
|
|
48
|
+
|
|
49
|
+
if (!PLATFORM_SUFFIX_REGEX.test(moduleName)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const parentPackage = moduleName.replace(PLATFORM_SUFFIX_REGEX, "");
|
|
54
|
+
|
|
55
|
+
if (!HARDHAT_NATIVE_BINDING_PACKAGES.includes(parentPackage)) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { missingPackage: moduleName, parentPackage };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function errorChain(error: Error): Error[] {
|
|
67
|
+
const chain: Error[] = [];
|
|
68
|
+
const seen = new Set<unknown>();
|
|
69
|
+
let current: unknown = error;
|
|
70
|
+
|
|
71
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
72
|
+
seen.add(current);
|
|
73
|
+
chain.push(current);
|
|
74
|
+
current = current.cause;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return chain;
|
|
78
|
+
}
|
package/src/internal/cli/hhu.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { globalFlag } from "../core/config.js";
|
|
|
19
19
|
import { TaskManagerImplementation } from "../core/tasks/task-manager.js";
|
|
20
20
|
import { getHardhatVersion } from "../utils/package.js";
|
|
21
21
|
|
|
22
|
-
import { printErrorMessages } from "./error-handler.js";
|
|
22
|
+
import { printErrorMessages } from "./error-handling/error-handler.js";
|
|
23
23
|
import { getGlobalHelpString } from "./help/get-global-help-string.js";
|
|
24
24
|
import { getHelpString } from "./help/get-help-string.js";
|
|
25
25
|
import {
|
package/src/internal/cli/main.ts
CHANGED
|
@@ -28,7 +28,7 @@ import { warnAboutUnusedLoadedPlugins } from "../core/plugins/unused-plugins-war
|
|
|
28
28
|
import { setGlobalHardhatRuntimeEnvironment } from "../global-hre-instance.js";
|
|
29
29
|
import { createHardhatRuntimeEnvironment } from "../hre-initialization.js";
|
|
30
30
|
|
|
31
|
-
import { printErrorMessages } from "./error-handler.js";
|
|
31
|
+
import { printErrorMessages } from "./error-handling/error-handler.js";
|
|
32
32
|
import { getGlobalHelpString } from "./help/get-global-help-string.js";
|
|
33
33
|
import { getHelpString } from "./help/get-help-string.js";
|
|
34
34
|
import {
|
|
@@ -11,3 +11,14 @@ export const OPTIMISM_CHAIN_TYPE = "op";
|
|
|
11
11
|
export const DEFAULT_NETWORK_NAME = "default";
|
|
12
12
|
|
|
13
13
|
export const DEFAULT_VERBOSITY = 2;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The verbosity level (`-vvvvv`) at and above which stack traces are always
|
|
17
|
+
* collected. Below it, stack traces are produced by re-running the failing
|
|
18
|
+
* test with tracing enabled — much cheaper, but tests with non-deterministic
|
|
19
|
+
* side effects (impure cheatcodes, a fork pinned to `latest`) can't be
|
|
20
|
+
* replayed and report an `UnsafeToReplay` warning instead. Always-on
|
|
21
|
+
* collection records step traces for every test, which balloons memory on
|
|
22
|
+
* large suites, so it's reserved for the highest verbosity.
|
|
23
|
+
*/
|
|
24
|
+
export const ALWAYS_COLLECT_STACK_TRACES_VERBOSITY = 5;
|
|
@@ -23,11 +23,33 @@ import {
|
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Creates a configuration variable, which will be fetched at runtime.
|
|
26
|
+
*
|
|
27
|
+
* The second argument is an options object with an optional `format` (a
|
|
28
|
+
* template that must include the `{variable}` marker) and an optional
|
|
29
|
+
* `default` value. The default is used as a fallback when the variable can't
|
|
30
|
+
* be resolved from any other source (see {@link ConfigurationVariable.default}).
|
|
26
31
|
*/
|
|
27
32
|
export function configVariable(
|
|
28
33
|
name: string,
|
|
29
|
-
format
|
|
34
|
+
options?: { format?: string; default?: string },
|
|
35
|
+
): ConfigurationVariable;
|
|
36
|
+
/**
|
|
37
|
+
* @deprecated Passing the `format` as a string is deprecated. Pass an options
|
|
38
|
+
* object instead: `configVariable(name, { format })`.
|
|
39
|
+
*/
|
|
40
|
+
export function configVariable(
|
|
41
|
+
name: string,
|
|
42
|
+
format: string,
|
|
43
|
+
): ConfigurationVariable;
|
|
44
|
+
export function configVariable(
|
|
45
|
+
name: string,
|
|
46
|
+
formatOrOptions: string | { format?: string; default?: string } = {},
|
|
30
47
|
): ConfigurationVariable {
|
|
48
|
+
const { format = CONFIGURATION_VARIABLE_MARKER, default: defaultValue } =
|
|
49
|
+
typeof formatOrOptions === "string"
|
|
50
|
+
? { format: formatOrOptions }
|
|
51
|
+
: formatOrOptions;
|
|
52
|
+
|
|
31
53
|
if (!format.includes(CONFIGURATION_VARIABLE_MARKER)) {
|
|
32
54
|
throw new HardhatError(
|
|
33
55
|
HardhatError.ERRORS.CORE.GENERAL
|
|
@@ -35,7 +57,18 @@ export function configVariable(
|
|
|
35
57
|
{ format, marker: CONFIGURATION_VARIABLE_MARKER },
|
|
36
58
|
);
|
|
37
59
|
}
|
|
38
|
-
|
|
60
|
+
|
|
61
|
+
const configurationVariable: ConfigurationVariable = {
|
|
62
|
+
_type: "ConfigurationVariable",
|
|
63
|
+
name,
|
|
64
|
+
format,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (defaultValue !== undefined) {
|
|
68
|
+
configurationVariable.default = defaultValue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return configurationVariable;
|
|
39
72
|
}
|
|
40
73
|
|
|
41
74
|
/**
|
|
@@ -131,7 +131,9 @@ export class LazyResolvedConfigurationVariable extends BaseResolvedConfiguration
|
|
|
131
131
|
"fetchValue",
|
|
132
132
|
[this.#variable],
|
|
133
133
|
async (_context, v) => {
|
|
134
|
-
|
|
134
|
+
// Fall back to the default only when the env var is unset. An empty
|
|
135
|
+
// string still takes precedence
|
|
136
|
+
const value = process.env[v.name] ?? v.default;
|
|
135
137
|
|
|
136
138
|
if (typeof value !== "string") {
|
|
137
139
|
throw new HardhatError(
|