diff --git a/.changeset/cyan-drinks-beg.md b/.changeset/cyan-drinks-beg.md new file mode 100644 index 00000000000..b6a3cebf870 --- /dev/null +++ b/.changeset/cyan-drinks-beg.md @@ -0,0 +1,6 @@ +--- +"@nomicfoundation/hardhat-utils": patch +"hardhat": patch +--- + +Added `--verbosity` (and `-v`, `-vv`, and the other shorthands) to all tasks, including TypeScript tests ([7983](https://github.com/NomicFoundation/hardhat/pull/7983)), ([7963](https://github.com/NomicFoundation/hardhat/issues/7963)). diff --git a/v-next/example-project/contracts/CallTypes.sol b/v-next/example-project/contracts/CallTypes.sol new file mode 100644 index 00000000000..94b330aff94 --- /dev/null +++ b/v-next/example-project/contracts/CallTypes.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "hardhat/console.sol"; + +/// @notice Demonstrates different EVM call types for trace output testing. +/// Used by scripts/demo-call-types.ts to exercise [CALL], [CREATE], +/// [STATICCALL], [DELEGATECALL], and [EVENT] trace tags. + +/// @notice Helper contract with view and write functions +contract Logic { + event ValueSet(address indexed setter, uint256 value); + + uint256 public value; + + function setValue(uint256 _value) external { + console.log("Logic.setValue:", _value); + value = _value; + emit ValueSet(msg.sender, _value); + } + + function getValue() external view returns (uint256) { + return value; + } + + function pureAdd(uint256 a, uint256 b) external pure returns (uint256) { + return a + b; + } + + function mustBePositive(uint256 _value) external { + require(_value > 0, "Value must be positive"); + console.log("Logic.mustBePositive:", _value); + value = _value; + emit ValueSet(msg.sender, _value); + } +} + +/// @notice Orchestrator that calls Logic, exercising multiple call types +contract Orchestrator { + event Orchestrated(uint256 result); + + Logic public logic; + + constructor(Logic _logic) { + logic = _logic; + } + + /// @notice Regular external CALL to Logic.setValue + function doCall(uint256 _value) external { + logic.setValue(_value); + } + + /// @notice Calls Logic.mustBePositive — reverts when _value == 0 + function doCallThatReverts(uint256 _value) external { + logic.mustBePositive(_value); + } + + /// @notice External view call to Logic → produces STATICCALL + function doStaticCall() external view returns (uint256) { + return logic.getValue(); + } + + /// @notice Explicit delegatecall to Logic.setValue → produces DELEGATECALL trace + function doDelegateCall(uint256 _value) external returns (bool success) { + bytes memory data = abi.encodeWithSelector(Logic.setValue.selector, _value); + (success, ) = address(logic).delegatecall(data); + } + + /// @notice Mixed: DELEGATECALL + CALL (Logic.setValue) + STATICCALL (Logic.getValue) + function doAllCallTypes(uint256 a, uint256 b) external returns (uint256) { + // DELEGATECALL to Logic.pureAdd (uses pureAdd to avoid storage collision) + bytes memory data = abi.encodeWithSelector(Logic.pureAdd.selector, a, b); + address(logic).delegatecall(data); + // CALL to Logic.setValue + logic.setValue(a + b); + // STATICCALL to Logic.getValue + uint256 readBack = logic.getValue(); + emit Orchestrated(readBack); + return readBack; + } +} + +/// @notice Factory that creates contracts in a single transaction +contract CallTypesFactory { + event ContractsDeployed(address indexed logic, address indexed orchestrator); + + function deploy() external returns (address, address) { + Logic logicContract = new Logic(); + Orchestrator orch = new Orchestrator(logicContract); + emit ContractsDeployed(address(logicContract), address(orch)); + return (address(logicContract), address(orch)); + } +} diff --git a/v-next/example-project/scripts/demo-trace-output.ts b/v-next/example-project/scripts/demo-trace-output.ts new file mode 100644 index 00000000000..42fed1f671c --- /dev/null +++ b/v-next/example-project/scripts/demo-trace-output.ts @@ -0,0 +1,178 @@ +/** + * Comprehensive demo of all trace output features. + * + * Run with dedup (normal): + * pnpm hardhat run scripts/demo-trace-output.ts -vvvv + * + * Run without dedup (all traces): + * pnpm hardhat run scripts/demo-trace-output.ts -vvvvv + * + * For ANSI color output (red headers on failure, dim on success): + * FORCE_COLOR=3 pnpm hardhat run scripts/demo-trace-output.ts -vvvv + * + * What to look for: + * - All call kinds: CALL, CREATE, STATICCALL, DELEGATECALL + * - Connection labels: "Trace from connection #0 (default)", "#1 (node)" + * - -vvvv: dedup active — single counter.write.inc() → 1 trace (not 3) + * - -vvvvv: no dedup — estimateGas + sendTx both shown per write + * - Red header on failed RPC, dim on success + * - Batch-mined txs grouped under 1 "Traces from" header + */ +import hre from "hardhat"; + +// ═══════════════════════════════════════════════════════════════════════ +// Setup: two connections to two independent EDR-simulated networks +// ═══════════════════════════════════════════════════════════════════════ +const connA = await hre.network.connect("default"); +const connB = await hre.network.connect("node"); + +const [counterA, counterB, revertContract] = await Promise.all([ + connA.viem.deployContract("Counter", []), + connB.viem.deployContract("Counter", []), + connA.viem.deployContract("Revert", []), +]); + +const logic = await connA.viem.deployContract("Logic", []); +const orchestrator = await connA.viem.deployContract("Orchestrator", [ + logic.address, +]); +const factory = await connA.viem.deployContract("CallTypesFactory", []); + +// ═══════════════════════════════════════════════════════════════════════ +// 1. All call kinds — CALL, CREATE, STATICCALL, DELEGATECALL, events +// ═══════════════════════════════════════════════════════════════════════ +console.log("\n╔══════════════════════════════════════════════════════╗"); +console.log("║ 1 — All call kinds ║"); +console.log("╚══════════════════════════════════════════════════════╝\n"); + +// CALL + event: Orchestrator → Logic.setValue (external CALL, emits ValueSet) +console.log(" 1a. CALL + event: Orchestrator.doCall(42)\n"); +await orchestrator.write.doCall([42n]); + +// STATICCALL: Orchestrator → Logic.getValue (view → STATICCALL) +console.log("\n 1b. STATICCALL: Orchestrator.doStaticCall()\n"); +await orchestrator.read.doStaticCall(); + +// CREATE: deploy new contracts inline (via CallTypesFactory) +console.log("\n 1c. CREATE: CallTypesFactory.deploy()\n"); +await factory.write.deploy(); + +// Mixed: DELEGATECALL + CALL + STATICCALL + event in one transaction +console.log( + "\n 1d. Mixed (all types in 1 tx): Orchestrator.doAllCallTypes(7, 3)\n", +); +await orchestrator.write.doAllCallTypes([7n, 3n]); + +// DELEGATECALL: Orchestrator → Logic.setValue via delegatecall +// NOTE: This must come last — delegatecall overwrites Orchestrator's storage +// (slot 0 = logic address), making subsequent calls to logic.* fail. +console.log("\n 1e. DELEGATECALL: Orchestrator.doDelegateCall(42)\n"); +await orchestrator.write.doDelegateCall([42n]); + +// ═══════════════════════════════════════════════════════════════════════ +// 2. Multi-connection — sequential txs on two networks +// Headers should show different connection labels / colors +// ═══════════════════════════════════════════════════════════════════════ +console.log("\n╔══════════════════════════════════════════════════════╗"); +console.log("║ 2 — Multi-connection, sequential ║"); +console.log("╚══════════════════════════════════════════════════════╝\n"); + +await counterA.write.inc(); +await counterB.write.inc(); + +// ═══════════════════════════════════════════════════════════════════════ +// 3. Multi-connection — concurrent txs on both networks +// Traces from each connection are atomic (no interleaving) +// ═══════════════════════════════════════════════════════════════════════ +console.log("\n╔══════════════════════════════════════════════════════╗"); +console.log("║ 3 — Multi-connection, concurrent (3 txs each) ║"); +console.log("╚══════════════════════════════════════════════════════╝\n"); + +await Promise.all([ + counterA.write.inc(), + counterA.write.inc(), + counterA.write.inc(), + counterB.write.inc(), + counterB.write.inc(), + counterB.write.inc(), +]); + +// ═══════════════════════════════════════════════════════════════════════ +// 4. Batch mining with evm_mine — grouped traces under a single header +// "Traces from connection #N (network): evm_mine" (plural) +// ═══════════════════════════════════════════════════════════════════════ +console.log("\n╔══════════════════════════════════════════════════════╗"); +console.log("║ 4 — evm_mine: 5 txs in one block (grouping demo) ║"); +console.log("╚══════════════════════════════════════════════════════╝\n"); +console.log(' → expect 1 "Traces from" header + 5 traces below it\n'); + +await connA.provider.request({ method: "evm_setAutomine", params: [false] }); +await Promise.all(Array.from({ length: 5 }, () => counterA.write.inc())); +await connA.provider.request({ method: "evm_mine", params: [] }); +await connA.provider.request({ method: "evm_setAutomine", params: [true] }); + +// ═══════════════════════════════════════════════════════════════════════ +// 5. Failed RPC calls — header should be red, method name highlighted +// ═══════════════════════════════════════════════════════════════════════ +console.log("\n╔══════════════════════════════════════════════════════╗"); +console.log("║ 5 — Failing calls (red header / method name) ║"); +console.log("╚══════════════════════════════════════════════════════╝\n"); + +// 5a. Revert contract with known error message +console.log(" 5a. Revert.boom() — reverts with 'Boom':\n"); +try { + await revertContract.read.boom(); +} catch (e: any) { + console.log(` → Reverted: ${e.details ?? e.message}\n`); +} + +// 5b. Invalid selector on Counter (no fallback) +console.log(" 5b. Invalid selector 0xdeadbeef on Counter:\n"); +try { + await connA.provider.request({ + method: "eth_call", + params: [{ to: counterA.address, data: "0xdeadbeef" }, "latest"], + }); +} catch { + console.log(" → Reverted as expected\n"); +} + +// 5c. Orchestrator.doCallThatReverts(0) — nested revert +// Deploy fresh orchestrator since the earlier doDelegateCall corrupted storage +console.log(" 5c. Orchestrator.doCallThatReverts(0) — nested revert:\n"); +const logic2 = await connA.viem.deployContract("Logic", []); +const orch2 = await connA.viem.deployContract("Orchestrator", [logic2.address]); +try { + await orch2.write.doCallThatReverts([0n]); +} catch (e: any) { + console.log(` → Reverted: ${e.details ?? e.message}\n`); +} + +// ═══════════════════════════════════════════════════════════════════════ +// 6. Deduplication — single write triggers estimateGas + sendTx + receipt +// but only ONE trace should appear +// ═══════════════════════════════════════════════════════════════════════ +console.log("\n╔══════════════════════════════════════════════════════╗"); +console.log("║ 6 — Deduplication ║"); +console.log("╚══════════════════════════════════════════════════════╝\n"); +console.log(" A single counter.write.inc() triggers 3 RPC calls:"); +console.log(" 1. eth_estimateGas → suppressed at -vvvv, shown at -vvvvv"); +console.log(" 2. eth_sendTransaction → always shown (this is the real tx)"); +console.log(" 3. eth_getTransactionReceipt → no traces (read-only)"); +console.log( + " At -vvvv: 1 trace. At -vvvvv: 2 traces (estimateGas + sendTx).\n", +); + +await counterA.write.inc(); + +// ═══════════════════════════════════════════════════════════════════════ +// Summary +// ═══════════════════════════════════════════════════════════════════════ +console.log("\n╔══════════════════════════════════════════════════════╗"); +console.log("║ Summary ║"); +console.log("╚══════════════════════════════════════════════════════╝"); +console.log(""); +console.log(" -vvvv: dedup + suppression active (1 trace per write)"); +console.log(" -vvvvv: no dedup (estimateGas + sendTx both shown)"); +console.log(""); +console.log("Done."); diff --git a/v-next/hardhat-utils/src/env.ts b/v-next/hardhat-utils/src/env.ts index ba2fb90ee0f..779d089ee56 100644 --- a/v-next/hardhat-utils/src/env.ts +++ b/v-next/hardhat-utils/src/env.ts @@ -7,7 +7,7 @@ import { camelToSnakeCase } from "./string.js"; * with each option adhering to its definition in the globalOptionDefinitions. */ export function setGlobalOptionsAsEnvVariables< - T extends Record, + T extends Record, >(globalOptions: T): void { for (const [name, value] of Object.entries(globalOptions)) { const envName = getEnvVariableNameFromGlobalOption(name); diff --git a/v-next/hardhat/src/internal/builtin-global-options.ts b/v-next/hardhat/src/internal/builtin-global-options.ts index 6bad98597f6..b2c42bfd82f 100644 --- a/v-next/hardhat/src/internal/builtin-global-options.ts +++ b/v-next/hardhat/src/internal/builtin-global-options.ts @@ -1,8 +1,10 @@ import type { GlobalOptionDefinitions } from "../types/global-options.js"; -import { globalFlag, globalOption } from "../config.js"; +import { globalFlag, globalLevel, globalOption } from "../config.js"; import { ArgumentType } from "../types/arguments.js"; +import { DEFAULT_VERBOSITY } from "./constants.js"; + export const BUILTIN_GLOBAL_OPTIONS_DEFINITIONS: GlobalOptionDefinitions = new Map([ [ @@ -49,6 +51,18 @@ export const BUILTIN_GLOBAL_OPTIONS_DEFINITIONS: GlobalOptionDefinitions = }), }, ], + [ + "verbosity", + { + pluginId: "builtin", + option: globalLevel({ + name: "verbosity", + shortName: "v", + description: "Verbosity level of the output.", + defaultValue: DEFAULT_VERBOSITY, + }), + }, + ], [ "version", { diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/edr-provider.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/edr-provider.ts index ebbf7e5c3bb..f36d17b3e51 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/edr-provider.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/edr-provider.ts @@ -15,6 +15,7 @@ import type { import type { RequireField } from "../../../../types/utils.js"; import type { DefaultHDAccountsConfigParams } from "../accounts/constants.js"; import type { JsonRpcRequestWrapperFunction } from "../network-manager.js"; +import type { TraceOutputManager } from "./utils/trace-output.js"; import type { SubscriptionEvent, Response, @@ -24,7 +25,7 @@ import type { GasReportConfig, } from "@nomicfoundation/edr"; -import { ContractDecoder } from "@nomicfoundation/edr"; +import { ContractDecoder, IncludeTraces } from "@nomicfoundation/edr"; import { assertHardhatInvariant, HardhatError, @@ -127,6 +128,10 @@ interface EdrProviderConfig { jsonRpcRequestWrapper?: JsonRpcRequestWrapperFunction; coverageConfig?: CoverageConfig; gasReportConfig?: GasReportConfig; + includeCallTraces?: IncludeTraces; + connectionId: number; + networkName: string; + verbosity: number; } export class EdrProvider extends BaseProvider { @@ -134,6 +139,7 @@ export class EdrProvider extends BaseProvider { #provider: Provider | undefined; #nextRequestId = 1; + readonly #traceOutput: TraceOutputManager | undefined; public static async createContractDecoder( tracingConfig: TracingConfigWithBuffers, @@ -152,6 +158,10 @@ export class EdrProvider extends BaseProvider { jsonRpcRequestWrapper, coverageConfig, gasReportConfig, + includeCallTraces, + verbosity, + connectionId, + networkName, }: EdrProviderConfig): Promise { const printLineFn = loggerConfig.printLineFn ?? printLine; const replaceLastLineFn = loggerConfig.replaceLastLineFn ?? replaceLastLine; @@ -161,6 +171,7 @@ export class EdrProvider extends BaseProvider { coverageConfig, gasReportConfig, chainDescriptors, + includeCallTraces, ); let edrProvider: EdrProvider; @@ -204,7 +215,28 @@ export class EdrProvider extends BaseProvider { contractDecoder, ); - edrProvider = new EdrProvider(provider, jsonRpcRequestWrapper); + const tracesEnabled = + includeCallTraces !== undefined && + includeCallTraces !== IncludeTraces.None; + + let traceOutput: TraceOutputManager | undefined; + if (tracesEnabled) { + const { TraceOutputManager: TraceOutputManagerImpl } = await import( + "./utils/trace-output.js" + ); + traceOutput = new TraceOutputManagerImpl( + printLineFn, + connectionId, + networkName, + verbosity, + ); + } + + edrProvider = new EdrProvider( + provider, + traceOutput, + jsonRpcRequestWrapper, + ); edrProviderWeakRef = new WeakRef(edrProvider); } catch (error) { ensureError(error); @@ -225,12 +257,22 @@ export class EdrProvider extends BaseProvider { */ private constructor( provider: Provider, + traceOutput: TraceOutputManager | undefined, jsonRpcRequestWrapper?: JsonRpcRequestWrapperFunction, ) { super(); this.#provider = provider; + this.#traceOutput = traceOutput; this.#jsonRpcRequestWrapper = jsonRpcRequestWrapper; + + // After a snapshot revert, the same transactions may run again. + // Reset traced hashes so their traces are printed a second time. + if (this.#traceOutput !== undefined) { + this.on(EDR_NETWORK_REVERT_SNAPSHOT_EVENT, () => { + this.#traceOutput?.clearTracedHashes(); + }); + } } public async request( @@ -293,6 +335,7 @@ export class EdrProvider extends BaseProvider { this.removeAllListeners(); // Clear the provider reference to help with garbage collection this.#provider = undefined; + this.#traceOutput?.clearTracedHashes(); } public async addCompilationResult( @@ -313,8 +356,11 @@ export class EdrProvider extends BaseProvider { async #handleEdrResponse( edrResponse: Response, + method: string, + params?: unknown[], ): Promise { let jsonRpcResponse: JsonRpcResponse; + let txHash: string | undefined; if (typeof edrResponse.data === "string") { jsonRpcResponse = JSON.parse(edrResponse.data); @@ -326,6 +372,12 @@ export class EdrProvider extends BaseProvider { const responseError = jsonRpcResponse.error; let error; + // Grab the tx hash so trace deduplication can recognize this transaction later + const errorData = responseError.data; + if (isEdrProviderErrorData(errorData)) { + txHash = errorData.transactionHash; + } + const stackTrace = edrResponse.stackTrace(); if (stackTrace?.kind === "StackTrace") { @@ -366,11 +418,33 @@ export class EdrProvider extends BaseProvider { error.data = responseError.data; } + this.#traceOutput?.outputCallTraces(edrResponse, method, txHash, true); + /* eslint-disable-next-line no-restricted-syntax -- we may throw non-Hardhat errors inside of an EthereumProvider */ throw error; } + if (this.#traceOutput !== undefined) { + // Output call traces for successful responses. The tx hash is resolved + // from the response/params so the trace manager can deduplicate. + + if ( + method === "eth_sendTransaction" || + method === "eth_sendRawTransaction" + ) { + txHash = + typeof jsonRpcResponse.result === "string" + ? jsonRpcResponse.result + : undefined; + } else if (method === "eth_getTransactionReceipt") { + // params[0] is the tx hash being queried — used to dedup receipt polling + txHash = typeof params?.[0] === "string" ? params[0] : undefined; + } + + this.#traceOutput.outputCallTraces(edrResponse, method, txHash, false); + } + return jsonRpcResponse; } @@ -422,7 +496,11 @@ export class EdrProvider extends BaseProvider { throw new UnknownError(error.message, error); } - return this.#handleEdrResponse(edrResponse); + return this.#handleEdrResponse( + edrResponse, + request.method, + Array.isArray(request.params) ? request.params : undefined, + ); } } @@ -431,6 +509,7 @@ export async function getProviderConfig( coverageConfig: CoverageConfig | undefined, gasReportConfig: GasReportConfig | undefined, chainDescriptors: ChainDescriptorsConfig, + includeCallTraces?: IncludeTraces, ): Promise { const specId = hardhatHardforkToEdrSpecId( networkConfig.hardfork, @@ -477,6 +556,7 @@ export async function getProviderConfig( observability: { codeCoverage: coverageConfig, gasReport: gasReportConfig, + includeCallTraces, }, ownedAccounts: ownedAccounts.map((account) => account.secretKey), precompileOverrides: [], diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/utils/trace-formatters.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/utils/trace-formatters.ts new file mode 100644 index 00000000000..927e48d59e5 --- /dev/null +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/utils/trace-formatters.ts @@ -0,0 +1,197 @@ +import type { Colorizer } from "../../../../utils/colorizer.js"; +import type { + LogTrace, + CallTrace, + DecodedTraceParameters, +} from "@nomicfoundation/edr"; + +import { LogKind, CallKind, IncludeTraces } from "@nomicfoundation/edr"; +import { bytesToHexString } from "@nomicfoundation/hardhat-utils/hex"; + +type NestedArray = Array>; + +export function verbosityToIncludeTraces(verbosity: number): IncludeTraces { + if (verbosity >= 4) { + return IncludeTraces.All; + } else if (verbosity >= 3) { + return IncludeTraces.Failing; + } + + return IncludeTraces.None; +} + +export function formatTraces( + traces: CallTrace[], + prefix: string, + colorizer: Colorizer, +): string { + const lines = traces.map((trace) => formatTrace(trace, colorizer)); + const formattedTraces = formatNestedArray(lines, prefix); + // Remove the trailing newline + return formattedTraces.slice(0, -1); +} + +function formatInputs( + inputs: DecodedTraceParameters | Uint8Array, + color?: (text: string) => string, +): string | undefined { + if (inputs instanceof Uint8Array) { + return inputs.length > 0 ? bytesToHexString(inputs) : undefined; + } else { + const formattedName = + color !== undefined ? color(inputs.name) : inputs.name; + return `${formattedName}(${inputs.arguments.join(", ")})`; + } +} + +function formatOutputs(outputs: string | Uint8Array): string | undefined { + if (outputs instanceof Uint8Array) { + return outputs.length > 0 ? bytesToHexString(outputs) : undefined; + } else { + return outputs; + } +} + +function formatLog(log: LogTrace, colorizer: Colorizer): string[] { + const { parameters } = log; + const tag = colorizer.yellow("[event]"); + const lines = []; + + if (Array.isArray(parameters)) { + const hexValues = parameters.map((bytes) => bytesToHexString(bytes)); + const topicCount = hexValues.length - 1; + + for (let i = 0; i < topicCount; i++) { + const prefix = i === 0 ? `${tag} topic 0` : ` topic ${i}`; + lines.push(`${prefix}: ${colorizer.cyan(hexValues[i])}`); + } + + if (hexValues.length > 0) { + const dataPrefix = topicCount > 0 ? " data" : `${tag} data`; + lines.push( + `${dataPrefix}: ${colorizer.cyan(hexValues[hexValues.length - 1])}`, + ); + } + } else { + lines.push( + `${tag} ${parameters.name}(${colorizer.cyan(parameters.arguments.join(", "))})`, + ); + } + return lines; +} + +function formatTrace( + trace: CallTrace, + colorizer: Colorizer, +): NestedArray { + const { + success, + address, + contract, + inputs, + gasUsed, + value, + kind, + isCheatcode, + outputs, + } = trace; + let color; + if (isCheatcode) { + color = colorizer.blue; + } else if (success) { + color = colorizer.green; + } else { + color = colorizer.red; + } + + const formattedInputs = formatInputs(inputs, color); + const formattedOutputs = formatOutputs(outputs); + + let openingLine: string; + let closingLine: string | undefined; + if (kind === CallKind.Create) { + openingLine = `[${gasUsed}] ${colorizer.yellow("→ new")} ${contract ?? ""}@${address}`; + // TODO: Uncomment this when the formattedInputs starts containing + // the address of where the contract was deployed instead of the code. + // if (formattedInputs !== undefined) { + // openingLine = `${openingLine}@${formattedInputs}`; + // } + } else { + openingLine = `[${gasUsed}] ${color(contract ?? address)}`; + if (formattedInputs !== undefined) { + openingLine = `${openingLine}::${formattedInputs}`; + } + if (value !== 0n) { + openingLine = `${openingLine} {value: ${value}}`; + } + if (kind === CallKind.StaticCall) { + openingLine = `${openingLine} ${colorizer.yellow("[staticcall]")}`; + } else if (kind === CallKind.DelegateCall) { + openingLine = `${openingLine} ${colorizer.yellow("[delegatecall]")}`; + } else if (kind === CallKind.CallCode) { + openingLine = `${openingLine} ${colorizer.yellow("[callcode]")}`; + } + } + if (formattedOutputs !== undefined) { + if ( + formattedOutputs === "EvmError: Revert" || + formattedOutputs.startsWith("revert:") + ) { + closingLine = `${color("←")} ${color("[Revert]")} ${formattedOutputs}`; + } else { + closingLine = `${color("←")} ${formattedOutputs}`; + } + } + + const lines = []; + lines.push(openingLine); + for (const child of trace.children) { + if (child.kind === LogKind.Log) { + lines.push(formatLog(child, colorizer)); + } else { + lines.push(formatTrace(child, colorizer)); + } + } + if (closingLine !== undefined) { + lines.push([closingLine]); + } + return lines; +} + +function formatNestedArray( + data: NestedArray, + prefix = "", + isTopLevel = true, +): string { + let output = ""; + + for (let i = 0; i < data.length; i++) { + const item = data[i]; + + if (Array.isArray(item) && typeof item[0] === "string") { + const [label, ...children] = item; + + if (isTopLevel) { + // Blank line between top-level traces + if (i > 0) { + output += "\n"; + } + + output += `${prefix}${label}\n`; + output += formatNestedArray(children, prefix, false); + } else { + const isLast = i === data.length - 1; + const connector = isLast ? " └─ " : " ├─ "; + const childPrefix = isLast ? " " : " │ "; + output += `${prefix}${connector}${label}\n`; + output += formatNestedArray(children, prefix + childPrefix, false); + } + } else if (typeof item === "string") { + const isLast = i === data.length - 1; + const connector = isLast ? " └─ " : " ├─ "; + output += `${prefix}${connector}${item}\n`; + } + } + + return output; +} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/utils/trace-output.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/utils/trace-output.ts new file mode 100644 index 00000000000..30b6539646f --- /dev/null +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/utils/trace-output.ts @@ -0,0 +1,137 @@ +import type { Response } from "@nomicfoundation/edr"; + +import chalk from "chalk"; +import debug from "debug"; + +import { formatTraces } from "./trace-formatters.js"; + +const log = debug("hardhat:core:hardhat-network:provider"); + +// Rotating palette for per-connection coloring of trace headers. +const LABEL_COLORS: Array<(text: string) => string> = [ + chalk.cyan, + chalk.magenta, + chalk.blueBright, + chalk.yellowBright, + chalk.cyanBright, + chalk.magentaBright, +]; + +// Keyed by `network name` (not connection label) so the map stays bounded +// by the number of distinct networks, not the number of connections. +const networkColorMap = new Map string>(); + +// These methods run a simulation before the actual transaction. We skip +// their traces on success to avoid duplicates, but still show them on +// failure since the real transaction won't be sent. +const TRACE_SUPPRESSED_METHODS = new Set(["eth_estimateGas"]); + +// Bounded set: receipt-polling deduplication only needs a small window. +// Once the cap is reached the set is cleared so memory stays bounded +// in long-running nodes. +const TRACED_TX_HASHES_CAP = 1024; + +/** + * Manages trace output formatting, deduplication, and coloring for a single + * provider connection. + */ +export class TraceOutputManager { + readonly #printLineFn: (line: string) => void; + readonly #connectionLabel: string; + readonly #labelColor: (text: string) => string; + readonly #verbosity: number; + readonly #tracedTxHashes = new Set(); + + constructor( + printLineFn: (line: string) => void, + connectionId: number, + networkName: string, + verbosity: number, + ) { + this.#printLineFn = printLineFn; + this.#connectionLabel = `connection #${connectionId} (${networkName})`; + this.#labelColor = this.#colorForNetwork(networkName); + this.#verbosity = verbosity; + } + + /** + * Output call traces from an EDR response, applying deduplication and + * suppression rules based on the verbosity level. + */ + public outputCallTraces( + edrResponse: Response, + method: string, + txHash: string | undefined, + failed: boolean, + ): void { + try { + // At verbosity < 5, suppress simulation-only methods on success and + // deduplicate traces for the same transaction. At verbosity >= 5 + // (#showAllTraces), every RPC call with traces is shown. + + if (this.#verbosity < 5) { + // Skip successful simulation-only methods, their trace will appear + // again in the subsequent eth_sendTransaction. Failed simulations + // are shown because the sendTransaction may never happen. + if (!failed && TRACE_SUPPRESSED_METHODS.has(method)) { + return; + } + + // Dedup: skip if we already traced this transaction. + // Prevents the same tx appearing multiple times from receipt polling. + if (txHash !== undefined && this.#tracedTxHashes.has(txHash)) { + return; + } + } + + const rawTraces = edrResponse.callTraces(); + + // EDR returns duplicate traces for eth_estimateGas, take only the first. + const callTraces = + TRACE_SUPPRESSED_METHODS.has(method) && rawTraces.length > 1 + ? [rawTraces[0]] + : rawTraces; + + if (callTraces.length === 0) { + return; + } + + if (txHash !== undefined) { + if (this.#tracedTxHashes.size >= TRACED_TX_HASHES_CAP) { + this.#tracedTxHashes.clear(); + } + + this.#tracedTxHashes.add(txHash); + } + + const coloredLabel = this.#labelColor(this.#connectionLabel); + const prefix = callTraces.length > 1 ? "Traces from" : "Trace from"; + const coloredPrefix = this.#labelColor(prefix); + const styledMethod = failed ? chalk.red(method) : chalk.dim(method); + const header = `${coloredPrefix} ${coloredLabel}: ${styledMethod}`; + + this.#printLineFn(`${header}\n${formatTraces(callTraces, " ", chalk)}`); + } catch (e) { + log("Failed to get call traces: %O", e); + } + } + + /** + * Clear the dedup set (e.g. on snapshot revert so replayed txs are traced again). + */ + public clearTracedHashes(): void { + this.#tracedTxHashes.clear(); + } + + #colorForNetwork(networkName: string): (text: string) => string { + let color = networkColorMap.get(networkName); + + if (color === undefined) { + const index = networkColorMap.size % LABEL_COLORS.length; + color = LABEL_COLORS[index]; + networkColorMap.set(networkName, color); + } + + return color; + } +} diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/hre.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/hre.ts index 89e98c28ee0..f061e35fec9 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/hre.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/hre.ts @@ -50,5 +50,6 @@ async function createNetworkManager( hre.config.chainDescriptors, hre.globalOptions.config, hre.config.paths.root, + hre.globalOptions.verbosity, ); } diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/network-manager.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/network-manager.ts index b4ff40cc88a..ecfcb356bc8 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/network-manager.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/network-manager.ts @@ -38,6 +38,7 @@ import { JsonRpcServerImplementation } from "../node/json-rpc/server.js"; import { EdrProvider } from "./edr/edr-provider.js"; import { getHardforks } from "./edr/types/hardfork.js"; import { edrGasReportToHardhatGasMeasurements } from "./edr/utils/convert-to-edr.js"; +import { verbosityToIncludeTraces } from "./edr/utils/trace-formatters.js"; import { HttpProvider } from "./http-provider.js"; import { NetworkConnectionImplementation } from "./network-connection.js"; @@ -56,6 +57,7 @@ export class NetworkManagerImplementation implements NetworkManager { readonly #chainDescriptors: Readonly; readonly #userProvidedConfigPath: Readonly; readonly #projectRoot: string; + readonly #verbosity: number; #nextConnectionId = 0; readonly #contractDecoderMutex = new AsyncMutex(); @@ -71,6 +73,7 @@ export class NetworkManagerImplementation implements NetworkManager { chainDescriptors: ChainDescriptorsConfig, userProvidedConfigPath: string | undefined, projectRoot: string, + verbosity: number, ) { this.#defaultNetwork = defaultNetwork; this.#defaultChainType = defaultChainType; @@ -81,6 +84,7 @@ export class NetworkManagerImplementation implements NetworkManager { this.#chainDescriptors = chainDescriptors; this.#userProvidedConfigPath = userProvidedConfigPath; this.#projectRoot = projectRoot; + this.#verbosity = verbosity; } public async connect< @@ -270,6 +274,8 @@ export class NetworkManagerImplementation implements NetworkManager { "Contract decoder should have been initialized before creating the provider", ); + const includeCallTraces = verbosityToIncludeTraces(this.#verbosity); + return EdrProvider.create({ chainDescriptors: this.#chainDescriptors, // The resolvedNetworkConfig can have its chainType set to `undefined` @@ -289,6 +295,10 @@ export class NetworkManagerImplementation implements NetworkManager { contractDecoder: this.#contractDecoder, coverageConfig, gasReportConfig, + includeCallTraces, + connectionId: networkConnection.id, + networkName: networkConnection.networkName, + verbosity: this.#verbosity, }); } diff --git a/v-next/hardhat/src/internal/builtin-plugins/solidity-test/formatters.ts b/v-next/hardhat/src/internal/builtin-plugins/solidity-test/formatters.ts index 91e0ed629fa..f6f633e6228 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/solidity-test/formatters.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/solidity-test/formatters.ts @@ -1,26 +1,4 @@ -import type { - LogTrace, - ArtifactId, - CallTrace, - DecodedTraceParameters, -} from "@nomicfoundation/edr"; - -import { LogKind, CallKind } from "@nomicfoundation/edr"; -import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors"; -import { bytesToHexString } from "@nomicfoundation/hardhat-utils/hex"; -import chalk from "chalk"; - -export interface Colorizer { - blue: (text: string) => string; - green: (text: string) => string; - red: (text: string) => string; - cyan: (text: string) => string; - yellow: (text: string) => string; - grey: (text: string) => string; - dim: (text: string) => string; -} - -type NestedArray = Array>; +import type { ArtifactId } from "@nomicfoundation/edr"; export function formatArtifactId( artifactId: ArtifactId, @@ -31,193 +9,3 @@ export function formatArtifactId( return `${sourceName}:${artifactId.name}`; } - -export function formatLogs( - logs: string[], - indent: number, - colorizer: Colorizer, -): string { - return colorizer.grey( - logs.map((log) => `${" ".repeat(indent)}${log}`).join("\n"), - ); -} - -function formatInputs( - inputs: DecodedTraceParameters | Uint8Array, - color?: (text: string) => string, -): string | undefined { - if (inputs instanceof Uint8Array) { - return inputs.length > 0 ? bytesToHexString(inputs) : undefined; - } else { - const formattedName = - color !== undefined ? color(inputs.name) : inputs.name; - return `${formattedName}(${inputs.arguments.join(", ")})`; - } -} - -function formatOutputs(outputs: string | Uint8Array): string | undefined { - if (outputs instanceof Uint8Array) { - return outputs.length > 0 ? bytesToHexString(outputs) : undefined; - } else { - return outputs; - } -} - -function formatLog(log: LogTrace, colorizer: Colorizer = chalk): string[] { - const { parameters } = log; - const lines = []; - if (Array.isArray(parameters)) { - const topics = parameters.map((topic) => bytesToHexString(topic)); - if (topics.length > 0) { - lines.push(`emit topic 0: ${colorizer.cyan(topics[0])}`); - } - for (let i = 1; i < topics.length - 1; i++) { - lines.push(` topic ${i}: ${colorizer.cyan(topics[i])}`); - } - if (topics.length > 1) { - lines.push(` data: ${colorizer.cyan(topics[topics.length - 1])}`); - } - } else { - lines.push( - `emit ${parameters.name}(${colorizer.cyan(parameters.arguments.join(", "))})`, - ); - } - return lines; -} - -function formatKind(kind: CallKind): string | undefined { - assertHardhatInvariant( - kind !== CallKind.Create, - "Unexpected call kind 'Create'", - ); - - switch (kind) { - case CallKind.Call: - return undefined; - case CallKind.CallCode: - return "callcode"; - case CallKind.DelegateCall: - return "delegatecall"; - case CallKind.StaticCall: - return "staticcall"; - } -} - -function formatTrace( - trace: CallTrace, - colorizer: Colorizer, -): NestedArray { - const { - success, - address, - contract, - inputs, - gasUsed, - value, - kind, - isCheatcode, - outputs, - } = trace; - let color; - if (isCheatcode) { - color = colorizer.blue; - } else if (success) { - color = colorizer.green; - } else { - color = colorizer.red; - } - - const formattedInputs = formatInputs(inputs, color); - const formattedOutputs = formatOutputs(outputs); - - let openingLine: string; - let closingLine: string | undefined; - if (kind === CallKind.Create) { - openingLine = `[${gasUsed}] ${colorizer.yellow("→ new")} ${contract ?? ""}@${address}`; - // TODO: Uncomment this when the formattedInputs starts containing - // the address of where the contract was deployed instead of the code. - // if (formattedInputs !== undefined) { - // openingLine = `${openingLine}@${formattedInputs}`; - // } - } else { - const formattedKind = formatKind(kind); - openingLine = `[${gasUsed}] ${color(contract ?? address)}`; - if (formattedInputs !== undefined) { - openingLine = `${openingLine}::${formattedInputs}`; - } - if (value !== BigInt(0)) { - openingLine = `${openingLine} {value: ${value}}`; - } - if (formattedKind !== undefined) { - openingLine = `${openingLine} ${colorizer.yellow(`[${formattedKind}]`)}`; - } - } - if (formattedOutputs !== undefined) { - if ( - formattedOutputs === "EvmError: Revert" || - formattedOutputs.startsWith("revert:") - ) { - closingLine = `${color("←")} ${color("[Revert]")} ${formattedOutputs}`; - } else { - closingLine = `${color("←")} ${formattedOutputs}`; - } - } - - const lines = []; - lines.push(openingLine); - for (const child of trace.children) { - if (child.kind === LogKind.Log) { - lines.push(formatLog(child)); - } else { - lines.push(formatTrace(child, colorizer)); - } - } - if (closingLine !== undefined) { - lines.push([closingLine]); - } - return lines; -} - -function formatNestedArray( - data: NestedArray, - prefix = "", - isTopLevel = true, -): string { - let output = ""; - - for (let i = 0; i < data.length; i++) { - const item = data[i]; - - if (Array.isArray(item) && typeof item[0] === "string") { - const [label, ...children] = item; - - if (isTopLevel) { - output += `${prefix}${label}\n`; - output += formatNestedArray(children, prefix, false); - } else { - const isLast = i === data.length - 1; - const connector = isLast ? " └─ " : " ├─ "; - const childPrefix = isLast ? " " : " │ "; - output += `${prefix}${connector}${label}\n`; - output += formatNestedArray(children, prefix + childPrefix, false); - } - } else if (typeof item === "string") { - const isLast = i === data.length - 1; - const connector = isLast ? " └─ " : " ├─ "; - output += `${prefix}${connector}${item}\n`; - } - } - - return output; -} - -export function formatTraces( - traces: CallTrace[], - prefix: string, - colorizer: Colorizer, -): string { - const lines = traces.map((trace) => formatTrace(trace, colorizer)); - const formattedTraces = formatNestedArray(lines, prefix); - // Remove the trailing newline - return formattedTraces.slice(0, -1); -} diff --git a/v-next/hardhat/src/internal/builtin-plugins/solidity-test/helpers.ts b/v-next/hardhat/src/internal/builtin-plugins/solidity-test/helpers.ts index 4645f032c40..00e3d2188cc 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/solidity-test/helpers.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/solidity-test/helpers.ts @@ -2,6 +2,7 @@ import type { RunOptions } from "./runner.js"; import type { Abi } from "../../../types/artifacts.js"; import type { ChainType } from "../../../types/network.js"; import type { SolidityTestConfig } from "../../../types/test.js"; +import type { Colorizer } from "../../utils/colorizer.js"; import type { SolidityTestRunnerConfigArgs, PathPermission, @@ -12,7 +13,6 @@ import type { import { opGenesisState, l1GenesisState, - IncludeTraces, FsAccessPermission, CollectStackTraces, opHardforkFromString, @@ -24,8 +24,9 @@ import chalk from "chalk"; import { DEFAULT_VERBOSITY, OPTIMISM_CHAIN_TYPE } from "../../constants.js"; import { resolveHardfork } from "../network-manager/config-resolution.js"; import { hardhatHardforkToEdrSpecId } from "../network-manager/edr/utils/convert-to-edr.js"; +import { verbosityToIncludeTraces } from "../network-manager/edr/utils/trace-formatters.js"; -import { type Colorizer, formatArtifactId } from "./formatters.js"; +import { formatArtifactId } from "./formatters.js"; interface SolidityTestConfigParams { chainType: ChainType; @@ -98,12 +99,7 @@ export async function solidityTestConfigToSolidityTestRunnerConfigArgs({ ? opGenesisState(opHardforkFromString(resolvedHardfork)) : l1GenesisState(l1HardforkFromString(resolvedHardfork)); - let includeTraces: IncludeTraces = IncludeTraces.None; - if (verbosity >= 5) { - includeTraces = IncludeTraces.All; - } else if (verbosity >= 3) { - includeTraces = IncludeTraces.Failing; - } + const includeTraces = verbosityToIncludeTraces(verbosity); const blockGasLimit = config.blockGasLimit === false ? undefined : config.blockGasLimit; diff --git a/v-next/hardhat/src/internal/builtin-plugins/solidity-test/index.ts b/v-next/hardhat/src/internal/builtin-plugins/solidity-test/index.ts index 9d1cde8c6f2..ad8333b017c 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/solidity-test/index.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/solidity-test/index.ts @@ -2,7 +2,6 @@ import type { HardhatPlugin } from "../../../types/plugins.js"; import { ArgumentType } from "hardhat/types/arguments"; -import { DEFAULT_VERBOSITY } from "../../constants.js"; import { task } from "../../core/config.js"; import "./type-extensions.js"; @@ -35,12 +34,6 @@ const hardhatPlugin: HardhatPlugin = { name: "noCompile", description: "Don't compile the project before running the tests", }) - .addLevel({ - name: "verbosity", - shortName: "v", - description: "Verbosity level of the test output", - defaultValue: DEFAULT_VERBOSITY, - }) .addOption({ name: "testSummaryIndex", description: diff --git a/v-next/hardhat/src/internal/builtin-plugins/solidity-test/reporter.ts b/v-next/hardhat/src/internal/builtin-plugins/solidity-test/reporter.ts index b241572a9e4..8dd8f50fbcd 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/solidity-test/reporter.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/solidity-test/reporter.ts @@ -3,6 +3,7 @@ import type { TestReporterResult, TestStatus, } from "./types.js"; +import type { Colorizer } from "../../utils/colorizer.js"; import type { TestResult } from "@nomicfoundation/edr"; import { bytesToHexString } from "@nomicfoundation/hardhat-utils/hex"; @@ -11,12 +12,9 @@ import chalk from "chalk"; import { sendErrorTelemetry } from "../../cli/telemetry/sentry/reporter.js"; import { SolidityTestStackTraceGenerationError } from "../network-manager/edr/stack-traces/stack-trace-generation-errors.js"; import { encodeStackTraceEntry } from "../network-manager/edr/stack-traces/stack-trace-solidity-errors.js"; +import { formatTraces } from "../network-manager/edr/utils/trace-formatters.js"; -import { - type Colorizer, - formatArtifactId, - formatTraces, -} from "./formatters.js"; +import { formatArtifactId } from "./formatters.js"; import { getMessageFromLastStackTraceEntry } from "./stack-trace-solidity-errors.js"; class Indenter { @@ -147,6 +145,8 @@ export async function* testReporter( suiteSuccessCount++; if (verbosity >= 5) { printSetUpTraces = true; + } + if (verbosity >= 4) { printExecutionTraces = true; } break; @@ -184,7 +184,7 @@ export async function* testReporter( if (printSetUpTraces && functionName === "setUp") { return true; } - if (printExecutionTraces && functionName !== "setUp()") { + if (printExecutionTraces && functionName !== "setUp") { return true; } return false; 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 325ec17feba..bf59a423380 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 @@ -41,7 +41,6 @@ interface TestActionArguments { chainType: string; grep?: string; noCompile: boolean; - verbosity: number; testSummaryIndex: number; } @@ -50,12 +49,14 @@ export interface SolidityTestRunResult extends TestRunResult { } const runSolidityTests: NewTaskActionFunction = async ( - { testFiles, chainType, grep, noCompile, verbosity, testSummaryIndex }, + { testFiles, chainType, grep, noCompile, testSummaryIndex }, hre, ): Promise> => { // Set an environment variable that plugins can use to detect when a process is running tests process.env.HH_TEST = "true"; + const verbosity = hre.globalOptions.verbosity; + // Sets the NODE_ENV environment variable to "test" so the code can detect that tests are running // This is done by other JS/TS test frameworks like vitest process.env.NODE_ENV ??= "test"; diff --git a/v-next/hardhat/src/internal/builtin-plugins/test/index.ts b/v-next/hardhat/src/internal/builtin-plugins/test/index.ts index f546f289a30..a61c954066d 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/test/index.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/test/index.ts @@ -1,7 +1,6 @@ import type { HardhatPlugin } from "../../../types/plugins.js"; import { ArgumentType } from "../../../types/arguments.js"; -import { DEFAULT_VERBOSITY } from "../../constants.js"; import { task } from "../../core/config.js"; import "./type-extensions.js"; @@ -33,12 +32,6 @@ const hardhatPlugin: HardhatPlugin = { name: "noCompile", description: "Do not compile the project before running the tests", }) - .addLevel({ - name: "verbosity", - shortName: "v", - description: "Verbosity level of the test output", - defaultValue: DEFAULT_VERBOSITY, - }) .setAction(async () => import("./task-action.js")) .build(), ], 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 d7950c4543a..6613e7781b1 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 @@ -24,7 +24,6 @@ interface TestActionArguments { chainType: string; grep: string | undefined; noCompile: boolean; - verbosity: number; } // Old plugins may only return { failed, passed } without skipped/todo, @@ -51,7 +50,7 @@ function isTestRunResult( } const runAllTests: NewTaskActionFunction = async ( - { testFiles, chainType, grep, noCompile, verbosity, ...otherArgs }, + { testFiles, chainType, grep, noCompile, ...otherArgs }, hre, ): Promise> => { // If this code is executed, it means the user has not specified a test runner. @@ -107,10 +106,6 @@ const runAllTests: NewTaskActionFunction = async ( args.chainType = chainType; } - if (subtask.options.has("verbosity")) { - args.verbosity = verbosity; - } - for (const [key, value] of Object.entries(otherArgs)) { if (subtask.options.has(key)) { args[key] = value; diff --git a/v-next/hardhat/src/internal/utils/colorizer.ts b/v-next/hardhat/src/internal/utils/colorizer.ts new file mode 100644 index 00000000000..d2d524843c5 --- /dev/null +++ b/v-next/hardhat/src/internal/utils/colorizer.ts @@ -0,0 +1,9 @@ +export interface Colorizer { + blue: (text: string) => string; + green: (text: string) => string; + red: (text: string) => string; + cyan: (text: string) => string; + yellow: (text: string) => string; + grey: (text: string) => string; + dim: (text: string) => string; +} diff --git a/v-next/hardhat/src/types/global-options.ts b/v-next/hardhat/src/types/global-options.ts index 6ea53b7d9a5..dfe82254db6 100644 --- a/v-next/hardhat/src/types/global-options.ts +++ b/v-next/hardhat/src/types/global-options.ts @@ -21,6 +21,7 @@ export interface GlobalOptions { help: boolean; init: boolean; showStackTraces: boolean; + verbosity: number; version: boolean; } diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/edr/utils/trace-formatters.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/edr/utils/trace-formatters.ts new file mode 100644 index 00000000000..27efae8ba2e --- /dev/null +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/edr/utils/trace-formatters.ts @@ -0,0 +1,362 @@ +import type { CallTrace, LogTrace } from "@nomicfoundation/edr"; + +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { CallKind, IncludeTraces, LogKind } from "@nomicfoundation/edr"; +import chalk from "chalk"; + +import { + formatTraces, + verbosityToIncludeTraces, +} from "../../../../../../src/internal/builtin-plugins/network-manager/edr/utils/trace-formatters.js"; + +function makeCallTrace(overrides: Partial = {}): CallTrace { + const trace: CallTrace = { + kind: CallKind.Call, + success: true, + isCheatcode: false, + gasUsed: 1000n, + value: 0n, + address: "0x1111111111111111111111111111111111111111", + contract: "TestContract", + inputs: { name: "testFunc", arguments: [] }, + outputs: new Uint8Array(), + children: [], + ...overrides, + }; + return trace; +} + +function makeLogTrace(parameters: LogTrace["parameters"]): LogTrace { + const trace: LogTrace = { kind: LogKind.Log, parameters }; + return trace; +} + +describe("verbosityToIncludeTraces", () => { + it("should return None for verbosity 0", () => { + assert.equal(verbosityToIncludeTraces(0), IncludeTraces.None); + }); + + it("should return None for verbosity 1", () => { + assert.equal(verbosityToIncludeTraces(1), IncludeTraces.None); + }); + + it("should return None for verbosity 2", () => { + assert.equal(verbosityToIncludeTraces(2), IncludeTraces.None); + }); + + it("should return Failing for verbosity 3", () => { + assert.equal(verbosityToIncludeTraces(3), IncludeTraces.Failing); + }); + + it("should return All for verbosity 4", () => { + assert.equal(verbosityToIncludeTraces(4), IncludeTraces.All); + }); + + it("should return All for verbosity 5", () => { + assert.equal(verbosityToIncludeTraces(5), IncludeTraces.All); + }); + + it("should return All for verbosity 6", () => { + assert.equal(verbosityToIncludeTraces(6), IncludeTraces.All); + }); +}); + +describe("formatTraces", () => { + it("should format traces correctly", async () => { + const traces = [ + { + kind: 0, + success: true, + isCheatcode: false, + gasUsed: 127552n, + value: 0n, + address: "0x9Cded789F1564C12102E41634157434dd1De9fE3", + contract: "FailingCounterTest", + inputs: { name: "setUp", arguments: [] }, + outputs: new Uint8Array(), + children: [ + { + kind: 3, + success: true, + isCheatcode: false, + gasUsed: 0n, + value: 0n, + address: "0x7c926CE5743033Cbe6f6cF7D6622EF70e05503A6", + contract: "console", + inputs: { name: "log", arguments: ['"Setting up"'] }, + outputs: new Uint8Array(), + children: [], + }, + { + kind: 4, + success: true, + isCheatcode: false, + gasUsed: 68915n, + value: 0n, + address: "0x373b22261122919Ad39F55ac0475dd0f82Bd2499", + contract: "Counter", + inputs: new Uint8Array([1, 2, 3]), + outputs: "344 bytes of code", + children: [], + }, + { + kind: 3, + success: true, + isCheatcode: false, + gasUsed: 0n, + value: 0n, + address: "0x7c926CE5743033Cbe6f6cF7D6622EF70e05503A6", + contract: "console", + inputs: { name: "log", arguments: ['"Counter set up"'] }, + outputs: new Uint8Array(), + children: [], + }, + ], + }, + { + kind: 0, + success: true, + isCheatcode: false, + gasUsed: 32272n, + value: 0n, + address: "0x9Cded789F1564C12102E41634157434dd1De9fE3", + contract: "FailingCounterTest", + inputs: { name: "testFailFuzzInc", arguments: ["1"] }, + outputs: new Uint8Array(), + children: [ + { + kind: 3, + success: true, + isCheatcode: false, + gasUsed: 0n, + value: 0n, + address: "0x7c926CE5743033Cbe6f6cF7D6622EF70e05503A6", + contract: "console", + inputs: { name: "log", arguments: ['"Fuzz testing inc fail"'] }, + outputs: new Uint8Array(), + children: [], + }, + { + kind: 0, + success: true, + isCheatcode: false, + gasUsed: 22397n, + value: 0n, + address: "0x373b22261122919Ad39F55ac0475dd0f82Bd2499", + contract: "Counter", + inputs: { name: "inc", arguments: [] }, + outputs: new Uint8Array(), + children: [], + }, + { + kind: 3, + success: true, + isCheatcode: false, + gasUsed: 402n, + value: 0n, + address: "0x373b22261122919Ad39F55ac0475dd0f82Bd2499", + contract: "Counter", + inputs: { name: "x", arguments: [] }, + outputs: "1", + children: [], + }, + ], + }, + ]; + + const expected = ` +[127552] ${chalk.green("FailingCounterTest")}::${chalk.green("setUp")}() + ├─ [0] ${chalk.green("console")}::${chalk.green("log")}("Setting up") ${chalk.yellow("[staticcall]")} + ├─ [68915] ${chalk.yellow("→ new")} Counter@0x373b22261122919Ad39F55ac0475dd0f82Bd2499 + │ └─ ${chalk.green("←")} 344 bytes of code + └─ [0] ${chalk.green("console")}::${chalk.green("log")}("Counter set up") ${chalk.yellow("[staticcall]")} + +[32272] ${chalk.green("FailingCounterTest")}::${chalk.green("testFailFuzzInc")}(1) + ├─ [0] ${chalk.green("console")}::${chalk.green("log")}("Fuzz testing inc fail") ${chalk.yellow("[staticcall]")} + ├─ [22397] ${chalk.green("Counter")}::${chalk.green("inc")}() + └─ [402] ${chalk.green("Counter")}::${chalk.green("x")}() ${chalk.yellow("[staticcall]")} + └─ ${chalk.green("←")} 1`.replace("\n", ""); + + const actual = formatTraces(traces, "", chalk); + + assert.equal(expected, actual); + }); + + it("should return an empty string for empty traces", async () => { + const traces: CallTrace[] = []; + + const expected = ""; + + const actual = formatTraces(traces, " ", chalk); + + assert.equal(expected, actual); + }); +}); + +describe("formatLog via formatTraces", () => { + it("should format a decoded event with [event] tag", () => { + const trace = makeCallTrace({ + children: [ + makeLogTrace({ + name: "Transfer", + arguments: ['"from"', '"to"', '"100"'], + }), + ], + }); + + const actual = formatTraces([trace], "", chalk); + + assert.ok( + actual.includes(chalk.yellow("[event]")), + "should contain yellow [event] tag", + ); + assert.ok( + actual.includes(`Transfer(${chalk.cyan('"from", "to", "100"')})`), + "should contain event name with cyan args", + ); + }); + + const rawEventCases: Array<{ + name: string; + params: Uint8Array[]; + includes: string[]; + excludes: string[]; + }> = [ + { + name: "1 element (data-only, no topics)", + params: [new Uint8Array([0xab, 0xcd])], + includes: ["data:"], + excludes: ["topic 0:"], + }, + { + name: "3 elements (2 topics + data)", + params: [ + new Uint8Array([0x01]), + new Uint8Array([0x02]), + new Uint8Array([0x03]), + ], + includes: ["topic 0:", "topic 1:", "data:"], + excludes: [], + }, + { + name: "empty parameters", + params: [], + includes: [], + excludes: ["[event]"], + }, + ]; + + for (const { name, params, includes, excludes } of rawEventCases) { + it(`should format a raw event with ${name}`, () => { + const trace = makeCallTrace({ children: [makeLogTrace(params)] }); + const actual = formatTraces([trace], "", chalk); + + for (const s of includes) { + assert.ok(actual.includes(s), `should contain "${s}"`); + } + + for (const s of excludes) { + assert.ok(!actual.includes(s), `should not contain "${s}"`); + } + }); + } +}); + +describe("formatTrace call kind tags", () => { + const kindTagCases: Array<{ + kind: CallKind; + name: string; + tag: string | undefined; + }> = [ + { kind: CallKind.Call, name: "Call", tag: undefined }, + { kind: CallKind.StaticCall, name: "StaticCall", tag: "[staticcall]" }, + { + kind: CallKind.DelegateCall, + name: "DelegateCall", + tag: "[delegatecall]", + }, + { kind: CallKind.CallCode, name: "CallCode", tag: "[callcode]" }, + ]; + + const allTags = ["[staticcall]", "[delegatecall]", "[callcode]"]; + + for (const { kind, name, tag } of kindTagCases) { + it(`should ${tag !== undefined ? `append ${tag}` : "append no tag"} for CallKind.${name}`, () => { + const trace = makeCallTrace({ kind }); + const actual = formatTraces([trace], "", chalk); + + if (tag !== undefined) { + assert.ok(actual.includes(chalk.yellow(tag)), `should contain ${tag}`); + } + + for (const other of allTags) { + if (other !== tag) { + assert.ok(!actual.includes(other), `should not contain ${other}`); + } + } + }); + } + + it("should format Create with '→ new' prefix and no kind tag", () => { + const trace = makeCallTrace({ + kind: CallKind.Create, + contract: "MyToken", + address: "0x1234", + }); + const actual = formatTraces([trace], "", chalk); + + assert.ok( + actual.includes(chalk.yellow("→ new")), + "should contain → new prefix", + ); + assert.ok( + actual.includes("MyToken@0x1234"), + "should contain contract@address", + ); + + for (const tag of allTags) { + assert.ok(!actual.includes(tag), `should not contain ${tag}`); + } + }); +}); + +describe("formatTrace outputs and coloring", () => { + const cases: Array<{ + name: string; + overrides: Partial; + includes: string[]; + }> = [ + { + name: "should show [Revert] for failed trace", + overrides: { success: false, outputs: "EvmError: Revert" }, + includes: [chalk.red("[Revert]"), "EvmError: Revert"], + }, + { + name: "should show ← arrow for normal output", + overrides: { outputs: "42" }, + includes: [chalk.green("←"), "42"], + }, + { + name: "should use red for failed traces", + overrides: { success: false, contract: "FailContract" }, + includes: [chalk.red("FailContract")], + }, + { + name: "should use blue for cheatcode traces", + overrides: { isCheatcode: true, contract: "CheatContract" }, + includes: [chalk.blue("CheatContract")], + }, + ]; + + for (const { name, overrides, includes } of cases) { + it(name, () => { + const trace = makeCallTrace(overrides); + const actual = formatTraces([trace], "", chalk); + for (const s of includes) { + assert.ok(actual.includes(s), `should contain "${s}"`); + } + }); + } +}); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/edr/utils/trace-output.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/edr/utils/trace-output.ts new file mode 100644 index 00000000000..42852d65db3 --- /dev/null +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/edr/utils/trace-output.ts @@ -0,0 +1,215 @@ +import type { CallTrace, Response } from "@nomicfoundation/edr"; + +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { CallKind } from "@nomicfoundation/edr"; + +import { TraceOutputManager } from "../../../../../../src/internal/builtin-plugins/network-manager/edr/utils/trace-output.js"; + +function makeCallTrace(overrides: Partial = {}): CallTrace { + return { + kind: CallKind.Call, + success: true, + isCheatcode: false, + gasUsed: 1000n, + value: 0n, + address: "0x1111111111111111111111111111111111111111", + contract: "TestContract", + inputs: { name: "testFunc", arguments: [] }, + outputs: new Uint8Array(), + children: [], + ...overrides, + }; +} + +function mockResponse(traces: CallTrace[]): Response { + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- + minimal mock: only callTraces() is used by outputCallTraces */ + return { callTraces: () => traces } as unknown as Response; +} + +describe("TraceOutputManager", () => { + it("should print traces when callTraces returns non-empty array", () => { + const lines: string[] = []; + const manager = new TraceOutputManager( + (line) => lines.push(line), + 0, + "testnet", + 4, + ); + + manager.outputCallTraces( + mockResponse([makeCallTrace()]), + "eth_sendTransaction", + "0xabc", + false, + ); + + assert.equal(lines.length, 1, "Should have printed one output"); + assert.ok( + lines[0].includes("TestContract"), + "Output should contain the trace", + ); + }); + + it("should not print when callTraces returns empty array", () => { + const lines: string[] = []; + const manager = new TraceOutputManager( + (line) => lines.push(line), + 0, + "testnet", + 4, + ); + + manager.outputCallTraces( + mockResponse([]), + "eth_sendTransaction", + "0xabc", + false, + ); + + assert.equal(lines.length, 0, "Should not have printed anything"); + }); + + it("should deduplicate by txHash at verbosity < 5", () => { + const lines: string[] = []; + const manager = new TraceOutputManager( + (line) => lines.push(line), + 0, + "testnet", + 4, + ); + + const response = mockResponse([makeCallTrace()]); + manager.outputCallTraces(response, "eth_sendTransaction", "0xabc", false); + manager.outputCallTraces( + response, + "eth_getTransactionReceipt", + "0xabc", + false, + ); + + assert.equal(lines.length, 1, "Should print only once for the same txHash"); + }); + + it("should include connection label and method in header", () => { + const lines: string[] = []; + const manager = new TraceOutputManager( + (line) => lines.push(line), + 3, + "myNetwork", + 4, + ); + + manager.outputCallTraces( + mockResponse([makeCallTrace()]), + "eth_sendTransaction", + "0xabc", + false, + ); + + assert.equal(lines.length, 1); + assert.ok( + lines[0].includes("connection #3 (myNetwork)"), + "Header should contain the connection label", + ); + assert.ok( + lines[0].includes("eth_sendTransaction"), + "Header should contain the method name", + ); + }); + + it("should suppress eth_estimateGas on success", () => { + const lines: string[] = []; + const manager = new TraceOutputManager( + (line) => lines.push(line), + 0, + "testnet", + 4, + ); + + manager.outputCallTraces( + mockResponse([makeCallTrace()]), + "eth_estimateGas", + undefined, + false, + ); + + assert.equal( + lines.length, + 0, + "Should suppress successful eth_estimateGas traces", + ); + }); + + it("should show eth_estimateGas on failure", () => { + const lines: string[] = []; + const manager = new TraceOutputManager( + (line) => lines.push(line), + 0, + "testnet", + 4, + ); + + manager.outputCallTraces( + mockResponse([makeCallTrace()]), + "eth_estimateGas", + undefined, + true, + ); + + assert.equal(lines.length, 1, "Should show failed eth_estimateGas traces"); + }); + + it("should bypass dedup and suppression at verbosity >= 5", () => { + const lines: string[] = []; + const manager = new TraceOutputManager( + (line) => lines.push(line), + 0, + "testnet", + 5, + ); + + const response = mockResponse([makeCallTrace()]); + + // Same txHash twice, both should print at verbosity 5 + manager.outputCallTraces(response, "eth_sendTransaction", "0xabc", false); + manager.outputCallTraces( + response, + "eth_getTransactionReceipt", + "0xabc", + false, + ); + + assert.equal(lines.length, 2, "Verbosity >= 5 should bypass dedup"); + + // eth_estimateGas on success should also print + manager.outputCallTraces(response, "eth_estimateGas", undefined, false); + + assert.equal(lines.length, 3, "Verbosity >= 5 should bypass suppression"); + }); + + it("should re-print after clearTracedHashes", () => { + const lines: string[] = []; + const manager = new TraceOutputManager( + (line) => lines.push(line), + 0, + "testnet", + 4, + ); + + const response = mockResponse([makeCallTrace()]); + manager.outputCallTraces(response, "eth_sendTransaction", "0xabc", false); + assert.equal(lines.length, 1); + + manager.clearTracedHashes(); + + manager.outputCallTraces(response, "eth_sendTransaction", "0xabc", false); + assert.equal( + lines.length, + 2, + "Should print again after clearing traced hashes", + ); + }); +}); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/network-manager.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/network-manager.ts index fce420a15df..d7caa49072d 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/network-manager.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/network-manager.ts @@ -146,6 +146,7 @@ describe("NetworkManagerImplementation", () => { chainDescriptors, hre.globalOptions.config, hre.config.paths.root, + hre.globalOptions.verbosity, ); }); @@ -836,6 +837,7 @@ describe("NetworkManagerImplementation", () => { chainDescriptors, hre.globalOptions.config, hre.config.paths.root, + hre.globalOptions.verbosity, ); }); @@ -914,6 +916,7 @@ describe("NetworkManagerImplementation", () => { hre.config.chainDescriptors, hre.globalOptions.config, hre.config.paths.root, + hre.globalOptions.verbosity, ); }); @@ -1004,6 +1007,7 @@ describe("NetworkManagerImplementation", () => { chainDescriptors, hre.globalOptions.config, hre.config.paths.root, + hre.globalOptions.verbosity, ); }); diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity-test/formatters.ts b/v-next/hardhat/test/internal/builtin-plugins/solidity-test/formatters.ts deleted file mode 100644 index 92d2b555de6..00000000000 --- a/v-next/hardhat/test/internal/builtin-plugins/solidity-test/formatters.ts +++ /dev/null @@ -1,166 +0,0 @@ -import type { CallTrace } from "@nomicfoundation/edr"; - -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; - -import chalk from "chalk"; - -import { - formatLogs, - formatTraces, -} from "../../../../src/internal/builtin-plugins/solidity-test/formatters.js"; - -describe("formatLogs", () => { - it("should format logs correctly", async () => { - const lines = ["a", "b", "c"]; - - const actual = ` a - b - c`; - - const expected = formatLogs(lines, 2, chalk); - - assert.equal(expected, chalk.grey(actual)); - }); - - it("should return an empty string for empty logs", async () => { - const lines: string[] = []; - - const expected = ""; - - const actual = formatLogs(lines, 2, chalk); - - assert.equal(expected, actual); - }); -}); - -describe("formatTraces", () => { - it("should format traces correctly", async () => { - const traces = [ - { - kind: 0, - success: true, - isCheatcode: false, - gasUsed: 127552n, - value: 0n, - address: "0x9Cded789F1564C12102E41634157434dd1De9fE3", - contract: "FailingCounterTest", - inputs: { name: "setUp", arguments: [] }, - outputs: new Uint8Array(), - children: [ - { - kind: 3, - success: true, - isCheatcode: false, - gasUsed: 0n, - value: 0n, - address: "0x7c926CE5743033Cbe6f6cF7D6622EF70e05503A6", - contract: "console", - inputs: { name: "log", arguments: ['"Setting up"'] }, - outputs: new Uint8Array(), - children: [], - }, - { - kind: 4, - success: true, - isCheatcode: false, - gasUsed: 68915n, - value: 0n, - address: "0x373b22261122919Ad39F55ac0475dd0f82Bd2499", - contract: "Counter", - inputs: new Uint8Array([1, 2, 3]), - outputs: "344 bytes of code", - children: [], - }, - { - kind: 3, - success: true, - isCheatcode: false, - gasUsed: 0n, - value: 0n, - address: "0x7c926CE5743033Cbe6f6cF7D6622EF70e05503A6", - contract: "console", - inputs: { name: "log", arguments: ['"Counter set up"'] }, - outputs: new Uint8Array(), - children: [], - }, - ], - }, - { - kind: 0, - success: true, - isCheatcode: false, - gasUsed: 32272n, - value: 0n, - address: "0x9Cded789F1564C12102E41634157434dd1De9fE3", - contract: "FailingCounterTest", - inputs: { name: "testFailFuzzInc", arguments: ["1"] }, - outputs: new Uint8Array(), - children: [ - { - kind: 3, - success: true, - isCheatcode: false, - gasUsed: 0n, - value: 0n, - address: "0x7c926CE5743033Cbe6f6cF7D6622EF70e05503A6", - contract: "console", - inputs: { name: "log", arguments: ['"Fuzz testing inc fail"'] }, - outputs: new Uint8Array(), - children: [], - }, - { - kind: 0, - success: true, - isCheatcode: false, - gasUsed: 22397n, - value: 0n, - address: "0x373b22261122919Ad39F55ac0475dd0f82Bd2499", - contract: "Counter", - inputs: { name: "inc", arguments: [] }, - outputs: new Uint8Array(), - children: [], - }, - { - kind: 3, - success: true, - isCheatcode: false, - gasUsed: 402n, - value: 0n, - address: "0x373b22261122919Ad39F55ac0475dd0f82Bd2499", - contract: "Counter", - inputs: { name: "x", arguments: [] }, - outputs: "1", - children: [], - }, - ], - }, - ]; - - const expected = ` -[127552] ${chalk.green("FailingCounterTest")}::${chalk.green("setUp")}() - ├─ [0] ${chalk.green("console")}::${chalk.green("log")}("Setting up") ${chalk.yellow("[staticcall]")} - ├─ [68915] ${chalk.yellow("→ new")} Counter@0x373b22261122919Ad39F55ac0475dd0f82Bd2499 - │ └─ ${chalk.green("←")} 344 bytes of code - └─ [0] ${chalk.green("console")}::${chalk.green("log")}("Counter set up") ${chalk.yellow("[staticcall]")} -[32272] ${chalk.green("FailingCounterTest")}::${chalk.green("testFailFuzzInc")}(1) - ├─ [0] ${chalk.green("console")}::${chalk.green("log")}("Fuzz testing inc fail") ${chalk.yellow("[staticcall]")} - ├─ [22397] ${chalk.green("Counter")}::${chalk.green("inc")}() - └─ [402] ${chalk.green("Counter")}::${chalk.green("x")}() ${chalk.yellow("[staticcall]")} - └─ ${chalk.green("←")} 1`.replace("\n", ""); - - const actual = formatTraces(traces, "", chalk); - - assert.equal(expected, actual); - }); - - it("should return an empty string for empty traces", async () => { - const traces: CallTrace[] = []; - - const expected = ""; - - const actual = formatTraces(traces, " ", chalk); - - assert.equal(expected, actual); - }); -}); diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity-test/helpers.ts b/v-next/hardhat/test/internal/builtin-plugins/solidity-test/helpers.ts index a71de123419..ab94036938e 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/solidity-test/helpers.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/solidity-test/helpers.ts @@ -50,22 +50,20 @@ describe("solidityTestConfigToSolidityTestRunnerConfigArgs", () => { } }); - it("should include failing traces for verbosity level 3 and 4", async () => { - for (const verbosity of [3, 4]) { - const args = await solidityTestConfigToSolidityTestRunnerConfigArgs({ - chainType: GENERIC_CHAIN_TYPE, - projectRoot: process.cwd(), - config: { fuzz: { seed: "0x1234" } }, - verbosity, - generateGasReport: false, - }); + it("should include failing traces for verbosity level 3", async () => { + const args = await solidityTestConfigToSolidityTestRunnerConfigArgs({ + chainType: GENERIC_CHAIN_TYPE, + projectRoot: process.cwd(), + config: { fuzz: { seed: "0x1234" } }, + verbosity: 3, + generateGasReport: false, + }); - assert.equal(args.includeTraces, IncludeTraces.Failing); - } + assert.equal(args.includeTraces, IncludeTraces.Failing); }); - it("should include all traces for verbosity level 5 and above", async () => { - for (const verbosity of [5, 6, 7]) { + it("should include all traces for verbosity level 4 and above", async () => { + for (const verbosity of [4, 5, 6, 7]) { const args = await solidityTestConfigToSolidityTestRunnerConfigArgs({ chainType: GENERIC_CHAIN_TYPE, projectRoot: process.cwd(), diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity-test/reporter.ts b/v-next/hardhat/test/internal/builtin-plugins/solidity-test/reporter.ts index 4853eac240f..60951ee3334 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/solidity-test/reporter.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/solidity-test/reporter.ts @@ -189,6 +189,7 @@ debug log 1) failing test Call Traces: [100] Foo + [90] Bar └─ [80] Baz @@ -273,6 +274,7 @@ debug log 1) failing test Call Traces: [100] Foo + [90] Bar └─ [80] Baz @@ -470,6 +472,7 @@ debug log 1) failing test Call Traces: [100] Foo + [90] Bar └─ [80] Baz diff --git a/v-next/hardhat/test/internal/cli/help/get-global-help-string.ts b/v-next/hardhat/test/internal/cli/help/get-global-help-string.ts index 913343f6bd2..f91080f669c 100644 --- a/v-next/hardhat/test/internal/cli/help/get-global-help-string.ts +++ b/v-next/hardhat/test/internal/cli/help/get-global-help-string.ts @@ -265,6 +265,7 @@ GLOBAL OPTIONS: --show-stack-traces Show stack traces (always enabled on CI servers) --user-option-1 userOption1 description --user-option-2 userOption2 description + --verbosity, -v Verbosity level of the output --version Show the version of hardhat To get help for a specific task run: npx hardhat [SUBTASK] --help`; @@ -386,6 +387,7 @@ GLOBAL OPTIONS: --show-stack-traces Show stack traces (always enabled on CI servers) --user-option-1 userOption1 description --user-option-2 userOption2 description + --verbosity, -v Verbosity level of the output --version Show the version of hardhat To get help for a specific task run: npx hardhat [SUBTASK] --help`; diff --git a/v-next/hardhat/test/internal/cli/main.ts b/v-next/hardhat/test/internal/cli/main.ts index 229abcc29ec..cfc29cecc09 100644 --- a/v-next/hardhat/test/internal/cli/main.ts +++ b/v-next/hardhat/test/internal/cli/main.ts @@ -295,6 +295,7 @@ GLOBAL OPTIONS: --init Initializes a Hardhat project --network The network to connect to --show-stack-traces Show stack traces (always enabled on CI servers) + --verbosity, -v Verbosity level of the output --version Show the version of hardhat To get help for a specific task run: npx hardhat [SUBTASK] --help`; @@ -369,6 +370,7 @@ GLOBAL OPTIONS: --init Initializes a Hardhat project --network The network to connect to --show-stack-traces Show stack traces (always enabled on CI servers) + --verbosity, -v Verbosity level of the output --version Show the version of hardhat `; @@ -410,6 +412,7 @@ GLOBAL OPTIONS: --init Initializes a Hardhat project --network The network to connect to --show-stack-traces Show stack traces (always enabled on CI servers) + --verbosity, -v Verbosity level of the output --version Show the version of hardhat `; @@ -824,8 +827,8 @@ GLOBAL OPTIONS: defaultValue: "default", }), task(["task4"]).addLevel({ - name: "verbosity", - shortName: "v", + name: "logLevel", + shortName: "l", }), ]; @@ -1045,7 +1048,7 @@ GLOBAL OPTIONS: } it("should get the task and its level argument when provided by long name", function () { - const command = "npx hardhat task4 --verbosity 4"; + const command = "npx hardhat task4 --log-level 4"; const cliArguments = command.split(" ").slice(2); const usedCliArguments = new Array(cliArguments.length).fill(false); @@ -1058,11 +1061,11 @@ GLOBAL OPTIONS: usedCliArguments, new Array(cliArguments.length).fill(true), ); - assert.deepEqual(res.taskArguments, { verbosity: 4 }); + assert.deepEqual(res.taskArguments, { logLevel: 4 }); }); it("should throw when level is provided by long name and not followed by a value", function () { - const command = "npx hardhat task4 --verbosity"; + const command = "npx hardhat task4 --log-level"; const cliArguments = command.split(" ").slice(2); const usedCliArguments = new Array(cliArguments.length).fill(false); @@ -1071,13 +1074,13 @@ GLOBAL OPTIONS: () => parseTaskAndArguments(cliArguments, usedCliArguments, hre), HardhatError.ERRORS.CORE.ARGUMENTS.MISSING_VALUE_FOR_ARGUMENT, { - argument: "--verbosity", + argument: "--log-level", }, ); }); it("should get the task and its level argument when provided by short name", function () { - const command = "npx hardhat task4 -vvvv"; + const command = "npx hardhat task4 -llll"; const cliArguments = command.split(" ").slice(2); const usedCliArguments = new Array(cliArguments.length).fill(false); @@ -1090,11 +1093,11 @@ GLOBAL OPTIONS: usedCliArguments, new Array(cliArguments.length).fill(true), ); - assert.deepEqual(res.taskArguments, { verbosity: 4 }); + assert.deepEqual(res.taskArguments, { logLevel: 4 }); }); it("should throw when level is provided by short name and followed by a value", function () { - const command = "npx hardhat task4 -v 4"; + const command = "npx hardhat task4 -l 4"; const cliArguments = command.split(" ").slice(2); const usedCliArguments = new Array(cliArguments.length).fill(false); diff --git a/v-next/hardhat/test/internal/hre-initialization.ts b/v-next/hardhat/test/internal/hre-initialization.ts index 762c77d3215..8e71143b675 100644 --- a/v-next/hardhat/test/internal/hre-initialization.ts +++ b/v-next/hardhat/test/internal/hre-initialization.ts @@ -252,6 +252,7 @@ describe("HRE initialization", () => { help: false, init: false, showStackTraces: false, + verbosity: 2, version: false, myGlobalOption: "default", network: undefined,