Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/social-seal-do.md
Original file line number Diff line number Diff line change
@@ -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)).
31 changes: 31 additions & 0 deletions v-next/hardhat/src/internal/builtin-plugins/coverage/helpers.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
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<Partial<HardhatRuntimeEnvironmentHooks>> => ({
created: async (context, hre) => {
if (context.globalOptions.coverage) {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -80,13 +77,7 @@ export default async (): Promise<Partial<SolidityHooks>> => ({
}
}

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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Partial<TestHooks>> => ({
onTestRunStart: async (context, id, next) => {
Expand All @@ -27,13 +24,7 @@ export async function testRunStart(
id: string,
): Promise<void> {
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);
}
}

Expand All @@ -42,13 +33,7 @@ export async function testWorkerDone(
id: string,
): Promise<void> {
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);
}
}

Expand All @@ -57,12 +42,6 @@ export async function testRunDone(
id: string,
): Promise<void> {
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -78,6 +79,10 @@ export class GasAnalyticsManagerImplementation implements GasAnalyticsManager {
}

public async reportGasStats(...ids: string[]): Promise<void> {
if (!this.#reportEnabled) {
return;
}

await this._loadGasMeasurements(...ids);

const gasStatsByContract = this._calculateGasStats();
Expand All @@ -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<string> {
const gasMeasurementsPath = path.join(this.#gasStatsPath, id);
await ensureDir(gasMeasurementsPath);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Comment thread
schaable marked this conversation as resolved.

/**
* The following helpers are kept for backward compatibility with older versions
* of test runner plugins (hardhat-mocha, hardhat-node-test-runner) that import
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Partial<HardhatRuntimeEnvironmentHooks>> => ({
created: async (context, hre) => {
Expand All @@ -12,12 +10,7 @@ export default async (): Promise<Partial<HardhatRuntimeEnvironmentHooks>> => ({
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Partial<TestHooks>> => ({
onTestRunStart: async (context, id, next) => {
Expand All @@ -27,13 +24,7 @@ export async function testRunStart(
id: string,
): Promise<void> {
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);
}
}

Expand All @@ -42,13 +33,7 @@ export async function testWorkerDone(
id: string,
): Promise<void> {
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);
}
}

Expand All @@ -57,12 +42,6 @@ export async function testRunDone(
id: string,
): Promise<void> {
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,7 @@ export interface GasAnalyticsManager {
clearGasMeasurements(id: string): Promise<void>;
saveGasMeasurements(id: string): Promise<void>;
reportGasStats(...ids: string[]): Promise<void>;

enableReport(): void;
disableReport(): void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -55,11 +53,6 @@ const runSolidityTests: NewTaskActionFunction<TestActionArguments> = async (
{ testFiles, chainType, grep, noCompile, verbosity, testSummaryIndex },
hre,
): Promise<Result<SolidityTestRunResult, SolidityTestRunResult>> => {
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";

Expand Down Expand Up @@ -143,14 +136,15 @@ const runSolidityTests: NewTaskActionFunction<TestActionArguments> = 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[]) => {
const tags = coverageData.map((tag) =>
Buffer.from(tag).toString("hex"),
);

await hre._coverage.addData(tags);
await coverage.addData(tags);
},
},
};
Expand Down Expand Up @@ -232,8 +226,9 @@ const runSolidityTests: NewTaskActionFunction<TestActionArguments> = async (
testContractFqns,
);

const gasAnalytics = getGasAnalyticsManager(hre);
for (const measurement of gasMeasurements) {
hre._gasAnalytics.addGasMeasurement(measurement);
gasAnalytics.addGasMeasurement(measurement);
}
}
})
Expand Down
Loading
Loading