Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(forge build): add --sizes and --names JSON compatibility #9321

Merged
merged 4 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
zerosnacks marked this conversation as resolved.
Show resolved Hide resolved
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
Loading