Skip to content

Commit 0ec018d

Browse files
authored
feat: make --gas-report JSON output compatible (#9063)
* add gas report generation in JSON * skip junit for now * add json formatted tests, trailing space and invalid formatting * avoid redundant modifications for calls count * replace existing tests with snapbox * clean up snapbox tests * merge in master * calls -> frames * use .is_jsonlines()
1 parent 15fdb2a commit 0ec018d

File tree

3 files changed

+367
-270
lines changed

3 files changed

+367
-270
lines changed

crates/forge/bin/cmd/test/mod.rs

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use clap::{Parser, ValueHint};
55
use eyre::{Context, OptionExt, Result};
66
use forge::{
77
decode::decode_console_logs,
8-
gas_report::GasReport,
8+
gas_report::{GasReport, GasReportKind},
99
multi_runner::matches_contract,
1010
result::{SuiteResult, TestOutcome, TestStatus},
1111
traces::{
@@ -112,7 +112,7 @@ pub struct TestArgs {
112112
json: bool,
113113

114114
/// Output test results as JUnit XML report.
115-
#[arg(long, conflicts_with = "json", help_heading = "Display options")]
115+
#[arg(long, conflicts_with_all(["json", "gas_report"]), help_heading = "Display options")]
116116
junit: bool,
117117

118118
/// Stop running tests after the first failure.
@@ -474,6 +474,9 @@ impl TestArgs {
474474

475475
trace!(target: "forge::test", "running all tests");
476476

477+
// If we need to render to a serialized format, we should not print anything else to stdout.
478+
let silent = self.gas_report && self.json;
479+
477480
let num_filtered = runner.matching_test_functions(filter).count();
478481
if num_filtered != 1 && (self.debug.is_some() || self.flamegraph || self.flamechart) {
479482
let action = if self.flamegraph {
@@ -500,7 +503,7 @@ impl TestArgs {
500503
}
501504

502505
// Run tests in a non-streaming fashion and collect results for serialization.
503-
if self.json {
506+
if !self.gas_report && self.json {
504507
let mut results = runner.test_collect(filter);
505508
results.values_mut().for_each(|suite_result| {
506509
for test_result in suite_result.test_results.values_mut() {
@@ -565,9 +568,13 @@ impl TestArgs {
565568
}
566569
let mut decoder = builder.build();
567570

568-
let mut gas_report = self
569-
.gas_report
570-
.then(|| GasReport::new(config.gas_reports.clone(), config.gas_reports_ignore.clone()));
571+
let mut gas_report = self.gas_report.then(|| {
572+
GasReport::new(
573+
config.gas_reports.clone(),
574+
config.gas_reports_ignore.clone(),
575+
if self.json { GasReportKind::JSON } else { GasReportKind::Markdown },
576+
)
577+
});
571578

572579
let mut gas_snapshots = BTreeMap::<String, BTreeMap<String, String>>::new();
573580

@@ -588,30 +595,34 @@ impl TestArgs {
588595
self.flamechart;
589596

590597
// Print suite header.
591-
println!();
592-
for warning in suite_result.warnings.iter() {
593-
eprintln!("{} {warning}", "Warning:".yellow().bold());
594-
}
595-
if !tests.is_empty() {
596-
let len = tests.len();
597-
let tests = if len > 1 { "tests" } else { "test" };
598-
println!("Ran {len} {tests} for {contract_name}");
598+
if !silent {
599+
println!();
600+
for warning in suite_result.warnings.iter() {
601+
eprintln!("{} {warning}", "Warning:".yellow().bold());
602+
}
603+
if !tests.is_empty() {
604+
let len = tests.len();
605+
let tests = if len > 1 { "tests" } else { "test" };
606+
println!("Ran {len} {tests} for {contract_name}");
607+
}
599608
}
600609

601610
// Process individual test results, printing logs and traces when necessary.
602611
for (name, result) in tests {
603-
shell::println(result.short_result(name))?;
604-
605-
// We only display logs at level 2 and above
606-
if verbosity >= 2 {
607-
// We only decode logs from Hardhat and DS-style console events
608-
let console_logs = decode_console_logs(&result.logs);
609-
if !console_logs.is_empty() {
610-
println!("Logs:");
611-
for log in console_logs {
612-
println!(" {log}");
612+
if !silent {
613+
shell::println(result.short_result(name))?;
614+
615+
// We only display logs at level 2 and above
616+
if verbosity >= 2 {
617+
// We only decode logs from Hardhat and DS-style console events
618+
let console_logs = decode_console_logs(&result.logs);
619+
if !console_logs.is_empty() {
620+
println!("Logs:");
621+
for log in console_logs {
622+
println!(" {log}");
623+
}
624+
println!();
613625
}
614-
println!();
615626
}
616627
}
617628

@@ -653,7 +664,7 @@ impl TestArgs {
653664
}
654665
}
655666

656-
if !decoded_traces.is_empty() {
667+
if !silent && !decoded_traces.is_empty() {
657668
shell::println("Traces:")?;
658669
for trace in &decoded_traces {
659670
shell::println(trace)?;
@@ -760,7 +771,9 @@ impl TestArgs {
760771
}
761772

762773
// Print suite summary.
763-
shell::println(suite_result.summary())?;
774+
if !silent {
775+
shell::println(suite_result.summary())?;
776+
}
764777

765778
// Add the suite result to the outcome.
766779
outcome.results.insert(contract_name, suite_result);
@@ -781,7 +794,7 @@ impl TestArgs {
781794
outcome.gas_report = Some(finalized);
782795
}
783796

784-
if !outcome.results.is_empty() {
797+
if !silent && !outcome.results.is_empty() {
785798
shell::println(outcome.summary(duration))?;
786799

787800
if self.summary {
@@ -1063,7 +1076,7 @@ contract FooBarTest is DSTest {
10631076
let call_cnts = gas_report
10641077
.contracts
10651078
.values()
1066-
.flat_map(|c| c.functions.values().flat_map(|f| f.values().map(|v| v.calls.len())))
1079+
.flat_map(|c| c.functions.values().flat_map(|f| f.values().map(|v| v.frames.len())))
10671080
.collect::<Vec<_>>();
10681081
// assert that all functions were called at least 100 times
10691082
assert!(call_cnts.iter().all(|c| *c > 100));

crates/forge/src/gas_report.rs

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,25 @@ use serde::{Deserialize, Serialize};
1212
use std::{collections::BTreeMap, fmt::Display};
1313
use yansi::Paint;
1414

15+
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
16+
pub enum GasReportKind {
17+
Markdown,
18+
JSON,
19+
}
20+
21+
impl Default for GasReportKind {
22+
fn default() -> Self {
23+
Self::Markdown
24+
}
25+
}
26+
1527
/// Represents the gas report for a set of contracts.
1628
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1729
pub struct GasReport {
1830
/// Whether to report any contracts.
1931
report_any: bool,
32+
/// What kind of report to generate.
33+
report_type: GasReportKind,
2034
/// Contracts to generate the report for.
2135
report_for: HashSet<String>,
2236
/// Contracts to ignore when generating the report.
@@ -30,11 +44,13 @@ impl GasReport {
3044
pub fn new(
3145
report_for: impl IntoIterator<Item = String>,
3246
ignore: impl IntoIterator<Item = String>,
47+
report_kind: GasReportKind,
3348
) -> Self {
3449
let report_for = report_for.into_iter().collect::<HashSet<_>>();
3550
let ignore = ignore.into_iter().collect::<HashSet<_>>();
3651
let report_any = report_for.is_empty() || report_for.contains("*");
37-
Self { report_any, report_for, ignore, ..Default::default() }
52+
let report_type = report_kind;
53+
Self { report_any, report_type, report_for, ignore, ..Default::default() }
3854
}
3955

4056
/// Whether the given contract should be reported.
@@ -113,7 +129,7 @@ impl GasReport {
113129
.or_default()
114130
.entry(signature.clone())
115131
.or_default();
116-
gas_info.calls.push(trace.gas_used);
132+
gas_info.frames.push(trace.gas_used);
117133
}
118134
}
119135
}
@@ -125,11 +141,12 @@ impl GasReport {
125141
for contract in self.contracts.values_mut() {
126142
for sigs in contract.functions.values_mut() {
127143
for func in sigs.values_mut() {
128-
func.calls.sort_unstable();
129-
func.min = func.calls.first().copied().unwrap_or_default();
130-
func.max = func.calls.last().copied().unwrap_or_default();
131-
func.mean = calc::mean(&func.calls);
132-
func.median = calc::median_sorted(&func.calls);
144+
func.frames.sort_unstable();
145+
func.min = func.frames.first().copied().unwrap_or_default();
146+
func.max = func.frames.last().copied().unwrap_or_default();
147+
func.mean = calc::mean(&func.frames);
148+
func.median = calc::median_sorted(&func.frames);
149+
func.calls = func.frames.len() as u64;
133150
}
134151
}
135152
}
@@ -145,6 +162,11 @@ impl Display for GasReport {
145162
continue;
146163
}
147164

165+
if self.report_type == GasReportKind::JSON {
166+
writeln!(f, "{}", serde_json::to_string(&contract).unwrap())?;
167+
continue;
168+
}
169+
148170
let mut table = Table::new();
149171
table.load_preset(ASCII_MARKDOWN);
150172
table.set_header([Cell::new(format!("{name} contract"))
@@ -176,7 +198,7 @@ impl Display for GasReport {
176198
Cell::new(gas_info.mean.to_string()).fg(Color::Yellow),
177199
Cell::new(gas_info.median.to_string()).fg(Color::Yellow),
178200
Cell::new(gas_info.max.to_string()).fg(Color::Red),
179-
Cell::new(gas_info.calls.len().to_string()),
201+
Cell::new(gas_info.calls.to_string()),
180202
]);
181203
})
182204
});
@@ -197,9 +219,12 @@ pub struct ContractInfo {
197219

198220
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
199221
pub struct GasInfo {
200-
pub calls: Vec<u64>,
222+
pub calls: u64,
201223
pub min: u64,
202224
pub mean: u64,
203225
pub median: u64,
204226
pub max: u64,
227+
228+
#[serde(skip)]
229+
pub frames: Vec<u64>,
205230
}

0 commit comments

Comments
 (0)