Skip to content

Commit

Permalink
feat(forge build): add --sizes and --names JSON compatibility (#…
Browse files Browse the repository at this point in the history
…9321)

* add --sizes and --names JSON compatibility + generalize report kind

* add additional json output tests

* fix feedback nit
  • Loading branch information
zerosnacks authored Nov 15, 2024
1 parent 9d7557f commit a79dfae
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 45 deletions.
2 changes: 1 addition & 1 deletion crates/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ num-format.workspace = true
reqwest.workspace = true
semver.workspace = true
serde_json.workspace = true
serde.workspace = true
serde = { workspace = true, features = ["derive"] }
thiserror.workspace = true
tokio.workspace = true
tracing.workspace = true
Expand Down
91 changes: 71 additions & 20 deletions crates/common/src/compile.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
//! Support for compiling [foundry_compilers::Project]

use crate::{term::SpinnerReporter, TestFunctionExt};
use crate::{
reports::{report_kind, ReportKind},
shell,
term::SpinnerReporter,
TestFunctionExt,
};
use comfy_table::{presets::ASCII_MARKDOWN, Attribute, Cell, CellAlignment, Color, Table};
use eyre::Result;
use foundry_block_explorers::contract::Metadata;
Expand Down Expand Up @@ -181,11 +186,13 @@ impl ProjectCompiler {
}

