@diamondslab/diamonds-hardhat-foundry 2.3.0 → 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.
@@ -1,37 +1,138 @@
1
1
  import { spawn } from "child_process";
2
2
  import { HardhatRuntimeEnvironment } from "hardhat/types";
3
3
  import { CoverageOptions } from "../types/config";
4
+ import { isFoundryInstalled } from "../utils/foundry";
4
5
  import { Logger } from "../utils/logger";
6
+ import { DeploymentManager } from "./DeploymentManager";
7
+ import { HelperGenerator } from "./HelperGenerator";
5
8
 
6
9
  /**
7
- * ForgeCoverageFramework - Orchestrates forge coverage execution for Diamond contracts
10
+ * ForgeCoverageFramework - Main orchestration class for Forge coverage with Diamonds
8
11
  *
9
- * Responsibilities:
10
- * 1. Build forge coverage command with all options
11
- * 2. Execute forge coverage with proper fork URL
12
- * 3. Stream output to terminal
13
- * 4. Handle errors appropriately
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
14
18
  */
15
19
  export class ForgeCoverageFramework {
16
- constructor(private hre: HardhatRuntimeEnvironment) {}
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
+ }
17
27
 
18
28
  /**
19
- * Run forge coverage with provided options
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
20
36
  *
21
37
  * @param options - Coverage execution options
22
- * @returns Promise<boolean> - true if coverage succeeds, false otherwise
38
+ * @returns Promise<boolean> - true if coverage succeeds
23
39
  */
24
40
  async runCoverage(options: CoverageOptions = {}): Promise<boolean> {
25
- Logger.section("Forge Coverage - Execution");
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
+ }
26
59
 
27
60
  try {
28
- // Build command arguments
29
- const args = this.buildCoverageCommand(options);
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 });
30
132
 
31
133
  Logger.info(`Executing: forge coverage ${args.join(" ")}`);
32
- Logger.info("⏳ Running coverage analysis (this may take a while)...\n");
134
+ Logger.info("⏳ Running coverage analysis (this may take a while)...");
33
135
 
34
- // Execute forge coverage
35
136
  const success = await this.executeForge(args);
36
137
 
37
138
  if (success) {
@@ -42,7 +143,7 @@ export class ForgeCoverageFramework {
42
143
 
43
144
  return success;
44
145
  } catch (error: any) {
45
- Logger.error(`Coverage execution error: ${error.message}`);
146
+ Logger.error(`Coverage execution failed: ${error.message}`);
46
147
  return false;
47
148
  }
48
149
  }
