Skip to content

Commit a79dfae

Browse files
authored
feat(forge build): add --sizes and --names JSON compatibility (#9321)
* add --sizes and --names JSON compatibility + generalize report kind * add additional json output tests * fix feedback nit
1 parent 9d7557f commit a79dfae

File tree

9 files changed

+175
-45
lines changed

9 files changed

+175
-45
lines changed

crates/common/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ num-format.workspace = true
5454
reqwest.workspace = true
5555
semver.workspace = true
5656
serde_json.workspace = true
57-
serde.workspace = true
57+
serde = { workspace = true, features = ["derive"] }
5858
thiserror.workspace = true
5959
tokio.workspace = true
6060
tracing.workspace = true

crates/common/src/compile.rs

+71-20
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
//! Support for compiling [foundry_compilers::Project]
22
3-
use crate::{term::SpinnerReporter, TestFunctionExt};
3+
use crate::{
4+
reports::{report_kind, ReportKind},
5+
shell,
6+
term::SpinnerReporter,
7+
TestFunctionExt,
8+
};
49
use comfy_table::{presets::ASCII_MARKDOWN, Attribute, Cell, CellAlignment, Color, Table};
510
use eyre::Result;
611
use foundry_block_explorers::contract::Metadata;
@@ -181,11 +186,13 @@ impl ProjectCompiler {
181186
}
182187

183188
if !quiet {
184-
if output.is_unchanged() {
185-
sh_println!("No files changed, compilation skipped")?;
186-
} else {
187-
// print the compiler output / warnings
188-
sh_println!("{output}")?;
189+
if !shell::is_json() {
190+
if output.is_unchanged() {
191+
sh_println!("No files changed, compilation skipped")?;
192+
} else {
193+
// print the compiler output / warnings
194+
sh_println!("{output}")?;
195+
}
189196
}
190197

191198
self.handle_output(&output);
@@ -205,26 +212,32 @@ impl ProjectCompiler {
205212
for (name, (_, version)) in output.versioned_artifacts() {
206213
artifacts.entry(version).or_default().push(name);
207214
}
208-
for (version, names) in artifacts {
209-
let _ = sh_println!(
210-
" compiler version: {}.{}.{}",
211-
version.major,
212-
version.minor,
213-
version.patch
214-
);
215-
for name in names {
216-
let _ = sh_println!(" - {name}");
215+
216+
if shell::is_json() {
217+
let _ = sh_println!("{}", serde_json::to_string(&artifacts).unwrap());
218+
} else {
219+
for (version, names) in artifacts {
220+
let _ = sh_println!(
221+
" compiler version: {}.{}.{}",
222+
version.major,
223+
version.minor,
224+
version.patch
225+
);
226+
for name in names {
227+
let _ = sh_println!(" - {name}");
228+
}
217229
}
218230
}
219231
}
220232

221233
if print_sizes {
222234
// add extra newline if names were already printed
223-
if print_names {
235+
if print_names && !shell::is_json() {
224236
let _ = sh_println!();
225237
}
226238

227-
let mut size_report = SizeReport { contracts: BTreeMap::new() };
239+
let mut size_report =
240+
SizeReport { report_kind: report_kind(), contracts: BTreeMap::new() };
228241

229242
let artifacts: BTreeMap<_, _> = output
230243
.artifact_ids()
@@ -278,6 +291,8 @@ const CONTRACT_INITCODE_SIZE_LIMIT: usize = 49152;
278291

279292
/// Contracts with info about their size
280293
pub struct SizeReport {
294+
/// What kind of report to generate.
295+
report_kind: ReportKind,
281296
/// `contract name -> info`
282297
pub contracts: BTreeMap<String, ContractInfo>,
283298
}
@@ -316,6 +331,43 @@ impl SizeReport {
316331

317332
impl Display for SizeReport {
318333
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
334+
match self.report_kind {
335+
ReportKind::Markdown => {
336+
let table = self.format_table_output();
337+
writeln!(f, "{table}")?;
338+
}
339+
ReportKind::JSON => {
340+
writeln!(f, "{}", self.format_json_output())?;
341+
}
342+
}
343+
344+
Ok(())
345+
}
346+
}
347+
348+
impl SizeReport {
349+
fn format_json_output(&self) -> String {
350+
let contracts = self
351+
.contracts
352+
.iter()
353+
.filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0))
354+
.map(|(name, contract)| {
355+
(
356+
name.clone(),
357+
serde_json::json!({
358+
"runtime_size": contract.runtime_size,
359+
"init_size": contract.init_size,
360+
"runtime_margin": CONTRACT_RUNTIME_SIZE_LIMIT as isize - contract.runtime_size as isize,
361+
"init_margin": CONTRACT_INITCODE_SIZE_LIMIT as isize - contract.init_size as isize,
362+
}),
363+
)
364+
})
365+
.collect::<serde_json::Map<_, _>>();
366+
367+
serde_json::to_string(&contracts).unwrap()
368+
}
369+
370+
fn format_table_output(&self) -> Table {
319371
let mut table = Table::new();
320372
table.load_preset(ASCII_MARKDOWN);
321373
table.set_header([
@@ -366,8 +418,7 @@ impl Display for SizeReport {
366418
]);
367419
}
368420

369-
writeln!(f, "{table}")?;
370-
Ok(())
421+
table
371422
}
372423
}
373424

@@ -476,7 +527,7 @@ pub fn etherscan_project(
476527
/// Configures the reporter and runs the given closure.
477528
pub fn with_compilation_reporter<O>(quiet: bool, f: impl FnOnce() -> O) -> O {
478529
#[allow(clippy::collapsible_else_if)]
479-
let reporter = if quiet {
530+
let reporter = if quiet || shell::is_json() {
480531
Report::new(NoReporter::default())
481532
} else {
482533
if std::io::stdout().is_terminal() {

crates/common/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pub mod errors;
2626
pub mod evm;
2727
pub mod fs;
2828
pub mod provider;
29+
pub mod reports;
2930
pub mod retry;
3031
pub mod selectors;
3132
pub mod serde_helpers;

crates/common/src/reports.rs

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
use serde::{Deserialize, Serialize};
2+
3+
use crate::shell;
4+
5+
#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
6+
pub enum ReportKind {
7+
#[default]
8+
Markdown,
9+
JSON,
10+
}
11+
12+
/// Determine the kind of report to generate based on the current shell.
13+
pub fn report_kind() -> ReportKind {
14+
if shell::is_json() {
15+
ReportKind::JSON
16+
} else {
17+
ReportKind::Markdown
18+
}
19+
}

crates/forge/bin/cmd/build.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,11 @@ impl BuildArgs {
105105
.print_names(self.names)
106106
.print_sizes(self.sizes)
107107
.ignore_eip_3860(self.ignore_eip_3860)
108-
.quiet(format_json)
109108
.bail(!format_json);
110109

111110
let output = compiler.compile(&project)?;
112111

113-
if format_json {
112+
if format_json && !self.names && !self.sizes {
114113
sh_println!("{}", serde_json::to_string_pretty(&output.output())?)?;
115114
}
116115

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

+1-2
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, GasReportKind},
8+
gas_report::GasReport,
99
multi_runner::matches_contract,
1010
result::{SuiteResult, TestOutcome, TestStatus},
1111
traces::{
@@ -583,7 +583,6 @@ impl TestArgs {
583583
config.gas_reports.clone(),
584584
config.gas_reports_ignore.clone(),
585585
config.gas_reports_include_tests,
586-
if shell::is_json() { GasReportKind::JSON } else { GasReportKind::Markdown },
587586
)
588587
});
589588

crates/forge/src/gas_report.rs

+17-20
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,23 @@ use crate::{
66
};
77
use alloy_primitives::map::HashSet;
88
use comfy_table::{presets::ASCII_MARKDOWN, *};
9-
use foundry_common::{calc, TestFunctionExt};
9+
use foundry_common::{
10+
calc,
11+
reports::{report_kind, ReportKind},
12+
TestFunctionExt,
13+
};
1014
use foundry_evm::traces::CallKind;
1115
use serde::{Deserialize, Serialize};
1216
use serde_json::json;
1317
use std::{collections::BTreeMap, fmt::Display};
1418

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-
2719
/// Represents the gas report for a set of contracts.
2820
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
2921
pub struct GasReport {
3022
/// Whether to report any contracts.
3123
report_any: bool,
3224
/// What kind of report to generate.
33-
report_type: GasReportKind,
25+
report_kind: ReportKind,
3426
/// Contracts to generate the report for.
3527
report_for: HashSet<String>,
3628
/// Contracts to ignore when generating the report.
@@ -47,13 +39,18 @@ impl GasReport {
4739
report_for: impl IntoIterator<Item = String>,
4840
ignore: impl IntoIterator<Item = String>,
4941
include_tests: bool,
50-
report_kind: GasReportKind,
5142
) -> Self {
5243
let report_for = report_for.into_iter().collect::<HashSet<_>>();
5344
let ignore = ignore.into_iter().collect::<HashSet<_>>();
5445
let report_any = report_for.is_empty() || report_for.contains("*");
55-
let report_type = report_kind;
56-
Self { report_any, report_type, report_for, ignore, include_tests, ..Default::default() }
46+
Self {
47+
report_any,
48+
report_kind: report_kind(),
49+
report_for,
50+
ignore,
51+
include_tests,
52+
..Default::default()
53+
}
5754
}
5855

5956
/// Whether the given contract should be reported.
@@ -158,8 +155,8 @@ impl GasReport {
158155

159156
impl Display for GasReport {
160157
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
161-
match self.report_type {
162-
GasReportKind::Markdown => {
158+
match self.report_kind {
159+
ReportKind::Markdown => {
163160
for (name, contract) in &self.contracts {
164161
if contract.functions.is_empty() {
165162
trace!(name, "gas report contract without functions");
@@ -171,7 +168,7 @@ impl Display for GasReport {
171168
writeln!(f, "\n")?;
172169
}
173170
}
174-
GasReportKind::JSON => {
171+
ReportKind::JSON => {
175172
writeln!(f, "{}", &self.format_json_output())?;
176173
}
177174
}

crates/forge/tests/cli/build.rs

+45
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,20 @@ forgetest!(initcode_size_exceeds_limit, |prj, cmd| {
8282
...
8383
"#
8484
]);
85+
86+
cmd.forge_fuse().args(["build", "--sizes", "--json"]).assert_failure().stdout_eq(
87+
str![[r#"
88+
{
89+
"HugeContract":{
90+
"runtime_size":202,
91+
"init_size":49359,
92+
"runtime_margin":24374,
93+
"init_margin":-207
94+
}
95+
}
96+
"#]]
97+
.is_json(),
98+
);
8599
});
86100

87101
forgetest!(initcode_size_limit_can_be_ignored, |prj, cmd| {
@@ -95,6 +109,23 @@ forgetest!(initcode_size_limit_can_be_ignored, |prj, cmd| {
95109
...
96110
"#
97111
]);
112+
113+
cmd.forge_fuse()
114+
.args(["build", "--sizes", "--ignore-eip-3860", "--json"])
115+
.assert_success()
116+
.stdout_eq(
117+
str![[r#"
118+
{
119+
"HugeContract": {
120+
"runtime_size": 202,
121+
"init_size": 49359,
122+
"runtime_margin": 24374,
123+
"init_margin": -207
124+
}
125+
}
126+
"#]]
127+
.is_json(),
128+
);
98129
});
99130

100131
// tests build output is as expected
@@ -118,6 +149,20 @@ forgetest_init!(build_sizes_no_forge_std, |prj, cmd| {
118149
...
119150
"#
120151
]);
152+
153+
cmd.forge_fuse().args(["build", "--sizes", "--json"]).assert_success().stdout_eq(
154+
str![[r#"
155+
{
156+
"Counter": {
157+
"runtime_size": 247,
158+
"init_size": 277,
159+
"runtime_margin": 24329,
160+
"init_margin": 48875
161+
}
162+
}
163+
"#]]
164+
.is_json(),
165+
);
121166
});
122167

123168
// tests that skip key in config can be used to skip non-compilable contract

crates/forge/tests/cli/cmd.rs

+19
Original file line numberDiff line numberDiff line change
@@ -2977,6 +2977,20 @@ Compiler run successful!
29772977
29782978
29792979
"#]]);
2980+
2981+
cmd.forge_fuse().args(["build", "--sizes", "--json"]).assert_success().stdout_eq(
2982+
str![[r#"
2983+
{
2984+
"Counter": {
2985+
"runtime_size": 247,
2986+
"init_size": 277,
2987+
"runtime_margin": 24329,
2988+
"init_margin": 48875
2989+
}
2990+
}
2991+
"#]]
2992+
.is_json(),
2993+
);
29802994
});
29812995

29822996
// checks that build --names includes all contracts even if unchanged
@@ -2992,6 +3006,11 @@ Compiler run successful!
29923006
...
29933007
29943008
"#]]);
3009+
3010+
cmd.forge_fuse()
3011+
.args(["build", "--names", "--json"])
3012+
.assert_success()
3013+
.stdout_eq(str![[r#""{...}""#]].is_json());
29953014
});
29963015

29973016
// <https://github.com/foundry-rs/foundry/issues/6816>

0 commit comments

Comments
 (0)