From fa8020c182ee7161f77d9f700233d9c346870011 Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Tue, 24 Feb 2026 17:21:44 +0000 Subject: [PATCH 1/4] feat: add test hooks and replace coverage/gas analytics lifecycle helpers --- v-next/hardhat-mocha/package.json | 3 +- v-next/hardhat-mocha/src/coverage.ts | 7 --- v-next/hardhat-mocha/src/gas-stats.ts | 7 --- v-next/hardhat-mocha/src/task-action.ts | 43 +++++------------- v-next/hardhat-mocha/src/test-worker-done.ts | 9 ++++ v-next/hardhat-node-test-runner/package.json | 3 +- .../hardhat-node-test-runner/src/coverage.ts | 7 --- .../hardhat-node-test-runner/src/gas-stats.ts | 7 --- .../src/task-action.ts | 33 ++++---------- .../src/test-worker-done.ts | 7 +++ v-next/hardhat/package.json | 2 - .../builtin-plugins/coverage/exports.ts | 5 --- .../builtin-plugins/coverage/helpers.ts | 45 ------------------- .../coverage/hook-handlers/test.ts | 37 +++++++++++++++ .../builtin-plugins/coverage/index.ts | 1 + .../builtin-plugins/gas-analytics/exports.ts | 5 --- .../builtin-plugins/gas-analytics/helpers.ts | 44 ------------------ .../gas-analytics/hook-handlers/test.ts | 37 +++++++++++++++ .../builtin-plugins/gas-analytics/index.ts | 1 + .../solidity-test/task-action.ts | 22 +++------ .../builtin-plugins/test/type-extensions.ts | 24 ++++++++++ 21 files changed, 144 insertions(+), 205 deletions(-) delete mode 100644 v-next/hardhat-mocha/src/coverage.ts delete mode 100644 v-next/hardhat-mocha/src/gas-stats.ts create mode 100644 v-next/hardhat-mocha/src/test-worker-done.ts delete mode 100644 v-next/hardhat-node-test-runner/src/coverage.ts delete mode 100644 v-next/hardhat-node-test-runner/src/gas-stats.ts create mode 100644 v-next/hardhat-node-test-runner/src/test-worker-done.ts delete mode 100644 v-next/hardhat/src/internal/builtin-plugins/coverage/exports.ts create mode 100644 v-next/hardhat/src/internal/builtin-plugins/coverage/hook-handlers/test.ts delete mode 100644 v-next/hardhat/src/internal/builtin-plugins/gas-analytics/exports.ts delete mode 100644 v-next/hardhat/src/internal/builtin-plugins/gas-analytics/helpers.ts create mode 100644 v-next/hardhat/src/internal/builtin-plugins/gas-analytics/hook-handlers/test.ts diff --git a/v-next/hardhat-mocha/package.json b/v-next/hardhat-mocha/package.json index d509c94061a..d1424a34cae 100644 --- a/v-next/hardhat-mocha/package.json +++ b/v-next/hardhat-mocha/package.json @@ -13,8 +13,7 @@ "type": "module", "exports": { ".": "./dist/src/index.js", - "./coverage": "./dist/src/coverage.js", - "./gas-stats": "./dist/src/gas-stats.js" + "./test-worker-done": "./dist/src/test-worker-done.js" }, "keywords": [ "ethereum", diff --git a/v-next/hardhat-mocha/src/coverage.ts b/v-next/hardhat-mocha/src/coverage.ts deleted file mode 100644 index 0cd0b09250c..00000000000 --- a/v-next/hardhat-mocha/src/coverage.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { markTestWorkerDone } from "hardhat/internal/coverage"; - -export const mochaHooks = { - async afterAll(): Promise { - await markTestWorkerDone("mocha"); - }, -}; diff --git a/v-next/hardhat-mocha/src/gas-stats.ts b/v-next/hardhat-mocha/src/gas-stats.ts deleted file mode 100644 index ad0b0a497fc..00000000000 --- a/v-next/hardhat-mocha/src/gas-stats.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { markTestWorkerDone } from "hardhat/internal/gas-analytics"; - -export const mochaHooks = { - async afterAll(): Promise { - await markTestWorkerDone("mocha"); - }, -}; diff --git a/v-next/hardhat-mocha/src/task-action.ts b/v-next/hardhat-mocha/src/task-action.ts index 1343d03efb8..b4b40d3bd23 100644 --- a/v-next/hardhat-mocha/src/task-action.ts +++ b/v-next/hardhat-mocha/src/task-action.ts @@ -8,16 +8,6 @@ import { HardhatError } from "@nomicfoundation/hardhat-errors"; import { setGlobalOptionsAsEnvVariables } from "@nomicfoundation/hardhat-utils/env"; import { getAllFilesMatching } from "@nomicfoundation/hardhat-utils/fs"; import debug from "debug"; -import { - markTestRunStart as initCoverage, - markTestWorkerDone as saveCoverageData, - markTestRunDone as reportCoverage, -} from "hardhat/internal/coverage"; -import { - markTestRunStart as initGasStats, - markTestWorkerDone as saveGasStats, - markTestRunDone as reportGasStats, -} from "hardhat/internal/gas-analytics"; import { createPerformanceTracker } from "./performance.js"; @@ -121,21 +111,15 @@ const testWithHardhat: NewTaskActionFunction = async ( hre.config.test.mocha.require = hre.config.test.mocha.require ?? []; hre.config.test.mocha.require.push(unhandledRejectionHook.href); - if (hre.globalOptions.coverage === true) { - const coverage = new URL( - import.meta.resolve("@nomicfoundation/hardhat-mocha/coverage"), + if ( + hre.globalOptions.coverage === true || + hre.globalOptions.gasStats === true + ) { + const testWorkerDone = new URL( + import.meta.resolve("@nomicfoundation/hardhat-mocha/test-worker-done"), ); - hre.config.test.mocha.require.push(coverage.href); - } - - if (hre.globalOptions.gasStats === true) { - const gasStats = new URL( - import.meta.resolve("@nomicfoundation/hardhat-mocha/gas-stats"), - ); - - hre.config.test.mocha.require = hre.config.test.mocha.require ?? []; - hre.config.test.mocha.require.push(gasStats.href); + hre.config.test.mocha.require.push(testWorkerDone.href); } process.env.NODE_OPTIONS = imports @@ -186,8 +170,7 @@ const testWithHardhat: NewTaskActionFunction = async ( perf.endPhase("Test file loading"); perf.startPhase("Test execution"); - await initCoverage("mocha"); - await initGasStats("mocha"); + await hre.hooks.runSequentialHandlers("test", "onTestRunStart", ["mocha"]); let total = 0; const testFailures = await new Promise((resolve) => { @@ -199,13 +182,11 @@ const testWithHardhat: NewTaskActionFunction = async ( perf.startPhase("Reporting"); if (hre.config.test.mocha.parallel !== true) { - // NOTE: We execute mocha tests in the main process. - await saveCoverageData("mocha"); - await saveGasStats("mocha"); + await hre.hooks.runSequentialHandlers("test", "onTestWorkerDone", [ + "mocha", + ]); } - // NOTE: This might print a coverage report. - await reportCoverage("mocha"); - await reportGasStats("mocha"); + await hre.hooks.runSequentialHandlers("test", "onTestRunDone", ["mocha"]); perf.endPhase("Reporting"); diff --git a/v-next/hardhat-mocha/src/test-worker-done.ts b/v-next/hardhat-mocha/src/test-worker-done.ts new file mode 100644 index 00000000000..bbc6d237204 --- /dev/null +++ b/v-next/hardhat-mocha/src/test-worker-done.ts @@ -0,0 +1,9 @@ +import hre from "hardhat"; + +export const mochaHooks = { + async afterAll(): Promise { + await hre.hooks.runSequentialHandlers("test", "onTestWorkerDone", [ + "mocha", + ]); + }, +}; diff --git a/v-next/hardhat-node-test-runner/package.json b/v-next/hardhat-node-test-runner/package.json index 1dd0dc53da6..0e901d6c3a4 100644 --- a/v-next/hardhat-node-test-runner/package.json +++ b/v-next/hardhat-node-test-runner/package.json @@ -13,8 +13,7 @@ "type": "module", "exports": { ".": "./dist/src/index.js", - "./coverage": "./dist/src/coverage.js", - "./gas-stats": "./dist/src/gas-stats.js" + "./test-worker-done": "./dist/src/test-worker-done.js" }, "keywords": [ "ethereum", diff --git a/v-next/hardhat-node-test-runner/src/coverage.ts b/v-next/hardhat-node-test-runner/src/coverage.ts deleted file mode 100644 index d1f5a35d7ec..00000000000 --- a/v-next/hardhat-node-test-runner/src/coverage.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { after } from "node:test"; - -import { markTestWorkerDone } from "hardhat/internal/coverage"; - -after(async () => { - await markTestWorkerDone("nodejs"); -}); diff --git a/v-next/hardhat-node-test-runner/src/gas-stats.ts b/v-next/hardhat-node-test-runner/src/gas-stats.ts deleted file mode 100644 index ce2fb75ebd0..00000000000 --- a/v-next/hardhat-node-test-runner/src/gas-stats.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { after } from "node:test"; - -import { markTestWorkerDone } from "hardhat/internal/gas-analytics"; - -after(async () => { - await markTestWorkerDone("nodejs"); -}); diff --git a/v-next/hardhat-node-test-runner/src/task-action.ts b/v-next/hardhat-node-test-runner/src/task-action.ts index 29526387a21..e15fc3e46ff 100644 --- a/v-next/hardhat-node-test-runner/src/task-action.ts +++ b/v-next/hardhat-node-test-runner/src/task-action.ts @@ -10,14 +10,6 @@ import { hardhatTestReporter } from "@nomicfoundation/hardhat-node-test-reporter import { setGlobalOptionsAsEnvVariables } from "@nomicfoundation/hardhat-utils/env"; import { getAllFilesMatching } from "@nomicfoundation/hardhat-utils/fs"; import { createNonClosingWriter } from "@nomicfoundation/hardhat-utils/stream"; -import { - markTestRunStart as initCoverage, - markTestRunDone as reportCoverage, -} from "hardhat/internal/coverage"; -import { - markTestRunStart as initGasStats, - markTestRunDone as reportGasStats, -} from "hardhat/internal/gas-analytics"; interface TestActionArguments { testFiles: string[]; @@ -92,20 +84,16 @@ const testWithHardhat: NewTaskActionFunction = async ( const tsx = new URL(import.meta.resolve("tsx/esm")); imports.push(tsx.href); - if (hre.globalOptions.coverage === true) { - const coverage = new URL( - import.meta.resolve("@nomicfoundation/hardhat-node-test-runner/coverage"), - ); - imports.push(coverage.href); - } - - if (hre.globalOptions.gasStats === true) { - const gasStats = new URL( + if ( + hre.globalOptions.coverage === true || + hre.globalOptions.gasStats === true + ) { + const testWorkerDone = new URL( import.meta.resolve( - "@nomicfoundation/hardhat-node-test-runner/gas-stats", + "@nomicfoundation/hardhat-node-test-runner/test-worker-done", ), ); - imports.push(gasStats.href); + imports.push(testWorkerDone.href); } process.env.NODE_OPTIONS = imports @@ -191,14 +179,11 @@ const testWithHardhat: NewTaskActionFunction = async ( }; } - await initCoverage("nodejs"); - await initGasStats("nodejs"); + await hre.hooks.runSequentialHandlers("test", "onTestRunStart", ["nodejs"]); const testResults = await runTests(); - // NOTE: This might print a coverage report. - await reportCoverage("nodejs"); - await reportGasStats("nodejs"); + await hre.hooks.runSequentialHandlers("test", "onTestRunDone", ["nodejs"]); if (testResults.failed > 0) { process.exitCode = 1; diff --git a/v-next/hardhat-node-test-runner/src/test-worker-done.ts b/v-next/hardhat-node-test-runner/src/test-worker-done.ts new file mode 100644 index 00000000000..ef310db2068 --- /dev/null +++ b/v-next/hardhat-node-test-runner/src/test-worker-done.ts @@ -0,0 +1,7 @@ +import { after } from "node:test"; + +import hre from "hardhat"; + +after(async () => { + await hre.hooks.runSequentialHandlers("test", "onTestWorkerDone", ["nodejs"]); +}); diff --git a/v-next/hardhat/package.json b/v-next/hardhat/package.json index 6afaedf8af6..feb4799384d 100644 --- a/v-next/hardhat/package.json +++ b/v-next/hardhat/package.json @@ -37,8 +37,6 @@ "./types/utils": "./dist/src/types/utils.js", "./types/solidity": "./dist/src/types/solidity.js", "./console.sol": "./console.sol", - "./internal/coverage": "./dist/src/internal/builtin-plugins/coverage/exports.js", - "./internal/gas-analytics": "./dist/src/internal/builtin-plugins/gas-analytics/exports.js", "./utils/contract-names": "./dist/src/utils/contract-names.js", "./types/runtime": "./dist/src/internal/deprecated-module-imported-from-hardhat2-plugin.js", "./builtin-tasks/task-names": "./dist/src/internal/deprecated-module-imported-from-hardhat2-plugin.js", diff --git a/v-next/hardhat/src/internal/builtin-plugins/coverage/exports.ts b/v-next/hardhat/src/internal/builtin-plugins/coverage/exports.ts deleted file mode 100644 index 021edb014d4..00000000000 --- a/v-next/hardhat/src/internal/builtin-plugins/coverage/exports.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { - markTestRunStart, - markTestRunDone, - markTestWorkerDone, -} from "./helpers.js"; diff --git a/v-next/hardhat/src/internal/builtin-plugins/coverage/helpers.ts b/v-next/hardhat/src/internal/builtin-plugins/coverage/helpers.ts index bfbb5d23846..f45f2dcb731 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/coverage/helpers.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/coverage/helpers.ts @@ -1,50 +1,5 @@ import path from "node:path"; -import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors"; - -import { HardhatRuntimeEnvironmentImplementation } from "../../core/hre.js"; - export function getCoveragePath(rootPath: string): string { return path.join(rootPath, "coverage"); } - -/** - * NOTE: The following helpers interact with the global HRE instance only; - * This is OK because: - * - They are intended for the internal use only. They are exposed via the - * internal public API only. - * - We know the HRE has been initialized by the time they are used. - */ - -export async function markTestRunStart(id: string): Promise { - const { default: hre } = await import("../../../index.js"); - if (hre.globalOptions.coverage === true) { - assertHardhatInvariant( - hre instanceof HardhatRuntimeEnvironmentImplementation, - "Expected HRE to be an instance of HardhatRuntimeEnvironmentImplementation", - ); - await hre._coverage.clearData(id); - } -} - -export async function markTestWorkerDone(id: string): Promise { - const { default: hre } = await import("../../../index.js"); - if (hre.globalOptions.coverage === true) { - assertHardhatInvariant( - hre instanceof HardhatRuntimeEnvironmentImplementation, - "Expected HRE to be an instance of HardhatRuntimeEnvironmentImplementation", - ); - await hre._coverage.saveData(id); - } -} - -export async function markTestRunDone(id: string): Promise { - const { default: hre } = await import("../../../index.js"); - if (hre.globalOptions.coverage === true) { - assertHardhatInvariant( - hre instanceof HardhatRuntimeEnvironmentImplementation, - "Expected HRE to be an instance of HardhatRuntimeEnvironmentImplementation", - ); - await hre._coverage.report(id); - } -} diff --git a/v-next/hardhat/src/internal/builtin-plugins/coverage/hook-handlers/test.ts b/v-next/hardhat/src/internal/builtin-plugins/coverage/hook-handlers/test.ts new file mode 100644 index 00000000000..a1c6e1436a4 --- /dev/null +++ b/v-next/hardhat/src/internal/builtin-plugins/coverage/hook-handlers/test.ts @@ -0,0 +1,37 @@ +import type { TestHooks } from "../../../../types/hooks.js"; + +import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors"; + +import { HardhatRuntimeEnvironmentImplementation } from "../../../core/hre.js"; + +export default async (): Promise> => ({ + onTestRunStart: async (context, id) => { + if (context.globalOptions.coverage === true) { + assertHardhatInvariant( + context instanceof HardhatRuntimeEnvironmentImplementation, + "Expected context to be an instance of HardhatRuntimeEnvironmentImplementation", + ); + await context._coverage.clearData(id); + } + }, + + onTestWorkerDone: async (context, id) => { + if (context.globalOptions.coverage === true) { + assertHardhatInvariant( + context instanceof HardhatRuntimeEnvironmentImplementation, + "Expected context to be an instance of HardhatRuntimeEnvironmentImplementation", + ); + await context._coverage.saveData(id); + } + }, + + onTestRunDone: async (context, id) => { + if (context.globalOptions.coverage === true) { + assertHardhatInvariant( + context instanceof HardhatRuntimeEnvironmentImplementation, + "Expected context to be an instance of HardhatRuntimeEnvironmentImplementation", + ); + await context._coverage.report(id); + } + }, +}); diff --git a/v-next/hardhat/src/internal/builtin-plugins/coverage/index.ts b/v-next/hardhat/src/internal/builtin-plugins/coverage/index.ts index c902a8e7d96..7ff67358d34 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/coverage/index.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/coverage/index.ts @@ -17,6 +17,7 @@ const hardhatPlugin: HardhatPlugin = { clean: () => import("./hook-handlers/clean.js"), hre: () => import("./hook-handlers/hre.js"), solidity: () => import("./hook-handlers/solidity.js"), + test: () => import("./hook-handlers/test.js"), }, npmPackage: "hardhat", }; diff --git a/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/exports.ts b/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/exports.ts deleted file mode 100644 index 021edb014d4..00000000000 --- a/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/exports.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { - markTestRunStart, - markTestRunDone, - markTestWorkerDone, -} from "./helpers.js"; diff --git a/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/helpers.ts b/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/helpers.ts deleted file mode 100644 index 5cb8da3907b..00000000000 --- a/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/helpers.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors"; - -import { HardhatRuntimeEnvironmentImplementation } from "../../core/hre.js"; - -/** - * NOTE: The following helpers interact with the global HRE instance only; - * This is OK because: - * - They are intended for the internal use only. They are exposed via the - * internal public API only. - * - We know the HRE has been initialized by the time they are used. - */ - -export async function markTestRunStart(id: string): Promise { - const { default: hre } = await import("../../../index.js"); - if (hre.globalOptions.gasStats === true) { - assertHardhatInvariant( - hre instanceof HardhatRuntimeEnvironmentImplementation, - "Expected HRE to be an instance of HardhatRuntimeEnvironmentImplementation", - ); - await hre._gasAnalytics.clearGasMeasurements(id); - } -} - -export async function markTestWorkerDone(id: string): Promise { - const { default: hre } = await import("../../../index.js"); - if (hre.globalOptions.gasStats === true) { - assertHardhatInvariant( - hre instanceof HardhatRuntimeEnvironmentImplementation, - "Expected HRE to be an instance of HardhatRuntimeEnvironmentImplementation", - ); - await hre._gasAnalytics.saveGasMeasurements(id); - } -} - -export async function markTestRunDone(id: string): Promise { - const { default: hre } = await import("../../../index.js"); - if (hre.globalOptions.gasStats === true) { - assertHardhatInvariant( - hre instanceof HardhatRuntimeEnvironmentImplementation, - "Expected HRE to be an instance of HardhatRuntimeEnvironmentImplementation", - ); - await hre._gasAnalytics.reportGasStats(id); - } -} diff --git a/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/hook-handlers/test.ts b/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/hook-handlers/test.ts new file mode 100644 index 00000000000..ada5282a277 --- /dev/null +++ b/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/hook-handlers/test.ts @@ -0,0 +1,37 @@ +import type { TestHooks } from "../../../../types/hooks.js"; + +import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors"; + +import { HardhatRuntimeEnvironmentImplementation } from "../../../core/hre.js"; + +export default async (): Promise> => ({ + onTestRunStart: async (context, id) => { + if (context.globalOptions.gasStats === true) { + assertHardhatInvariant( + context instanceof HardhatRuntimeEnvironmentImplementation, + "Expected context to be an instance of HardhatRuntimeEnvironmentImplementation", + ); + await context._gasAnalytics.clearGasMeasurements(id); + } + }, + + onTestWorkerDone: async (context, id) => { + if (context.globalOptions.gasStats === true) { + assertHardhatInvariant( + context instanceof HardhatRuntimeEnvironmentImplementation, + "Expected context to be an instance of HardhatRuntimeEnvironmentImplementation", + ); + await context._gasAnalytics.saveGasMeasurements(id); + } + }, + + onTestRunDone: async (context, id) => { + if (context.globalOptions.gasStats === true) { + assertHardhatInvariant( + context instanceof HardhatRuntimeEnvironmentImplementation, + "Expected context to be an instance of HardhatRuntimeEnvironmentImplementation", + ); + await context._gasAnalytics.reportGasStats(id); + } + }, +}); diff --git a/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/index.ts b/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/index.ts index 79c3a390464..caf18c10a3c 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/index.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/index.ts @@ -16,6 +16,7 @@ const hardhatPlugin: HardhatPlugin = { ], hookHandlers: { hre: () => import("./hook-handlers/hre.js"), + test: () => import("./hook-handlers/test.js"), }, npmPackage: "hardhat", }; diff --git a/v-next/hardhat/src/internal/builtin-plugins/solidity-test/task-action.ts b/v-next/hardhat/src/internal/builtin-plugins/solidity-test/task-action.ts index 87872c33567..72a3f95f7af 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/solidity-test/task-action.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/solidity-test/task-action.ts @@ -22,16 +22,6 @@ import { getFullyQualifiedName } from "../../../utils/contract-names.js"; import { HardhatRuntimeEnvironmentImplementation } from "../../core/hre.js"; import { isSupportedChainType } from "../../edr/chain-type.js"; import { ArtifactManagerImplementation } from "../artifacts/artifact-manager.js"; -import { - markTestRunStart as initCoverage, - markTestWorkerDone as saveCoverageData, - markTestRunDone as reportCoverage, -} from "../coverage/helpers.js"; -import { - markTestRunStart as initGasStats, - markTestWorkerDone as saveGasStatsData, - markTestRunDone as reportGasStats, -} from "../gas-analytics/helpers.js"; import { edrGasReportToHardhatGasMeasurements } from "../network-manager/edr/utils/convert-to-edr.js"; import { getEdrArtifacts, getBuildInfos } from "./edr-artifacts.js"; @@ -186,8 +176,7 @@ const runSolidityTests: NewTaskActionFunction = async ( const options: RunOptions = solidityTestConfigToRunOptions(solidityTestConfig); - await initCoverage("solidity"); - await initGasStats("solidity"); + await hre.hooks.runSequentialHandlers("test", "onTestRunStart", ["solidity"]); const runStream = run( chainType, @@ -271,12 +260,11 @@ const runSolidityTests: NewTaskActionFunction = async ( includesErrors = true; } - await saveCoverageData("solidity"); - await saveGasStatsData("solidity"); + await hre.hooks.runSequentialHandlers("test", "onTestWorkerDone", [ + "solidity", + ]); - // this may print coverage and gas statistics reports - await reportCoverage("solidity"); - await reportGasStats("solidity"); + await hre.hooks.runSequentialHandlers("test", "onTestRunDone", ["solidity"]); if (includesFailures || includesErrors) { process.exitCode = 1; diff --git a/v-next/hardhat/src/internal/builtin-plugins/test/type-extensions.ts b/v-next/hardhat/src/internal/builtin-plugins/test/type-extensions.ts index 28f015820dc..8a98521a589 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/test/type-extensions.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/test/type-extensions.ts @@ -39,5 +39,29 @@ declare module "../../../types/hooks.js" { filePath: string, ) => Promise, ) => Promise; + + /** + * This hook is triggered at the start of a test run, before tests execute. + * + * @param context The hook context. + * @param id A string identifier for the test runner (e.g., "solidity", "nodejs", "mocha"). + */ + onTestRunStart: (context: HookContext, id: string) => Promise; + + /** + * This hook is triggered when a test worker has finished executing. + * + * @param context The hook context. + * @param id A string identifier for the test runner (e.g., "solidity", "nodejs", "mocha"). + */ + onTestWorkerDone: (context: HookContext, id: string) => Promise; + + /** + * This hook is triggered at the end of a test run, after all tests have completed. + * + * @param context The hook context. + * @param id A string identifier for the test runner (e.g., "solidity", "nodejs", "mocha"). + */ + onTestRunDone: (context: HookContext, id: string) => Promise; } } From c75569337898931f9b234b36a882794f68c2165c Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Tue, 24 Feb 2026 19:07:50 +0000 Subject: [PATCH 2/4] add changeset --- .changeset/yellow-lands-kiss.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/yellow-lands-kiss.md diff --git a/.changeset/yellow-lands-kiss.md b/.changeset/yellow-lands-kiss.md new file mode 100644 index 00000000000..a0eabbc88c9 --- /dev/null +++ b/.changeset/yellow-lands-kiss.md @@ -0,0 +1,7 @@ +--- +"@nomicfoundation/hardhat-node-test-runner": patch +"@nomicfoundation/hardhat-mocha": patch +"hardhat": patch +--- + +Add `onTestRunStart`, `onTestWorkerDone`, and `onTestRunDone` test hooks. From 182440ba16840cdff1d438569305b19496557edf Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Tue, 24 Feb 2026 19:14:55 +0000 Subject: [PATCH 3/4] update peer-bumps --- .peer-bumps.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.peer-bumps.json b/.peer-bumps.json index 9793df6f17c..25512a7c41e 100644 --- a/.peer-bumps.json +++ b/.peer-bumps.json @@ -7,6 +7,16 @@ "v-next/config" ], "bumps": [ + { + "package": "@nomicfoundation/hardhat-mocha", + "peer": "hardhat", + "reason": "It depends on the new TestHooks#onTestRunStart, TestHooks#onTestWorkerDone, and TestHooks#onTestRunDone hooks" + }, + { + "package": "@nomicfoundation/hardhat-node-test-runner", + "peer": "hardhat", + "reason": "It depends on the new TestHooks#onTestRunStart, TestHooks#onTestWorkerDone, and TestHooks#onTestRunDone hooks" + }, { "package": "@nomicfoundation/hardhat-foundry", "peer": "hardhat", From 9912d2a53c78b30199b6c3d7d277677cae562391 Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Thu, 26 Feb 2026 13:50:49 +0000 Subject: [PATCH 4/4] refactor: replace the sequential hooks with chained hooks --- v-next/hardhat-mocha/src/task-action.ts | 23 ++++++++++++++---- v-next/hardhat-mocha/src/test-worker-done.ts | 9 ++++--- .../src/task-action.ts | 14 +++++++++-- .../src/test-worker-done.ts | 7 +++++- .../coverage/hook-handlers/test.ts | 12 +++++++--- .../gas-analytics/hook-handlers/test.ts | 12 +++++++--- .../solidity-test/task-action.ts | 23 ++++++++++++++---- .../builtin-plugins/test/type-extensions.ts | 24 ++++++++++++++++--- 8 files changed, 99 insertions(+), 25 deletions(-) diff --git a/v-next/hardhat-mocha/src/task-action.ts b/v-next/hardhat-mocha/src/task-action.ts index b4b40d3bd23..fd405a42d56 100644 --- a/v-next/hardhat-mocha/src/task-action.ts +++ b/v-next/hardhat-mocha/src/task-action.ts @@ -170,7 +170,12 @@ const testWithHardhat: NewTaskActionFunction = async ( perf.endPhase("Test file loading"); perf.startPhase("Test execution"); - await hre.hooks.runSequentialHandlers("test", "onTestRunStart", ["mocha"]); + await hre.hooks.runHandlerChain( + "test", + "onTestRunStart", + ["mocha"], + async () => {}, + ); let total = 0; const testFailures = await new Promise((resolve) => { @@ -182,11 +187,19 @@ const testWithHardhat: NewTaskActionFunction = async ( perf.startPhase("Reporting"); if (hre.config.test.mocha.parallel !== true) { - await hre.hooks.runSequentialHandlers("test", "onTestWorkerDone", [ - "mocha", - ]); + await hre.hooks.runHandlerChain( + "test", + "onTestWorkerDone", + ["mocha"], + async () => {}, + ); } - await hre.hooks.runSequentialHandlers("test", "onTestRunDone", ["mocha"]); + await hre.hooks.runHandlerChain( + "test", + "onTestRunDone", + ["mocha"], + async () => {}, + ); perf.endPhase("Reporting"); diff --git a/v-next/hardhat-mocha/src/test-worker-done.ts b/v-next/hardhat-mocha/src/test-worker-done.ts index bbc6d237204..0c869cd8e13 100644 --- a/v-next/hardhat-mocha/src/test-worker-done.ts +++ b/v-next/hardhat-mocha/src/test-worker-done.ts @@ -2,8 +2,11 @@ import hre from "hardhat"; export const mochaHooks = { async afterAll(): Promise { - await hre.hooks.runSequentialHandlers("test", "onTestWorkerDone", [ - "mocha", - ]); + await hre.hooks.runHandlerChain( + "test", + "onTestWorkerDone", + ["mocha"], + async () => {}, + ); }, }; diff --git a/v-next/hardhat-node-test-runner/src/task-action.ts b/v-next/hardhat-node-test-runner/src/task-action.ts index e15fc3e46ff..c1a22b76107 100644 --- a/v-next/hardhat-node-test-runner/src/task-action.ts +++ b/v-next/hardhat-node-test-runner/src/task-action.ts @@ -179,11 +179,21 @@ const testWithHardhat: NewTaskActionFunction = async ( }; } - await hre.hooks.runSequentialHandlers("test", "onTestRunStart", ["nodejs"]); + await hre.hooks.runHandlerChain( + "test", + "onTestRunStart", + ["nodejs"], + async () => {}, + ); const testResults = await runTests(); - await hre.hooks.runSequentialHandlers("test", "onTestRunDone", ["nodejs"]); + await hre.hooks.runHandlerChain( + "test", + "onTestRunDone", + ["nodejs"], + async () => {}, + ); if (testResults.failed > 0) { process.exitCode = 1; diff --git a/v-next/hardhat-node-test-runner/src/test-worker-done.ts b/v-next/hardhat-node-test-runner/src/test-worker-done.ts index ef310db2068..ad4e60b8a39 100644 --- a/v-next/hardhat-node-test-runner/src/test-worker-done.ts +++ b/v-next/hardhat-node-test-runner/src/test-worker-done.ts @@ -3,5 +3,10 @@ import { after } from "node:test"; import hre from "hardhat"; after(async () => { - await hre.hooks.runSequentialHandlers("test", "onTestWorkerDone", ["nodejs"]); + await hre.hooks.runHandlerChain( + "test", + "onTestWorkerDone", + ["nodejs"], + async () => {}, + ); }); diff --git a/v-next/hardhat/src/internal/builtin-plugins/coverage/hook-handlers/test.ts b/v-next/hardhat/src/internal/builtin-plugins/coverage/hook-handlers/test.ts index a1c6e1436a4..08f8fb9c7ea 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/coverage/hook-handlers/test.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/coverage/hook-handlers/test.ts @@ -5,7 +5,9 @@ import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors"; import { HardhatRuntimeEnvironmentImplementation } from "../../../core/hre.js"; export default async (): Promise> => ({ - onTestRunStart: async (context, id) => { + onTestRunStart: async (context, id, next) => { + await next(context, id); + if (context.globalOptions.coverage === true) { assertHardhatInvariant( context instanceof HardhatRuntimeEnvironmentImplementation, @@ -15,7 +17,9 @@ export default async (): Promise> => ({ } }, - onTestWorkerDone: async (context, id) => { + onTestWorkerDone: async (context, id, next) => { + await next(context, id); + if (context.globalOptions.coverage === true) { assertHardhatInvariant( context instanceof HardhatRuntimeEnvironmentImplementation, @@ -25,7 +29,9 @@ export default async (): Promise> => ({ } }, - onTestRunDone: async (context, id) => { + onTestRunDone: async (context, id, next) => { + await next(context, id); + if (context.globalOptions.coverage === true) { assertHardhatInvariant( context instanceof HardhatRuntimeEnvironmentImplementation, diff --git a/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/hook-handlers/test.ts b/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/hook-handlers/test.ts index ada5282a277..99700f37fd1 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/hook-handlers/test.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/hook-handlers/test.ts @@ -5,7 +5,9 @@ import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors"; import { HardhatRuntimeEnvironmentImplementation } from "../../../core/hre.js"; export default async (): Promise> => ({ - onTestRunStart: async (context, id) => { + onTestRunStart: async (context, id, next) => { + await next(context, id); + if (context.globalOptions.gasStats === true) { assertHardhatInvariant( context instanceof HardhatRuntimeEnvironmentImplementation, @@ -15,7 +17,9 @@ export default async (): Promise> => ({ } }, - onTestWorkerDone: async (context, id) => { + onTestWorkerDone: async (context, id, next) => { + await next(context, id); + if (context.globalOptions.gasStats === true) { assertHardhatInvariant( context instanceof HardhatRuntimeEnvironmentImplementation, @@ -25,7 +29,9 @@ export default async (): Promise> => ({ } }, - onTestRunDone: async (context, id) => { + onTestRunDone: async (context, id, next) => { + await next(context, id); + if (context.globalOptions.gasStats === true) { assertHardhatInvariant( context instanceof HardhatRuntimeEnvironmentImplementation, diff --git a/v-next/hardhat/src/internal/builtin-plugins/solidity-test/task-action.ts b/v-next/hardhat/src/internal/builtin-plugins/solidity-test/task-action.ts index 72a3f95f7af..0e0624adf6d 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/solidity-test/task-action.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/solidity-test/task-action.ts @@ -176,7 +176,12 @@ const runSolidityTests: NewTaskActionFunction = async ( const options: RunOptions = solidityTestConfigToRunOptions(solidityTestConfig); - await hre.hooks.runSequentialHandlers("test", "onTestRunStart", ["solidity"]); + await hre.hooks.runHandlerChain( + "test", + "onTestRunStart", + ["solidity"], + async () => {}, + ); const runStream = run( chainType, @@ -260,11 +265,19 @@ const runSolidityTests: NewTaskActionFunction = async ( includesErrors = true; } - await hre.hooks.runSequentialHandlers("test", "onTestWorkerDone", [ - "solidity", - ]); + await hre.hooks.runHandlerChain( + "test", + "onTestWorkerDone", + ["solidity"], + async () => {}, + ); - await hre.hooks.runSequentialHandlers("test", "onTestRunDone", ["solidity"]); + await hre.hooks.runHandlerChain( + "test", + "onTestRunDone", + ["solidity"], + async () => {}, + ); if (includesFailures || includesErrors) { process.exitCode = 1; diff --git a/v-next/hardhat/src/internal/builtin-plugins/test/type-extensions.ts b/v-next/hardhat/src/internal/builtin-plugins/test/type-extensions.ts index 8a98521a589..b135c5a0801 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/test/type-extensions.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/test/type-extensions.ts @@ -45,23 +45,41 @@ declare module "../../../types/hooks.js" { * * @param context The hook context. * @param id A string identifier for the test runner (e.g., "solidity", "nodejs", "mocha"). + * @param next A function to call the next handler for this hook, or the + * default implementation if no more handlers exist. */ - onTestRunStart: (context: HookContext, id: string) => Promise; + onTestRunStart: ( + context: HookContext, + id: string, + next: (nextContext: HookContext, id: string) => Promise, + ) => Promise; /** * This hook is triggered when a test worker has finished executing. * * @param context The hook context. * @param id A string identifier for the test runner (e.g., "solidity", "nodejs", "mocha"). + * @param next A function to call the next handler for this hook, or the + * default implementation if no more handlers exist. */ - onTestWorkerDone: (context: HookContext, id: string) => Promise; + onTestWorkerDone: ( + context: HookContext, + id: string, + next: (nextContext: HookContext, id: string) => Promise, + ) => Promise; /** * This hook is triggered at the end of a test run, after all tests have completed. * * @param context The hook context. * @param id A string identifier for the test runner (e.g., "solidity", "nodejs", "mocha"). + * @param next A function to call the next handler for this hook, or the + * default implementation if no more handlers exist. */ - onTestRunDone: (context: HookContext, id: string) => Promise; + onTestRunDone: ( + context: HookContext, + id: string, + next: (nextContext: HookContext, id: string) => Promise, + ) => Promise; } }