UNPKG

hardhat

Version:

Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.

277 lines 12.8 kB
import { bytesToHexString } from "@nomicfoundation/hardhat-utils/hex"; 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 { formatArtifactId, formatTraces, } from "./formatters.js"; import { getMessageFromLastStackTraceEntry } from "./stack-trace-solidity-errors.js"; class Indenter { #indentation; constructor() { this.#indentation = 2; } inc() { this.#indentation += 2; } dec() { this.#indentation = Math.max(0, this.#indentation - 2); } prefix() { return " ".repeat(this.#indentation); } t(strings, ...values) { const line = strings.reduce((acc, str, i) => acc + str + (values[i] ?? ""), ""); return this.prefix() + line; } } /** * This is a solidity test reporter. It is intended to be composed with the * solidity test runner's test stream. It was based on the hardhat node test * reporter's design. */ export async function* testReporter(source, sourceNameToUserSourceName, verbosity, testSummaryIndex = 0, colorizer = chalk) { let runSuccessCount = 0; let runFailureCount = testSummaryIndex === 0 ? 1 : testSummaryIndex; let runSkippedCount = 0; const failures = []; const indenter = new Indenter(); let firstSuite = true; for await (const event of source) { switch (event.type) { case "suite:done": { const { data: suiteResult } = event; const suiteTestCount = suiteResult.testResults.length; if (suiteTestCount === 0) { continue; } if (firstSuite) { firstSuite = false; } else { yield "\n"; } let suiteSuccessCount = 0; let suiteSkippedCount = 0; const formattedArtifactId = formatArtifactId(suiteResult.id, sourceNameToUserSourceName); yield indenter.t `${formattedArtifactId}\n`; if (suiteResult.warnings.length > 0) { indenter.inc(); for (const warning of suiteResult.warnings) { yield indenter.t `${colorizer.yellow("Warning")}${colorizer.grey(`: ${warning}`)}\n`; } indenter.dec(); yield "\n"; } indenter.inc(); // NOTE: The test results are in reverse run order, so we reverse them // again to display them in the correct order. for (const [testIndex, testResult] of suiteResult.testResults .reverse() .entries()) { const name = testResult.name; const status = testResult.status; let details = ""; const detailsItems = []; for (const [key, value] of Object.entries(testResult.kind)) { if (key === "runs") { detailsItems.push(`runs: ${value}`); } } if (detailsItems.length > 0) { details = ` (${detailsItems.join(", ")})`; } const printDecodedLogs = (status === "Success" && verbosity >= 2) || (status === "Failure" && verbosity >= 1); let printSetUpTraces = false; let printExecutionTraces = false; if (printDecodedLogs) { const decodedLogs = testResult.decodedLogs ?? []; for (const log of decodedLogs) { yield `${log}\n`; } } switch (status) { case "Success": { let successOutput = `${colorizer.green("✔")} ${colorizer.grey(name)}`; if (details !== "") { successOutput += colorizer.dim(details); } yield indenter.t `${successOutput}\n`; suiteSuccessCount++; if (verbosity >= 5) { printSetUpTraces = true; printExecutionTraces = true; } break; } case "Failure": { failures.push({ testResult, contractName: suiteResult.id.name }); yield indenter.t `${colorizer.red(`${runFailureCount}) ${name}`)}\n`; runFailureCount++; if (verbosity >= 3) { printExecutionTraces = true; } if (verbosity >= 4) { printSetUpTraces = true; } break; } case "Skipped": { yield indenter.t `${colorizer.cyan(`- ${name}`)}\n`; suiteSkippedCount++; break; } } let printExtraSpace = false; if (printSetUpTraces || printExecutionTraces) { const callTraces = testResult.callTraces().filter(({ inputs }) => { if (printSetUpTraces && printExecutionTraces) { return true; } let functionName; if (!(inputs instanceof Uint8Array)) { functionName = inputs.name; } if (printSetUpTraces && functionName === "setUp") { return true; } if (printExecutionTraces && functionName !== "setUp()") { return true; } return false; }); if (callTraces.length > 0) { indenter.inc(); yield indenter.t `Call Traces:\n`; indenter.inc(); yield `${formatTraces(callTraces, indenter.prefix(), colorizer)}\n`; indenter.dec(); indenter.dec(); if (testIndex < suiteResult.testResults.length - 1) { printExtraSpace = true; } } } if (printExtraSpace) { yield "\n"; } } indenter.dec(); runSuccessCount += suiteSuccessCount; runSkippedCount += suiteSkippedCount; break; } case "run:done": { break; } } } // testSummaryIndex of 0 means task is being run directly, so summary is handled here // and not by the parent `test` task. if (testSummaryIndex === 0) { yield "\n"; yield "\n"; yield indenter.t `${colorizer.green(`${runSuccessCount} passing`)}\n`; if (failures.length > 0) { yield indenter.t `${colorizer.red(`${failures.length} failing`)}\n`; } if (runSkippedCount > 0) { yield indenter.t `${colorizer.cyan(`${runSkippedCount} skipped`)}\n`; } } let failureOutput = ""; let failureIndex = 1; if (failures.length > 0) { function* output(str) { if (testSummaryIndex === 0) { yield str; } else { failureOutput += str; } } yield* output("\n"); let firstFailure = true; for (const { testResult: failure, contractName } of failures) { if (!firstFailure) { yield* output("\n"); } firstFailure = false; yield* output(indenter.t `${failureIndex}) ${contractName}#${failure.name}\n`); failureIndex++; indenter.inc(); const stackTrace = failure.stackTrace(); let reason; if (stackTrace?.kind === "StackTrace") { reason = getMessageFromLastStackTraceEntry(stackTrace.entries[stackTrace.entries.length - 1]); } if (reason === undefined || reason === "") { reason = failure.reason?.startsWith("FFI is disabled") === true ? "FFI is disabled; set `test.solidity.ffi` to `true` in your Hardhat config to allow tests to call external commands" : failure.reason ?? "Unknown error"; } yield* output(indenter.t `${colorizer.red(`Error: ${reason}`)}\n`); // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check -- Ignore Cases not matched: undefined switch (stackTrace?.kind) { case "StackTrace": const stackTraceStack = []; for (const entry of stackTrace.entries.reverse()) { const callsite = encodeStackTraceEntry(entry); if (callsite !== undefined) { indenter.inc(); stackTraceStack.push(indenter.t `at ${callsite.toString()}`); indenter.dec(); } } if (stackTraceStack.length > 0) { yield* output(`${colorizer.grey(stackTraceStack.join("\n"))}\n`); } yield* output("\n"); break; case "UnexpectedError": await sendErrorTelemetry(new SolidityTestStackTraceGenerationError(stackTrace.errorMessage)); yield* output(indenter.t `Stack Trace Warning: ${colorizer.grey(stackTrace.errorMessage)}\n`); break; case "UnsafeToReplay": if (stackTrace.globalForkLatest === true) { yield* output(indenter.t `Stack Trace Warning: ${colorizer.grey("The test is not safe to replay because a fork url without a fork block number was provided.")}\n`); yield* output(indenter.t `Try rerunning your tests with -vvv or above.\n`); } if (stackTrace.impureCheatcodes.length > 0) { yield* output(indenter.t `Stack Trace Warning: ${colorizer.grey(`The test is not safe to replay because it uses impure cheatcodes: ${stackTrace.impureCheatcodes.join(", ")}`)}\n`); yield* output(indenter.t `Try rerunning your tests with -vvv or above.\n`); } break; case "HeuristicFailed": default: break; } if (failure.counterexample !== undefined && failure.counterexample !== null) { const counterexamples = "sequence" in failure.counterexample ? failure.counterexample.sequence : [failure.counterexample]; for (const counterexample of counterexamples) { yield* output(indenter.t `Counterexample:\n`); indenter.inc(); for (const [key, value] of Object.entries(counterexample)) { const counterExampleDetails = `${key}: ${Buffer.isBuffer(value) ? bytesToHexString(value) : value}`; yield* output(indenter.t `${colorizer.grey(counterExampleDetails)}\n`); } indenter.dec(); } } indenter.dec(); } } if (testSummaryIndex > 0) { yield { failed: failures.length, passed: runSuccessCount, skipped: runSkippedCount, failureOutput, }; } } //# sourceMappingURL=reporter.js.map