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
JavaScript
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