@diamondslab/diamonds-hardhat-foundry 2.2.3 → 2.4.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.
@@ -0,0 +1,390 @@
1
+ import { spawn } from "child_process";
2
+ import { HardhatRuntimeEnvironment } from "hardhat/types";
3
+ import { CoverageOptions } from "../types/config";
4
+ import { isFoundryInstalled } from "../utils/foundry";
5
+ import { Logger } from "../utils/logger";
6
+ import { DeploymentManager } from "./DeploymentManager";
7
+ import { HelperGenerator } from "./HelperGenerator";
8
+
9
+ /**
10
+ * ForgeCoverageFramework - Main orchestration class for Forge coverage with Diamonds
11
+ *
12
+ * Coordinates:
13
+ * 1. Diamond deployment via DeploymentManager
14
+ * 2. Helper generation via HelperGenerator
15
+ * 3. Forge coverage execution
16
+ *
17
+ * Mirrors ForgeFuzzingFramework design for consistency and code reuse
18
+ */
19
+ export class ForgeCoverageFramework {
20
+ private deploymentManager: DeploymentManager;
21
+ private helperGenerator: HelperGenerator;
22
+
23
+ constructor(private hre: HardhatRuntimeEnvironment) {
24
+ this.deploymentManager = new DeploymentManager(hre);
25
+ this.helperGenerator = new HelperGenerator(hre);
26
+ }
27
+
28
+ /**
29
+ * Run complete Forge coverage workflow
30
+ *
31
+ * Workflow:
32
+ * 1. Validate Foundry installation
33
+ * 2. Deploy or reuse Diamond
34
+ * 3. Generate Solidity helpers
35
+ * 4. Run forge coverage with options
36
+ *
37
+ * @param options - Coverage execution options
38
+ * @returns Promise<boolean> - true if coverage succeeds
39
+ */
40
+ async runCoverage(options: CoverageOptions = {}): Promise<boolean> {
41
+ const {
42
+ diamondName = "ExampleDiamond",
43
+ networkName = "hardhat",
44
+ force = false,
45
+ skipDeployment = false,
46
+ skipHelpers = false,
47
+ writeDeployedDiamondData = false,
48
+ } = options;
49
+
50
+ Logger.section("Running Forge Coverage with Diamond");
51
+
52
+ // Step 1: Validate Foundry
53
+ if (!isFoundryInstalled()) {
54
+ Logger.error(
55
+ "Foundry is not installed. Please install it: https://book.getfoundry.sh/getting-started/installation"
56
+ );
57
+ return false;
58
+ }
59
+
60
+ try {
61
+ // Step 2: Ensure Diamond deployment
62
+ if (!skipDeployment) {
63
+ Logger.section("Step 1/3: Ensuring Diamond Deployment");
64
+ await this.deploymentManager.ensureDeployment(
65
+ diamondName,
66
+ networkName,
67
+ force,
68
+ writeDeployedDiamondData
69
+ );
70
+ } else {
71
+ Logger.info("Skipping deployment (using existing)");
72
+ }
73
+
74
+ // Step 3: Generate helpers
75
+ if (!skipHelpers) {
76
+ Logger.section("Step 2/3: Generating Solidity Helpers");
77
+
78
+ const deployment = await this.deploymentManager.getDeployment(
79
+ diamondName,
80
+ networkName
81
+ );
82
+
83
+ if (!deployment) {
84
+ Logger.warn("⚠ No deployment record found");
85
+ if (!skipDeployment) {
86
+ Logger.info("ℹ Using cached deployment (ephemeral)");
87
+ }
88
+ } else {
89
+ Logger.info("Using deployment record");
90
+ }
91
+
92
+ const provider = this.hre.ethers.provider;
93
+ const network = await provider.getNetwork();
94
+ const chainId = Number(network.chainId);
95
+
96
+ const deploymentData = deployment
97
+ ? deployment.getDeployedDiamondData()
98
+ : await this.deploymentManager
99
+ .ensureDeployment(diamondName, networkName, false, false)
100
+ .then((d) => d.getDeployedDiamondData());
101
+
102
+ await this.helperGenerator.generateDeploymentHelpers(
103
+ diamondName,
104
+ networkName,
105
+ chainId,
106
+ deploymentData,
107
+ deployment || undefined
108
+ );
109
+ } else {
110
+ Logger.info("Skipping helper generation");
111
+ }
112
+
113
+ // Step 4: Run coverage
114
+ Logger.section("Step 3/3: Running Forge Coverage");
115
+
116
+ // Get fork URL for network (same pattern as ForgeFuzzingFramework)
117
+ const provider = this.hre.ethers.provider;
118
+ let forkUrl: string;
119
+
120
+ if (networkName !== "hardhat") {
121
+ forkUrl = (provider as any).connection?.url || "http://127.0.0.1:8545";
122
+ } else {
123
+ forkUrl = "http://127.0.0.1:8545";
124
+ Logger.warn(
125
+ "⚠️ Network is \"hardhat\" - defaulting to localhost fork: http://127.0.0.1:8545"
126
+ );
127
+ Logger.warn("💡 Make sure Hardhat node is running: npx hardhat node");
128
+ Logger.warn("💡 Or specify network explicitly: --network localhost");
129
+ }
130
+
131
+ const args = this.buildCoverageCommand({ ...options, forkUrl });
132
+
133
+ Logger.info(`Executing: forge coverage ${args.join(" ")}`);
134
+ Logger.info("⏳ Running coverage analysis (this may take a while)...");
135
+
136
+ const success = await this.executeForge(args);
137
+
138
+ if (success) {
139
+ Logger.success("✅ Coverage analysis completed successfully");
140
+ } else {
141
+ Logger.error("❌ Coverage analysis failed");
142
+ }
143
+
144
+ return success;
145
+ } catch (error: any) {
146
+ Logger.error(`Coverage execution failed: ${error.message}`);
147
+ return false;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Build complete forge coverage command arguments
153
+ *
154
+ * @param options - Coverage options
155
+ * @returns Array of command arguments
156
+ */
157
+ private buildCoverageCommand(options: CoverageOptions): string[] {
158
+ const args: string[] = [];
159
+
160
+ // Add fork URL if provided
161
+ if (options.forkUrl) {
162
+ args.push("--fork-url", options.forkUrl);
163
+ }
164
+
165
+ // Add all option groups
166
+ args.push(...this.buildReportOptions(options));
167
+ args.push(...this.buildFilterOptions(options));
168
+ args.push(...this.buildDisplayOptions(options));
169
+ args.push(...this.buildTestOptions(options));
170
+ args.push(...this.buildEvmOptions(options));
171
+ args.push(...this.buildBuildOptions(options));
172
+
173
+ return args.filter((arg) => arg !== "");
174
+ }
175
+
176
+ /**
177
+ * Build report-related options
178
+ */
179
+ private buildReportOptions(options: CoverageOptions): string[] {
180
+ const args: string[] = [];
181
+
182
+ // Multiple --report flags
183
+ if (options.report && options.report.length > 0) {
184
+ for (const reportType of options.report) {
185
+ args.push("--report", reportType);
186
+ }
187
+ }
188
+
189
+ if (options.reportFile) {
190
+ args.push("--report-file", options.reportFile);
191
+ }
192
+
193
+ if (options.lcovVersion) {
194
+ args.push("--lcov-version", options.lcovVersion);
195
+ }
196
+
197
+ if (options.includeLibs) {
198
+ args.push("--include-libs");
199
+ }
200
+
201
+ if (options.excludeTests) {
202
+ args.push("--exclude-tests");
203
+ }
204
+
205
+ if (options.irMinimum) {
206
+ args.push("--ir-minimum");
207
+ }
208
+
209
+ return args;
210
+ }
211
+
212
+ /**
213
+ * Build test filtering options
214
+ */
215
+ private buildFilterOptions(options: CoverageOptions): string[] {
216
+ const args: string[] = [];
217
+
218
+ if (options.matchTest) {
219
+ args.push("--match-test", options.matchTest);
220
+ }
221
+
222
+ if (options.noMatchTest) {
223
+ args.push("--no-match-test", options.noMatchTest);
224
+ }
225
+
226
+ if (options.matchContract) {
227
+ args.push("--match-contract", options.matchContract);
228
+ }
229
+
230
+ if (options.noMatchContract) {
231
+ args.push("--no-match-contract", options.noMatchContract);
232
+ }
233
+
234
+ if (options.matchPath) {
235
+ args.push("--match-path", options.matchPath);
236
+ }
237
+
238
+ if (options.noMatchPath) {
239
+ args.push("--no-match-path", options.noMatchPath);
240
+ }
241
+
242
+ if (options.noMatchCoverage) {
243
+ args.push("--no-match-coverage", options.noMatchCoverage);
244
+ }
245
+
246
+ return args;
247
+ }
248
+
249
+ /**
250
+ * Build display options
251
+ */
252
+ private buildDisplayOptions(options: CoverageOptions): string[] {
253
+ const args: string[] = [];
254
+
255
+ // Verbosity (-v, -vv, -vvv, etc.)
256
+ if (options.verbosity && options.verbosity > 0) {
257
+ args.push("-" + "v".repeat(options.verbosity));
258
+ }
259
+
260
+ if (options.quiet) {
261
+ args.push("--quiet");
262
+ }
263
+
264
+ if (options.json) {
265
+ args.push("--json");
266
+ }
267
+
268
+ if (options.md) {
269
+ args.push("--md");
270
+ }
271
+
272
+ if (options.color) {
273
+ args.push("--color", options.color);
274
+ }
275
+
276
+ return args;
277
+ }
278
+
279
+ /**
280
+ * Build test execution options
281
+ */
282
+ private buildTestOptions(options: CoverageOptions): string[] {
283
+ const args: string[] = [];
284
+
285
+ if (options.threads !== undefined) {
286
+ args.push("--threads", options.threads.toString());
287
+ }
288
+
289
+ if (options.fuzzRuns !== undefined) {
290
+ args.push("--fuzz-runs", options.fuzzRuns.toString());
291
+ }
292
+
293
+ if (options.fuzzSeed) {
294
+ args.push("--fuzz-seed", options.fuzzSeed);
295
+ }
296
+
297
+ if (options.failFast) {
298
+ args.push("--fail-fast");
299
+ }
300
+
301
+ if (options.allowFailure) {
302
+ args.push("--allow-failure");
303
+ }
304
+
305
+ return args;
306
+ }
307
+
308
+ /**
309
+ * Build EVM options
310
+ */
311
+ private buildEvmOptions(options: CoverageOptions): string[] {
312
+ const args: string[] = [];
313
+
314
+ if (options.forkBlockNumber !== undefined) {
315
+ args.push("--fork-block-number", options.forkBlockNumber.toString());
316
+ }
317
+
318
+ if (options.initialBalance) {
319
+ args.push("--initial-balance", options.initialBalance);
320
+ }
321
+
322
+ if (options.sender) {
323
+ args.push("--sender", options.sender);
324
+ }
325
+
326
+ if (options.ffi) {
327
+ args.push("--ffi");
328
+ }
329
+
330
+ return args;
331
+ }
332
+
333
+ /**
334
+ * Build build options
335
+ */
336
+ private buildBuildOptions(options: CoverageOptions): string[] {
337
+ const args: string[] = [];
338
+
339
+ if (options.force) {
340
+ args.push("--force");
341
+ }
342
+
343
+ if (options.noCache) {
344
+ args.push("--no-cache");
345
+ }
346
+
347
+ if (options.optimize) {
348
+ args.push("--optimize");
349
+ }
350
+
351
+ if (options.optimizerRuns !== undefined) {
352
+ args.push("--optimizer-runs", options.optimizerRuns.toString());
353
+ }
354
+
355
+ if (options.viaIr) {
356
+ args.push("--via-ir");
357
+ }
358
+
359
+ return args;
360
+ }
361
+
362
+ /**
363
+ * Execute forge coverage command and stream output
364
+ *
365
+ * @param args - Command arguments
366
+ * @returns Promise<boolean> - true if command succeeds
367
+ */
368
+ private executeForge(args: string[]): Promise<boolean> {
369
+ return new Promise((resolve, reject) => {
370
+ const forge = spawn("forge", ["coverage", ...args], {
371
+ cwd: this.hre.config.paths.root,
372
+ stdio: "inherit",
373
+ shell: true,
374
+ });
375
+
376
+ forge.on("close", (code) => {
377
+ if (code === 0) {
378
+ resolve(true);
379
+ } else {
380
+ resolve(false);
381
+ }
382
+ });
383
+
384
+ forge.on("error", (error) => {
385
+ Logger.error(`Failed to execute forge: ${error.message}`);
386
+ reject(error);
387
+ });
388
+ });
389
+ }
390
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { extendConfig, extendEnvironment, internalTask, task } from "hardhat/config";
2
+ import "./tasks/coverage";
2
3
  import "./tasks/deploy";
3
4
  import "./tasks/generate-helpers";
4
5
  import "./tasks/init";
@@ -7,17 +8,17 @@ import "./types/hardhat";
7
8
 
8
9
  import { existsSync, writeFileSync } from "fs";
9
10
  import {
10
- TASK_COMPILE_GET_REMAPPINGS,
11
- TASK_COMPILE_TRANSFORM_IMPORT_NAME,
11
+ TASK_COMPILE_GET_REMAPPINGS,
12
+ TASK_COMPILE_TRANSFORM_IMPORT_NAME,
12
13
  } from "hardhat/builtin-tasks/task-names";
13
14
  import { HardhatRuntimeEnvironment } from "hardhat/types";
14
15
  import path from "path";
15
16
  import picocolors from "picocolors";
16
17
  import {
17
- getForgeConfig,
18
- getRemappings,
19
- HardhatFoundryError,
20
- installDependency,
18
+ getForgeConfig,
19
+ getRemappings,
20
+ HardhatFoundryError,
21
+ installDependency,
21
22
  } from "./foundry";
22
23
  import { validateConfig } from "./utils/validation";
23
24
 
@@ -0,0 +1,258 @@
1
+ import { task, types } from "hardhat/config";
2
+ import { HardhatRuntimeEnvironment } from "hardhat/types";
3
+ import { Logger } from "../utils/logger";
4
+
5
+ /**
6
+ * Task: diamonds-forge:coverage
7
+ *
8
+ * Runs Forge coverage with Diamond deployment.
9
+ * - Ensures Diamond deployment exists
10
+ * - Generates Solidity helpers
11
+ * - Runs forge coverage with specified options
12
+ *
13
+ * Use Hardhat's built-in --network flag to specify the network
14
+ *
15
+ * Design: Mirrors diamonds-forge:test task structure for consistency
16
+ */
17
+ task("diamonds-forge:coverage", "Run forge coverage for Diamond contracts")
18
+ .addOptionalParam(
19
+ "diamondName",
20
+ "Name of the Diamond to analyze",
21
+ "ExampleDiamond",
22
+ types.string
23
+ )
24
+ // Report options
25
+ .addOptionalParam(
26
+ "report",
27
+ "Report type (summary, lcov, debug, bytecode) - comma-separated for multiple",
28
+ undefined,
29
+ types.string
30
+ )
31
+ .addOptionalParam(
32
+ "reportFile",
33
+ "Output path for report file",
34
+ undefined,
35
+ types.string
36
+ )
37
+ .addOptionalParam(
38
+ "lcovVersion",
39
+ "LCOV format version (v1 or v2)",
40
+ undefined,
41
+ types.string
42
+ )
43
+ .addFlag("includeLibs", "Include libraries in coverage report")
44
+ .addFlag("excludeTests", "Exclude tests from coverage report")
45
+ .addFlag("irMinimum", "Enable viaIR with minimum optimization")
46
+ // Test filtering
47
+ .addOptionalParam(
48
+ "matchTest",
49
+ "Run tests matching pattern",
50
+ undefined,
51
+ types.string
52
+ )
53
+ .addOptionalParam(
54
+ "noMatchTest",
55
+ "Exclude tests matching pattern",
56
+ undefined,
57
+ types.string
58
+ )
59
+ .addOptionalParam(
60
+ "matchContract",
61
+ "Run contracts matching pattern",
62
+ undefined,
63
+ types.string
64
+ )
65
+ .addOptionalParam(
66
+ "noMatchContract",
67
+ "Exclude contracts matching pattern",
68
+ undefined,
69
+ types.string
70
+ )
71
+ .addOptionalParam(
72
+ "matchPath",
73
+ "Run files matching glob",
74
+ undefined,
75
+ types.string
76
+ )
77
+ .addOptionalParam(
78
+ "noMatchPath",
79
+ "Exclude files matching glob",
80
+ undefined,
81
+ types.string
82
+ )
83
+ .addOptionalParam(
84
+ "noMatchCoverage",
85
+ "Exclude files from coverage report",
86
+ undefined,
87
+ types.string
88
+ )
89
+ // Display options
90
+ .addOptionalParam(
91
+ "verbosity",
92
+ "Verbosity level (1-5)",
93
+ undefined,
94
+ types.int
95
+ )
96
+ .addFlag("quiet", "Suppress log output")
97
+ .addFlag("json", "Format output as JSON")
98
+ .addFlag("md", "Format output as Markdown")
99
+ .addOptionalParam(
100
+ "color",
101
+ "Color mode (auto, always, never)",
102
+ undefined,
103
+ types.string
104
+ )
105
+ // Test execution options
106
+ .addOptionalParam(
107
+ "threads",
108
+ "Number of threads to use",
109
+ undefined,
110
+ types.int
111
+ )
112
+ .addOptionalParam(
113
+ "fuzzRuns",
114
+ "Number of fuzz runs",
115
+ undefined,
116
+ types.int
117
+ )
118
+ .addOptionalParam(
119
+ "fuzzSeed",
120
+ "Seed for fuzz randomness",
121
+ undefined,
122
+ types.string
123
+ )
124
+ .addFlag("failFast", "Stop on first failure")
125
+ .addFlag("allowFailure", "Exit 0 even if tests fail")
126
+ // EVM options
127
+ .addOptionalParam(
128
+ "forkBlockNumber",
129
+ "Fork from specific block number",
130
+ undefined,
131
+ types.int
132
+ )
133
+ .addOptionalParam(
134
+ "initialBalance",
135
+ "Initial balance for test contracts",
136
+ undefined,
137
+ types.string
138
+ )
139
+ .addOptionalParam(
140
+ "sender",
141
+ "Test sender address",
142
+ undefined,
143
+ types.string
144
+ )
145
+ .addFlag("ffi", "Enable FFI cheatcode")
146
+ // Build/deployment options
147
+ .addFlag("force", "Force recompile and redeploy")
148
+ .addFlag("noCache", "Disable cache")
149
+ .addFlag("optimize", "Enable Solidity optimizer")
150
+ .addOptionalParam(
151
+ "optimizerRuns",
152
+ "Optimizer runs",
153
+ undefined,
154
+ types.int
155
+ )
156
+ .addFlag("viaIr", "Use Yul IR compilation")
157
+ .addFlag("skipDeployment", "Skip Diamond deployment step")
158
+ .addFlag("skipHelpers", "Skip helper generation step")
159
+ .addFlag("saveDeployment", "Write deployment data to file for reuse")
160
+ .setAction(async (taskArgs, hre: HardhatRuntimeEnvironment) => {
161
+ Logger.section("Running Forge Coverage with Diamond");
162
+
163
+ const diamondName = taskArgs.diamondName;
164
+ const networkName = hre.network.name;
165
+
166
+ Logger.info(`Diamond: ${diamondName}`);
167
+ Logger.info(`Network: ${networkName}`);
168
+
169
+ // Log key options
170
+ if (taskArgs.report) Logger.info(`Report: ${taskArgs.report}`);
171
+ if (taskArgs.reportFile) Logger.info(`Report File: ${taskArgs.reportFile}`);
172
+ if (taskArgs.matchTest) Logger.info(`Match Test: ${taskArgs.matchTest}`);
173
+ if (taskArgs.matchContract) Logger.info(`Match Contract: ${taskArgs.matchContract}`);
174
+ if (taskArgs.skipDeployment) Logger.info("Skip Deployment: true");
175
+ if (taskArgs.skipHelpers) Logger.info("Skip Helpers: true");
176
+ if (taskArgs.saveDeployment) Logger.info("Save Deployment: true");
177
+
178
+ // Lazy-load framework to avoid circular dependency during config loading
179
+ const { ForgeCoverageFramework } = await import(
180
+ "../framework/ForgeCoverageFramework.js"
181
+ );
182
+ type CoverageOptions = Parameters<
183
+ InstanceType<typeof ForgeCoverageFramework>["runCoverage"]
184
+ >[0];
185
+
186
+ // Parse comma-separated report types
187
+ const reportTypes = taskArgs.report
188
+ ? taskArgs.report.split(",").map((r: string) => r.trim())
189
+ : undefined;
190
+
191
+ // Create coverage options (matches test.ts pattern)
192
+ const options: CoverageOptions = {
193
+ diamondName,
194
+ networkName,
195
+ force: taskArgs.force,
196
+ skipDeployment: taskArgs.skipDeployment,
197
+ skipHelpers: taskArgs.skipHelpers,
198
+ writeDeployedDiamondData: taskArgs.saveDeployment,
199
+ // Report options
200
+ report: reportTypes,
201
+ reportFile: taskArgs.reportFile,
202
+ lcovVersion: taskArgs.lcovVersion,
203
+ includeLibs: taskArgs.includeLibs,
204
+ excludeTests: taskArgs.excludeTests,
205
+ irMinimum: taskArgs.irMinimum,
206
+ // Test filtering
207
+ matchTest: taskArgs.matchTest,
208
+ noMatchTest: taskArgs.noMatchTest,
209
+ matchContract: taskArgs.matchContract,
210
+ noMatchContract: taskArgs.noMatchContract,
211
+ matchPath: taskArgs.matchPath,
212
+ noMatchPath: taskArgs.noMatchPath,
213
+ noMatchCoverage: taskArgs.noMatchCoverage,
214
+ // Display options
215
+ verbosity: taskArgs.verbosity,
216
+ quiet: taskArgs.quiet,
217
+ json: taskArgs.json,
218
+ md: taskArgs.md,
219
+ color: taskArgs.color,
220
+ // Test execution
221
+ threads: taskArgs.threads,
222
+ fuzzRuns: taskArgs.fuzzRuns,
223
+ fuzzSeed: taskArgs.fuzzSeed,
224
+ failFast: taskArgs.failFast,
225
+ allowFailure: taskArgs.allowFailure,
226
+ // EVM options
227
+ forkBlockNumber: taskArgs.forkBlockNumber,
228
+ initialBalance: taskArgs.initialBalance,
229
+ sender: taskArgs.sender,
230
+ ffi: taskArgs.ffi,
231
+ // Build options
232
+ noCache: taskArgs.noCache,
233
+ optimize: taskArgs.optimize,
234
+ optimizerRuns: taskArgs.optimizerRuns,
235
+ viaIr: taskArgs.viaIr,
236
+ };
237
+
238
+ // Run coverage using the framework (same pattern as test.ts)
239
+ const framework = new ForgeCoverageFramework(hre);
240
+
241
+ try {
242
+ const success = await framework.runCoverage(options);
243
+
244
+ if (success) {
245
+ Logger.section("Coverage Analysis Complete");
246
+ Logger.success("✅ Coverage completed successfully!");
247
+ process.exitCode = 0;
248
+ } else {
249
+ Logger.section("Coverage Analysis Complete");
250
+ Logger.error("❌ Coverage analysis failed");
251
+ process.exitCode = 1;
252
+ }
253
+ } catch (error: any) {
254
+ Logger.error(`Coverage execution failed: ${error.message}`);
255
+ process.exitCode = 1;
256
+ throw error;
257
+ }
258
+ });