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,454 @@
|
|
|
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
|
+
readdir,
|
|
10
|
+
readJsonFile,
|
|
11
|
+
remove,
|
|
12
|
+
writeJsonFile,
|
|
13
|
+
} from "@nomicfoundation/hardhat-utils/fs";
|
|
14
|
+
import chalk from "chalk";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
getFullyQualifiedName,
|
|
18
|
+
parseFullyQualifiedName,
|
|
19
|
+
} from "../../../utils/contract-names.js";
|
|
20
|
+
|
|
21
|
+
import { getUserFqn } from "./gas-analytics-manager.js";
|
|
22
|
+
import { formatSectionHeader } from "./helpers.js";
|
|
23
|
+
|
|
24
|
+
export const SNAPSHOT_CHEATCODES_DIR = "snapshots";
|
|
25
|
+
|
|
26
|
+
export type SnapshotCheatcodesMap = Map<
|
|
27
|
+
string, // group
|
|
28
|
+
Record<
|
|
29
|
+
string, // name
|
|
30
|
+
string // value
|
|
31
|
+
>
|
|
32
|
+
>;
|
|
33
|
+
|
|
34
|
+
export type SnapshotCheatcodesWithMetadataMap = Map<
|
|
35
|
+
string, // group
|
|
36
|
+
Record<
|
|
37
|
+
string, // name
|
|
38
|
+
{
|
|
39
|
+
value: string;
|
|
40
|
+
metadata: {
|
|
41
|
+
source: string;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
>
|
|
45
|
+
>;
|
|
46
|
+
|
|
47
|
+
export interface SnapshotCheatcode {
|
|
48
|
+
group: string;
|
|
49
|
+
name: string;
|
|
50
|
+
value: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SnapshotCheatcodeChange {
|
|
54
|
+
group: string;
|
|
55
|
+
name: string;
|
|
56
|
+
expected: number;
|
|
57
|
+
actual: number;
|
|
58
|
+
source: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SnapshotCheatcodesComparison {
|
|
62
|
+
added: SnapshotCheatcode[];
|
|
63
|
+
removed: SnapshotCheatcode[];
|
|
64
|
+
changed: SnapshotCheatcodeChange[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface SnapshotCheatcodesCheckResult {
|
|
68
|
+
passed: boolean;
|
|
69
|
+
comparison: SnapshotCheatcodesComparison;
|
|
70
|
+
written: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getSnapshotCheatcodesPath(
|
|
74
|
+
basePath: string,
|
|
75
|
+
filename: string,
|
|
76
|
+
): string {
|
|
77
|
+
return path.join(basePath, SNAPSHOT_CHEATCODES_DIR, filename);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function extractSnapshotCheatcodes(
|
|
81
|
+
suiteResults: SuiteResult[],
|
|
82
|
+
): SnapshotCheatcodesWithMetadataMap {
|
|
83
|
+
const snapshots: SnapshotCheatcodesWithMetadataMap = new Map();
|
|
84
|
+
for (const { id: suiteId, testResults } of suiteResults) {
|
|
85
|
+
for (const { valueSnapshotGroups: snapshotGroups } of testResults) {
|
|
86
|
+
if (snapshotGroups === undefined) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const userFqn = getUserFqn(
|
|
91
|
+
getFullyQualifiedName(suiteId.source, suiteId.name),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
for (const group of snapshotGroups) {
|
|
95
|
+
let snapshot = snapshots.get(group.name);
|
|
96
|
+
if (snapshot === undefined) {
|
|
97
|
+
snapshot = {};
|
|
98
|
+
snapshots.set(group.name, snapshot);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const entry of group.entries) {
|
|
102
|
+
snapshot[entry.name] = {
|
|
103
|
+
value: entry.value,
|
|
104
|
+
metadata: {
|
|
105
|
+
source: parseFullyQualifiedName(userFqn).sourceName,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return snapshots;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function deleteOrphanedSnapshotFiles(
|
|
117
|
+
snapshotsDir: string,
|
|
118
|
+
currentGroups: Set<string>,
|
|
119
|
+
): Promise<void> {
|
|
120
|
+
let dirEntries: string[];
|
|
121
|
+
try {
|
|
122
|
+
dirEntries = await readdir(snapshotsDir);
|
|
123
|
+
|
|
124
|
+
for (const entry of dirEntries) {
|
|
125
|
+
if (entry.endsWith(".json")) {
|
|
126
|
+
const groupName = entry.slice(0, -5); // remove .json
|
|
127
|
+
if (!currentGroups.has(groupName)) {
|
|
128
|
+
const filePath = path.join(snapshotsDir, entry);
|
|
129
|
+
await remove(filePath);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} catch (error) {
|
|
134
|
+
ensureError(error);
|
|
135
|
+
// Directory doesn't exist yet, nothing to clean up
|
|
136
|
+
if (error instanceof FileNotFoundError) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
throw new HardhatError(
|
|
140
|
+
HardhatError.ERRORS.CORE.SOLIDITY_TESTS.SNAPSHOT_WRITE_ERROR,
|
|
141
|
+
{ snapshotsPath: snapshotsDir, error: error.message },
|
|
142
|
+
error,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function writeSnapshotCheatcodes(
|
|
148
|
+
basePath: string,
|
|
149
|
+
snapshotCheatcodes: SnapshotCheatcodesWithMetadataMap,
|
|
150
|
+
): Promise<void> {
|
|
151
|
+
const snapshotsDir = path.join(basePath, SNAPSHOT_CHEATCODES_DIR);
|
|
152
|
+
|
|
153
|
+
// Delete old files that are no longer in the map
|
|
154
|
+
const currentGroups = new Set(snapshotCheatcodes.keys());
|
|
155
|
+
await deleteOrphanedSnapshotFiles(snapshotsDir, currentGroups);
|
|
156
|
+
|
|
157
|
+
// Write current snapshot files
|
|
158
|
+
for (const [snapshotGroup, snapshot] of snapshotCheatcodes) {
|
|
159
|
+
const snapshotCheatcodesPath = getSnapshotCheatcodesPath(
|
|
160
|
+
basePath,
|
|
161
|
+
`${snapshotGroup}.json`,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const snapshotWithoutMetadata: Record<string, string> = {};
|
|
165
|
+
for (const [name, entry] of Object.entries(snapshot)) {
|
|
166
|
+
snapshotWithoutMetadata[name] = entry.value;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
await writeJsonFile(snapshotCheatcodesPath, snapshotWithoutMetadata);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
ensureError(error);
|
|
173
|
+
throw new HardhatError(
|
|
174
|
+
HardhatError.ERRORS.CORE.SOLIDITY_TESTS.SNAPSHOT_WRITE_ERROR,
|
|
175
|
+
{ snapshotsPath: snapshotCheatcodesPath, error: error.message },
|
|
176
|
+
error,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function readSnapshotCheatcodes(
|
|
183
|
+
basePath: string,
|
|
184
|
+
): Promise<SnapshotCheatcodesMap> {
|
|
185
|
+
const snapshots: SnapshotCheatcodesMap = new Map();
|
|
186
|
+
const snapshotsDir = path.join(basePath, SNAPSHOT_CHEATCODES_DIR);
|
|
187
|
+
|
|
188
|
+
let dirEntries: string[];
|
|
189
|
+
try {
|
|
190
|
+
dirEntries = await readdir(snapshotsDir);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
ensureError(error);
|
|
193
|
+
|
|
194
|
+
// Re-throw as-is to allow the caller to handle this case specifically
|
|
195
|
+
if (error instanceof FileNotFoundError) {
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
throw new HardhatError(
|
|
200
|
+
HardhatError.ERRORS.CORE.SOLIDITY_TESTS.SNAPSHOT_READ_ERROR,
|
|
201
|
+
{ snapshotsPath: snapshotsDir, error: error.message },
|
|
202
|
+
error,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
for (const entry of dirEntries) {
|
|
207
|
+
if (entry.endsWith(".json")) {
|
|
208
|
+
const snapshotGroup = entry.slice(0, -5); // remove .json extension
|
|
209
|
+
const snapshotCheatcodesPath = getSnapshotCheatcodesPath(basePath, entry);
|
|
210
|
+
|
|
211
|
+
let snapshot: Record<string, string>;
|
|
212
|
+
try {
|
|
213
|
+
snapshot = await readJsonFile(snapshotCheatcodesPath);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
ensureError(error);
|
|
216
|
+
throw new HardhatError(
|
|
217
|
+
HardhatError.ERRORS.CORE.SOLIDITY_TESTS.SNAPSHOT_READ_ERROR,
|
|
218
|
+
{ snapshotsPath: snapshotCheatcodesPath, error: error.message },
|
|
219
|
+
error,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
snapshots.set(snapshotGroup, snapshot);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return snapshots;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function stringifySnapshotCheatcodes(
|
|
231
|
+
snapshots: SnapshotCheatcode[],
|
|
232
|
+
): string {
|
|
233
|
+
const lines: string[] = [];
|
|
234
|
+
for (const { group, name, value } of snapshots) {
|
|
235
|
+
lines.push(`${group}#${name}: ${value}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return lines.sort((a, b) => a.localeCompare(b)).join("\n");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function compareSnapshotCheatcodes(
|
|
242
|
+
previousSnapshotsMap: SnapshotCheatcodesMap,
|
|
243
|
+
currentSnapshotsMap: SnapshotCheatcodesWithMetadataMap,
|
|
244
|
+
): SnapshotCheatcodesComparison {
|
|
245
|
+
const added: SnapshotCheatcode[] = [];
|
|
246
|
+
const removed: SnapshotCheatcode[] = [];
|
|
247
|
+
const changed: SnapshotCheatcodeChange[] = [];
|
|
248
|
+
const seenPreviousEntries = new Set<string>();
|
|
249
|
+
|
|
250
|
+
for (const [group, currentSnapshots] of currentSnapshotsMap) {
|
|
251
|
+
const previousSnapshots = previousSnapshotsMap.get(group);
|
|
252
|
+
|
|
253
|
+
for (const [name, currentEntry] of Object.entries(currentSnapshots)) {
|
|
254
|
+
const key = `${group}#${name}`;
|
|
255
|
+
|
|
256
|
+
if (
|
|
257
|
+
previousSnapshots === undefined ||
|
|
258
|
+
!Object.hasOwn(previousSnapshots, name)
|
|
259
|
+
) {
|
|
260
|
+
added.push({ group, name, value: currentEntry.value });
|
|
261
|
+
} else {
|
|
262
|
+
seenPreviousEntries.add(key);
|
|
263
|
+
const previousValue = previousSnapshots[name];
|
|
264
|
+
if (previousValue !== currentEntry.value) {
|
|
265
|
+
changed.push({
|
|
266
|
+
group,
|
|
267
|
+
name,
|
|
268
|
+
expected: Number(previousValue),
|
|
269
|
+
actual: Number(currentEntry.value),
|
|
270
|
+
source: currentEntry.metadata.source,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
for (const [group, previousSnapshots] of previousSnapshotsMap) {
|
|
278
|
+
for (const [name, previousValue] of Object.entries(previousSnapshots)) {
|
|
279
|
+
const key = `${group}#${name}`;
|
|
280
|
+
if (!seenPreviousEntries.has(key)) {
|
|
281
|
+
removed.push({ group, name, value: previousValue });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const sortByKey = <T extends { group: string; name: string }>(
|
|
287
|
+
a: T,
|
|
288
|
+
b: T,
|
|
289
|
+
): number => `${a.group}#${a.name}`.localeCompare(`${b.group}#${b.name}`);
|
|
290
|
+
|
|
291
|
+
// Sort the results for consistent output
|
|
292
|
+
return {
|
|
293
|
+
added: added.sort(sortByKey),
|
|
294
|
+
removed: removed.sort(sortByKey),
|
|
295
|
+
changed: changed.sort(sortByKey),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export async function checkSnapshotCheatcodes(
|
|
300
|
+
basePath: string,
|
|
301
|
+
suiteResults: SuiteResult[],
|
|
302
|
+
): Promise<SnapshotCheatcodesCheckResult> {
|
|
303
|
+
const snapshotCheatcodes = extractSnapshotCheatcodes(suiteResults);
|
|
304
|
+
|
|
305
|
+
let previousSnapshotCheatcodes;
|
|
306
|
+
try {
|
|
307
|
+
previousSnapshotCheatcodes = await readSnapshotCheatcodes(basePath);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
if (error instanceof FileNotFoundError) {
|
|
310
|
+
// Only write if there are cheatcodes to save
|
|
311
|
+
const written = snapshotCheatcodes.size > 0;
|
|
312
|
+
if (written) {
|
|
313
|
+
await writeSnapshotCheatcodes(basePath, snapshotCheatcodes);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
passed: true,
|
|
318
|
+
comparison: {
|
|
319
|
+
added: [],
|
|
320
|
+
removed: [],
|
|
321
|
+
changed: [],
|
|
322
|
+
},
|
|
323
|
+
written,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const comparison = compareSnapshotCheatcodes(
|
|
331
|
+
previousSnapshotCheatcodes,
|
|
332
|
+
snapshotCheatcodes,
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
// Update snapshots when functions are added or removed (but not changed)
|
|
336
|
+
const hasAddedOrRemoved =
|
|
337
|
+
comparison.added.length > 0 || comparison.removed.length > 0;
|
|
338
|
+
if (comparison.changed.length === 0 && hasAddedOrRemoved) {
|
|
339
|
+
await writeSnapshotCheatcodes(basePath, snapshotCheatcodes);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
passed: comparison.changed.length === 0,
|
|
344
|
+
comparison,
|
|
345
|
+
written: hasAddedOrRemoved,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function logSnapshotCheatcodesSection(
|
|
350
|
+
result: SnapshotCheatcodesCheckResult,
|
|
351
|
+
logger: typeof console.log = console.log,
|
|
352
|
+
): void {
|
|
353
|
+
const { comparison, written } = result;
|
|
354
|
+
const changedLength = comparison.changed.length;
|
|
355
|
+
const addedLength = comparison.added.length;
|
|
356
|
+
const removedLength = comparison.removed.length;
|
|
357
|
+
const hasChanges = changedLength > 0;
|
|
358
|
+
const hasAdded = addedLength > 0;
|
|
359
|
+
const hasRemoved = removedLength > 0;
|
|
360
|
+
const hasAnyDifferences = hasChanges || hasAdded || hasRemoved;
|
|
361
|
+
const isFirstTimeWrite = written && !hasAnyDifferences;
|
|
362
|
+
|
|
363
|
+
// Nothing to report
|
|
364
|
+
if (!isFirstTimeWrite && !hasAnyDifferences) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
logger(
|
|
369
|
+
formatSectionHeader("Snapshot cheatcodes", {
|
|
370
|
+
changedLength,
|
|
371
|
+
addedLength,
|
|
372
|
+
removedLength,
|
|
373
|
+
}),
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
if (isFirstTimeWrite) {
|
|
377
|
+
logger();
|
|
378
|
+
logger(
|
|
379
|
+
chalk.green(
|
|
380
|
+
" No existing snapshots found. Snapshot cheatcodes written successfully",
|
|
381
|
+
),
|
|
382
|
+
);
|
|
383
|
+
logger();
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (hasChanges) {
|
|
388
|
+
logger();
|
|
389
|
+
printSnapshotCheatcodeChanges(comparison.changed, logger);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (hasAdded) {
|
|
393
|
+
logger();
|
|
394
|
+
logger(` Added ${comparison.added.length} snapshot(s):`);
|
|
395
|
+
const addedLines = stringifySnapshotCheatcodes(comparison.added).split(
|
|
396
|
+
"\n",
|
|
397
|
+
);
|
|
398
|
+
for (const line of addedLines) {
|
|
399
|
+
logger(chalk.green(` + ${line}`));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (hasRemoved) {
|
|
404
|
+
logger();
|
|
405
|
+
logger(` Removed ${comparison.removed.length} snapshot(s):`);
|
|
406
|
+
const removedLines = stringifySnapshotCheatcodes(comparison.removed).split(
|
|
407
|
+
"\n",
|
|
408
|
+
);
|
|
409
|
+
for (const line of removedLines) {
|
|
410
|
+
logger(chalk.red(` - ${line}`));
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
logger();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function printSnapshotCheatcodeChanges(
|
|
418
|
+
changes: SnapshotCheatcodeChange[],
|
|
419
|
+
logger: typeof console.log = console.log,
|
|
420
|
+
): void {
|
|
421
|
+
for (let i = 0; i < changes.length; i++) {
|
|
422
|
+
const change = changes[i];
|
|
423
|
+
const isLast = i === changes.length - 1;
|
|
424
|
+
|
|
425
|
+
logger(` ${change.group}#${change.name}`);
|
|
426
|
+
logger(chalk.grey(` (in ${change.source})`));
|
|
427
|
+
|
|
428
|
+
const diff = change.actual - change.expected;
|
|
429
|
+
const formattedDiff = diff > 0 ? `Δ+${diff}` : `Δ${diff}`;
|
|
430
|
+
|
|
431
|
+
let gasChange = `${formattedDiff}`;
|
|
432
|
+
if (change.expected > 0) {
|
|
433
|
+
const percent = (diff / change.expected) * 100;
|
|
434
|
+
const formattedPercent =
|
|
435
|
+
percent >= 0 ? `+${percent.toFixed(2)}%` : `${percent.toFixed(2)}%`;
|
|
436
|
+
gasChange = `${formattedPercent}, ${formattedDiff}`;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Color: green for decrease (improvement), red for increase (regression)
|
|
440
|
+
const formattedGasChange =
|
|
441
|
+
diff < 0 ? chalk.green(gasChange) : chalk.red(gasChange);
|
|
442
|
+
|
|
443
|
+
logger(chalk.grey(` Expected: ${change.expected}`));
|
|
444
|
+
logger(
|
|
445
|
+
chalk.grey(` Actual: ${change.actual} (`) +
|
|
446
|
+
formattedGasChange +
|
|
447
|
+
chalk.grey(")"),
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
if (!isLast) {
|
|
451
|
+
logger();
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { TaskOverrideActionFunction } from "../../../../../types/tasks.js";
|
|
2
|
+
import type { Result } from "../../../../../types/utils.js";
|
|
3
|
+
import type { SolidityTestRunResult } from "../../../solidity-test/task-action.js";
|
|
4
|
+
import type { FunctionGasSnapshotCheckResult } from "../../function-gas-snapshots.js";
|
|
5
|
+
import type { SnapshotCheatcodesCheckResult } from "../../snapshot-cheatcodes.js";
|
|
6
|
+
import type { SuiteResult } from "@nomicfoundation/edr";
|
|
7
|
+
|
|
8
|
+
import { HardhatError } from "@nomicfoundation/hardhat-errors";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
|
|
11
|
+
import { errorResult } from "../../../../../utils/result.js";
|
|
12
|
+
import {
|
|
13
|
+
checkFunctionGasSnapshots,
|
|
14
|
+
extractFunctionGasSnapshots,
|
|
15
|
+
logFunctionGasSnapshotsSection,
|
|
16
|
+
writeFunctionGasSnapshots,
|
|
17
|
+
} from "../../function-gas-snapshots.js";
|
|
18
|
+
import {
|
|
19
|
+
checkSnapshotCheatcodes,
|
|
20
|
+
extractSnapshotCheatcodes,
|
|
21
|
+
logSnapshotCheatcodesSection,
|
|
22
|
+
writeSnapshotCheatcodes,
|
|
23
|
+
} from "../../snapshot-cheatcodes.js";
|
|
24
|
+
|
|
25
|
+
interface GasAnalyticsTestActionArguments {
|
|
26
|
+
snapshot: boolean;
|
|
27
|
+
snapshotCheck: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SnapshotResult {
|
|
31
|
+
functionGasSnapshotsWritten: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SnapshotCheckResult {
|
|
35
|
+
functionGasSnapshotsCheck: FunctionGasSnapshotCheckResult;
|
|
36
|
+
snapshotCheatcodesCheck: SnapshotCheatcodesCheckResult;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const runSolidityTests: TaskOverrideActionFunction<
|
|
40
|
+
GasAnalyticsTestActionArguments
|
|
41
|
+
> = async (args, hre, runSuper) => {
|
|
42
|
+
const superResult: Result<SolidityTestRunResult, SolidityTestRunResult> =
|
|
43
|
+
await runSuper(args);
|
|
44
|
+
const testsPassed = superResult.success;
|
|
45
|
+
const solidityTestRunResult = testsPassed
|
|
46
|
+
? superResult.value
|
|
47
|
+
: superResult.error;
|
|
48
|
+
const suiteResults = solidityTestRunResult.suiteResults;
|
|
49
|
+
const rootPath = hre.config.paths.root;
|
|
50
|
+
|
|
51
|
+
if (args.snapshot && args.snapshotCheck) {
|
|
52
|
+
throw new HardhatError(
|
|
53
|
+
HardhatError.ERRORS.CORE.SOLIDITY_TESTS.MUTUALLY_EXCLUSIVE_SNAPSHOT_FLAGS,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let snapshotCheckPassed = true;
|
|
58
|
+
if (args.snapshot) {
|
|
59
|
+
const snapshotResult = await handleSnapshot(
|
|
60
|
+
rootPath,
|
|
61
|
+
suiteResults,
|
|
62
|
+
testsPassed,
|
|
63
|
+
);
|
|
64
|
+
logSnapshotResult(snapshotResult);
|
|
65
|
+
} else if (testsPassed && args.snapshotCheck) {
|
|
66
|
+
const snapshotCheckResult = await handleSnapshotCheck(
|
|
67
|
+
rootPath,
|
|
68
|
+
suiteResults,
|
|
69
|
+
);
|
|
70
|
+
logSnapshotCheckResult(snapshotCheckResult);
|
|
71
|
+
snapshotCheckPassed =
|
|
72
|
+
snapshotCheckResult.functionGasSnapshotsCheck.passed &&
|
|
73
|
+
snapshotCheckResult.snapshotCheatcodesCheck.passed;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!snapshotCheckPassed) {
|
|
77
|
+
return errorResult(solidityTestRunResult);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return superResult;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export async function handleSnapshot(
|
|
84
|
+
basePath: string,
|
|
85
|
+
suiteResults: SuiteResult[],
|
|
86
|
+
testsPassed: boolean,
|
|
87
|
+
): Promise<SnapshotResult> {
|
|
88
|
+
if (testsPassed) {
|
|
89
|
+
const functionGasSnapshots = extractFunctionGasSnapshots(suiteResults);
|
|
90
|
+
await writeFunctionGasSnapshots(basePath, functionGasSnapshots);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const snapshotCheatcodes = extractSnapshotCheatcodes(suiteResults);
|
|
94
|
+
await writeSnapshotCheatcodes(basePath, snapshotCheatcodes);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
functionGasSnapshotsWritten: testsPassed,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function logSnapshotResult(
|
|
102
|
+
result: SnapshotResult,
|
|
103
|
+
logger: typeof console.log = console.log,
|
|
104
|
+
): void {
|
|
105
|
+
if (result.functionGasSnapshotsWritten) {
|
|
106
|
+
logger(chalk.green("Function gas snapshots written successfully"));
|
|
107
|
+
logger();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function handleSnapshotCheck(
|
|
112
|
+
basePath: string,
|
|
113
|
+
suiteResults: SuiteResult[],
|
|
114
|
+
): Promise<SnapshotCheckResult> {
|
|
115
|
+
const functionGasSnapshotsCheck = await checkFunctionGasSnapshots(
|
|
116
|
+
basePath,
|
|
117
|
+
suiteResults,
|
|
118
|
+
);
|
|
119
|
+
const snapshotCheatcodesCheck = await checkSnapshotCheatcodes(
|
|
120
|
+
basePath,
|
|
121
|
+
suiteResults,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
functionGasSnapshotsCheck,
|
|
126
|
+
snapshotCheatcodesCheck,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function logSnapshotCheckResult(
|
|
131
|
+
{ functionGasSnapshotsCheck, snapshotCheatcodesCheck }: SnapshotCheckResult,
|
|
132
|
+
logger: typeof console.log = console.log,
|
|
133
|
+
): void {
|
|
134
|
+
logger(
|
|
135
|
+
functionGasSnapshotsCheck.passed && snapshotCheatcodesCheck.passed
|
|
136
|
+
? chalk.green("Snapshot check passed")
|
|
137
|
+
: chalk.red("Snapshot check failed"),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const functionGasHasOutput =
|
|
141
|
+
functionGasSnapshotsCheck.written ||
|
|
142
|
+
functionGasSnapshotsCheck.comparison.changed.length > 0 ||
|
|
143
|
+
functionGasSnapshotsCheck.comparison.added.length > 0 ||
|
|
144
|
+
functionGasSnapshotsCheck.comparison.removed.length > 0;
|
|
145
|
+
const snapshotCheatcodesHasOutput =
|
|
146
|
+
snapshotCheatcodesCheck.written ||
|
|
147
|
+
snapshotCheatcodesCheck.comparison.changed.length > 0 ||
|
|
148
|
+
snapshotCheatcodesCheck.comparison.added.length > 0 ||
|
|
149
|
+
snapshotCheatcodesCheck.comparison.removed.length > 0;
|
|
150
|
+
|
|
151
|
+
// Add an extra newline if function gas snapshots have output
|
|
152
|
+
if (functionGasHasOutput) {
|
|
153
|
+
logger();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
logFunctionGasSnapshotsSection(functionGasSnapshotsCheck, logger);
|
|
157
|
+
|
|
158
|
+
// Add an extra newline if only snapshot cheatcodes have output
|
|
159
|
+
// (logFunctionGasSnapshotsSection adds one if it has output)
|
|
160
|
+
if (!functionGasHasOutput && snapshotCheatcodesHasOutput) {
|
|
161
|
+
logger();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
logSnapshotCheatcodesSection(snapshotCheatcodesCheck, logger);
|
|
165
|
+
|
|
166
|
+
if (!functionGasSnapshotsCheck.passed || !snapshotCheatcodesCheck.passed) {
|
|
167
|
+
logger(chalk.yellow("To update snapshots, run your tests with --snapshot"));
|
|
168
|
+
logger();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export default runSolidityTests;
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
} from "../../../types/config.js";
|
|
7
7
|
import type { HardhatUserConfigValidationError } from "../../../types/hooks.js";
|
|
8
8
|
import type {
|
|
9
|
+
SolidityTestConfig,
|
|
9
10
|
SolidityTestForkingConfig,
|
|
10
11
|
SolidityTestUserConfig,
|
|
11
12
|
} from "../../../types/test.js";
|
|
@@ -22,6 +23,10 @@ import {
|
|
|
22
23
|
} from "@nomicfoundation/hardhat-zod-utils";
|
|
23
24
|
import { z } from "zod";
|
|
24
25
|
|
|
26
|
+
// the keccak256 of "built for ethereum"
|
|
27
|
+
export const DEFAULT_FUZZ_SEED =
|
|
28
|
+
"0x7727ea51af0441c20da14dcd68a15dac8c9ebd589c5be8fa8c87c1d3720450bc";
|
|
29
|
+
|
|
25
30
|
const solidityTestUserConfigType = z.object({
|
|
26
31
|
timeout: z.number().optional(),
|
|
27
32
|
fsPermissions: z
|
|
@@ -152,6 +157,7 @@ export async function resolveSolidityTestUserConfig(
|
|
|
152
157
|
const solidityTest = {
|
|
153
158
|
rpcCachePath: defaultRpcCachePath,
|
|
154
159
|
...userConfig.test?.solidity,
|
|
160
|
+
fuzz: resolveFuzzConfig(userConfig.test?.solidity?.fuzz),
|
|
155
161
|
forking: resolvedForking,
|
|
156
162
|
};
|
|
157
163
|
|
|
@@ -170,3 +176,12 @@ export async function resolveSolidityTestUserConfig(
|
|
|
170
176
|
},
|
|
171
177
|
};
|
|
172
178
|
}
|
|
179
|
+
|
|
180
|
+
export function resolveFuzzConfig(
|
|
181
|
+
fuzzUserConfig: SolidityTestUserConfig["fuzz"] = {},
|
|
182
|
+
): SolidityTestConfig["fuzz"] {
|
|
183
|
+
return {
|
|
184
|
+
...fuzzUserConfig,
|
|
185
|
+
seed: fuzzUserConfig.seed ?? DEFAULT_FUZZ_SEED,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
@@ -38,10 +38,6 @@ interface SolidityTestConfigParams {
|
|
|
38
38
|
generateGasReport: boolean;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
function hexStringToBuffer(hexString: string): Buffer {
|
|
42
|
-
return Buffer.from(hexStringToBytes(hexString));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
41
|
export function solidityTestConfigToRunOptions(
|
|
46
42
|
config: SolidityTestConfig,
|
|
47
43
|
): RunOptions {
|
|
@@ -85,16 +81,12 @@ export async function solidityTestConfigToSolidityTestRunnerConfigArgs({
|
|
|
85
81
|
})) ?? [],
|
|
86
82
|
].flat(1);
|
|
87
83
|
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const blockCoinbase: Buffer | undefined =
|
|
95
|
-
config.coinbase === undefined
|
|
96
|
-
? undefined
|
|
97
|
-
: hexStringToBuffer(config.coinbase);
|
|
84
|
+
const hexToBytes = (hex: string | undefined) =>
|
|
85
|
+
hex !== undefined ? hexStringToBytes(hex) : undefined;
|
|
86
|
+
|
|
87
|
+
const sender = hexToBytes(config.from);
|
|
88
|
+
const txOrigin = hexToBytes(config.txOrigin);
|
|
89
|
+
const blockCoinbase = hexToBytes(config.coinbase);
|
|
98
90
|
|
|
99
91
|
const resolvedHardfork = hardhatHardforkToEdrSpecId(
|
|
100
92
|
resolveHardfork(hardfork, chainType),
|