Skip to content

Commit 8557f97

Browse files
authored
Merge pull request #6652 from NomicFoundation/coverage-v2
feat: collect instrumentation metadata and node:test coverage data
2 parents 7e4ce40 + 3c8a3c5 commit 8557f97

File tree

36 files changed

+1730
-106
lines changed

36 files changed

+1730
-106
lines changed

.changeset/beige-paws-explode.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@nomicfoundation/hardhat-node-test-runner": patch
3+
---
4+
5+
Implemented coverage data collection from the test node task

.changeset/early-dancers-type.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@nomicfoundation/hardhat-errors": patch
3+
---
4+
5+
Added the definitions for Hardhat coverage related errors

.changeset/fair-penguins-collect.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"hardhat": patch
3+
---
4+
5+
Implemented coverage markdown and lcov reporting

.changeset/funny-singers-collect.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"hardhat": patch
3+
---
4+
5+
Implemented source instrumentation for the coverage data collection

.changeset/sharp-flowers-invent.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@nomicfoundation/hardhat-mocha": patch
3+
---
4+
5+
Implemented coverage data collection from the test mocha task

.changeset/ten-apes-camp.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"hardhat": patch
3+
---
4+
5+
Implemented coverage plugin to enable coverage data collection from the test tasks

v-next/example-project/test/mocha/mocha-test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,16 @@ describe("Mocha test with chai-matchers", () => {
2727
expect("0x0000010AB").to.not.hexEqual("0x0010abc");
2828
});
2929
});
30+
31+
describe("Rocket test", () => {
32+
it("should launch the Apollo 11 rocket", async () => {
33+
const connection = await hre.network.connect();
34+
35+
const Rocket = await connection.ethers.getContractFactory("Rocket");
36+
const rocket = await Rocket.deploy("Apollo 11");
37+
38+
await rocket.launch();
39+
40+
expect(await rocket.status()).to.equal("lift-off");
41+
});
42+
});

