Skip to content

Commit 5956ecb

Browse files
committed
feat: format call traces
1 parent e3e9757 commit 5956ecb

File tree

4 files changed

+132
-42
lines changed

4 files changed

+132
-42
lines changed

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

Lines changed: 119 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ import type {
66
} from "@ignored/edr-optimism";
77

88
import { LogKind, CallKind } from "@ignored/edr-optimism";
9+
import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors";
910
import { bytesToHexString } from "@nomicfoundation/hardhat-utils/hex";
1011
import chalk from "chalk";
1112

13+
type NestedArray<T> = Array<T | NestedArray<T>>;
14+
1215
export function formatArtifactId(
1316
artifactId: ArtifactId,
1417
sourceNameToUserSourceName: Map<string, string>,
@@ -25,51 +28,68 @@ export function formatLogs(logs: string[], indent: number): string {
2528
);
2629
}
2730

28-
export function formatInputs(inputs: DecodedTraceParameters | Uint8Array): string {
31+
export function formatInputs(
32+
inputs: DecodedTraceParameters | Uint8Array,
33+
color?: (text: string) => string,
34+
): string | undefined {
2935
if (inputs instanceof Uint8Array) {
30-
return bytesToHexString(inputs);
36+
return inputs.length > 0 ? bytesToHexString(inputs) : undefined;
3137
} else {
32-
return `${inputs.name}(${inputs.arguments.join(", ")})`;
38+
const formattedName =
39+
color !== undefined ? color(inputs.name) : inputs.name;
40+
return `${formattedName}(${inputs.arguments.join(", ")})`;
3341
}
3442
}
3543

36-
function formatOutputs(outputs: string | Uint8Array): string {
44+
function formatOutputs(outputs: string | Uint8Array): string | undefined {
3745
if (outputs instanceof Uint8Array) {
38-
return bytesToHexString(outputs);
46+
return outputs.length > 0 ? bytesToHexString(outputs) : undefined;
3947
} else {
4048
return outputs;
4149
}
4250
}
4351

44-
function formatLog(log: LogTrace, indent: number): string {
52+
function formatLog(log: LogTrace): string[] {
4553
const { parameters } = log;
54+
const lines = [];
4655
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})`)}`;
56+
const topics = parameters.map((topic) => bytesToHexString(topic));
57+
if (topics.length > 0) {
58+
lines.push(`emit topic 0: ${chalk.cyan(topics[0])}`);
59+
}
60+
for (let i = 1; i < topics.length - 1; i++) {
61+
lines.push(` topic ${i}: ${chalk.cyan(topics[i])}`);
62+
}
63+
if (topics.length > 1) {
64+
lines.push(` data: ${chalk.cyan(topics[topics.length - 1])}`);
65+
}
5266
} else {
53-
return `${" ".repeat(indent)}${parameters.name}(${parameters.arguments.join(", ")})`;
67+
lines.push(
68+
`emit ${parameters.name}(chalk.cyan(${parameters.arguments.join(", ")}))`,
69+
);
5470
}
71+
return lines;
5572
}
5673