@@ -80,9 +181,9 @@ export class ForgeCoverageFramework {
80
181
 
81
182
  // Multiple --report flags
82
183
  if (options.report && options.report.length > 0) {
83
- options.report.forEach((reportType) => {
184
+ for (const reportType of options.report) {
84
185
  args.push("--report", reportType);
85
- });
186
+ }
86
187
  }
87
188
 
88
189
  if (options.reportFile) {
@@ -266,12 +367,13 @@ export class ForgeCoverageFramework {
266
367
  */
267
368
  private executeForge(args: string[]): Promise<boolean> {
268
369
  return new Promise((resolve, reject) => {
269
- const forgeProcess = spawn("forge", ["coverage", ...args], {
370
+ const forge = spawn("forge", ["coverage", ...args], {
270
371
  cwd: this.hre.config.paths.root,
271
- stdio: "inherit", // Stream output directly to terminal
372
+ stdio: "inherit",
373
+ shell: true,
272
374
  });
273
375
 
274
- forgeProcess.on("close", (code) => {
376
+ forge.on("close", (code) => {
275
377
  if (code === 0) {
276
378
  resolve(true);
277
379
  } else {
@@ -279,8 +381,9 @@ export class ForgeCoverageFramework {
279
381
  }
280
382
  });
281
383
 
282
- forgeProcess.on("error", (error) => {
283
- reject(new Error(`Failed to execute forge coverage: ${error.message}`));
384
+ forge.on("error", (error) => {
385
+ Logger.error(`Failed to execute forge: ${error.message}`);
386
+ reject(error);
284
387
  });
285
388
  });
286
389
  }
@@ -11,18 +11,20 @@ import { Logger } from "../utils/logger";
11
11
  * - Runs forge coverage with specified options
12
12
  *
13
13
  * Use Hardhat's built-in --network flag to specify the network
14
+ *
15
+ * Design: Mirrors diamonds-forge:test task structure for consistency
14
16
  */
15
17
  task("diamonds-forge:coverage", "Run forge coverage for Diamond contracts")
16
- .addParam(
18
+ .addOptionalParam(
17
19
  "diamondName",
18
20
  "Name of the Diamond to analyze",
19
- undefined,
21
+ "ExampleDiamond",
20
22
  types.string
21
23
  )
22
24
  // Report options
23
25
  .addOptionalParam(
24
26
  "report",
25
- "Report type (summary, lcov, debug, bytecode) - can be used multiple times by passing comma-separated values",
27
+ "Report type (summary, lcov, debug, bytecode) - comma-separated for multiple",
26
28
  undefined,
27
29
  types.string
28
30
  )
@@ -34,7 +36,7 @@ task("diamonds-forge:coverage", "Run forge coverage for Diamond contracts")
34
36
  )
35
37
  .addOptionalParam(
36
38
  "lcovVersion",
37
- "LCOV format version",
39
+ "LCOV format version (v1 or v2)",
38
40
  undefined,
39
41
  types.string
40
42
  )
@@ -44,50 +46,50 @@ task("diamonds-forge:coverage", "Run forge coverage for Diamond contracts")
44
46
  // Test filtering
45
47
  .addOptionalParam(
46
48
  "matchTest",
47
- "Run tests matching pattern (--match-test)",
49
+ "Run tests matching pattern",
48
50
  undefined,
49
51
  types.string
50
52
  )
51
53
  .addOptionalParam(
52
54
  "noMatchTest",
53
- "Exclude tests matching pattern (--no-match-test)",
55
+ "Exclude tests matching pattern",
54
56
  undefined,
55
57
  types.string
56
58
  )
57
59
  .addOptionalParam(
58
60
  "matchContract",
59
- "Run contracts matching pattern (--match-contract)",
61
+ "Run contracts matching pattern",
60
62
  undefined,
61
63
  types.string
62
64
  )
63
65
  .addOptionalParam(
64
66
  "noMatchContract",
65
- "Exclude contracts matching pattern (--no-match-contract)",
67
+ "Exclude contracts matching pattern",
66
68
  undefined,
67
69
  types.string
68
70
  )
69
71
  .addOptionalParam(
70
72
  "matchPath",
71
- "Run files matching glob (--match-path)",
73
+ "Run files matching glob",
72
74
  undefined,
73
75
  types.string
74
76
  )
75
77
  .addOptionalParam(
76
78
  "noMatchPath",
77
- "Exclude files matching glob (--no-match-path)",
79
+ "Exclude files matching glob",
78
80
  undefined,
79
81
  types.string
80
82
  )
81
83
  .addOptionalParam(
82
84
  "noMatchCoverage",
83
- "Exclude files from coverage report (--no-match-coverage)",
85
+ "Exclude files from coverage report",
84
86
  undefined,
85
87
  types.string
86
88
  )
87
89
  // Display options
88
90
  .addOptionalParam(
89
91
  "verbosity",
90
- "Verbosity level (1-5, more v's = more verbose)",
92
+ "Verbosity level (1-5)",
91
93
  undefined,
92
94
  types.int
93
95
  )
@@ -141,7 +143,7 @@ task("diamonds-forge:coverage", "Run forge coverage for Diamond contracts")
141
143
  types.string
142
144
  )
143
145
  .addFlag("ffi", "Enable FFI cheatcode")
144
- // Build options
146
+ // Build/deployment options
145
147
  .addFlag("force", "Force recompile and redeploy")
146
148
  .addFlag("noCache", "Disable cache")
147
149
  .addFlag("optimize", "Enable Solidity optimizer")
@@ -152,151 +154,100 @@ task("diamonds-forge:coverage", "Run forge coverage for Diamond contracts")
152
154
  types.int
153
155
  )
154
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")
155
160
  .setAction(async (taskArgs, hre: HardhatRuntimeEnvironment) => {
156
161
  Logger.section("Running Forge Coverage with Diamond");
157
162
 
158
163
  const diamondName = taskArgs.diamondName;
159
164
  const networkName = hre.network.name;
160
165
 
161
- // Validate required parameters
162
- if (!diamondName) {
163
- Logger.error("--diamond-name is required");
164
- process.exitCode = 1;
165
- return;
166
- }
167
-
168
166
  Logger.info(`Diamond: ${diamondName}`);
169
167
  Logger.info(`Network: ${networkName}`);
170
168
 
171
- try {
172
- // Lazy-load framework classes to avoid circular dependency
173
- const { DeploymentManager } = await import("../framework/DeploymentManager.js");
174
- const { HelperGenerator } = await import("../framework/HelperGenerator.js");
175
-
176
- // Step 1: Ensure Diamond deployment
177
- Logger.section("Step 1/3: Ensuring Diamond Deployment");
178
-
179
- const deploymentManager = new DeploymentManager(hre);
180
-
181
- await deploymentManager.ensureDeployment(
182
- diamondName,
183
- networkName,
184
- taskArgs.force || false,
185
- false // Don't write deployment data for coverage (ephemeral by default)
186
- );
187
-
188
- // Step 2: Generate helpers
189
- Logger.section("Step 2/3: Generating Solidity Helpers");
190
-
191
- const deployment = await deploymentManager.getDeployment(
192
- diamondName,
193
- networkName
194
- );
195
-
196
- if (!deployment) {
197
- Logger.error("No deployment found. Cannot generate helpers.");
198
- process.exitCode = 1;
199
- return;
200
- }
201
-
202
- const provider = hre.ethers.provider;
203
- const network = await provider.getNetwork();
204
- const chainId = Number(network.chainId);
205
- const deploymentData = deployment.getDeployedDiamondData();
206
-
207
- const helperGenerator = new HelperGenerator(hre);
208
- await helperGenerator.generateDeploymentHelpers(
209
- diamondName,
210
- networkName,
211
- chainId,
212
- deploymentData,
213
- deployment
214
- );
215
-
216
- // Step 3: Run coverage
217
- Logger.section("Step 3/3: Running Forge Coverage");
218
-
219
- // Construct fork URL for network
220
- // Coverage requires forking from a running network to access deployed contracts
221
- let forkUrl: string;
222
- if (networkName !== "hardhat") {
223
- // Use the configured network's URL
224
- forkUrl = (provider as any)._hardhatProvider?._wrapped?.url || "http://127.0.0.1:8545";
225
- Logger.info(`Forking from ${networkName}: ${forkUrl}`);
226
- } else {
227
- // Default to localhost for hardhat network
228
- // This assumes user has `npx hardhat node` running
229
- forkUrl = "http://127.0.0.1:8545";
230
- Logger.warn(`⚠️ Network is "${networkName}" - defaulting to localhost fork: ${forkUrl}`);
231
- Logger.warn(`💡 Make sure Hardhat node is running: npx hardhat node`);
232
- Logger.warn(`💡 Or specify network explicitly: --network localhost`);
233
- }
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");
234
177
 
235
- // Lazy-load framework to avoid circular dependency
236
- const { ForgeCoverageFramework } = await import("../framework/ForgeCoverageFramework.js");
237
- type CoverageOptions = Parameters<InstanceType<typeof ForgeCoverageFramework>["runCoverage"]>[0];
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];
238
185
 
239
- // Build coverage options from task args
240
- const options: CoverageOptions = {
241
- // Fork URL
242
- forkUrl,
186
+ // Parse comma-separated report types
187
+ const reportTypes = taskArgs.report
188
+ ? taskArgs.report.split(",").map((r: string) => r.trim())
189
+ : undefined;
243
190
 
244
- // Report options
245
- report: taskArgs.report ? taskArgs.report.split(",") : undefined,
246
- reportFile: taskArgs.reportFile,
247
- lcovVersion: taskArgs.lcovVersion,
248
- includeLibs: taskArgs.includeLibs,
249
- excludeTests: taskArgs.excludeTests,
250
- irMinimum: taskArgs.irMinimum,
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
+ };
251
237
 
252
- // Test filtering
253
- matchTest: taskArgs.matchTest,
254
- noMatchTest: taskArgs.noMatchTest,
255
- matchContract: taskArgs.matchContract,
256
- noMatchContract: taskArgs.noMatchContract,
257
- matchPath: taskArgs.matchPath,
258
- noMatchPath: taskArgs.noMatchPath,
259
- noMatchCoverage: taskArgs.noMatchCoverage,
238
+ // Run coverage using the framework (same pattern as test.ts)
239
+ const framework = new ForgeCoverageFramework(hre);
260
240
 
261
- // Display options
262
- verbosity: taskArgs.verbosity,
263
- quiet: taskArgs.quiet,
264
- json: taskArgs.json,
265
- md: taskArgs.md,
266
- color: taskArgs.color as "auto" | "always" | "never" | undefined,
267
-
268
- // Test execution options
269
- threads: taskArgs.threads,
270
- fuzzRuns: taskArgs.fuzzRuns,
271
- fuzzSeed: taskArgs.fuzzSeed,
272
- failFast: taskArgs.failFast,
273
- allowFailure: taskArgs.allowFailure,
274
-
275
- // EVM options
276
- forkBlockNumber: taskArgs.forkBlockNumber,
277
- initialBalance: taskArgs.initialBalance,
278
- sender: taskArgs.sender,
279
- ffi: taskArgs.ffi,
280
-
281
- // Build options
282
- force: taskArgs.force,
283
- noCache: taskArgs.noCache,
284
- optimize: taskArgs.optimize,
285
- optimizerRuns: taskArgs.optimizerRuns,
286
- viaIr: taskArgs.viaIr,
287
- };
288
-
289
- // Run coverage
290
- const framework = new ForgeCoverageFramework(hre);
241
+ try {
291
242
  const success = await framework.runCoverage(options);
292
243
 
293
244
  if (success) {
294
245
  Logger.section("Coverage Analysis Complete");
295
- Logger.success("Coverage analysis completed successfully!");
246
+ Logger.success("Coverage completed successfully!");
296
247
  process.exitCode = 0;
297
248
  } else {
298
249
  Logger.section("Coverage Analysis Complete");
299
- Logger.error("Coverage analysis failed");
250
+ Logger.error("Coverage analysis failed");
300
251
  process.exitCode = 1;
301
252
  }
302
253
  } catch (error: any) {
@@ -66,8 +66,21 @@ export type ColorMode = "auto" | "always" | "never";
66
66
  /**
67
67
  * Coverage options for forge coverage command
68
68
  * Maps to all available forge coverage command-line options
69
+ * Plus orchestration options for Diamond deployment and helpers
69
70
  */
70
71
  export interface CoverageOptions {
72
+ // Orchestration options (matching ForgeFuzzingFramework pattern)
73
+ /** Name of the Diamond to analyze */
74
+ diamondName?: string;
75
+ /** Network name for deployment (hardhat, localhost, etc.) */
76
+ networkName?: string;
77
+ /** Skip Diamond deployment step */
78
+ skipDeployment?: boolean;
79
+ /** Skip helper generation step */
80
+ skipHelpers?: boolean;
81
+ /** Write deployment data to file for reuse */
82
+ writeDeployedDiamondData?: boolean;
83
+
71
84
  // Report options
72
85
  /** Report type(s) - can specify multiple */
73
86
  report?: CoverageReportType[];