v-next/hardhat-errors/src/descriptors.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@ export const ERROR_CATEGORIES: {
121121
max: 1299,
122122
websiteSubTitle: "Hardhat test plugin errors",
123123
},
124+
COVERAGE: {
125+
min: 1300,
126+
max: 1399,
127+
websiteSubTitle: "Hardhat coverage errors",
128+
},
124129
},
125130
},
126131
IGNITION: {
@@ -478,6 +483,12 @@ Please install a version of the peer dependency that meets the plugin's requirem
478483
websiteTitle: "Plugin hook factory is not a valid file URL",
479484
websiteDescription: `The loading of a plugin's hook factory failed as the import path is not a valid file:// URL.`,
480485
},
486+
UNEXPECTED_HOOK_PARAM_MODIFICATION: {
487+
number: 301,
488+
messageTemplate: `Parameter "{paramName}" of hook "{hookCategoryName}#{hookName}" is not allowed to be modified`,
489+
websiteTitle: "Unexpected hook parameter modification",
490+
websiteDescription: `The parameter is not allowed to be modified`,
491+
},
481492
},
482493
TASK_DEFINITIONS: {
483494
INVALID_FILE_ACTION: {
@@ -1227,6 +1238,20 @@ Please use the fully qualified name of the contract to disambiguate it.`,
12271238
websiteDescription: `Cannot determine a test runner for the test files. This may be because the files are not correctly included in the test paths defined by the test plugins in the Hardhat configuration. If they are correctly included, this likely indicates an issue with a plugin failing to detect the files.`,
12281239
},
12291240
},
1241+
COVERAGE: {
1242+
SOURCE_NOT_INSTRUMENTED: {
1243+
number: 1300,
1244+
messageTemplate: `The source file "{sourceName}" could not be instrumented for coverage.`,
1245+
websiteTitle: "Source file not instrumented for coverage",
1246+
websiteDescription: `The source file could not be instrumented for coverage.`,
1247+
},
1248+
IMPORT_PATH_ALREADY_DEFINED: {
1249+
number: 1301,
1250+
messageTemplate: `The import path "{importPath}" is already defined in the compilation sources`,
1251+
websiteTitle: "Import path already defined in compilation sources",
1252+
websiteDescription: `The import path is already defined in the compilation sources`,
1253+
},
1254+
},
12301255
},
12311256
IGNITION: {
12321257
GENERAL: {

v-next/hardhat-mocha/src/task-action.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import { fileURLToPath } from "node:url";
77

88
import { HardhatError } from "@nomicfoundation/hardhat-errors";
99
import { getAllFilesMatching } from "@nomicfoundation/hardhat-utils/fs";
10+
import {
11+
markTestRunDone,
12+
markTestRunStart,
13+
markTestWorkerDone,
14+
} from "hardhat/internal/coverage";
1015

1116
interface TestActionArguments {
1217
testFiles: string[];
@@ -93,10 +98,17 @@ const testWithHardhat: NewTaskActionFunction<TestActionArguments> = async (
9398
// which supports both ESM and CJS
9499
await mocha.loadFilesAsync();
95100

101+
await markTestRunStart("mocha");
102+
96103
const testFailures = await new Promise<number>((resolve) => {
97104
mocha.run(resolve);
98105
});
99106

107+
// NOTE: We execute mocha tests in the main process.
108+
await markTestWorkerDone("mocha");
109+
// NOTE: This might print a coverage report.
110+
await markTestRunDone("mocha");
111+
100112
if (testFailures > 0) {
101113
process.exitCode = 1;
102114
}

v-next/hardhat-node-test-runner/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"license": "MIT",
1313
"type": "module",
1414
"exports": {
15-
".": "./dist/src/index.js"
15+
".": "./dist/src/index.js",
16+
"./coverage": "./dist/src/coverage.js"
1617
},
1718
"keywords": [
1819
"ethereum",
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { after } from "node:test";
2+
3+
import { markTestWorkerDone } from "hardhat/internal/coverage";
4+
5+
after(async () => {
6+
await markTestWorkerDone("node");
7+
});

v-next/hardhat-node-test-runner/src/task-action.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { URL } from "node:url";
99
import { hardhatTestReporter } from "@nomicfoundation/hardhat-node-test-reporter";
1010
import { getAllFilesMatching } from "@nomicfoundation/hardhat-utils/fs";
1111
import { createNonClosingWriter } from "@nomicfoundation/hardhat-utils/stream";
12+
import { markTestRunStart, markTestRunDone } from "hardhat/internal/coverage";
1213

1314
interface TestActionArguments {
1415
testFiles: string[];
@@ -66,8 +67,25 @@ const testWithHardhat: NewTaskActionFunction<TestActionArguments> = async (
6667
return 0;
6768
}
6869

70+
const imports = [];
71+
6972
const tsx = new URL(import.meta.resolve("tsx/esm"));
70-
process.env.NODE_OPTIONS = `--import "${tsx.href}"`;
73+
imports.push(tsx.href);
74+
75+
if (hre.globalOptions.coverage === true) {
76+
// NOTE: We set the HARDHAT_COVERAGE environment variable here because, as of now,
77+
// the global options are not automatically passed to the child processes.
78+
process.env.HARDHAT_COVERAGE = "true";
79+
80+
const coverage = new URL(
81+
import.meta.resolve("@nomicfoundation/hardhat-node-test-runner/coverage"),
82+
);
83+
imports.push(coverage.href);
84+
}
85+
86+
process.env.NODE_OPTIONS = imports
87+
.map((href) => `--import "${href}"`)
88+
.join(" ");
7189

7290
async function runTests(): Promise<number> {
7391
let failures = 0;
@@ -106,8 +124,13 @@ const testWithHardhat: NewTaskActionFunction<TestActionArguments> = async (
106124
return failures;
107125
}
108126

127+
await markTestRunStart("node");
128+
109129
const testFailures = await runTests();
110130

131+
// NOTE: This might print a coverage report.
132+
await markTestRunDone("node");
133+
111134
if (testFailures > 0) {
112135
process.exitCode = 1;
113136
}

v-next/hardhat/coverage.sol

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity >=0.4.22 <0.9.0;
3+
4+
library __HardhatCoverage {
5+
address constant COVERAGE_ADDRESS =
6+
0xc0bEc0BEc0BeC0bEC0beC0bEC0bEC0beC0beC0BE;
7+
8+
function _sendHitImplementation(uint256 coverageId) private view {
9+
address coverageAddress = COVERAGE_ADDRESS;
10+
/// @solidity memory-safe-assembly
11+
assembly {
12+
let ptr := mload(0x40) // Get free memory pointer
13+
mstore(ptr, coverageId) // Store coverageId at free memory
14+
pop(
15+
staticcall(
16+
gas(),
17+
coverageAddress,
18+
ptr,
19+
32, // Size of uint256 is 32 bytes
20+
0,
21+
0
22+
)
23+
)
24+
}
25+
}
26+
27+
function _castToPure(
28+
function(uint256) internal view fnIn
29+
) private pure returns (function(uint256) pure fnOut) {
30+
assembly {
31+
fnOut := fnIn
32+
}
33+
}
34+
35+
function sendHit(uint256 coverageId) internal pure {
36+
_castToPure(_sendHitImplementation)(coverageId);
37+
}
38+
}

v-next/hardhat/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
"./types/user-interruptions": "./dist/src/types/user-interruptions.js",
3535
"./types/utils": "./dist/src/types/utils.js",
3636
"./types/solidity": "./dist/src/types/solidity.js",
37-
"./console.sol": "./console.sol"
37+
"./console.sol": "./console.sol",
38+
"./internal/coverage": "./dist/src/internal/builtin-plugins/coverage/exports.js"
3839
},
3940
"keywords": [
4041
"ethereum",

0 commit comments

Comments
 (0)