if !quiet {
if output.is_unchanged() {
sh_println!("No files changed, compilation skipped")?;
} else {
// print the compiler output / warnings
sh_println!("{output}")?;
if !shell::is_json() {
if output.is_unchanged() {
sh_println!("No files changed, compilation skipped")?;
} else {
// print the compiler output / warnings
sh_println!("{output}")?;
}
}

self.handle_output(&output);
Expand All @@ -205,26 +212,32 @@ impl ProjectCompiler {
for (name, (_, version)) in output.versioned_artifacts() {
artifacts.entry(version).or_default().push(name);
}
for (version, names) in artifacts {
let _ = sh_println!(
" compiler version: {}.{}.{}",
version.major,
version.minor,
version.patch
);
for name in names {
let _ = sh_println!(" - {name}");

if shell::is_json() {
let _ = sh_println!("{}", serde_json::to_string(&artifacts).unwrap());
} else {
for (version, names) in artifacts {
let _ = sh_println!(
" compiler version: {}.{}.{}",
version.major,
version.minor,
version.patch
);
for name in names {
let _ = sh_println!(" - {name}");
}
}
}
}

if print_sizes {
// add extra newline if names were already printed
if print_names {
if print_names && !shell::is_json() {
let _ = sh_println!();
}

let mut size_report = SizeReport { contracts: BTreeMap::new() };
let mut size_report =
SizeReport { report_kind: report_kind(), contracts: BTreeMap::new() };

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

/// Contracts with info about their size
pub struct SizeReport {
/// What kind of report to generate.
report_kind: ReportKind,
/// `contract name -> info`
pub contracts: BTreeMap<String, ContractInfo>,
}
Expand Down Expand Up @@ -316,6 +331,43 @@ impl SizeReport {

impl Display for SizeReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
match self.report_kind {
ReportKind::Markdown => {
let table = self.format_table_output();
writeln!(f, "{table}")?;
}
ReportKind::JSON => {
writeln!(f, "{}", self.format_json_output())?;
}
}

Ok(())
}
}

impl SizeReport {
fn format_json_output(&self) -> String {
let contracts = self
.contracts
.iter()
.filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0))
.map(|(name, contract)| {
(
name.clone(),
serde_json::json!({
"runtime_size": contract.runtime_size,
"init_size": contract.init_size,
"runtime_margin": CONTRACT_RUNTIME_SIZE_LIMIT as isize - contract.runtime_size as isize,
"init_margin": CONTRACT_INITCODE_SIZE_LIMIT as isize - contract.init_size as isize,
}),
)
})
.collect::<serde_json::Map<_, _>>();

serde_json::to_string(&contracts).unwrap()
}

fn format_table_output(&self) -> Table {
let mut table = Table::new();
table.load_preset(ASCII_MARKDOWN);
table.set_header([
Expand Down Expand Up @@ -366,8 +418,7 @@ impl Display for SizeReport {
]);
}

writeln!(f, "{table}")?;
Ok(())
table
}
}

Expand Down Expand Up @@ -476,7 +527,7 @@ pub fn etherscan_project(
/// Configures the reporter and runs the given closure.
pub fn with_compilation_reporter<O>(quiet: bool, f: impl FnOnce() -> O) -> O {
#[allow(clippy::collapsible_else_if)]
let reporter = if quiet {
let reporter = if quiet || shell::is_json() {
Report::new(NoReporter::default())
} else {
if std::io::stdout().is_terminal() {
Expand Down
1 change: 1 addition & 0 deletions crates/common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub mod errors;
pub mod evm;
pub mod fs;
pub mod provider;
pub mod reports;
pub mod retry;
pub mod selectors;
pub mod serde_helpers;
Expand Down
19 changes: 19 additions & 0 deletions crates/common/src/reports.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use serde::{Deserialize, Serialize};

use crate::shell;

#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum ReportKind {
#[default]
Markdown,
JSON,
}

/// Determine the kind of report to generate based on the current shell.
pub fn report_kind() -> ReportKind {
if shell::is_json() {
ReportKind::JSON
} else {
ReportKind::Markdown
}
}
3 changes: 1 addition & 2 deletions crates/forge/bin/cmd/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,11 @@ impl BuildArgs {
.print_names(self.names)
.print_sizes(self.sizes)
.ignore_eip_3860(self.ignore_eip_3860)
.quiet(format_json)
.bail(!format_json);

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

if format_json {
if format_json && !self.names && !self.sizes {
sh_println!("{}", serde_json::to_string_pretty(&output.output())?)?;
}

Expand Down
3 changes: 1 addition & 2 deletions crates/forge/bin/cmd/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use clap::{Parser, ValueHint};
use eyre::{Context, OptionExt, Result};
use forge::{
decode::decode_console_logs,
gas_report::{GasReport, GasReportKind},
gas_report::GasReport,
multi_runner::matches_contract,
result::{SuiteResult, TestOutcome, TestStatus},
traces::{
Expand Down Expand Up @@ -583,7 +583,6 @@ impl TestArgs {
config.gas_reports.clone(),
config.gas_reports_ignore.clone(),
config.gas_reports_include_tests,
if shell::is_json() { GasReportKind::JSON } else { GasReportKind::Markdown },
)
});

Expand Down
37 changes: 17 additions & 20 deletions crates/forge/src/gas_report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,23 @@ use crate::{
};
use alloy_primitives::map::HashSet;
use comfy_table::{presets::ASCII_MARKDOWN, *};
use foundry_common::{calc, TestFunctionExt};
use foundry_common::{
calc,
reports::{report_kind, ReportKind},
TestFunctionExt,
};
use foundry_evm::traces::CallKind;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::{collections::BTreeMap, fmt::Display};

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum GasReportKind {
Markdown,
JSON,
}

impl Default for GasReportKind {
fn default() -> Self {
Self::Markdown
}
}

/// Represents the gas report for a set of contracts.
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct GasReport {
/// Whether to report any contracts.
report_any: bool,
/// What kind of report to generate.
report_type: GasReportKind,
report_kind: ReportKind,
/// Contracts to generate the report for.
report_for: HashSet<String>,
/// Contracts to ignore when generating the report.
Expand All @@ -47,13 +39,18 @@ impl GasReport {
report_for: impl IntoIterator<Item = String>,
ignore: impl IntoIterator<Item = String>,
include_tests: bool,
report_kind: GasReportKind,
) -> Self {
let report_for = report_for.into_iter().collect::<HashSet<_>>();
let ignore = ignore.into_iter().collect::<HashSet<_>>();
let report_any = report_for.is_empty() || report_for.contains("*");
let report_type = report_kind;
Self { report_any, report_type, report_for, ignore, include_tests, ..Default::default() }
Self {
report_any,
report_kind: report_kind(),
report_for,
ignore,
include_tests,
..Default::default()
}
}

/// Whether the given contract should be reported.
Expand Down Expand Up @@ -158,8 +155,8 @@ impl GasReport {

impl Display for GasReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
match self.report_type {
GasReportKind::Markdown => {
match self.report_kind {
ReportKind::Markdown => {
for (name, contract) in &self.contracts {
if contract.functions.is_empty() {
trace!(name, "gas report contract without functions");
Expand All @@ -171,7 +168,7 @@ impl Display for GasReport {
writeln!(f, "\n")?;
}
}
GasReportKind::JSON => {
ReportKind::JSON => {
writeln!(f, "{}", &self.format_json_output())?;
}
}
Expand Down
45 changes: 45 additions & 0 deletions crates/forge/tests/cli/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,20 @@ forgetest!(initcode_size_exceeds_limit, |prj, cmd| {
...
"#
]);

cmd.forge_fuse().args(["build", "--sizes", "--json"]).assert_failure().stdout_eq(
str![[r#"
{
"HugeContract":{
"runtime_size":202,
"init_size":49359,
"runtime_margin":24374,
"init_margin":-207
}
}
"#]]
.is_json(),
);
});

forgetest!(initcode_size_limit_can_be_ignored, |prj, cmd| {
Expand All @@ -95,6 +109,23 @@ forgetest!(initcode_size_limit_can_be_ignored, |prj, cmd| {
...
"#
]);

cmd.forge_fuse()
.args(["build", "--sizes", "--ignore-eip-3860", "--json"])
.assert_success()
.stdout_eq(
str![[r#"
{
"HugeContract": {
"runtime_size": 202,
"init_size": 49359,
"runtime_margin": 24374,
"init_margin": -207
}
}
"#]]
.is_json(),
);
});

// tests build output is as expected
Expand All @@ -118,6 +149,20 @@ forgetest_init!(build_sizes_no_forge_std, |prj, cmd| {
...
"#
]);

cmd.forge_fuse().args(["build", "--sizes", "--json"]).assert_success().stdout_eq(
str![[r#"
{
"Counter": {
"runtime_size": 247,
"init_size": 277,
"runtime_margin": 24329,
"init_margin": 48875
}
}
"#]]
.is_json(),
);
});

// tests that skip key in config can be used to skip non-compilable contract
Expand Down
19 changes: 19 additions & 0 deletions crates/forge/tests/cli/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2977,6 +2977,20 @@ Compiler run successful!
"#]]);

cmd.forge_fuse().args(["build", "--sizes", "--json"]).assert_success().stdout_eq(
str![[r#"
{
"Counter": {
"runtime_size": 247,
"init_size": 277,
"runtime_margin": 24329,
"init_margin": 48875
}
}
"#]]
.is_json(),
);
});

// checks that build --names includes all contracts even if unchanged
Expand All @@ -2992,6 +3006,11 @@ Compiler run successful!
...
"#]]);

cmd.forge_fuse()
.args(["build", "--names", "--json"])
.assert_success()
.stdout_eq(str![[r#""{...}""#]].is_json());
});

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

0 comments on commit a79dfae

Please sign in to comment.