Skip to content

Commit e3e9757

Browse files
committed
feat: implement call traces display for solidity tests
1 parent baad071 commit e3e9757

File tree

10 files changed

+178
-16
lines changed

10 files changed

+178
-16
lines changed

.changeset/fluffy-days-study.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"hardhat": patch
3+
---
4+
5+
Added call traces support for solidity tests (they can be enabled via the `-vvvv` verbosity level flag)

v-next/example-project/contracts/Counter.t.sol

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,31 @@ contract CounterTest {
88
Counter counter;
99

1010
function setUp() public {
11+
console.log("Setting up");
1112
counter = new Counter();
13+
console.log("Counter set up");
1214
}
1315

1416
function testInitialValue() public view {
17+
console.log("Testing initial value");
1518
require(counter.x() == 0, "Initial value should be 0");
1619
}
1720

1821
function testFailInitialValue() public view {
22+
console.log("Testing initial value fail");
1923
require(counter.x() == 1, "Initial value should be 1");
2024
}
2125

2226
function testFuzzInc(uint8 x) public {
27+
console.log("Fuzz testing inc");
2328
for (uint8 i = 0; i < x; i++) {
2429
counter.inc();
2530
}
2631
require(counter.x() == x, "Value after calling inc x times should be x");
2732
}
2833

2934
function testFailFuzzInc(uint8 x) public {
35+
console.log("Fuzz testing inc fail");
3036
for (uint8 i = 0; i < x; i++) {
3137
counter.inc();
3238
}

v-next/hardhat/src/internal/builtin-plugins/solidity-test/formatters.ts

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1-
import type { ArtifactId } from "@ignored/edr-optimism";
1+
import type {
2+
LogTrace,
3+
ArtifactId,
4+
CallTrace,
5+
DecodedTraceParameters,
6+
} from "@ignored/edr-optimism";
27

8+
import { LogKind, CallKind } from "@ignored/edr-optimism";
9+
import { bytesToHexString } from "@nomicfoundation/hardhat-utils/hex";
310
import chalk from "chalk";
411

512
export function formatArtifactId(
@@ -11,3 +18,84 @@ export function formatArtifactId(
1118

1219
return `${chalk.bold(`${sourceName}:${artifactId.name}`)} (v${artifactId.solcVersion})`;
1320
}
21+
22+
export function formatLogs(logs: string[], indent: number): string {
23+
return chalk.grey(
24+
logs.map((log) => `${" ".repeat(indent)}${log}`).join("\n"),
25+
);
26+
}
27+
28+
export function formatInputs(inputs: DecodedTraceParameters | Uint8Array): string {
29+
if (inputs instanceof Uint8Array) {
30+
return bytesToHexString(inputs);
31+
} else {
32+
return `${inputs.name}(${inputs.arguments.join(", ")})`;
33+
}
34+
}
35+
36+
function formatOutputs(outputs: string | Uint8Array): string {
37+
if (outputs instanceof Uint8Array) {
38+
return bytesToHexString(outputs);
39+
} else {
40+
return outputs;
41+
}
42+
}
43+
44+
function formatLog(log: LogTrace, indent: number): string {
45+
const { parameters } = log;
46+
if (Array.isArray(parameters)) {
47+
const topics = parameters
48+
.slice(0, parameters.length - 1)
49+
.map((topic) => bytesToHexString(topic));
50+
const data = bytesToHexString(parameters[parameters.length - 1]);
51+
return `${" ".repeat(indent)}${chalk.grey(`(topics: [${topics.join(", ")}], data: ${data})`)}`;
52+
} else {
53+
return `${" ".repeat(indent)}${parameters.name}(${parameters.arguments.join(", ")})`;
54+
}
55+
}
56+
57+
function formatKind(kind: CallKind): string {
58+
switch (kind) {
59+
case CallKind.Call:
60+
return "Call";
61+
case CallKind.CallCode:
62+
return "CallCode";
63+
case CallKind.DelegateCall:
64+
return "DelegateCall";
65+
case CallKind.StaticCall:
66+
return "StaticCall";
67+
case CallKind.Create:
68+
return "Create";
69+
}
70+
}
71+
72+
function formatTrace(trace: CallTrace, indent: number): string {
73+
const {
74+
success,
75+
contract,
76+
inputs,
77+
gasUsed,
78+
value,
79+
kind,
80+
isCheatcode,
81+
outputs,
82+
} = trace;
83+
const color = success ? chalk.blue : chalk.yellow;
84+
const sign = success ? "✔" : "✘";
85+
const label = success ? "Succeeded" : "Failed";
86+
const lines = [
87+
`${" ".repeat(indent)}${color(`${sign} ${formatKind(kind)} ${label}`)}: ${contract}::${formatInputs(inputs)}${formatOutputs(outputs)} ${chalk.grey(`(gas: ${gasUsed}, tokens: ${value}, cheatcode: ${isCheatcode})`)}`,
88+
];
89+
for (const child of trace.children) {
90+
if (child.kind === LogKind.Log) {
91+
lines.push(formatLog(child, indent + 2));
92+
} else {
93+
lines.push(formatTrace(child, indent + 2));
94+
}
95+
}
96+
return lines.join("\n");
97+
}
98+
99+
export function formatTraces(traces: CallTrace[], indent: number): string {
100+
return traces.map((trace) => formatTrace(trace, indent)).join("\n");
101+
}

v-next/hardhat/src/internal/builtin-plugins/solidity-test/helpers.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
opLatestHardfork,
1818
l1GenesisState,
1919
l1HardforkLatest,
20+
IncludeTraces,
2021
} from "@ignored/edr-optimism";
2122
import { hexStringToBytes } from "@nomicfoundation/hardhat-utils/hex";
2223

@@ -36,6 +37,7 @@ export function solidityTestConfigToSolidityTestRunnerConfigArgs(
3637
chainType: ChainType,
3738
projectRoot: string,
3839
config: SolidityTestConfig,
40+
verbosity: number,
3941
testPattern?: string,
4042
): SolidityTestRunnerConfigArgs {
4143
const fsPermissions: PathPermission[] | undefined = [
@@ -105,6 +107,13 @@ export function solidityTestConfigToSolidityTestRunnerConfigArgs(
105107
? opGenesisState(opLatestHardfork())
106108
: l1GenesisState(l1HardforkLatest());
107109

110+
let includeTraces: IncludeTraces = IncludeTraces.None;
111+
if (verbosity >= 5) {
112+
includeTraces = IncludeTraces.All;
113+
} else if (verbosity >= 3) {
114+
includeTraces = IncludeTraces.Failing;
115+
}
116+
108117
return {
109118
projectRoot,
110119
...config,
@@ -116,6 +125,7 @@ export function solidityTestConfigToSolidityTestRunnerConfigArgs(
116125
blockCoinbase,
117126
rpcStorageCaching,
118127
testPattern,
128+
includeTraces,
119129
};
120130
}
121131

v-next/hardhat/src/internal/builtin-plugins/solidity-test/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const hardhatPlugin: HardhatPlugin = {
3838
name: "verbosity",
3939
shortName: "v",
4040
description: "Log verbosity",
41-
defaultValue: 2
41+
defaultValue: 2,
4242
})
4343
.setAction(import.meta.resolve("./task-action.js"))
4444
.build(),

v-next/hardhat/src/internal/builtin-plugins/solidity-test/reporter.ts

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import chalk from "chalk";
1010

1111
import { encodeStackTraceEntry } from "../network-manager/edr/stack-traces/stack-trace-solidity-errors.js";
1212

13-
import { formatArtifactId } from "./formatters.js";
13+
import { formatArtifactId, formatInputs, formatLogs, formatTraces } from "./formatters.js";
1414
import { getMessageFromLastStackTraceEntry } from "./stack-trace-solidity-errors.js";
1515

1616
/**
@@ -21,6 +21,7 @@ import { getMessageFromLastStackTraceEntry } from "./stack-trace-solidity-errors
2121
export async function* testReporter(
2222
source: TestEventSource,
2323
sourceNameToUserSourceName: Map<string, string>,
24+
verbosity: number,
2425
): TestReporterResult {
2526
let runTestCount = 0;
2627
let runSuccessCount = 0;
@@ -68,16 +69,36 @@ export async function* testReporter(
6869
)
6970
.join(", ");
7071

72+
let printDecodedLogs = false;
73+
let printSetUpTraces = false;
74+
let printExecutionTraces = false;
75+
7176
switch (status) {
7277
case "Success": {
7378
yield `${chalk.green("✔ Passed")}: ${name} ${chalk.grey(`(${details})`)}\n`;
7479
suiteSuccessCount++;
80+
if (verbosity >= 2) {
81+
printDecodedLogs = true;
82+
}
83+
if (verbosity >= 5) {
84+
printSetUpTraces = true;
85+
printExecutionTraces = true;
86+
}
7587
break;
7688
}
7789
case "Failure": {
7890
failures.push(testResult);
7991
yield `${chalk.red(`✘ Failed(${failures.length})`)}: ${name} ${chalk.grey(`(${details})`)}\n`;
8092
suiteFailureCount++;
93+
if (verbosity >= 1) {
94+
printDecodedLogs = true;
95+
}
96+
if (verbosity >= 3) {
97+
printExecutionTraces = true;
98+
}
99+
if (verbosity >= 4) {
100+
printSetUpTraces = true;
101+
}
81102
break;
82103
}
83104
case "Skipped": {
@@ -86,6 +107,41 @@ export async function* testReporter(
86107
break;
87108
}
88109
}
110+
111+
let printExtraSpace = false;
112+
113+
if (printDecodedLogs) {
114+
const decodedLogs = testResult.decodedLogs ?? [];
115+
if (decodedLogs.length > 0) {
116+
yield `Decoded Logs:\n${formatLogs(decodedLogs, 2)}\n`;
117+
printExtraSpace = true;
118+
}
119+
}
120+
121+
if (printSetUpTraces || printExecutionTraces) {
122+
const callTraces = testResult.callTraces().filter(({inputs}) => {
123+
if (printSetUpTraces && printExecutionTraces) {
124+
return true;
125+
}
126+
const formattedInputs = formatInputs(inputs);
127+
if (printSetUpTraces && formattedInputs === "setUp()") {
128+
return true;
129+
}
130+
if (printExecutionTraces && formattedInputs !== "setUp()") {
131+
return true;
132+
}
133+
return false;
134+
});
135+
136+
if (callTraces.length > 0) {
137+
yield `Call Traces:\n${formatTraces(callTraces, 2)}\n`;
138+
printExtraSpace = true;
139+
}
140+
}
141+
142+
if (printExtraSpace) {
143+
yield "\n";
144+
}
89145
}
90146

91147
const suiteSummary = `${suiteTestCount} tests, ${suiteSuccessCount} passed, ${suiteFailureCount} failed, ${suiteSkippedCount} skipped`;
@@ -166,14 +222,6 @@ export async function* testReporter(
166222
break;
167223
}
168224

169-
if (
170-
failure.decodedLogs !== undefined &&
171-
failure.decodedLogs !== null &&
172-
failure.decodedLogs.length > 0
173-
) {
174-
yield `Decoded Logs:\n${chalk.grey(failure.decodedLogs.map((log) => ` ${log}`).join("\n"))}\n`;
175-
}
176-
177225
if (
178226
failure.counterexample !== undefined &&
179227
failure.counterexample !== null

v-next/hardhat/src/internal/builtin-plugins/solidity-test/task-action.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ const runSolidityTests: NewTaskActionFunction<TestActionArguments> = async (
115115
chainType,
116116
hre.config.paths.root,
117117
solidityTestConfig,
118+
hre.globalOptions.verbosity,
118119
grep,
119120
);
120121
const tracingConfig: TracingConfigWithBuffers = {
@@ -149,7 +150,13 @@ const runSolidityTests: NewTaskActionFunction<TestActionArguments> = async (
149150
}
150151
}
151152
})
152-
.compose((source) => testReporter(source, sourceNameToUserSourceName));
153+
.compose((source) =>
154+
testReporter(
155+
source,
156+
sourceNameToUserSourceName,
157+
hre.globalOptions.verbosity,
158+
),
159+
);
153160

154161
const outputStream = testReporterStream.pipe(
155162
createNonClosingWriter(process.stdout),

v-next/hardhat/src/internal/builtin-plugins/test/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const hardhatPlugin: HardhatPlugin = {
3333
name: "verbosity",
3434
shortName: "v",
3535
description: "Log verbosity",
36-
defaultValue: 2
36+
defaultValue: 2,
3737
})
3838
.setAction(import.meta.resolve("./task-action.js"))
3939
.build(),

v-next/hardhat/src/internal/cli/main.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
import { isCi } from "@nomicfoundation/hardhat-utils/ci";
1313
import { readClosestPackageJson } from "@nomicfoundation/hardhat-utils/package";
1414
import { kebabToCamelCase } from "@nomicfoundation/hardhat-utils/string";
15-
import chalk from "chalk";
1615
import debug from "debug";
1716
import { register } from "tsx/esm/api";
1817

v-next/hardhat/test/internal/cli/main.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type {
1111
} from "../../../src/types/tasks.js";
1212

1313
import assert from "node:assert/strict";
14-
import { afterEach, before, describe, it, mock } from "node:test";
14+
import { afterEach, before, describe, it } from "node:test";
1515
import { pathToFileURL } from "node:url";
1616

1717
import { HardhatError } from "@nomicfoundation/hardhat-errors";
@@ -22,7 +22,6 @@ import {
2222
} from "@nomicfoundation/hardhat-test-utils";
2323
import { isCi } from "@nomicfoundation/hardhat-utils/ci";
2424
import chalk from "chalk";
25-
import debug from "debug";
2625

2726
import {
2827
main,

0 commit comments

Comments
 (0)