hardhat 3.1.2 → 3.1.4
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 +18 -0
- package/dist/src/internal/builtin-plugins/coverage/coverage-manager.d.ts +14 -14
- package/dist/src/internal/builtin-plugins/coverage/coverage-manager.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/coverage/coverage-manager.js +66 -110
- package/dist/src/internal/builtin-plugins/coverage/coverage-manager.js.map +1 -1
- package/dist/src/internal/builtin-plugins/coverage/hook-handlers/solidity.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/coverage/hook-handlers/solidity.js +13 -6
- package/dist/src/internal/builtin-plugins/coverage/hook-handlers/solidity.js.map +1 -1
- package/dist/src/internal/builtin-plugins/coverage/process-coverage.d.ts +21 -0
- package/dist/src/internal/builtin-plugins/coverage/process-coverage.d.ts.map +1 -0
- package/dist/src/internal/builtin-plugins/coverage/process-coverage.js +291 -0
- package/dist/src/internal/builtin-plugins/coverage/process-coverage.js.map +1 -0
- package/dist/src/internal/builtin-plugins/coverage/reports/html.d.ts +3 -0
- package/dist/src/internal/builtin-plugins/coverage/reports/html.d.ts.map +1 -0
- package/dist/src/internal/builtin-plugins/coverage/reports/html.js +37 -0
- package/dist/src/internal/builtin-plugins/coverage/reports/html.js.map +1 -0
- package/dist/src/internal/builtin-plugins/coverage/types.d.ts +9 -3
- package/dist/src/internal/builtin-plugins/coverage/types.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/network-manager/config-resolution.js +1 -1
- package/dist/src/internal/builtin-plugins/solidity/build-system/compiler/downloader.d.ts +1 -1
- package/dist/src/internal/builtin-plugins/solidity/build-system/compiler/downloader.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/solidity/build-system/compiler/downloader.js +26 -25
- package/dist/src/internal/builtin-plugins/solidity/build-system/compiler/downloader.js.map +1 -1
- package/dist/src/internal/builtin-plugins/solidity/build-system/solc-info.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/solidity/build-system/solc-info.js +3 -0
- package/dist/src/internal/builtin-plugins/solidity/build-system/solc-info.js.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 +53 -53
- package/dist/src/internal/builtin-plugins/test/task-action.js.map +1 -1
- package/package.json +3 -2
- package/src/internal/builtin-plugins/coverage/coverage-manager.ts +121 -176
- package/src/internal/builtin-plugins/coverage/hook-handlers/solidity.ts +19 -8
- package/src/internal/builtin-plugins/coverage/process-coverage.ts +442 -0
- package/src/internal/builtin-plugins/coverage/reports/html.ts +56 -0
- package/src/internal/builtin-plugins/coverage/types.ts +8 -3
- package/src/internal/builtin-plugins/network-manager/config-resolution.ts +1 -1
- package/src/internal/builtin-plugins/solidity/build-system/compiler/downloader.ts +40 -26
- package/src/internal/builtin-plugins/solidity/build-system/solc-info.ts +3 -0
- package/src/internal/builtin-plugins/test/task-action.ts +63 -66
- package/templates/hardhat-3/01-node-test-runner-viem/package.json +1 -1
- package/templates/hardhat-3/02-mocha-ethers/package.json +4 -4
- package/templates/hardhat-3/03-minimal/package.json +1 -1
|
@@ -3,14 +3,19 @@ import type { CoverageMetadata } from "../types.js";
|
|
|
3
3
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
addStatementCoverageInstrumentation,
|
|
8
|
+
latestSupportedSolidityVersion,
|
|
9
|
+
} from "@nomicfoundation/edr";
|
|
7
10
|
import {
|
|
8
11
|
assertHardhatInvariant,
|
|
9
12
|
HardhatError,
|
|
10
13
|
} from "@nomicfoundation/hardhat-errors";
|
|
11
14
|
import { ensureError } from "@nomicfoundation/hardhat-utils/error";
|
|
12
15
|
import { readUtf8File } from "@nomicfoundation/hardhat-utils/fs";
|
|
16
|
+
import { findClosestPackageRoot } from "@nomicfoundation/hardhat-utils/package";
|
|
13
17
|
import debug from "debug";
|
|
18
|
+
import { satisfies } from "semver";
|
|
14
19
|
|
|
15
20
|
import { CoverageManagerImplementation } from "../coverage-manager.js";
|
|
16
21
|
|
|
@@ -35,6 +40,13 @@ export default async (): Promise<Partial<SolidityHooks>> => ({
|
|
|
35
40
|
|
|
36
41
|
if (context.globalOptions.coverage && !isTestSource) {
|
|
37
42
|
try {
|
|
43
|
+
const latestSupportedVersion = latestSupportedSolidityVersion();
|
|
44
|
+
if (!satisfies(solcVersion, `<=${latestSupportedVersion}`)) {
|
|
45
|
+
console.log(
|
|
46
|
+
`Solidity version ${solcVersion} is not yet supported for coverage instrumentation. Hardhat will try the latest supported version ${latestSupportedVersion} instead.`,
|
|
47
|
+
);
|
|
48
|
+
solcVersion = latestSupportedVersion;
|
|
49
|
+
}
|
|
38
50
|
const { source, metadata } = addStatementCoverageInstrumentation(
|
|
39
51
|
fileContent,
|
|
40
52
|
sourceName,
|
|
@@ -63,13 +75,11 @@ export default async (): Promise<Partial<SolidityHooks>> => ({
|
|
|
63
75
|
fsPath,
|
|
64
76
|
);
|
|
65
77
|
const tag = Buffer.from(m.tag).toString("hex");
|
|
66
|
-
const startLine = lineNumbers[m.startUtf16];
|
|
67
|
-
const endLine = lineNumbers[m.endUtf16 - 1];
|
|
68
78
|
coverageMetadata.push({
|
|
69
79
|
relativePath,
|
|
70
80
|
tag,
|
|
71
|
-
|
|
72
|
-
|
|
81
|
+
startUtf16: m.startUtf16,
|
|
82
|
+
endUtf16: m.endUtf16,
|
|
73
83
|
});
|
|
74
84
|
break;
|
|
75
85
|
default:
|
|
@@ -124,9 +134,10 @@ export default async (): Promise<Partial<SolidityHooks>> => ({
|
|
|
124
134
|
// NOTE: We add the coverage.sol straight into sources here. The alternative
|
|
125
135
|
// would be to do it during the resolution phase. However, we decided this
|
|
126
136
|
// is a simpler solution, at least for now.
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
137
|
+
const packageRoot = await findClosestPackageRoot(import.meta.url);
|
|
138
|
+
const coverageSolPath = path.join(packageRoot, "coverage.sol");
|
|
139
|
+
|
|
140
|
+
const content = await readUtf8File(coverageSolPath);
|
|
130
141
|
solcInput.sources[COVERAGE_LIBRARY_PATH] = { content };
|
|
131
142
|
}
|
|
132
143
|
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CoverageMetadata,
|
|
3
|
+
ReportCoverageStatement,
|
|
4
|
+
Statement,
|
|
5
|
+
} from "./types.js";
|
|
6
|
+
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
|
|
9
|
+
// Use constants for the Uint8Array to improve memory usage (1 byte vs 8 bytes per item)
|
|
10
|
+
const STATUS_NOT_EXECUTED = 0; // equivalent to false
|
|
11
|
+
const STATUS_EXECUTED = 1; // equivalent to true
|
|
12
|
+
const STATUS_IGNORED = 2; // equivalent to null
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Processes the raw EDR coverage information for a file and returns the executed and
|
|
16
|
+
* non-executed statements and lines.
|
|
17
|
+
*
|
|
18
|
+
* @param fileContent The original file content being analyzed
|
|
19
|
+
* @param metadata Coverage metadata received from EDR for this file
|
|
20
|
+
* @param hitTags The coverage tags recorded as executed during the test run
|
|
21
|
+
* for this specific file.
|
|
22
|
+
*
|
|
23
|
+
* @returns An object containing:
|
|
24
|
+
* - statements: the executed and not-executed statements
|
|
25
|
+
* - lines: the executed and not-executed line numbers
|
|
26
|
+
*/
|
|
27
|
+
export function getProcessedCoverageInfo(
|
|
28
|
+
fileContent: string,
|
|
29
|
+
metadata: CoverageMetadata,
|
|
30
|
+
hitTags: Set<string>,
|
|
31
|
+
): {
|
|
32
|
+
lines: {
|
|
33
|
+
executed: Map<number, string>;
|
|
34
|
+
unexecuted: Map<number, string>;
|
|
35
|
+
};
|
|
36
|
+
} {
|
|
37
|
+
const statementsByExecution = partitionStatementsByExecution(
|
|
38
|
+
metadata,
|
|
39
|
+
hitTags,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const { start, end } = getCoverageBounds(
|
|
43
|
+
statementsByExecution,
|
|
44
|
+
fileContent.length,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const characterCoverage = buildCharacterCoverage(
|
|
48
|
+
fileContent,
|
|
49
|
+
start,
|
|
50
|
+
end,
|
|
51
|
+
statementsByExecution.unexecuted,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// printStatementsForDebugging(fileContent, statementsByExecution);
|
|
55
|
+
// printCharacterCoverageForDebugging(fileContent, characterCoverage);
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
lines: partitionLinesByExecution(fileContent, characterCoverage),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function partitionStatementsByExecution(
|
|
63
|
+
metadata: CoverageMetadata,
|
|
64
|
+
hitTags: Set<string>,
|
|
65
|
+
): {
|
|
66
|
+
executed: Statement[];
|
|
67
|
+
unexecuted: Statement[];
|
|
68
|
+
} {
|
|
69
|
+
const executed: Statement[] = [];
|
|
70
|
+
const unexecuted: Statement[] = [];
|
|
71
|
+
|
|
72
|
+
for (const node of metadata) {
|
|
73
|
+
if (hitTags.has(node.tag)) {
|
|
74
|
+
executed.push(node);
|
|
75
|
+
} else {
|
|
76
|
+
unexecuted.push(node);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
executed,
|
|
82
|
+
unexecuted,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Determine the minimum and maximum character indexes that define the range
|
|
87
|
+
// where coverage should be calculated, excluding parts that the tests never
|
|
88
|
+
// reach. This removes irrelevant sections from coverage analysis, such as the
|
|
89
|
+
// beginning or end of each Solidity file.
|
|
90
|
+
// Example:
|
|
91
|
+
// // SPDX-License-Identifier: MIT
|
|
92
|
+
// pragma solidity ^0.8.0;
|
|
93
|
+
function getCoverageBounds(
|
|
94
|
+
statementsByExecution: {
|
|
95
|
+
executed: Statement[];
|
|
96
|
+
unexecuted: Statement[];
|
|
97
|
+
},
|
|
98
|
+
fileLength: number,
|
|
99
|
+
): { start: number; end: number } {
|
|
100
|
+
let start = fileLength;
|
|
101
|
+
let end = 0;
|
|
102
|
+
|
|
103
|
+
const { executed, unexecuted } = statementsByExecution;
|
|
104
|
+
|
|
105
|
+
for (const s of [...executed, ...unexecuted]) {
|
|
106
|
+
if (s.startUtf16 < start) {
|
|
107
|
+
start = s.startUtf16;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (s.endUtf16 > end) {
|
|
111
|
+
end = s.endUtf16;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { start, end };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Return an array with the same length as the file content. Each position in
|
|
119
|
+
// the array corresponds to a character in the file. The value at each position
|
|
120
|
+
// indicates whether that character was executed during tests (STATUS_EXECUTED),
|
|
121
|
+
// not executed (STATUS_NOT_EXECUTED), or not relevant for coverage
|
|
122
|
+
// (STATUS_IGNORED). STATUS_IGNORED is used to indicate characters that are not
|
|
123
|
+
// executed during test, such as those found at the start or end of every Solidity file.
|
|
124
|
+
// Example:
|
|
125
|
+
// // SPDX-License-Identifier: MIT
|
|
126
|
+
// pragma solidity ^0.8.0;
|
|
127
|
+
function buildCharacterCoverage(
|
|
128
|
+
fileContent: string,
|
|
129
|
+
start: number,
|
|
130
|
+
end: number,
|
|
131
|
+
unexecutedStatements: Statement[],
|
|
132
|
+
): Uint8Array {
|
|
133
|
+
// Use Uint8Array instead of Array<null | boolean> for memory efficiency.
|
|
134
|
+
// We initialize with STATUS_IGNORED (equivalent to filling with null)
|
|
135
|
+
const characterCoverage = new Uint8Array(fileContent.length).fill(
|
|
136
|
+
STATUS_IGNORED,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Initially mark all characters that may be executed during the tests as
|
|
140
|
+
// STATUS_EXECUTED. They will be set to false later if they are not executed.
|
|
141
|
+
// The coverage statement received from EDR will provide this information.
|
|
142
|
+
// Setting everything to true first simplifies the logic for extracting
|
|
143
|
+
// coverage data. Starting with false and toggling only covered characters
|
|
144
|
+
// would make statement processing more complex.
|
|
145
|
+
for (let i = start; i < end; i++) {
|
|
146
|
+
if (charMustBeIgnored(fileContent[i])) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
characterCoverage[i] = STATUS_EXECUTED;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const statement of unexecutedStatements) {
|
|
154
|
+
for (let i = statement.startUtf16; i < statement.endUtf16; i++) {
|
|
155
|
+
if (characterCoverage[i] === STATUS_IGNORED) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
characterCoverage[i] = STATUS_NOT_EXECUTED;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
markNonExecutablePatterns(fileContent, characterCoverage);
|
|
164
|
+
|
|
165
|
+
return characterCoverage;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// The following characters are not relevant for the code coverage, so they
|
|
169
|
+
// must be ignored when marking characters as executed (true) or not executed (false)
|
|
170
|
+
function charMustBeIgnored(char: string): boolean {
|
|
171
|
+
const code = char.charCodeAt(0);
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
code === 32 || // Space
|
|
175
|
+
(code >= 9 && code <= 13) || // \t (9), \n (10), \v (11), \f (12), \r (13)
|
|
176
|
+
code === 123 || // {
|
|
177
|
+
code === 125 // }
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Mark different types of substrings as not relevant for code coverage (set them to STATUS_IGNORED).
|
|
182
|
+
// For example, all "else" substrings or comments are not relevant for code coverage.
|
|
183
|
+
function markNonExecutablePatterns(
|
|
184
|
+
fileContent: string,
|
|
185
|
+
characterCoverage: Uint8Array,
|
|
186
|
+
) {
|
|
187
|
+
// Comments that start with //
|
|
188
|
+
markMatchingCharsWithIgnored(
|
|
189
|
+
fileContent,
|
|
190
|
+
characterCoverage,
|
|
191
|
+
/\/\/.*?(?=\n|$)/g,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// Comments wrapped between /* and */
|
|
195
|
+
markMatchingCharsWithIgnored(
|
|
196
|
+
fileContent,
|
|
197
|
+
characterCoverage,
|
|
198
|
+
/\/\*[\s\S]*?\*\//g,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// Lines containing `else`, since they do not represent executable code by themselves.
|
|
202
|
+
// Keep in mind that `else` is preceded by a closing brace and followed by an opening brace.
|
|
203
|
+
// This can span multiple lines.
|
|
204
|
+
markMatchingCharsWithIgnored(
|
|
205
|
+
fileContent,
|
|
206
|
+
characterCoverage,
|
|
207
|
+
/\}\s*\belse\b\s*\{/g,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
// Lines containing the function signature. This can span multiple lines
|
|
211
|
+
markMatchingCharsWithIgnored(
|
|
212
|
+
fileContent,
|
|
213
|
+
characterCoverage,
|
|
214
|
+
/^\s*(function\s+[A-Za-z_$][A-Za-z0-9_$]*\s*\([\s\S]*?\)[^{]*?)(?=\s*\{)/gm,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// Lines containing the catch signature. This can span multiple lines.
|
|
218
|
+
markMatchingCharsWithIgnored(
|
|
219
|
+
fileContent,
|
|
220
|
+
characterCoverage,
|
|
221
|
+
/\bcatch\b(?:\s+[A-Za-z_][A-Za-z0-9_]*)?\s*(?:\([\s\S]*?\))?(?=\s*\{)/g,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function markMatchingCharsWithIgnored(
|
|
226
|
+
fileContent: string,
|
|
227
|
+
characterCoverage: Uint8Array,
|
|
228
|
+
regex: RegExp,
|
|
229
|
+
) {
|
|
230
|
+
for (const match of fileContent.matchAll(regex)) {
|
|
231
|
+
for (let i = match.index; i < match.index + match[0].length; i++) {
|
|
232
|
+
characterCoverage[i] = STATUS_IGNORED;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Generate non overlapping coverage statements. Every character between the start
|
|
238
|
+
// and end indexes is guaranteed to be either executed or not executed.
|
|
239
|
+
// Unlike the statements received from EDR, which may include nested sub
|
|
240
|
+
// statements, these are processed and have no sub statements or overlapping statements.
|
|
241
|
+
// Example:
|
|
242
|
+
// [
|
|
243
|
+
// { start: 12, end: 20, executed: true },
|
|
244
|
+
// { start: 15, end: 17, executed: true }
|
|
245
|
+
// ]
|
|
246
|
+
// ->
|
|
247
|
+
// [
|
|
248
|
+
// { start: 12, end: 20, executed: true }
|
|
249
|
+
// ]
|
|
250
|
+
/* eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
251
|
+
-- currently not used, but it will be used to create more detailed.
|
|
252
|
+
It will be used to return statements from the function `getProcessedCoverageInfo` */
|
|
253
|
+
function getProcessedExecutedStatements(characterCoverage: Uint8Array): {
|
|
254
|
+
executed: ReportCoverageStatement[];
|
|
255
|
+
unexecuted: ReportCoverageStatement[];
|
|
256
|
+
} {
|
|
257
|
+
const executed = generateProcessedStatements(
|
|
258
|
+
characterCoverage,
|
|
259
|
+
STATUS_EXECUTED,
|
|
260
|
+
);
|
|
261
|
+
const unexecuted = generateProcessedStatements(
|
|
262
|
+
characterCoverage,
|
|
263
|
+
STATUS_NOT_EXECUTED,
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
executed,
|
|
268
|
+
unexecuted,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Based on the marked file, where each character is marked as either
|
|
273
|
+
// executed (STATUS_EXECUTED) or not executed (STATUS_NOT_EXECUTED),
|
|
274
|
+
// generate non-overlapping statements that indicate the start and
|
|
275
|
+
// end indices, along with whether they were executed or not.
|
|
276
|
+
function generateProcessedStatements(
|
|
277
|
+
characterCoverage: Uint8Array,
|
|
278
|
+
targetStatus: number,
|
|
279
|
+
): ReportCoverageStatement[] {
|
|
280
|
+
const ranges: ReportCoverageStatement[] = [];
|
|
281
|
+
const fileLength = characterCoverage.length;
|
|
282
|
+
|
|
283
|
+
let start = -1;
|
|
284
|
+
|
|
285
|
+
for (let i = 0; i < fileLength; i++) {
|
|
286
|
+
if (characterCoverage[i] === targetStatus) {
|
|
287
|
+
if (start === -1) {
|
|
288
|
+
start = i; // begin new range
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
if (start !== -1) {
|
|
292
|
+
// Map back to boolean for the output object
|
|
293
|
+
ranges.push({
|
|
294
|
+
startUtf16: start,
|
|
295
|
+
endUtf16: i - 1,
|
|
296
|
+
executed: targetStatus === STATUS_EXECUTED,
|
|
297
|
+
});
|
|
298
|
+
start = -1;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// close last range if file ends inside a run
|
|
304
|
+
if (start !== -1) {
|
|
305
|
+
ranges.push({
|
|
306
|
+
startUtf16: start,
|
|
307
|
+
endUtf16: fileLength - 1,
|
|
308
|
+
executed: targetStatus === STATUS_EXECUTED,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return ranges;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Return the executed and non executed lines based on the marked file.
|
|
316
|
+
// Some lines are excluded from the execution count, for example, comments.
|
|
317
|
+
function partitionLinesByExecution(
|
|
318
|
+
fileContent: string,
|
|
319
|
+
characterCoverage: Uint8Array,
|
|
320
|
+
): {
|
|
321
|
+
executed: Map<number, string>;
|
|
322
|
+
unexecuted: Map<number, string>;
|
|
323
|
+
} {
|
|
324
|
+
const executed: Map<number, string> = new Map();
|
|
325
|
+
const unexecuted: Map<number, string> = new Map();
|
|
326
|
+
|
|
327
|
+
let lineStart = 0;
|
|
328
|
+
let isLineIgnored = true;
|
|
329
|
+
let isLineExecutedOrIgnored = true;
|
|
330
|
+
let lineNumber = 1; // File lines start at 1
|
|
331
|
+
|
|
332
|
+
// Helper to process a line and push to correct map
|
|
333
|
+
const processLine = (endIndex: number) => {
|
|
334
|
+
// Only process if the line isn't entirely ignored
|
|
335
|
+
if (!isLineIgnored) {
|
|
336
|
+
const lineText = fileContent.slice(lineStart, endIndex);
|
|
337
|
+
|
|
338
|
+
if (isLineExecutedOrIgnored) {
|
|
339
|
+
executed.set(lineNumber, lineText);
|
|
340
|
+
} else {
|
|
341
|
+
unexecuted.set(lineNumber, lineText);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Reset state for the next line
|
|
346
|
+
lineStart = endIndex + 1;
|
|
347
|
+
isLineIgnored = true;
|
|
348
|
+
isLineExecutedOrIgnored = true;
|
|
349
|
+
lineNumber++;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
for (let i = 0; i < fileContent.length; i++) {
|
|
353
|
+
const char = fileContent[i];
|
|
354
|
+
const status = characterCoverage[i];
|
|
355
|
+
|
|
356
|
+
if (status !== STATUS_IGNORED) {
|
|
357
|
+
isLineIgnored = false;
|
|
358
|
+
}
|
|
359
|
+
if (status === STATUS_NOT_EXECUTED) {
|
|
360
|
+
isLineExecutedOrIgnored = false;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (char === "\n") {
|
|
364
|
+
processLine(i);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Handle the final line if the file doesn't end in a newline
|
|
369
|
+
if (lineStart < fileContent.length) {
|
|
370
|
+
processLine(fileContent.length);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return { executed, unexecuted };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Enable this function while debugging to display the coverage for a file.
|
|
377
|
+
// The file will be printed with green characters when they are executed,
|
|
378
|
+
// red characters when they are not executed,
|
|
379
|
+
// and gray characters when they are irrelevant for code coverage.
|
|
380
|
+
/* eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
381
|
+
-- this function can be enabled for debugging purposes */
|
|
382
|
+
function printStatementsForDebugging(
|
|
383
|
+
fileContent: string,
|
|
384
|
+
statementsByExecution: {
|
|
385
|
+
executed: Statement[];
|
|
386
|
+
unexecuted: Statement[];
|
|
387
|
+
},
|
|
388
|
+
): void {
|
|
389
|
+
const relativePath =
|
|
390
|
+
statementsByExecution.executed.length > 0
|
|
391
|
+
? statementsByExecution.executed[0].relativePath
|
|
392
|
+
: statementsByExecution.unexecuted[0].relativePath;
|
|
393
|
+
|
|
394
|
+
console.debug("Statements for file: " + relativePath);
|
|
395
|
+
|
|
396
|
+
console.debug("Executed statements:");
|
|
397
|
+
let counter = 0;
|
|
398
|
+
for (const statement of statementsByExecution.executed) {
|
|
399
|
+
console.debug(counter++ + " ---");
|
|
400
|
+
|
|
401
|
+
for (let i = statement.startUtf16; i < statement.endUtf16; i++) {
|
|
402
|
+
process.stdout.write(chalk.gray(fileContent[i]));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
console.debug();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
console.debug();
|
|
409
|
+
|
|
410
|
+
console.debug("Unexecuted statements:");
|
|
411
|
+
counter = 0;
|
|
412
|
+
for (const statement of statementsByExecution.unexecuted) {
|
|
413
|
+
console.debug(counter++ + " ---");
|
|
414
|
+
|
|
415
|
+
for (let i = statement.startUtf16; i < statement.endUtf16; i++) {
|
|
416
|
+
process.stdout.write(chalk.gray(fileContent[i]));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
console.debug();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Enable this function while debugging to display the coverage for a file.
|
|
424
|
+
// The file will be printed with green characters when they are executed,
|
|
425
|
+
// red characters when they are not executed,
|
|
426
|
+
// and gray characters when they are irrelevant for code coverage.
|
|
427
|
+
/* eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
428
|
+
-- this function can be enabled for debugging purposes */
|
|
429
|
+
function printCharacterCoverageForDebugging(
|
|
430
|
+
fileContent: string,
|
|
431
|
+
characterCoverage: Uint8Array,
|
|
432
|
+
): void {
|
|
433
|
+
for (let i = 0; i < characterCoverage.length; i++) {
|
|
434
|
+
if (characterCoverage[i] === STATUS_IGNORED) {
|
|
435
|
+
process.stdout.write(chalk.gray(fileContent[i]));
|
|
436
|
+
} else if (characterCoverage[i] === STATUS_EXECUTED) {
|
|
437
|
+
process.stdout.write(chalk.green(fileContent[i]));
|
|
438
|
+
} else {
|
|
439
|
+
process.stdout.write(chalk.red(fileContent[i]));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Report } from "../coverage-manager.js";
|
|
2
|
+
import type { FileCoverageData } from "@nomicfoundation/hardhat-vendored/coverage/types";
|
|
3
|
+
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { mkdir } from "@nomicfoundation/hardhat-utils/fs";
|
|
7
|
+
import {
|
|
8
|
+
istanbulLibCoverage,
|
|
9
|
+
istanbulLibReport,
|
|
10
|
+
istanbulReports,
|
|
11
|
+
} from "@nomicfoundation/hardhat-vendored/coverage";
|
|
12
|
+
|
|
13
|
+
export async function generateHtmlReport(
|
|
14
|
+
report: Report,
|
|
15
|
+
htmlReportPath: string,
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
const baseDir = process.cwd();
|
|
18
|
+
const coverageMap = istanbulLibCoverage.createCoverageMap({});
|
|
19
|
+
|
|
20
|
+
await mkdir(htmlReportPath);
|
|
21
|
+
|
|
22
|
+
// Construct coverage data for each tested file,
|
|
23
|
+
// detailing whether each line was executed or not.
|
|
24
|
+
for (const [p, coverageInfo] of Object.entries(report)) {
|
|
25
|
+
const testedFilePath = path.join(baseDir, p);
|
|
26
|
+
|
|
27
|
+
const fc: FileCoverageData = {
|
|
28
|
+
path: testedFilePath,
|
|
29
|
+
statementMap: {},
|
|
30
|
+
fnMap: {}, // Cannot be derived from current report data
|
|
31
|
+
branchMap: {}, // Cannot be derived from current report data
|
|
32
|
+
s: {},
|
|
33
|
+
f: {}, // Cannot be derived from current report data
|
|
34
|
+
b: {},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
for (const [line, count] of coverageInfo.lineExecutionCounts) {
|
|
38
|
+
fc.statementMap[line] = {
|
|
39
|
+
start: { line, column: 0 },
|
|
40
|
+
end: { line, column: 0 },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// TODO: currently EDR does not provide per-statement coverage counts
|
|
44
|
+
fc.s[line] = count > 0 ? 1 : 0; // mark as covered if hit at least once
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
coverageMap.addFileCoverage(fc);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const context = istanbulLibReport.createContext({
|
|
51
|
+
dir: htmlReportPath,
|
|
52
|
+
coverageMap,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
istanbulReports.create("html", undefined).execute(context);
|
|
56
|
+
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
type Tag = string;
|
|
2
2
|
export interface Statement {
|
|
3
3
|
relativePath: string;
|
|
4
4
|
tag: Tag;
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
startUtf16: number;
|
|
6
|
+
endUtf16: number;
|
|
7
|
+
}
|
|
8
|
+
export interface ReportCoverageStatement {
|
|
9
|
+
startUtf16: number;
|
|
10
|
+
endUtf16: number;
|
|
11
|
+
executed: boolean;
|
|
7
12
|
}
|
|
8
13
|
export type CoverageMetadata = Statement[];
|
|
9
14
|
export type CoverageData = Tag[];
|
|
@@ -85,7 +85,7 @@ export function resolveEdrNetwork(
|
|
|
85
85
|
networkConfig.allowBlocksWithSameTimestamp ?? false,
|
|
86
86
|
allowUnlimitedContractSize:
|
|
87
87
|
networkConfig.allowUnlimitedContractSize ?? false,
|
|
88
|
-
blockGasLimit: BigInt(networkConfig.blockGasLimit ??
|
|
88
|
+
blockGasLimit: BigInt(networkConfig.blockGasLimit ?? 60_000_000n),
|
|
89
89
|
coinbase: resolveCoinbase(networkConfig.coinbase),
|
|
90
90
|
|
|
91
91
|
forking: resolveForkingConfig(
|
|
@@ -40,7 +40,7 @@ const LINUX_ARM64_REPOSITORY_URL =
|
|
|
40
40
|
|
|
41
41
|
export enum CompilerPlatform {
|
|
42
42
|
LINUX = "linux-amd64",
|
|
43
|
-
LINUX_ARM64 = "linux-
|
|
43
|
+
LINUX_ARM64 = "linux-arm64",
|
|
44
44
|
WINDOWS = "windows-amd64",
|
|
45
45
|
MACOS = "macosx-amd64",
|
|
46
46
|
WASM = "wasm",
|
|
@@ -48,6 +48,7 @@ export enum CompilerPlatform {
|
|
|
48
48
|
|
|
49
49
|
interface CompilerBuild {
|
|
50
50
|
path: string;
|
|
51
|
+
url?: string;
|
|
51
52
|
version: string;
|
|
52
53
|
longVersion: string;
|
|
53
54
|
sha256: string;
|
|
@@ -341,46 +342,59 @@ export class CompilerDownloaderImplementation implements CompilerDownloader {
|
|
|
341
342
|
}
|
|
342
343
|
}
|
|
343
344
|
|
|
344
|
-
|
|
345
|
+
// download the list in case the cached list contains older ARM64 Linux builds without URL
|
|
346
|
+
return list.builds
|
|
347
|
+
.map((b) => b.path.startsWith("solc-v") && b.url === undefined)
|
|
348
|
+
.reduce((a, b) => a || b, false);
|
|
345
349
|
}
|
|
346
350
|
|
|
347
351
|
async #downloadCompilerList(): Promise<void> {
|
|
348
352
|
log(`Downloading compiler list for platform ${this.#platform}`);
|
|
349
|
-
let url: string;
|
|
350
|
-
|
|
351
|
-
if (this.#onLinuxArm()) {
|
|
352
|
-
url = `${LINUX_ARM64_REPOSITORY_URL}/list.json`;
|
|
353
|
-
} else {
|
|
354
|
-
url = `${COMPILER_REPOSITORY_URL}/${this.#platform}/list.json`;
|
|
355
|
-
}
|
|
356
353
|
const downloadPath = this.#getCompilerListPath();
|
|
357
354
|
|
|
358
|
-
|
|
355
|
+
// download hte official solc compiler list (now that ARM64 Linus is supported)
|
|
356
|
+
await this.#downloadFunction(
|
|
357
|
+
`${COMPILER_REPOSITORY_URL}/${this.#platform}/list.json`,
|
|
358
|
+
downloadPath,
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
// for Linux ARM64, we need to merge the official list with our custom builds
|
|
362
|
+
if (this.#platform === CompilerPlatform.LINUX_ARM64) {
|
|
363
|
+
// cache the official list since the file will be overwritten below
|
|
364
|
+
const officialCompilerList: CompilerList =
|
|
365
|
+
await readJsonFile(downloadPath);
|
|
366
|
+
|
|
367
|
+
await this.#downloadFunction(
|
|
368
|
+
`${LINUX_ARM64_REPOSITORY_URL}/list.json`,
|
|
369
|
+
downloadPath,
|
|
370
|
+
);
|
|
359
371
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
for (const build of
|
|
372
|
+
// add missing information and an explicit URL for download
|
|
373
|
+
const armLinuxcompilerList: CompilerList =
|
|
374
|
+
await readJsonFile(downloadPath);
|
|
375
|
+
for (const build of armLinuxcompilerList.builds) {
|
|
364
376
|
build.path = `solc-v${build.version}`;
|
|
377
|
+
build.url = LINUX_ARM64_REPOSITORY_URL;
|
|
365
378
|
build.longVersion = build.version;
|
|
366
379
|
}
|
|
367
380
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
381
|
+
// merge the official and custom lists
|
|
382
|
+
officialCompilerList.builds = officialCompilerList.builds.concat(
|
|
383
|
+
armLinuxcompilerList.builds,
|
|
384
|
+
);
|
|
385
|
+
officialCompilerList.releases = {
|
|
386
|
+
...officialCompilerList.releases,
|
|
387
|
+
...armLinuxcompilerList.releases,
|
|
388
|
+
};
|
|
371
389
|
|
|
372
|
-
|
|
373
|
-
|
|
390
|
+
await writeJsonFile(downloadPath, officialCompilerList);
|
|
391
|
+
}
|
|
374
392
|
}
|
|
375
393
|
|
|
376
394
|
async #downloadCompiler(build: CompilerBuild): Promise<string> {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
url = `${LINUX_ARM64_REPOSITORY_URL}/${build.path}`;
|
|
381
|
-
} else {
|
|
382
|
-
url = `${COMPILER_REPOSITORY_URL}/${this.#platform}/${build.path}`;
|
|
383
|
-
}
|
|
395
|
+
// use the explicit URL if available or the default solc download URL if not
|
|
396
|
+
const defaultUrl = `${COMPILER_REPOSITORY_URL}/${this.#platform}`;
|
|
397
|
+
const url = `${build.url ?? defaultUrl}/${build.path}`;
|
|
384
398
|
|
|
385
399
|
log(`Downloading compiler ${build.version} from ${url}`);
|
|
386
400
|
|