From f4210c4c61312964856104ddd8ff0c5d12289057 Mon Sep 17 00:00:00 2001 From: derch Date: Tue, 9 Aug 2022 03:18:00 -0500 Subject: [PATCH] feat(forge): add contract ignore list for gas reports --- Cargo.lock | 1 + cli/src/cmd/forge/test/mod.rs | 2 +- cli/test-utils/src/util.rs | 2 + cli/tests/it/cmd.rs | 397 ++++++++++++++++++++++++++++++++++ cli/tests/it/config.rs | 1 + config/src/lib.rs | 3 + forge/Cargo.toml | 1 + forge/src/gas_report.rs | 40 ++-- 8 files changed, 430 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 188f12c85947..ce2e867a3f18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1901,6 +1901,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "yansi", ] [[package]] diff --git a/cli/src/cmd/forge/test/mod.rs b/cli/src/cmd/forge/test/mod.rs index 61b4cb0229a6..d95f535601f3 100644 --- a/cli/src/cmd/forge/test/mod.rs +++ b/cli/src/cmd/forge/test/mod.rs @@ -492,7 +492,7 @@ fn test( thread::spawn(move || runner.test(&filter, Some(tx), include_fuzz_tests).unwrap()); let mut results: BTreeMap = BTreeMap::new(); - let mut gas_report = GasReport::new(config.gas_reports); + let mut gas_report = GasReport::new(config.gas_reports, config.gas_reports_ignore); for (contract_name, suite_result) in rx { let mut tests = suite_result.test_results.clone(); println!(); diff --git a/cli/test-utils/src/util.rs b/cli/test-utils/src/util.rs index 8ab6e8e16560..d77a66f965b3 100644 --- a/cli/test-utils/src/util.rs +++ b/cli/test-utils/src/util.rs @@ -501,6 +501,8 @@ impl TestCommand { } /// Runs the command and prints its output + /// You have to pass --nocapture to cargo test or the print won't be displayed. + /// The full command would be: cargo test -- --nocapture pub fn print_output(&mut self) { let output = self.execute(); println!("stdout: {}", String::from_utf8_lossy(&output.stdout)); diff --git a/cli/tests/it/cmd.rs b/cli/tests/it/cmd.rs index c3ce8a22ecc1..afaa080ad341 100644 --- a/cli/tests/it/cmd.rs +++ b/cli/tests/it/cmd.rs @@ -861,3 +861,400 @@ contract MyTokenCopy is MyToken { assert!(output.contains("Compiler run successful",)); } ); + +forgetest!(gas_report_all_contracts, |prj: TestProject, mut cmd: TestCommand| { + prj.insert_ds_test(); + prj.inner() + .add_source( + "Contracts.sol", + r#" +//SPDX-license-identifier: MIT +pragma solidity ^0.8.0; + +import "./test.sol"; + +contract ContractOne { + int public i; + + constructor() { + i = 0; + } + + function foo() public{ + while(i<5){ + i++; + } + } +} + +contract ContractOneTest is DSTest { + ContractOne c1; + + function setUp() public { + c1 = new ContractOne(); + } + + function testFoo() public { + c1.foo(); + } +} + + +contract ContractTwo { + int public i; + + constructor() { + i = 0; + } + + function bar() public{ + while(i<50){ + i++; + } + } +} + +contract ContractTwoTest is DSTest { + ContractTwo c2; + + function setUp() public { + c2 = new ContractTwo(); + } + + function testBar() public { + c2.bar(); + } +} + +contract ContractThree { + int public i; + + constructor() { + i = 0; + } + + function baz() public{ + while(i<500){ + i++; + } + } +} + +contract ContractThreeTest is DSTest { + ContractThree c3; + + function setUp() public { + c3 = new ContractThree(); + } + + function testBaz() public { + c3.baz(); + } +} + "#, + ) + .unwrap(); + + // report for all + prj.write_config(Config { + gas_reports: (vec!["*".to_string()]), + gas_reports_ignore: (vec![]), + ..Default::default() + }); + cmd.forge_fuse(); + let first_out = cmd.arg("test").arg("--gas-report").stdout(); + assert!(first_out.contains("foo") && first_out.contains("bar") && first_out.contains("baz")); + // cmd.arg("test").arg("--gas-report").print_output(); + + cmd.forge_fuse(); + prj.write_config(Config { gas_reports: (vec![]), ..Default::default() }); + cmd.forge_fuse(); + let second_out = cmd.arg("test").arg("--gas-report").stdout(); + assert!(second_out.contains("foo") && second_out.contains("bar") && second_out.contains("baz")); + // cmd.arg("test").arg("--gas-report").print_output(); + + cmd.forge_fuse(); + prj.write_config(Config { gas_reports: (vec!["*".to_string()]), ..Default::default() }); + cmd.forge_fuse(); + let third_out = cmd.arg("test").arg("--gas-report").stdout(); + assert!(third_out.contains("foo") && third_out.contains("bar") && third_out.contains("baz")); + // cmd.arg("test").arg("--gas-report").print_output(); + + cmd.forge_fuse(); + prj.write_config(Config { + gas_reports: (vec![ + "ContractOne".to_string(), + "ContractTwo".to_string(), + "ContractThree".to_string(), + ]), + ..Default::default() + }); + cmd.forge_fuse(); + let fourth_out = cmd.arg("test").arg("--gas-report").stdout(); + assert!(fourth_out.contains("foo") && fourth_out.contains("bar") && fourth_out.contains("baz")); + // cmd.arg("test").arg("--gas-report").print_output(); +}); + +forgetest!(gas_report_some_contracts, |prj: TestProject, mut cmd: TestCommand| { + prj.insert_ds_test(); + prj.inner() + .add_source( + "Contracts.sol", + r#" +//SPDX-license-identifier: MIT +pragma solidity ^0.8.0; + +import "./test.sol"; + +contract ContractOne { + int public i; + + constructor() { + i = 0; + } + + function foo() public{ + while(i<5){ + i++; + } + } +} + +contract ContractOneTest is DSTest { + ContractOne c1; + + function setUp() public { + c1 = new ContractOne(); + } + + function testFoo() public { + c1.foo(); + } +} + + +contract ContractTwo { + int public i; + + constructor() { + i = 0; + } + + function bar() public{ + while(i<50){ + i++; + } + } +} + +contract ContractTwoTest is DSTest { + ContractTwo c2; + + function setUp() public { + c2 = new ContractTwo(); + } + + function testBar() public { + c2.bar(); + } +} + +contract ContractThree { + int public i; + + constructor() { + i = 0; + } + + function baz() public{ + while(i<500){ + i++; + } + } +} + +contract ContractThreeTest is DSTest { + ContractThree c3; + + function setUp() public { + c3 = new ContractThree(); + } + + function testBaz() public { + c3.baz(); + } +} + "#, + ) + .unwrap(); + + // report for One + prj.write_config(Config { + gas_reports: (vec!["ContractOne".to_string()]), + gas_reports_ignore: (vec![]), + ..Default::default() + }); + cmd.forge_fuse(); + let first_out = cmd.arg("test").arg("--gas-report").stdout(); + assert!(first_out.contains("foo") && !first_out.contains("bar") && !first_out.contains("baz")); + // cmd.arg("test").arg("--gas-report").print_output(); + + // report for Two + cmd.forge_fuse(); + prj.write_config(Config { + gas_reports: (vec!["ContractTwo".to_string()]), + ..Default::default() + }); + cmd.forge_fuse(); + let second_out = cmd.arg("test").arg("--gas-report").stdout(); + assert!( + !second_out.contains("foo") && second_out.contains("bar") && !second_out.contains("baz") + ); + // cmd.arg("test").arg("--gas-report").print_output(); + + // report for Three + cmd.forge_fuse(); + prj.write_config(Config { + gas_reports: (vec!["ContractThree".to_string()]), + ..Default::default() + }); + cmd.forge_fuse(); + let third_out = cmd.arg("test").arg("--gas-report").stdout(); + assert!(!third_out.contains("foo") && !third_out.contains("bar") && third_out.contains("baz")); + // cmd.arg("test").arg("--gas-report").print_output(); +}); + +forgetest!(gas_ignore_some_contracts, |prj: TestProject, mut cmd: TestCommand| { + prj.insert_ds_test(); + prj.inner() + .add_source( + "Contracts.sol", + r#" +//SPDX-license-identifier: MIT +pragma solidity ^0.8.0; + +import "./test.sol"; + +contract ContractOne { + int public i; + + constructor() { + i = 0; + } + + function foo() public{ + while(i<5){ + i++; + } + } +} + +contract ContractOneTest is DSTest { + ContractOne c1; + + function setUp() public { + c1 = new ContractOne(); + } + + function testFoo() public { + c1.foo(); + } +} + + +contract ContractTwo { + int public i; + + constructor() { + i = 0; + } + + function bar() public{ + while(i<50){ + i++; + } + } +} + +contract ContractTwoTest is DSTest { + ContractTwo c2; + + function setUp() public { + c2 = new ContractTwo(); + } + + function testBar() public { + c2.bar(); + } +} + +contract ContractThree { + int public i; + + constructor() { + i = 0; + } + + function baz() public{ + while(i<500){ + i++; + } + } +} + +contract ContractThreeTest is DSTest { + ContractThree c3; + + function setUp() public { + c3 = new ContractThree(); + } + + function testBaz() public { + c3.baz(); + } +} + "#, + ) + .unwrap(); + + // ignore ContractOne + prj.write_config(Config { + gas_reports: (vec!["*".to_string()]), + gas_reports_ignore: (vec!["ContractOne".to_string()]), + ..Default::default() + }); + cmd.forge_fuse(); + let first_out = cmd.arg("test").arg("--gas-report").stdout(); + assert!(!first_out.contains("foo") && first_out.contains("bar") && first_out.contains("baz")); + // cmd.arg("test").arg("--gas-report").print_output(); + + // ignore ContractTwo + cmd.forge_fuse(); + prj.write_config(Config { + gas_reports: (vec![]), + gas_reports_ignore: (vec!["ContractTwo".to_string()]), + ..Default::default() + }); + cmd.forge_fuse(); + let second_out = cmd.arg("test").arg("--gas-report").stdout(); + assert!( + second_out.contains("foo") && !second_out.contains("bar") && second_out.contains("baz") + ); + // cmd.arg("test").arg("--gas-report").print_output(); + + // ignore ContractThree + cmd.forge_fuse(); + prj.write_config(Config { + gas_reports: (vec![ + "ContractOne".to_string(), + "ContractTwo".to_string(), + "ContractThree".to_string(), + ]), + gas_reports_ignore: (vec!["ContractThree".to_string()]), + ..Default::default() + }); + cmd.forge_fuse(); + let third_out = cmd.arg("test").arg("--gas-report").stdout(); + assert!(third_out.contains("foo") && third_out.contains("bar") && third_out.contains("baz")); + // cmd.arg("test").arg("--gas-report").print_output(); +}); diff --git a/cli/tests/it/config.rs b/cli/tests/it/config.rs index e129569447c0..ea4b2cd42e2e 100644 --- a/cli/tests/it/config.rs +++ b/cli/tests/it/config.rs @@ -35,6 +35,7 @@ forgetest!(can_extract_config_values, |prj: TestProject, mut cmd: TestCommand| { force: true, evm_version: EvmVersion::Byzantium, gas_reports: vec!["Contract".to_string()], + gas_reports_ignore: vec![], solc: Some(SolcReq::Local(PathBuf::from("custom-solc"))), auto_detect_solc: false, offline: true, diff --git a/config/src/lib.rs b/config/src/lib.rs index c54c0c0659c1..552ec2068c03 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -132,6 +132,8 @@ pub struct Config { pub evm_version: EvmVersion, /// list of contracts to report gas of pub gas_reports: Vec, + /// list of contracts to ignore for gas reports + pub gas_reports_ignore: Vec, /// The Solc instance to use if any. /// /// This takes precedence over `auto_detect_solc`, if a version is set then this overrides @@ -1445,6 +1447,7 @@ impl Default for Config { force: false, evm_version: Default::default(), gas_reports: vec!["*".to_string()], + gas_reports_ignore: vec![], solc: None, auto_detect_solc: true, offline: false, diff --git a/forge/Cargo.toml b/forge/Cargo.toml index a94b6f2e96a7..b83ca85f02f4 100644 --- a/forge/Cargo.toml +++ b/forge/Cargo.toml @@ -18,6 +18,7 @@ serde = "1.0.130" regex = { version = "1.5.4", default-features = false } hex = "0.4.3" glob = "0.3.0" +yansi = "0.5.1" # TODO: Trim down tokio = { version = "1", features = ["time"] } tracing = "0.1" diff --git a/forge/src/gas_report.rs b/forge/src/gas_report.rs index 96c3e546af54..99a9e0ed744b 100644 --- a/forge/src/gas_report.rs +++ b/forge/src/gas_report.rs @@ -10,6 +10,7 @@ use std::{collections::BTreeMap, fmt::Display}; #[derive(Default, Debug, Serialize, Deserialize)] pub struct GasReport { pub report_for: Vec, + pub ignore: Vec, pub contracts: BTreeMap, } @@ -30,22 +31,17 @@ pub struct GasInfo { } impl GasReport { - pub fn new(report_for: Vec) -> Self { - Self { report_for, ..Default::default() } + pub fn new(report_for: Vec, ignore: Vec) -> Self { + Self { report_for, ignore, ..Default::default() } } pub fn analyze(&mut self, traces: &[(TraceKind, CallTraceArena)]) { - let report_for_all = self.report_for.is_empty() || self.report_for.iter().any(|s| s == "*"); traces.iter().for_each(|(_, trace)| { - self.analyze_trace(trace, report_for_all); + self.analyze_node(0, trace); }); } - fn analyze_trace(&mut self, trace: &CallTraceArena, report_for_all: bool) { - self.analyze_node(0, trace, report_for_all); - } - - fn analyze_node(&mut self, node_index: usize, arena: &CallTraceArena, report_for_all: bool) { + fn analyze_node(&mut self, node_index: usize, arena: &CallTraceArena) { let node = &arena.arena[node_index]; let trace = &node.trace; @@ -54,12 +50,24 @@ impl GasReport { } if let Some(name) = &trace.contract { - // checking contract allowlist for reporting by extracting name out of identifier - let report_for = self - .report_for - .iter() - .any(|s| s == name.rsplit(':').next().unwrap_or(name.as_str())); - if report_for || report_for_all { + let contract_name = name.rsplit(':').next().unwrap_or(name.as_str()).to_string(); + // If the user listed the contract in 'gas_reports' (the foundry.toml field) a + // report for the contract is generated even if it's listed in the ignore + // list. This is addressed this way because getting a report you don't expect is + // preferable than not getting one you expect. A warning is printed to stderr + // indicating the "double listing". + if self.report_for.contains(&contract_name) && self.ignore.contains(&contract_name) { + eprintln!( + "{}: {} is listed in both 'gas_reports' and 'gas_reports_ignore'.", + yansi::Paint::yellow("warning").bold(), + contract_name + ); + } + let report_contract = (!self.ignore.contains(&contract_name) && + self.report_for.contains(&"*".to_string())) || + (!self.ignore.contains(&contract_name) && self.report_for.is_empty()) || + (self.report_for.contains(&contract_name)); + if report_contract { let mut contract_report = self.contracts.entry(name.to_string()).or_insert_with(Default::default); @@ -86,7 +94,7 @@ impl GasReport { } node.children.iter().for_each(|index| { - self.analyze_node(*index, arena, report_for_all); + self.analyze_node(*index, arena); }); }