57-
function formatKind(kind: CallKind): string {
74+
function formatKind(kind: CallKind): string | undefined {
75+
assertHardhatInvariant(
76+
kind !== CallKind.Create,
77+
"Unexpected call kind 'Create'",
78+
);
79+
5880
switch (kind) {
5981
case CallKind.Call:
60-
return "Call";
82+
return undefined;
6183
case CallKind.CallCode:
62-
return "CallCode";
84+
return "callcode";
6385
case CallKind.DelegateCall:
64-
return "DelegateCall";
86+
return "delegatecall";
6587
case CallKind.StaticCall:
66-
return "StaticCall";
67-
case CallKind.Create:
68-
return "Create";
88+
return "staticcall";
6989
}
7090
}
7191

72-
function formatTrace(trace: CallTrace, indent: number): string {
92+
function formatTrace(trace: CallTrace): NestedArray<string> {
7393
const {
7494
success,
7595
contract,
@@ -80,22 +100,91 @@ function formatTrace(trace: CallTrace, indent: number): string {
80100
isCheatcode,
81101
outputs,
82102
} = 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-
];
103+
let color;
104+
if (isCheatcode) {
105+
color = chalk.blue;
106+
} else if (success) {
107+
color = chalk.green;
108+
} else {
109+
color = chalk.red;
110+
}
111+
112+
const formattedInputs = formatInputs(inputs, color);
113+
const formattedOutputs = formatOutputs(outputs);
114+
115+
let openingLine: string;
116+
let closingLine: string | undefined;
117+
if (kind === CallKind.Create) {
118+
openingLine = `[${gasUsed}] ${chalk.yellow("→ new")} ${contract}`;
119+
if (formattedInputs !== undefined) {
120+
openingLine = `${openingLine}@${formattedInputs}`;
121+
}
122+
} else {
123+
const formattedKind = formatKind(kind);
124+
openingLine = `[${gasUsed}] ${color(contract)}`;
125+
if (formattedInputs !== undefined) {
126+
openingLine = `${openingLine}::${formattedInputs}`;
127+
}
128+
if (value !== BigInt(0)) {
129+
openingLine = `${openingLine} {value: ${value}}`;
130+
}
131+
if (formattedKind !== undefined) {
132+
openingLine = `${openingLine} ${chalk.yellow(`[${formattedKind}]`)}`;
133+
}
134+
}
135+
if (formattedOutputs !== undefined) {
136+
closingLine = `${color("←")} ${formattedOutputs}`;
137+
}
138+
139+
const lines = [];
140+
lines.push(openingLine);
89141
for (const child of trace.children) {
90142
if (child.kind === LogKind.Log) {
91-
lines.push(formatLog(child, indent + 2));
143+
lines.push(formatLog(child));
92144
} else {
93-
lines.push(formatTrace(child, indent + 2));
145+
lines.push(formatTrace(child));
146+
}
147+
}
148+
if (closingLine !== undefined) {
149+
lines.push([closingLine]);
150+
}
151+
return lines;
152+
}
153+
154+
function formatNestedArray(
155+
data: NestedArray<string>,
156+
prefix = "",
157+
isTopLevel = true,
158+
): string {
159+
let output = "";
160+
161+
for (let i = 0; i < data.length; i++) {
162+
const item = data[i];
163+
164+
if (Array.isArray(item) && typeof item[0] === "string") {
165+
const [label, ...children] = item;
166+
167+
if (isTopLevel) {
168+
output += `${prefix}${label}\n`;
169+
output += formatNestedArray(children, prefix, false);
170+
} else {
171+
const isLast = i === data.length - 1;
172+
const connector = isLast ? " └─ " : " ├─ ";
173+
const childPrefix = isLast ? " " : " | ";
174+
output += `${prefix}${connector}${label}\n`;
175+
output += formatNestedArray(children, prefix + childPrefix, false);
176+
}
177+
} else if (typeof item === "string") {
178+
const isLast = i === data.length - 1;
179+
const connector = isLast ? " └─ " : " ├─ ";
180+
output += `${prefix}${connector}${item}\n`;
94181
}
95182
}
96-
return lines.join("\n");
183+
184+
return output;
97185
}
98186

99187
export function formatTraces(traces: CallTrace[], indent: number): string {
100-
return traces.map((trace) => formatTrace(trace, indent)).join("\n");
188+
const lines = traces.map(formatTrace);
189+
return formatNestedArray(lines, " ".repeat(indent));
101190
}

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

Lines changed: 8 additions & 5 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, formatInputs, formatLogs, formatTraces } from "./formatters.js";
13+
import { formatArtifactId, formatLogs, formatTraces } from "./formatters.js";
1414
import { getMessageFromLastStackTraceEntry } from "./stack-trace-solidity-errors.js";
1515

1616
/**
@@ -119,15 +119,18 @@ export async function* testReporter(
119119
}
120120

121121
if (printSetUpTraces || printExecutionTraces) {
122-
const callTraces = testResult.callTraces().filter(({inputs}) => {
122+
const callTraces = testResult.callTraces().filter(({ inputs }) => {
123123
if (printSetUpTraces && printExecutionTraces) {
124124
return true;
125125
}
126-
const formattedInputs = formatInputs(inputs);
127-
if (printSetUpTraces && formattedInputs === "setUp()") {
126+
let functionName: string | undefined;
127+
if (!(inputs instanceof Uint8Array)) {
128+
functionName = inputs.name;
129+
}
130+
if (printSetUpTraces && functionName === "setUp") {
128131
return true;
129132
}
130-
if (printExecutionTraces && formattedInputs !== "setUp()") {
133+
if (printExecutionTraces && functionName !== "setUp()") {
131134
return true;
132135
}
133136
return false;

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

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@ interface TestActionArguments {
3131
chainType: string;
3232
grep?: string;
3333
noCompile: boolean;
34+
verbosity: number;
3435
}
3536

3637
const runSolidityTests: NewTaskActionFunction<TestActionArguments> = async (
37-
{ testFiles, chainType, grep, noCompile },
38+
{ testFiles, chainType, grep, noCompile, verbosity },
3839
hre,
3940
) => {
4041
let rootFilePaths: string[];
@@ -115,7 +116,7 @@ const runSolidityTests: NewTaskActionFunction<TestActionArguments> = async (
115116
chainType,
116117
hre.config.paths.root,
117118
solidityTestConfig,
118-
hre.globalOptions.verbosity,
119+
verbosity,
119120
grep,
120121
);
121122
const tracingConfig: TracingConfigWithBuffers = {
@@ -151,11 +152,7 @@ const runSolidityTests: NewTaskActionFunction<TestActionArguments> = async (
151152
}
152153
})
153154
.compose((source) =>
154-
testReporter(
155-
source,
156-
sourceNameToUserSourceName,
157-
hre.globalOptions.verbosity,
158-
),
155+
testReporter(source, sourceNameToUserSourceName, verbosity),
159156
);
160157

161158
const outputStream = testReporterStream.pipe(

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ interface TestActionArguments {
1717
chainType: string;
1818
grep: string | undefined;
1919
noCompile: boolean;
20+
verbosity: number;
2021
}
2122

2223
const runAllTests: NewTaskActionFunction<TestActionArguments> = async (

0 commit comments

Comments
 (0)