diff --git a/.changeset/social-seal-do.md b/.changeset/social-seal-do.md new file mode 100644 index 00000000000..0b28d0a7f12 --- /dev/null +++ b/.changeset/social-seal-do.md @@ -0,0 +1,5 @@ +--- +"hardhat": patch +--- + +Aggregate `--gas-stats` output when using multiple test runners, printing a single consolidated table at the end instead of separate tables per runner ([#7500](https://github.com/NomicFoundation/hardhat/issues/7500)). 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 023396fcf71..75210908147 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/coverage/helpers.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/coverage/helpers.ts @@ -1,5 +1,14 @@ +import type { CoverageManager } from "./types.js"; +import type { HookContext } from "../../../types/hooks.js"; +import type { HardhatRuntimeEnvironment } from "../../../types/hre.js"; + import path from "node:path"; +import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors"; + +import { HardhatRuntimeEnvironmentImplementation } from "../../core/hre.js"; + +import { CoverageManagerImplementation } from "./coverage-manager.js"; import { testRunDone, testRunStart, @@ -10,6 +19,28 @@ export function getCoveragePath(rootPath: string): string { return path.join(rootPath, "coverage"); } +export function getCoverageManager( + hookContextOrHre: HookContext | HardhatRuntimeEnvironment, +): CoverageManager { + assertHardhatInvariant( + "_coverage" in hookContextOrHre && + hookContextOrHre._coverage instanceof CoverageManagerImplementation, + "Expected _coverage to be an instance of CoverageManagerImplementation", + ); + return hookContextOrHre._coverage; +} + +export function setCoverageManager( + hre: HardhatRuntimeEnvironment, + coverageManager: CoverageManager, +): void { + assertHardhatInvariant( + hre instanceof HardhatRuntimeEnvironmentImplementation, + "Expected HRE to be an instance of HardhatRuntimeEnvironmentImplementation", + ); + hre._coverage = coverageManager; +} + /** * The following helpers are kept for backward compatibility with older versions * of test runner plugins (hardhat-mocha, hardhat-node-test-runner) that import diff --git a/v-next/hardhat/src/internal/builtin-plugins/coverage/hook-handlers/hre.ts b/v-next/hardhat/src/internal/builtin-plugins/coverage/hook-handlers/hre.ts index dfdb16a8f2d..ce018a0496c 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/coverage/hook-handlers/hre.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/coverage/hook-handlers/hre.ts @@ -1,10 +1,7 @@ import type { HardhatRuntimeEnvironmentHooks } from "../../../../types/hooks.js"; -import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors"; - -import { HardhatRuntimeEnvironmentImplementation } from "../../../core/hre.js"; import { CoverageManagerImplementation } from "../coverage-manager.js"; -import { getCoveragePath } from "../helpers.js"; +import { getCoveragePath, setCoverageManager } from "../helpers.js"; export default async (): Promise> => ({ created: async (context, hre) => { @@ -12,12 +9,7 @@ export default async (): Promise> => ({ const coveragePath = getCoveragePath(hre.config.paths.root); const coverageManager = new CoverageManagerImplementation(coveragePath); - assertHardhatInvariant( - hre instanceof HardhatRuntimeEnvironmentImplementation, - "Expected HRE to be an instance of HardhatRuntimeEnvironmentImplementation", - ); - - hre._coverage = coverageManager; + setCoverageManager(hre, coverageManager); // NOTE: We register this hook dynamically because we use the information about // the existence of onCoverageData hook handlers to determine whether coverage diff --git a/v-next/hardhat/src/internal/builtin-plugins/coverage/hook-handlers/solidity.ts b/v-next/hardhat/src/internal/builtin-plugins/coverage/hook-handlers/solidity.ts index 8aa16b69165..05bac75e329 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/coverage/hook-handlers/solidity.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/coverage/hook-handlers/solidity.ts @@ -3,16 +3,13 @@ import type { CoverageMetadata } from "../types.js"; import path from "node:path"; -import { - assertHardhatInvariant, - HardhatError, -} from "@nomicfoundation/hardhat-errors"; +import { HardhatError } from "@nomicfoundation/hardhat-errors"; import { ensureError } from "@nomicfoundation/hardhat-utils/error"; import { readUtf8File } from "@nomicfoundation/hardhat-utils/fs"; import { findClosestPackageRoot } from "@nomicfoundation/hardhat-utils/package"; import debug from "debug"; -import { CoverageManagerImplementation } from "../coverage-manager.js"; +import { getCoverageManager } from "../helpers.js"; import { instrumentSolidityFileForCompilationJob } from "../instrumentation.js"; const log = debug("hardhat:core:coverage:hook-handlers:solidity"); @@ -80,13 +77,7 @@ export default async (): Promise> => ({ } } - assertHardhatInvariant( - "_coverage" in context && - context._coverage instanceof CoverageManagerImplementation, - "Expected _coverage to be defined in the HookContext, as it's should be defined in the HRE", - ); - - await context._coverage.addMetadata(coverageMetadata); + await getCoverageManager(context).addMetadata(coverageMetadata); return await next(context, sourceName, fsPath, source, solcVersion); } catch (e) { 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 78fd84b20f4..bb891587ed6 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 @@ -1,9 +1,6 @@ import type { HookContext, TestHooks } from "../../../../types/hooks.js"; -import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors"; -import { isObject } from "@nomicfoundation/hardhat-utils/lang"; - -import { CoverageManagerImplementation } from "../coverage-manager.js"; +import { getCoverageManager } from "../helpers.js"; export default async (): Promise> => ({ onTestRunStart: async (context, id, next) => { @@ -27,13 +24,7 @@ export async function testRunStart( id: string, ): Promise { if (context.globalOptions.coverage === true) { - assertHardhatInvariant( - "_coverage" in context && - isObject(context._coverage) && - context._coverage instanceof CoverageManagerImplementation, - "Expected HookContext#_coverage to be an instance of CoverageManagerImplementation", - ); - await context._coverage.clearData(id); + await getCoverageManager(context).clearData(id); } } @@ -42,13 +33,7 @@ export async function testWorkerDone( id: string, ): Promise { if (context.globalOptions.coverage === true) { - assertHardhatInvariant( - "_coverage" in context && - isObject(context._coverage) && - context._coverage instanceof CoverageManagerImplementation, - "Expected HookContext#_coverage to be an instance of CoverageManagerImplementation", - ); - await context._coverage.saveData(id); + await getCoverageManager(context).saveData(id); } } @@ -57,12 +42,6 @@ export async function testRunDone( id: string, ): Promise { if (context.globalOptions.coverage === true) { - assertHardhatInvariant( - "_coverage" in context && - isObject(context._coverage) && - context._coverage instanceof CoverageManagerImplementation, - "Expected HookContext#_coverage to be an instance of CoverageManagerImplementation", - ); - await context._coverage.report(id); + await getCoverageManager(context).report(id); } } diff --git a/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/gas-analytics-manager.ts b/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/gas-analytics-manager.ts index 28b613433e0..c8bac6008e3 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/gas-analytics-manager.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/gas-analytics-manager.ts @@ -51,6 +51,7 @@ interface ContractGasMeasurements { export class GasAnalyticsManagerImplementation implements GasAnalyticsManager { public gasMeasurements: GasMeasurement[] = []; readonly #gasStatsPath: string; + #reportEnabled = true; constructor(gasStatsRootPath: string) { this.#gasStatsPath = path.join(gasStatsRootPath, "gas-stats"); @@ -78,6 +79,10 @@ export class GasAnalyticsManagerImplementation implements GasAnalyticsManager { } public async reportGasStats(...ids: string[]): Promise { + if (!this.#reportEnabled) { + return; + } + await this._loadGasMeasurements(...ids); const gasStatsByContract = this._calculateGasStats(); @@ -89,6 +94,14 @@ export class GasAnalyticsManagerImplementation implements GasAnalyticsManager { gasStatsLog("Printed markdown report"); } + public enableReport(): void { + this.#reportEnabled = true; + } + + public disableReport(): void { + this.#reportEnabled = false; + } + async #getGasMeasurementsPath(id: string): Promise { const gasMeasurementsPath = path.join(this.#gasStatsPath, id); await ensureDir(gasMeasurementsPath); 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 index b02ec39ffd4..249289b497d 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/helpers.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/helpers.ts @@ -1,11 +1,42 @@ +import type { GasAnalyticsManager } from "./types.js"; +import type { HookContext } from "../../../types/hooks.js"; +import type { HardhatRuntimeEnvironment } from "../../../types/hre.js"; + +import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors"; import chalk from "chalk"; +import { HardhatRuntimeEnvironmentImplementation } from "../../core/hre.js"; + +import { GasAnalyticsManagerImplementation } from "./gas-analytics-manager.js"; import { testRunDone, testRunStart, testWorkerDone, } from "./hook-handlers/test.js"; +export function getGasAnalyticsManager( + hookContextOrHre: HookContext | HardhatRuntimeEnvironment, +): GasAnalyticsManager { + assertHardhatInvariant( + "_gasAnalytics" in hookContextOrHre && + hookContextOrHre._gasAnalytics instanceof + GasAnalyticsManagerImplementation, + "Expected _gasAnalytics to be an instance of GasAnalyticsManagerImplementation", + ); + return hookContextOrHre._gasAnalytics; +} + +export function setGasAnalyticsManager( + hre: HardhatRuntimeEnvironment, + gasAnalyticsManager: GasAnalyticsManager, +): void { + assertHardhatInvariant( + hre instanceof HardhatRuntimeEnvironmentImplementation, + "Expected HRE to be an instance of HardhatRuntimeEnvironmentImplementation", + ); + hre._gasAnalytics = gasAnalyticsManager; +} + /** * The following helpers are kept for backward compatibility with older versions * of test runner plugins (hardhat-mocha, hardhat-node-test-runner) that import diff --git a/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/hook-handlers/hre.ts b/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/hook-handlers/hre.ts index 7d8ee01c93b..28d2e8f6eee 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/hook-handlers/hre.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/hook-handlers/hre.ts @@ -1,9 +1,7 @@ import type { HardhatRuntimeEnvironmentHooks } from "../../../../types/hooks.js"; -import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors"; - -import { HardhatRuntimeEnvironmentImplementation } from "../../../core/hre.js"; import { GasAnalyticsManagerImplementation } from "../gas-analytics-manager.js"; +import { setGasAnalyticsManager } from "../helpers.js"; export default async (): Promise> => ({ created: async (context, hre) => { @@ -12,12 +10,7 @@ export default async (): Promise> => ({ hre.config.paths.cache, ); - assertHardhatInvariant( - hre instanceof HardhatRuntimeEnvironmentImplementation, - "Expected HRE to be an instance of HardhatRuntimeEnvironmentImplementation", - ); - - hre._gasAnalytics = gasAnalyticsManager; + setGasAnalyticsManager(hre, gasAnalyticsManager); // NOTE: We register this hook dynamically to avoid a circular dependency // between gas-analytics and network-manager plugins. The network-manager 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 1fecf07c182..519a045ea79 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 @@ -1,9 +1,6 @@ import type { HookContext, TestHooks } from "../../../../types/hooks.js"; -import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors"; -import { isObject } from "@nomicfoundation/hardhat-utils/lang"; - -import { GasAnalyticsManagerImplementation } from "../gas-analytics-manager.js"; +import { getGasAnalyticsManager } from "../helpers.js"; export default async (): Promise> => ({ onTestRunStart: async (context, id, next) => { @@ -27,13 +24,7 @@ export async function testRunStart( id: string, ): Promise { if (context.globalOptions.gasStats === true) { - assertHardhatInvariant( - "_gasAnalytics" in context && - isObject(context._gasAnalytics) && - context._gasAnalytics instanceof GasAnalyticsManagerImplementation, - "Expected HookContext#_gasAnalytics to be an instance of GasAnalyticsManagerImplementation", - ); - await context._gasAnalytics.clearGasMeasurements(id); + await getGasAnalyticsManager(context).clearGasMeasurements(id); } } @@ -42,13 +33,7 @@ export async function testWorkerDone( id: string, ): Promise { if (context.globalOptions.gasStats === true) { - assertHardhatInvariant( - "_gasAnalytics" in context && - isObject(context._gasAnalytics) && - context._gasAnalytics instanceof GasAnalyticsManagerImplementation, - "Expected HookContext#_gasAnalytics to be an instance of GasAnalyticsManagerImplementation", - ); - await context._gasAnalytics.saveGasMeasurements(id); + await getGasAnalyticsManager(context).saveGasMeasurements(id); } } @@ -57,12 +42,6 @@ export async function testRunDone( id: string, ): Promise { if (context.globalOptions.gasStats === true) { - assertHardhatInvariant( - "_gasAnalytics" in context && - isObject(context._gasAnalytics) && - context._gasAnalytics instanceof GasAnalyticsManagerImplementation, - "Expected HookContext#_gasAnalytics to be an instance of GasAnalyticsManagerImplementation", - ); - await context._gasAnalytics.reportGasStats(id); + await getGasAnalyticsManager(context).reportGasStats(id); } } diff --git a/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/types.ts b/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/types.ts index e304742f0e0..1d3b92f524f 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/types.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/types.ts @@ -21,4 +21,7 @@ export interface GasAnalyticsManager { clearGasMeasurements(id: string): Promise; saveGasMeasurements(id: string): Promise; reportGasStats(...ids: string[]): Promise; + + enableReport(): void; + disableReport(): void; } 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 8d40b00d152..8195c105ae2 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 @@ -14,18 +14,16 @@ import type { import { finished } from "node:stream/promises"; -import { - assertHardhatInvariant, - HardhatError, -} from "@nomicfoundation/hardhat-errors"; +import { HardhatError } from "@nomicfoundation/hardhat-errors"; import { resolveFromRoot } from "@nomicfoundation/hardhat-utils/path"; import { createNonClosingWriter } from "@nomicfoundation/hardhat-utils/stream"; import { getFullyQualifiedName } from "../../../utils/contract-names.js"; import { errorResult, successfulResult } from "../../../utils/result.js"; -import { HardhatRuntimeEnvironmentImplementation } from "../../core/hre.js"; import { isSupportedChainType } from "../../edr/chain-type.js"; import { ArtifactManagerImplementation } from "../artifacts/artifact-manager.js"; +import { getCoverageManager } from "../coverage/helpers.js"; +import { getGasAnalyticsManager } from "../gas-analytics/helpers.js"; import { edrGasReportToHardhatGasMeasurements } from "../network-manager/edr/utils/convert-to-edr.js"; import { getEdrArtifacts, getBuildInfos } from "./edr-artifacts.js"; @@ -55,11 +53,6 @@ const runSolidityTests: NewTaskActionFunction = async ( { testFiles, chainType, grep, noCompile, verbosity, testSummaryIndex }, hre, ): Promise> => { - assertHardhatInvariant( - hre instanceof HardhatRuntimeEnvironmentImplementation, - "Expected HRE to be an instance of HardhatRuntimeEnvironmentImplementation", - ); - // Set an environment variable that plugins can use to detect when a process is running tests process.env.HH_TEST = "true"; @@ -143,6 +136,7 @@ const runSolidityTests: NewTaskActionFunction = async ( const solidityTestConfig = hre.config.test.solidity; let observabilityConfig: ObservabilityConfig | undefined; if (hre.globalOptions.coverage) { + const coverage = getCoverageManager(hre); observabilityConfig = { codeCoverage: { onCollectedCoverageCallback: async (coverageData: Uint8Array[]) => { @@ -150,7 +144,7 @@ const runSolidityTests: NewTaskActionFunction = async ( Buffer.from(tag).toString("hex"), ); - await hre._coverage.addData(tags); + await coverage.addData(tags); }, }, }; @@ -232,8 +226,9 @@ const runSolidityTests: NewTaskActionFunction = async ( testContractFqns, ); + const gasAnalytics = getGasAnalyticsManager(hre); for (const measurement of gasMeasurements) { - hre._gasAnalytics.addGasMeasurement(measurement); + gasAnalytics.addGasMeasurement(measurement); } } }) diff --git a/v-next/hardhat/src/internal/builtin-plugins/test/task-action.ts b/v-next/hardhat/src/internal/builtin-plugins/test/task-action.ts index 400f24bd00b..63f6b4c94c7 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/test/task-action.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/test/task-action.ts @@ -7,10 +7,7 @@ import type { import type { TestSummary } from "../../../types/test.js"; import type { Result } from "../../../types/utils.js"; -import { - assertHardhatInvariant, - HardhatError, -} from "@nomicfoundation/hardhat-errors"; +import { HardhatError } from "@nomicfoundation/hardhat-errors"; import { isObject } from "@nomicfoundation/hardhat-utils/lang"; import chalk, { type ChalkInstance } from "chalk"; @@ -19,7 +16,8 @@ import { isResult, successfulResult, } from "../../../utils/result.js"; -import { HardhatRuntimeEnvironmentImplementation } from "../../core/hre.js"; +import { getCoverageManager } from "../coverage/helpers.js"; +import { getGasAnalyticsManager } from "../gas-analytics/helpers.js"; interface TestActionArguments { testFiles: string[]; @@ -73,18 +71,19 @@ const runAllTests: NewTaskActionFunction = async ( } if (hre.globalOptions.coverage === true) { - assertHardhatInvariant( - hre instanceof HardhatRuntimeEnvironmentImplementation, - "Expected HRE to be an instance of HardhatRuntimeEnvironmentImplementation", - ); - hre._coverage.disableReport(); + getCoverageManager(hre).disableReport(); + } + + if (hre.globalOptions.gasStats === true) { + getGasAnalyticsManager(hre).disableReport(); } const testSummaries: Record = {}; + const ranSubtaskIds: string[] = []; let failureIndex = 1; let hasFailures = false; - for (const subtask of thisTask.subtasks.values()) { + for (const [subtaskKey, subtask] of thisTask.subtasks.entries()) { const files = getTestFilesForSubtask(subtask, testFiles, subtasksToFiles); if (files === undefined) { @@ -93,6 +92,8 @@ const runAllTests: NewTaskActionFunction = async ( continue; } + ranSubtaskIds.push(subtaskKey); + const args: TaskArguments = { testFiles: files, grep, @@ -228,13 +229,16 @@ const runAllTests: NewTaskActionFunction = async ( console.log(); if (hre.globalOptions.coverage === true) { - assertHardhatInvariant( - hre instanceof HardhatRuntimeEnvironmentImplementation, - "Expected HRE to be an instance of HardhatRuntimeEnvironmentImplementation", - ); - const ids = Array.from(thisTask.subtasks.keys()); - hre._coverage.enableReport(); - await hre._coverage.report(...ids); + const coverage = getCoverageManager(hre); + coverage.enableReport(); + await coverage.report(...ranSubtaskIds); + console.log(); + } + + if (hre.globalOptions.gasStats === true) { + const gasAnalytics = getGasAnalyticsManager(hre); + gasAnalytics.enableReport(); + await gasAnalytics.reportGasStats(...ranSubtaskIds); console.log(); } diff --git a/v-next/hardhat/test/internal/builtin-plugins/coverage/coverage-manager.ts b/v-next/hardhat/test/internal/builtin-plugins/coverage/coverage-manager.ts index 5051ef2ebcd..1eeab9688f3 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/coverage/coverage-manager.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/coverage/coverage-manager.ts @@ -24,6 +24,7 @@ import chalk from "chalk"; import { createHardhatRuntimeEnvironment } from "../../../../src/hre.js"; import { CoverageManagerImplementation } from "../../../../src/internal/builtin-plugins/coverage/coverage-manager.js"; +import { setCoverageManager } from "../../../../src/internal/builtin-plugins/coverage/helpers.js"; import { COVERAGE_TEST_SCENARIO_DO_WHILE_LOOP } from "../../../fixture-projects/coverage/contracts/do-while-loop/coverage-edr-info.js"; import { COVERAGE_TEST_SCENARIO_FOR_LOOP } from "../../../fixture-projects/coverage/contracts/for-loop/coverage-edr-info.js"; import { COVERAGE_TEST_SCENARIO_FUNCTIONS } from "../../../fixture-projects/coverage/contracts/functions/coverage-edr-info.js"; @@ -388,13 +389,16 @@ describe("CoverageManagerImplementation", () => { }); describe("CoverageManagerImplementation - report data processing", () => { - // - // The following tests use fixture projects to validate coverage report generation. - // For each scenario, there is a .sol file containing a specific feature (e.g. if/else condition, while loop, etc.) - // and a .t.sol test file that verifies that feature. - // The result of the coverage processing is compared against the expected output defined in the same directory - // where these Solidity files are located. - // + disableConsole(); + + /* + * The following tests use fixture projects to validate coverage report + * generation. For each scenario, there is a .sol file containing a specific + * feature (e.g. if/else condition, while loop, etc.) and a .t.sol test file + * that verifies that feature. The result of the coverage processing is + * compared against the expected output defined in the same directory where + * these Solidity files are located. + */ const testScenarios: CoverageTestScenario[] = [ COVERAGE_TEST_SCENARIO_DO_WHILE_LOOP, COVERAGE_TEST_SCENARIO_FOR_LOOP, @@ -422,9 +426,7 @@ describe("CoverageManagerImplementation - report data processing", () => { hre.globalOptions.coverage = true; - /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions - -- For the test we need to access to the hidden _coverage property */ - (hre as any)._coverage = coverageManagerTmp; + setCoverageManager(hre, coverageManagerTmp); await hre.tasks.getTask(["compile"]).run({ quiet: true, @@ -489,9 +491,7 @@ describe("report generation", () => { hre.globalOptions.coverage = true; - /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions - -- For the test we need to access to the hidden _coverage property */ - (hre as any)._coverage = coverageManagerTmp; + setCoverageManager(hre, coverageManagerTmp); await hre.tasks.getTask(["compile"]).run({ quiet: true, diff --git a/v-next/hardhat/test/internal/builtin-plugins/gas-analytics/gas-analytics-manager.ts b/v-next/hardhat/test/internal/builtin-plugins/gas-analytics/gas-analytics-manager.ts index 047b192ae3e..fa94756d408 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/gas-analytics/gas-analytics-manager.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/gas-analytics/gas-analytics-manager.ts @@ -4,6 +4,7 @@ import assert from "node:assert/strict"; import path from "node:path"; import { afterEach, before, describe, it } from "node:test"; +import { disableConsole } from "@nomicfoundation/hardhat-test-utils"; import { emptyDir, getAllFilesMatching, @@ -21,6 +22,8 @@ import { } from "../../../../src/internal/builtin-plugins/gas-analytics/gas-analytics-manager.js"; describe("gas-analytics-manager", () => { + disableConsole(); + describe("GasAnalyticsManager", () => { let tmpDir: string; before(async () => { @@ -221,6 +224,36 @@ describe("gas-analytics-manager", () => { assert.deepEqual(newManager.gasMeasurements[1], measurement2); }); + it("should load gas measurements from multiple IDs", async () => { + const manager = new GasAnalyticsManagerImplementation(tmpDir); + const measurement1: GasMeasurement = { + type: "function", + contractFqn: "project/contracts/MyContract.sol:MyContract", + functionSig: "transfer(address,uint256)", + gas: 25000, + }; + const measurement2: GasMeasurement = { + type: "function", + contractFqn: "project/contracts/MyContract.sol:MyContract", + functionSig: "approve(address,uint256)", + gas: 46000, + }; + + manager.addGasMeasurement(measurement1); + await manager.saveGasMeasurements("runner-1"); + + manager.gasMeasurements = []; + manager.addGasMeasurement(measurement2); + await manager.saveGasMeasurements("runner-2"); + + const newManager = new GasAnalyticsManagerImplementation(tmpDir); + await newManager._loadGasMeasurements("runner-1", "runner-2"); + + assert.equal(newManager.gasMeasurements.length, 2); + assert.deepEqual(newManager.gasMeasurements[0], measurement1); + assert.deepEqual(newManager.gasMeasurements[1], measurement2); + }); + it("should load gas measurements from multiple files", async () => { const manager = new GasAnalyticsManagerImplementation(tmpDir); const measurement1: GasMeasurement = { @@ -253,6 +286,89 @@ describe("gas-analytics-manager", () => { }); }); + describe("reportGasStats", () => { + afterEach(async () => { + await emptyDir(tmpDir); + }); + + it("should not generate output when report is disabled", async (t) => { + const consoleMock = t.mock.method(console, "log"); + const manager = new GasAnalyticsManagerImplementation(tmpDir); + manager.addGasMeasurement({ + type: "function", + contractFqn: "project/contracts/MyContract.sol:MyContract", + functionSig: "transfer(address,uint256)", + gas: 25000, + }); + await manager.saveGasMeasurements("test-id"); + + manager.disableReport(); + await manager.reportGasStats("test-id"); + + assert.equal(consoleMock.mock.callCount(), 0); + }); + + it("should generate output after enableReport is called", async (t) => { + const consoleMock = t.mock.method(console, "log"); + const manager = new GasAnalyticsManagerImplementation(tmpDir); + manager.addGasMeasurement({ + type: "function", + contractFqn: "project/contracts/MyContract.sol:MyContract", + functionSig: "transfer(address,uint256)", + gas: 25000, + }); + await manager.saveGasMeasurements("test-id"); + + manager.disableReport(); + manager.enableReport(); + await manager.reportGasStats("test-id"); + + assert.ok( + consoleMock.mock.callCount() > 0, + "Should have generated output", + ); + const output = consoleMock.mock.calls + .map((call) => String(call.arguments[0] ?? "")) + .join("\n"); + assert.ok( + output.includes("transfer"), + "Report should contain the function name", + ); + }); + + it("should aggregate data from multiple runner IDs", async (t) => { + const consoleMock = t.mock.method(console, "log"); + const manager = new GasAnalyticsManagerImplementation(tmpDir); + + manager.addGasMeasurement({ + type: "function", + contractFqn: "project/contracts/MyContract.sol:MyContract", + functionSig: "transfer(address,uint256)", + gas: 25000, + }); + await manager.saveGasMeasurements("runner-1"); + + manager.gasMeasurements = []; + manager.addGasMeasurement({ + type: "function", + contractFqn: "project/contracts/MyContract.sol:MyContract", + functionSig: "transfer(address,uint256)", + gas: 35000, + }); + await manager.saveGasMeasurements("runner-2"); + + await manager.reportGasStats("runner-1", "runner-2"); + + const output = consoleMock.mock.calls + .map((call) => String(call.arguments[0] ?? "")) + .join("\n"); + assert.ok( + output.includes("25000") && output.includes("35000"), + "Report should contain the numbers from both runners as they should be displayed as min/max for the same function call", + ); + }); + }); + describe("_aggregateGasMeasurements", () => { it("should return empty map for no measurements", () => { const manager = new GasAnalyticsManagerImplementation(tmpDir); diff --git a/v-next/hardhat/test/internal/builtin-plugins/test/task-action.ts b/v-next/hardhat/test/internal/builtin-plugins/test/task-action.ts index 6088873c5eb..da98640040d 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/test/task-action.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/test/task-action.ts @@ -1,8 +1,11 @@ +import type { HardhatPlugin } from "../../../../src/types/plugins.js"; + import assert from "node:assert/strict"; import { afterEach, describe, it } from "node:test"; import { overrideTask, task } from "../../../../src/config.js"; import { createHardhatRuntimeEnvironment } from "../../../../src/hre.js"; +import { getGasAnalyticsManager } from "../../../../src/internal/builtin-plugins/gas-analytics/helpers.js"; import { ArgumentType } from "../../../../src/types/arguments.js"; import { successfulResult, errorResult } from "../../../../src/utils/result.js"; @@ -289,4 +292,62 @@ describe("test/task-action", function () { assert.deepEqual(result, { success: true, value: undefined }); }); }); + + describe("gas stats reporting only includes data from subtasks that ran", function () { + it("should not include stale data from a skipped runner in the gas stats report", async (t) => { + const consoleMock = t.mock.method(console, "log", () => {}); + + // Plugin that maps "runner-a-test.ts" → "runner-a", leaving "runner-b" unregistered + const fileMapperPlugin: HardhatPlugin = { + id: "test-file-mapper", + hookHandlers: { + test: async () => ({ + default: async () => ({ + registerFileForTestRunner: async (context, filePath, next) => { + if (filePath === "runner-a-test.ts") return "runner-a"; + return next(context, filePath); + }, + }), + }), + }, + }; + + const hre = await createHardhatRuntimeEnvironment( + { + plugins: [fileMapperPlugin], + tasks: [ + solidityNoOp, + mockRunner("runner-a", () => undefined), + mockRunner("runner-b", () => undefined), + ], + }, + { gasStats: true }, + ); + + // Simulate a stale previous run: runner-b has data saved to disk + const gasAnalytics = getGasAnalyticsManager(hre); + gasAnalytics.addGasMeasurement({ + type: "function", + contractFqn: "project/contracts/MyContract.sol:MyContract", + functionSig: "staleFunctionFromRunnerB()", + gas: 99999, + }); + await gasAnalytics.saveGasMeasurements("runner-b"); + + // Run only testFiles mapped to runner-a — runner-b is skipped + await hre.tasks.getTask("test").run({ + noCompile: true, + testFiles: ["runner-a-test.ts"], + }); + + const output = consoleMock.mock.calls + .map((call) => String(call.arguments[0] ?? "")) + .join("\n"); + + assert.ok( + !output.includes("staleFunctionFromRunnerB"), + "Gas stats report should NOT include stale data from runner-b which was skipped", + ); + }); + }); });