Skip to content

Commit c7d3f41

Browse files
authored
Merge pull request #6989 from NomicFoundation/fs-traces
feat: implement call traces display for solidity tests
2 parents ac33930 + e8a23a2 commit c7d3f41

File tree

20 files changed

+510
-76
lines changed

20 files changed

+510
-76
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; call traces can be enabled via the `-vvvv` verbosity level flag ([#6830](https://github.com/NomicFoundation/hardhat/issues/6830))

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-global-options.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,6 @@ export const BUILTIN_GLOBAL_OPTIONS_DEFINITIONS: GlobalOptionDefinitions =
4949
}),
5050
},
5151
],
52-
[
53-
"verbose",
54-
{
55-
pluginId: "builtin",
56-
option: globalFlag({
57-
name: "verbose",
58-
description: "Enables Hardhat verbose logging.",
59-
}),
60-
},
61-
],
6252
[
6353
"version",
6454
{

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

Lines changed: 189 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
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 { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors";
10+
import { bytesToHexString } from "@nomicfoundation/hardhat-utils/hex";
311
import chalk from "chalk";
412

13+
type NestedArray<T> = Array<T | NestedArray<T>>;
14+
515
export function formatArtifactId(
616
artifactId: ArtifactId,
717
sourceNameToUserSourceName: Map<string, string>,
@@ -11,3 +21,181 @@ export function formatArtifactId(
1121

1222
return `${chalk.bold(`${sourceName}:${artifactId.name}`)} (v${artifactId.solcVersion})`;
1323
}
24+
25+
export function formatLogs(logs: string[], indent: number): string {
26+
return chalk.grey(
27+
logs.map((log) => `${" ".repeat(indent)}${log}`).join("\n"),
28+
);
29+
}
30+
31+
function formatInputs(
32+
inputs: DecodedTraceParameters | Uint8Array,
33+
color?: (text: string) => string,
34+
): string | undefined {
35+
if (inputs instanceof Uint8Array) {
36+
return inputs.length > 0 ? bytesToHexString(inputs) : undefined;
37+
} else {
38+
const formattedName =
39+
color !== undefined ? color(inputs.name) : inputs.name;
40+
return `${formattedName}(${inputs.arguments.join(", ")})`;
41+
}
42+
}
43+
44+
function formatOutputs(outputs: string | Uint8Array): string | undefined {
45+
if (outputs instanceof Uint8Array) {
46+
return outputs.length > 0 ? bytesToHexString(outputs) : undefined;
47+
} else {
48+
return outputs;
49+
}
50+
}
51+
52+
function formatLog(log: LogTrace): string[] {
53+
const { parameters } = log;
54+
const lines = [];
55+
if (Array.isArray(parameters)) {
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+
}
66+
} else {
67+
lines.push(
68+
`emit ${parameters.name}(${chalk.cyan(parameters.arguments.join(", "))})`,
69+
);
70+
}
71+
return lines;
72+
}
73+
74+
function formatKind(kind: CallKind): string | undefined {
75+
assertHardhatInvariant(
76+
kind !== CallKind.Create,
77+
"Unexpected call kind 'Create'",
78+
);
79+
80+
switch (kind) {
81+
case CallKind.Call:
82+
return undefined;
83+
case CallKind.CallCode:
84+
return "callcode";
85+
case CallKind.DelegateCall:
86+
return "delegatecall";
87+
case CallKind.StaticCall:
88+
return "staticcall";
89+
}
90+
}
91+
92+
function formatTrace(trace: CallTrace): NestedArray<string> {
93+
const {
94+
success,
95+
contract,
96+
inputs,
97+
gasUsed,
98+
value,
99+
kind,
100+
isCheatcode,
101+
outputs,
102+
} = trace;
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+
// TODO: Uncomment this when the formattedInputs starts containing
120+
// the address of where the contract was deployed instead of the code.
121+
// if (formattedInputs !== undefined) {
122+
// openingLine = `${openingLine}@${formattedInputs}`;
123+
// }
124+
} else {
125+
const formattedKind = formatKind(kind);
126+
openingLine = `[${gasUsed}] ${color(contract)}`;
127+
if (formattedInputs !== undefined) {
128+
openingLine = `${openingLine}::${formattedInputs}`;
129+
}
130+
if (value !== BigInt(0)) {
131+
openingLine = `${openingLine} {value: ${value}}`;
132+
}
133+
if (formattedKind !== undefined) {
134+
openingLine = `${openingLine} ${chalk.yellow(`[${formattedKind}]`)}`;
135+
}
136+
}
137+
if (formattedOutputs !== undefined) {
138+
if (
139+
formattedOutputs === "EvmError: Revert" ||
140+
formattedOutputs.startsWith("revert:")
141+
) {
142+
closingLine = `${color("←")} ${color("[Revert]")} ${formattedOutputs}`;
143+
} else {
144+
closingLine = `${color("←")} ${formattedOutputs}`;
145+
}
146+
}
147+
148+
const lines = [];
149+
lines.push(openingLine);
150+
for (const child of trace.children) {
151+
if (child.kind === LogKind.Log) {
152+
lines.push(formatLog(child));
153+
} else {
154+
lines.push(formatTrace(child));
155+
}
156+
}
157+
if (closingLine !== undefined) {
158+
lines.push([closingLine]);
159+
}
160+
return lines;
161+
}
162+
163+
function formatNestedArray(
164+
data: NestedArray<string>,
165+
prefix = "",
166+
isTopLevel = true,
167+
): string {
168+
let output = "";
169+
170+
for (let i = 0; i < data.length; i++) {
171+
const item = data[i];
172+
173+
if (Array.isArray(item) && typeof item[0] === "string") {
174+
const [label, ...children] = item;
175+
176+
if (isTopLevel) {
177+
output += `${prefix}${label}\n`;
178+
output += formatNestedArray(children, prefix, false);
179+
} else {
180+
const isLast = i === data.length - 1;
181+
const connector = isLast ? " └─ " : " ├─ ";
182+
const childPrefix = isLast ? " " : " | ";
183+
output += `${prefix}${connector}${label}\n`;
184+
output += formatNestedArray(children, prefix + childPrefix, false);
185+
}
186+
} else if (typeof item === "string") {
187+
const isLast = i === data.length - 1;
188+
const connector = isLast ? " └─ " : " ├─ ";
189+
output += `${prefix}${connector}${item}\n`;
190+
}
191+
}
192+
193+
return output;
194+
}
195+
196+
export function formatTraces(traces: CallTrace[], indent: number): string {
197+
const lines = traces.map(formatTrace);
198+
const formattedTraces = formatNestedArray(lines, " ".repeat(indent));
199+
// Remove the trailing newline
200+
return formattedTraces.slice(0, -1);
201+
}

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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ const hardhatPlugin: HardhatPlugin = {
3434
name: "noCompile",
3535
description: "Don't compile the project before running the tests",
3636
})
37+
.addLevel({
38+
name: "verbosity",
39+
shortName: "v",
40+
description: "Verbosity level of the test output",
41+
defaultValue: 2,
42+
})
3743
.setAction(import.meta.resolve("./task-action.js"))
3844
.build(),
3945
],

0 commit comments

Comments
 (0)