diff --git a/crates/cheatcodes/src/config.rs b/crates/cheatcodes/src/config.rs index f93ab4f5867c..a2b9a5f5b091 100644 --- a/crates/cheatcodes/src/config.rs +++ b/crates/cheatcodes/src/config.rs @@ -1,8 +1,8 @@ use super::Result; use crate::{script::ScriptWallets, Vm::Rpc}; use alloy_primitives::Address; -use foundry_common::fs::normalize_path; -use foundry_compilers::{utils::canonicalize, ArtifactId, ProjectPathsConfig}; +use foundry_common::{fs::normalize_path, ContractsByArtifact}; +use foundry_compilers::{utils::canonicalize, ProjectPathsConfig}; use foundry_config::{ cache::StorageCachingConfig, fs_permissions::FsAccessKind, Config, FsPermissions, ResolvedRpcEndpoints, @@ -12,6 +12,7 @@ use semver::Version; use std::{ collections::HashMap, path::{Path, PathBuf}, + sync::Arc, time::Duration, }; @@ -47,7 +48,7 @@ pub struct CheatsConfig { /// Artifacts which are guaranteed to be fresh (either recompiled or cached). /// If Some, `vm.getDeployedCode` invocations are validated to be in scope of this list. /// If None, no validation is performed. - pub available_artifacts: Option>, + pub available_artifacts: Option>, /// Version of the script/test contract which is currently running. pub running_version: Option, } @@ -57,7 +58,7 @@ impl CheatsConfig { pub fn new( config: &Config, evm_opts: EvmOpts, - available_artifacts: Option>, + available_artifacts: Option>, script_wallets: Option, running_version: Option, ) -> Self { diff --git a/crates/cheatcodes/src/fs.rs b/crates/cheatcodes/src/fs.rs index 9b22c4d8d2ae..cecfc8822177 100644 --- a/crates/cheatcodes/src/fs.rs +++ b/crates/cheatcodes/src/fs.rs @@ -4,7 +4,7 @@ use super::string::parse; use crate::{Cheatcode, Cheatcodes, Result, Vm::*}; use alloy_dyn_abi::DynSolType; use alloy_json_abi::ContractObject; -use alloy_primitives::U256; +use alloy_primitives::{Bytes, U256}; use alloy_sol_types::SolValue; use dialoguer::{Input, Password}; use foundry_common::fs; @@ -251,24 +251,14 @@ impl Cheatcode for writeLineCall { impl Cheatcode for getCodeCall { fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { artifactPath: path } = self; - let object = read_bytecode(state, path)?; - if let Some(bin) = object.bytecode { - Ok(bin.abi_encode()) - } else { - Err(fmt_err!("No bytecode for contract. Is it abstract or unlinked?")) - } + Ok(get_artifact_code(state, path, false)?.abi_encode()) } } impl Cheatcode for getDeployedCodeCall { fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { artifactPath: path } = self; - let object = read_bytecode(state, path)?; - if let Some(bin) = object.deployed_bytecode { - Ok(bin.abi_encode()) - } else { - Err(fmt_err!("No deployed bytecode for contract. Is it abstract or unlinked?")) - } + Ok(get_artifact_code(state, path, true)?.abi_encode()) } } @@ -282,9 +272,9 @@ impl Cheatcode for getDeployedCodeCall { /// - `path/to/contract.sol:0.8.23` /// - `ContractName` /// - `ContractName:0.8.23` -fn get_artifact_path(state: &Cheatcodes, path: &str) -> Result { - if path.ends_with(".json") { - Ok(PathBuf::from(path)) +fn get_artifact_code(state: &Cheatcodes, path: &str, deployed: bool) -> Result { + let path = if path.ends_with(".json") { + PathBuf::from(path) } else { let mut parts = path.split(':'); @@ -314,11 +304,11 @@ fn get_artifact_path(state: &Cheatcodes, path: &str) -> Result { None }; - // Use available artifacts list if available - if let Some(available_ids) = &state.config.available_artifacts { - let filtered = available_ids + // Use available artifacts list if present + if let Some(artifacts) = &state.config.available_artifacts { + let filtered = artifacts .iter() - .filter(|id| { + .filter(|(id, _)| { // name might be in the form of "Counter.0.8.23" let id_name = id.name.split('.').next().unwrap(); @@ -356,7 +346,7 @@ fn get_artifact_path(state: &Cheatcodes, path: &str) -> Result { .and_then(|version| { let filtered = filtered .into_iter() - .filter(|id| id.version == *version) + .filter(|(id, _)| id.version == *version) .collect::>(); (filtered.len() == 1).then_some(filtered[0]) @@ -365,31 +355,33 @@ fn get_artifact_path(state: &Cheatcodes, path: &str) -> Result { } }?; - Ok(artifact.path.clone()) + if deployed { + return Ok(artifact.1.deployed_bytecode.clone()) + } else { + return Ok(artifact.1.bytecode.clone()) + } } else { - let path_in_artifacts = - match (file.map(|f| f.to_string_lossy().to_string()), contract_name) { - (Some(file), Some(contract_name)) => Ok(format!("{file}/{contract_name}.json")), - (None, Some(contract_name)) => { - Ok(format!("{contract_name}.sol/{contract_name}.json")) - } - (Some(file), None) => { - let name = file.replace(".sol", ""); - Ok(format!("{file}/{name}.json")) - } - _ => Err(fmt_err!("Invalid artifact path")), - }?; - Ok(state.config.paths.artifacts.join(path_in_artifacts)) + match (file.map(|f| f.to_string_lossy().to_string()), contract_name) { + (Some(file), Some(contract_name)) => { + PathBuf::from(format!("{file}/{contract_name}.json")) + } + (None, Some(contract_name)) => { + PathBuf::from(format!("{contract_name}.sol/{contract_name}.json")) + } + (Some(file), None) => { + let name = file.replace(".sol", ""); + PathBuf::from(format!("{file}/{name}.json")) + } + _ => return Err(fmt_err!("Invalid artifact path")), + } } - } -} + }; -/// Reads the bytecode object(s) from the matching artifact -fn read_bytecode(state: &Cheatcodes, path: &str) -> Result { - let path = get_artifact_path(state, path)?; let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?; let data = fs::read_to_string(path)?; - serde_json::from_str::(&data).map_err(Into::into) + let artifact = serde_json::from_str::(&data)?; + let maybe_bytecode = if deployed { artifact.deployed_bytecode } else { artifact.bytecode }; + maybe_bytecode.ok_or_else(|| fmt_err!("No bytecode for contract. Is it abstract or unlinked?")) } impl Cheatcode for ffiCall { diff --git a/crates/common/src/contracts.rs b/crates/common/src/contracts.rs index d400e0a0824e..561d7229b528 100644 --- a/crates/common/src/contracts.rs +++ b/crates/common/src/contracts.rs @@ -32,6 +32,27 @@ type ArtifactWithContractRef<'a> = (&'a ArtifactId, &'a ContractData); pub struct ContractsByArtifact(pub BTreeMap); impl ContractsByArtifact { + /// Creates a new instance by collecting all artifacts with present bytecode from an iterator. + /// + /// It is recommended to use this method with an output of + /// [foundry_linking::Linker::get_linked_artifacts]. + pub fn new(artifacts: impl IntoIterator) -> Self { + Self( + artifacts + .into_iter() + .filter_map(|(id, artifact)| { + let name = id.name.clone(); + let bytecode = artifact.bytecode.and_then(|b| b.into_bytes())?; + let deployed_bytecode = + artifact.deployed_bytecode.and_then(|b| b.into_bytes())?; + let abi = artifact.abi?; + + Some((id, ContractData { name, abi, bytecode, deployed_bytecode })) + }) + .collect(), + ) + } + /// Finds a contract which has a similar bytecode as `code`. pub fn find_by_creation_code(&self, code: &[u8]) -> Option { self.iter() diff --git a/crates/evm/evm/src/executors/invariant/error.rs b/crates/evm/evm/src/executors/invariant/error.rs index 0695ad6c4eff..ae122d0fa7be 100644 --- a/crates/evm/evm/src/executors/invariant/error.rs +++ b/crates/evm/evm/src/executors/invariant/error.rs @@ -149,7 +149,7 @@ impl FailedInvariantCaseData { pub fn replay( &self, mut executor: Executor, - known_contracts: Option<&ContractsByArtifact>, + known_contracts: &ContractsByArtifact, mut ided_contracts: ContractsByAddress, logs: &mut Vec, traces: &mut Traces, diff --git a/crates/evm/evm/src/executors/invariant/funcs.rs b/crates/evm/evm/src/executors/invariant/funcs.rs index daa326b0c9f6..b3a913fb3e65 100644 --- a/crates/evm/evm/src/executors/invariant/funcs.rs +++ b/crates/evm/evm/src/executors/invariant/funcs.rs @@ -71,7 +71,7 @@ pub fn assert_invariants( pub fn replay_run( invariant_contract: &InvariantContract<'_>, mut executor: Executor, - known_contracts: Option<&ContractsByArtifact>, + known_contracts: &ContractsByArtifact, mut ided_contracts: ContractsByAddress, logs: &mut Vec, traces: &mut Traces, diff --git a/crates/evm/traces/src/lib.rs b/crates/evm/traces/src/lib.rs index 81f13f95a230..a52fd3b42eaa 100644 --- a/crates/evm/traces/src/lib.rs +++ b/crates/evm/traces/src/lib.rs @@ -12,7 +12,7 @@ use foundry_common::contracts::{ContractsByAddress, ContractsByArtifact}; use foundry_evm_core::constants::CHEATCODE_ADDRESS; use futures::{future::BoxFuture, FutureExt}; use serde::{Deserialize, Serialize}; -use std::{collections::BTreeMap, fmt::Write}; +use std::fmt::Write; use yansi::{Color, Paint}; /// Call trace address identifiers. @@ -293,12 +293,8 @@ fn trace_color(trace: &CallTrace) -> Color { } /// Given a list of traces and artifacts, it returns a map connecting address to abi -pub fn load_contracts( - traces: Traces, - known_contracts: Option<&ContractsByArtifact>, -) -> ContractsByAddress { - let Some(contracts) = known_contracts else { return BTreeMap::new() }; - let mut local_identifier = LocalTraceIdentifier::new(contracts); +pub fn load_contracts(traces: Traces, known_contracts: &ContractsByArtifact) -> ContractsByAddress { + let mut local_identifier = LocalTraceIdentifier::new(known_contracts); let mut decoder = CallTraceDecoderBuilder::new().build(); for (_, trace) in &traces { decoder.identify(trace, &mut local_identifier); @@ -308,7 +304,7 @@ pub fn load_contracts( .contracts .iter() .filter_map(|(addr, name)| { - if let Ok(Some((_, contract))) = contracts.find_by_name_or_identifier(name) { + if let Ok(Some((_, contract))) = known_contracts.find_by_name_or_identifier(name) { return Some((*addr, (name.clone(), contract.abi.clone()))); } None diff --git a/crates/forge/bin/cmd/coverage.rs b/crates/forge/bin/cmd/coverage.rs index bbd99de331e2..fc919934f037 100644 --- a/crates/forge/bin/cmd/coverage.rs +++ b/crates/forge/bin/cmd/coverage.rs @@ -7,7 +7,6 @@ use forge::{ analysis::SourceAnalyzer, anchors::find_anchors, BytecodeReporter, ContractId, CoverageReport, CoverageReporter, DebugReporter, LcovReporter, SummaryReporter, }, - inspectors::CheatsConfig, opts::EvmOpts, result::SuiteResult, revm::primitives::SpecId, @@ -28,7 +27,11 @@ use foundry_compilers::{ use foundry_config::{Config, SolcReq}; use rustc_hash::FxHashMap; use semver::Version; -use std::{collections::HashMap, path::PathBuf, sync::mpsc::channel}; +use std::{ + collections::HashMap, + path::PathBuf, + sync::{mpsc::channel, Arc}, +}; use yansi::Paint; /// A map, keyed by contract ID, to a tuple of the deployment source map and the runtime source map. @@ -101,7 +104,7 @@ impl CoverageArgs { let report = self.prepare(&config, output.clone())?; p_println!(!self.opts.silent => "Running tests..."); - self.collect(project, output, report, config, evm_opts).await + self.collect(project, output, report, Arc::new(config), evm_opts).await } /// Builds the project. @@ -308,29 +311,20 @@ impl CoverageArgs { project: Project, output: ProjectCompileOutput, mut report: CoverageReport, - config: Config, + config: Arc, evm_opts: EvmOpts, ) -> Result<()> { let root = project.paths.root; - let artifact_ids = output.artifact_ids().map(|(id, _)| id).collect(); - // Build the contract runner let env = evm_opts.evm_env().await?; - let mut runner = MultiContractRunnerBuilder::default() + let mut runner = MultiContractRunnerBuilder::new(config.clone()) .initial_balance(evm_opts.initial_balance) .evm_spec(config.evm_spec_id()) .sender(evm_opts.sender) .with_fork(evm_opts.get_fork(&config, env.clone())) - .with_cheats_config(CheatsConfig::new( - &config, - evm_opts.clone(), - Some(artifact_ids), - None, - None, - )) .with_test_options(TestOptions { - fuzz: config.fuzz, + fuzz: config.fuzz.clone(), invariant: config.invariant, ..Default::default() }) @@ -338,31 +332,32 @@ impl CoverageArgs { .build(&root, output, env, evm_opts)?; // Run tests - let known_contracts = runner.known_contracts.clone(); let filter = self.filter; let (tx, rx) = channel::<(String, SuiteResult)>(); let handle = tokio::task::spawn_blocking(move || runner.test(&filter, tx)); // Add hit data to the coverage report - let data = rx - .into_iter() - .flat_map(|(_, suite)| suite.test_results.into_values()) - .filter_map(|mut result| result.coverage.take()) - .flat_map(|hit_maps| { - hit_maps.0.into_values().filter_map(|map| { + let data = rx.into_iter().flat_map(|(_, suite)| { + let mut hits = Vec::new(); + for (_, mut result) in suite.test_results { + let Some(hit_maps) = result.coverage.take() else { continue }; + + for map in hit_maps.0.into_values() { if let Some((id, _)) = - known_contracts.find_by_deployed_code(map.bytecode.as_ref()) + suite.known_contracts.find_by_deployed_code(map.bytecode.as_ref()) { - Some((id, map, true)) + hits.push((id.clone(), map, true)); } else if let Some((id, _)) = - known_contracts.find_by_creation_code(map.bytecode.as_ref()) + suite.known_contracts.find_by_creation_code(map.bytecode.as_ref()) { - Some((id, map, false)) - } else { - None + hits.push((id.clone(), map, false)); } - }) - }); + } + } + + hits + }); + for (artifact_id, hits, is_deployed_code) in data { // TODO: Note down failing tests if let Some(source_id) = report.get_source_id( diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index 16f5ec516633..4a120adc7355 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -5,7 +5,6 @@ use eyre::Result; use forge::{ decode::decode_console_logs, gas_report::GasReport, - inspectors::CheatsConfig, multi_runner::matches_contract, result::{SuiteResult, TestOutcome, TestStatus}, traces::{identifier::SignaturesIdentifier, CallTraceDecoderBuilder, TraceKind}, @@ -35,7 +34,7 @@ use regex::Regex; use std::{ collections::{BTreeMap, BTreeSet}, path::PathBuf, - sync::mpsc::channel, + sync::{mpsc::channel, Arc}, time::Instant, }; use watchexec::config::{InitConfig, RuntimeConfig}; @@ -274,21 +273,14 @@ impl TestArgs { // Clone the output only if we actually need it later for the debugger. let output_clone = should_debug.then(|| output.clone()); - let artifact_ids = output.artifact_ids().map(|(id, _)| id).collect(); + let config = Arc::new(config); - let runner = MultiContractRunnerBuilder::default() + let runner = MultiContractRunnerBuilder::new(config.clone()) .set_debug(should_debug) .initial_balance(evm_opts.initial_balance) .evm_spec(config.evm_spec_id()) .sender(evm_opts.sender) .with_fork(evm_opts.get_fork(&config, env.clone())) - .with_cheats_config(CheatsConfig::new( - &config, - evm_opts.clone(), - Some(artifact_ids), - None, - None, // populated separately for each test contract - )) .with_test_options(test_options) .enable_isolation(evm_opts.isolate) .build(project_root, output, env, evm_opts)?; @@ -328,7 +320,7 @@ impl TestArgs { .debug_arenas(test_result.debug.as_slice()) .sources(sources) .breakpoints(test_result.breakpoints.clone()); - if let Some(decoder) = &outcome.decoder { + if let Some(decoder) = &outcome.last_run_decoder { builder = builder.decoder(decoder); } let mut debugger = builder.build(); @@ -342,7 +334,7 @@ impl TestArgs { pub async fn run_tests( &self, mut runner: MultiContractRunner, - config: Config, + config: Arc, verbosity: u8, filter: &ProjectPathsAwareFilter, ) -> eyre::Result { @@ -367,15 +359,7 @@ impl TestArgs { return Ok(TestOutcome::new(results, self.allow_failure)); } - // Set up trace identifiers. - let known_contracts = runner.known_contracts.clone(); let remote_chain_id = runner.evm_opts.get_remote_chain_id(); - let mut identifier = TraceIdentifiers::new().with_local(&known_contracts); - - // Avoid using etherscan for gas report as we decode more traces and this will be expensive. - if !self.gas_report { - identifier = identifier.with_etherscan(&config, remote_chain_id)?; - } // Run tests. let (tx, rx) = channel::<(String, SuiteResult)>(); @@ -385,24 +369,9 @@ impl TestArgs { move || runner.test(&filter, tx) }); - let mut gas_report = - self.gas_report.then(|| GasReport::new(config.gas_reports, config.gas_reports_ignore)); - - // Build the trace decoder. - let mut builder = CallTraceDecoderBuilder::new() - .with_known_contracts(&known_contracts) - .with_verbosity(verbosity); - // Signatures are of no value for gas reports. - if !self.gas_report { - builder = builder.with_signature_identifier(SignaturesIdentifier::new( - Config::foundry_cache_dir(), - config.offline, - )?); - } - let mut decoder = builder.build(); - - // We identify addresses if we're going to print *any* trace or gas report. - let identify_addresses = verbosity >= 3 || self.gas_report || self.debug.is_some(); + let mut gas_report = self + .gas_report + .then(|| GasReport::new(config.gas_reports.clone(), config.gas_reports_ignore.clone())); let mut outcome = TestOutcome::empty(self.allow_failure); @@ -410,6 +379,32 @@ impl TestArgs { for (contract_name, suite_result) in rx { let tests = &suite_result.test_results; + // Set up trace identifiers. + let known_contracts = suite_result.known_contracts.clone(); + let mut identifier = TraceIdentifiers::new().with_local(&known_contracts); + + // Avoid using etherscan for gas report as we decode more traces and this will be + // expensive. + if !self.gas_report { + identifier = identifier.with_etherscan(&config, remote_chain_id)?; + } + + // Build the trace decoder. + let mut builder = CallTraceDecoderBuilder::new() + .with_known_contracts(&known_contracts) + .with_verbosity(verbosity); + // Signatures are of no value for gas reports. + if !self.gas_report { + builder = builder.with_signature_identifier(SignaturesIdentifier::new( + Config::foundry_cache_dir(), + config.offline, + )?); + } + let mut decoder = builder.build(); + + // We identify addresses if we're going to print *any* trace or gas report. + let identify_addresses = verbosity >= 3 || self.gas_report || self.debug.is_some(); + // Print suite header. println!(); for warning in suite_result.warnings.iter() { @@ -515,6 +510,7 @@ impl TestArgs { // Add the suite result to the outcome. outcome.results.insert(contract_name, suite_result); + outcome.last_run_decoder = Some(decoder); // Stop processing the remaining suites if any test failed and `fail_fast` is set. if self.fail_fast && any_test_failed { @@ -525,8 +521,6 @@ impl TestArgs { trace!(target: "forge::test", len=outcome.results.len(), %any_test_failed, "done with results"); - outcome.decoder = Some(decoder); - if let Some(gas_report) = gas_report { let finalized = gas_report.finalize(); shell::println(&finalized)?; diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index 8bfb1477b4e0..9fed56982a8b 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -4,10 +4,9 @@ use crate::{result::SuiteResult, ContractRunner, TestFilter, TestOptions}; use alloy_json_abi::{Function, JsonAbi}; use alloy_primitives::{Address, Bytes, U256}; use eyre::Result; -use foundry_common::{get_contract_name, ContractData, ContractsByArtifact, TestFunctionExt}; -use foundry_compilers::{ - artifacts::Libraries, contracts::ArtifactContracts, Artifact, ArtifactId, ProjectCompileOutput, -}; +use foundry_common::{get_contract_name, ContractsByArtifact, TestFunctionExt}; +use foundry_compilers::{artifacts::Libraries, Artifact, ArtifactId, ProjectCompileOutput}; +use foundry_config::Config; use foundry_evm::{ backend::Backend, decode::RevertDecoder, executors::ExecutorBuilder, fork::CreateFork, inspectors::CheatsConfig, opts::EvmOpts, revm, @@ -16,6 +15,7 @@ use foundry_linking::{LinkOutput, Linker}; use rayon::prelude::*; use revm::primitives::SpecId; use std::{ + borrow::Borrow, collections::BTreeMap, fmt::Debug, path::Path, @@ -39,8 +39,6 @@ pub struct MultiContractRunner { /// Mapping of contract name to JsonAbi, creation bytecode and library bytecode which /// needs to be deployed & linked against pub contracts: DeployableContracts, - /// Compiled contracts by name that have an JsonAbi and runtime bytecode - pub known_contracts: ContractsByArtifact, /// The EVM instance used in the test runner pub evm_opts: EvmOpts, /// The configured evm @@ -51,12 +49,10 @@ pub struct MultiContractRunner { pub revert_decoder: RevertDecoder, /// The address which will be used as the `from` field in all EVM calls pub sender: Option
, - /// A map of contract names to absolute source file paths - pub source_paths: BTreeMap, /// The fork to use at launch pub fork: Option, - /// Additional cheatcode inspector related settings derived from the `Config` - pub cheats_config: Arc, + /// Project config. + pub config: Arc, /// Whether to collect coverage info pub coverage: bool, /// Whether to collect debug info @@ -65,6 +61,8 @@ pub struct MultiContractRunner { pub test_options: TestOptions, /// Whether to enable call isolation pub isolation: bool, + /// Output of the project compilation + pub output: ProjectCompileOutput, } impl MultiContractRunner { @@ -180,8 +178,18 @@ impl MultiContractRunner { let identifier = artifact_id.identifier(); let mut span_name = identifier.as_str(); - let mut cheats_config = self.cheats_config.as_ref().clone(); - cheats_config.running_version = Some(artifact_id.version.clone()); + let linker = + Linker::new(self.config.project_paths().root, self.output.artifact_ids().collect()); + let linked_contracts = linker.get_linked_artifacts(&contract.libraries).unwrap_or_default(); + let known_contracts = Arc::new(ContractsByArtifact::new(linked_contracts)); + + let cheats_config = CheatsConfig::new( + &self.config, + self.evm_opts.clone(), + Some(known_contracts.clone()), + None, + Some(artifact_id.version.clone()), + ); let executor = ExecutorBuilder::new() .inspectors(|stack| { @@ -212,7 +220,7 @@ impl MultiContractRunner { &self.revert_decoder, self.debug, ); - let r = runner.run_tests(filter, &self.test_options, Some(&self.known_contracts)); + let r = runner.run_tests(filter, &self.test_options, known_contracts); debug!(duration=?r.duration, "executed all tests in contract"); @@ -221,7 +229,7 @@ impl MultiContractRunner { } /// Builder used for instantiating the multi-contract runner -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug)] #[must_use = "builders do nothing unless you call `build` on them"] pub struct MultiContractRunnerBuilder { /// The address which will be used to deploy the initial contracts and send all @@ -233,8 +241,8 @@ pub struct MultiContractRunnerBuilder { pub evm_spec: Option, /// The fork to use at launch pub fork: Option, - /// Additional cheatcode inspector related settings derived from the `Config` - pub cheats_config: Option, + /// Project config. + pub config: Arc, /// Whether or not to collect coverage info pub coverage: bool, /// Whether or not to collect debug info @@ -246,6 +254,20 @@ pub struct MultiContractRunnerBuilder { } impl MultiContractRunnerBuilder { + pub fn new(config: Arc) -> Self { + Self { + config, + sender: Default::default(), + initial_balance: Default::default(), + evm_spec: Default::default(), + fork: Default::default(), + coverage: Default::default(), + debug: Default::default(), + isolation: Default::default(), + test_options: Default::default(), + } + } + pub fn sender(mut self, sender: Address) -> Self { self.sender = Some(sender); self @@ -266,11 +288,6 @@ impl MultiContractRunnerBuilder { self } - pub fn with_cheats_config(mut self, cheats_config: CheatsConfig) -> Self { - self.cheats_config = Some(cheats_config); - self - } - pub fn with_test_options(mut self, test_options: TestOptions) -> Self { self.test_options = Some(test_options); self @@ -300,107 +317,71 @@ impl MultiContractRunnerBuilder { env: revm::primitives::Env, evm_opts: EvmOpts, ) -> Result { - // This is just the contracts compiled, but we need to merge this with the read cached - // artifacts. - let contracts = output - .with_stripped_file_prefixes(root) - .into_artifacts() - .map(|(i, c)| (i, c.into_contract_bytecode())) - .collect::(); - - let source_paths = contracts - .iter() - .map(|(i, _)| (i.identifier(), root.join(&i.source).to_string_lossy().into())) - .collect::>(); + let output = output.with_stripped_file_prefixes(root); + let linker = Linker::new(root, output.artifact_ids().collect()); - let linker = Linker::new( - root, - contracts.iter().map(|(id, artifact)| (id.clone(), artifact)).collect(), - ); + // Build revert decoder from ABIs of all artifacts. + let abis = linker + .contracts + .iter() + .filter_map(|(_, contract)| contract.abi.as_ref().map(|abi| abi.borrow())); + let revert_decoder = RevertDecoder::new().with_abis(abis); // Create a mapping of name => (abi, deployment code, Vec) let mut deployable_contracts = DeployableContracts::default(); - let mut known_contracts = ContractsByArtifact::default(); - - for (id, contract) in contracts.iter() { + for (id, contract) in linker.contracts.iter() { let Some(abi) = contract.abi.as_ref() else { continue; }; - let name = id.name.clone(); - - let LinkOutput { libs_to_deploy, libraries } = - linker.link_with_nonce_or_address(Default::default(), evm_opts.sender, 1, id)?; - - let linked_contract = linker.link(id, &libraries)?; - - // get bytes if deployable, else add to known contracts and continue. - // interfaces and abstract contracts should be known to enable fuzzing of their ABI - // but they should not be deployable and their source code should be skipped by the - // debugger and linker. - let Some(bytecode) = linked_contract - .get_bytecode_bytes() - .map(|b| b.into_owned()) - .filter(|b| !b.is_empty()) - else { - known_contracts.insert( - id.clone(), - ContractData { - abi: abi.clone(), - bytecode: Bytes::new(), - deployed_bytecode: Bytes::new(), - name, - }, - ); - continue; - }; - - // if it's a test, add it to deployable contracts + // if it's a test, link it and add to deployable contracts if abi.constructor.as_ref().map(|c| c.inputs.is_empty()).unwrap_or(true) && abi.functions().any(|func| func.name.is_test() || func.name.is_invariant_test()) { + let LinkOutput { libs_to_deploy, libraries } = linker.link_with_nonce_or_address( + Default::default(), + evm_opts.sender, + 1, + id, + )?; + + let linked_contract = linker.link(id, &libraries)?; + + let Some(bytecode) = linked_contract + .get_bytecode_bytes() + .map(|b| b.into_owned()) + .filter(|b| !b.is_empty()) + else { + continue; + }; + deployable_contracts.insert( id.clone(), TestContract { - abi: abi.clone(), - bytecode: bytecode.clone(), + abi: abi.clone().into_owned(), + bytecode, libs_to_deploy, libraries, }, ); } - - if let Some(bytes) = linked_contract.get_deployed_bytecode_bytes() { - known_contracts.insert( - id.clone(), - ContractData { - abi: abi.clone(), - bytecode, - deployed_bytecode: bytes.into_owned(), - name, - }, - ); - } } - let revert_decoder = - RevertDecoder::new().with_abis(known_contracts.values().map(|c| &c.abi)); Ok(MultiContractRunner { contracts: deployable_contracts, - known_contracts, evm_opts, env, evm_spec: self.evm_spec.unwrap_or(SpecId::MERGE), sender: self.sender, revert_decoder, - source_paths, fork: self.fork, - cheats_config: self.cheats_config.unwrap_or_default().into(), + config: self.config, coverage: self.coverage, debug: self.debug, test_options: self.test_options.unwrap_or_default(), isolation: self.isolation, + output, }) } } diff --git a/crates/forge/src/result.rs b/crates/forge/src/result.rs index a50e59b9515a..09c3661dc719 100644 --- a/crates/forge/src/result.rs +++ b/crates/forge/src/result.rs @@ -1,7 +1,9 @@ //! Test outcomes. use alloy_primitives::{Address, Log}; -use foundry_common::{evm::Breakpoints, get_contract_name, get_file_name, shell}; +use foundry_common::{ + evm::Breakpoints, get_contract_name, get_file_name, shell, ContractsByArtifact, +}; use foundry_compilers::artifacts::Libraries; use foundry_evm::{ coverage::HitMaps, @@ -14,6 +16,7 @@ use serde::{Deserialize, Serialize}; use std::{ collections::{BTreeMap, HashMap}, fmt::{self, Write}, + sync::Arc, time::Duration, }; use yansi::Paint; @@ -34,7 +37,7 @@ pub struct TestOutcome { /// This is `None` if traces and logs were not decoded. /// /// Note that `Address` fields only contain the last executed test case's data. - pub decoder: Option, + pub last_run_decoder: Option, /// The gas report, if requested. pub gas_report: Option, } @@ -42,7 +45,7 @@ pub struct TestOutcome { impl TestOutcome { /// Creates a new test outcome with the given results. pub fn new(results: BTreeMap, allow_failure: bool) -> Self { - Self { results, allow_failure, decoder: None, gas_report: None } + Self { results, allow_failure, last_run_decoder: None, gas_report: None } } /// Creates a new empty test outcome. @@ -196,6 +199,9 @@ pub struct SuiteResult { pub warnings: Vec, /// Libraries used to link test contract. pub libraries: Libraries, + /// Contracts linked with correct libraries. + #[serde(skip)] + pub known_contracts: Arc, } impl SuiteResult { @@ -204,8 +210,9 @@ impl SuiteResult { test_results: BTreeMap, warnings: Vec, libraries: Libraries, + known_contracts: Arc, ) -> Self { - Self { duration, test_results, warnings, libraries } + Self { duration, test_results, warnings, libraries, known_contracts } } /// Returns an iterator over all individual succeeding tests and their names. diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index 26af217870ae..ad335e767480 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -30,6 +30,7 @@ use rayon::prelude::*; use std::{ borrow::Cow, collections::{BTreeMap, HashMap}, + sync::Arc, time::Instant, }; @@ -178,7 +179,7 @@ impl<'a> ContractRunner<'a> { mut self, filter: &dyn TestFilter, test_options: &TestOptions, - known_contracts: Option<&ContractsByArtifact>, + known_contracts: Arc, ) -> SuiteResult { info!("starting tests"); let start = Instant::now(); @@ -207,6 +208,7 @@ impl<'a> ContractRunner<'a> { .into(), warnings, self.contract.libraries.clone(), + known_contracts, ) } @@ -244,6 +246,7 @@ impl<'a> ContractRunner<'a> { .into(), warnings, self.contract.libraries.clone(), + known_contracts, ) } @@ -265,7 +268,7 @@ impl<'a> ContractRunner<'a> { ); let identified_contracts = - has_invariants.then(|| load_contracts(setup.traces.clone(), known_contracts)); + has_invariants.then(|| load_contracts(setup.traces.clone(), &known_contracts)); let test_results = functions .par_iter() .map(|&func| { @@ -281,7 +284,7 @@ impl<'a> ContractRunner<'a> { setup, *invariant_config, func, - known_contracts, + &known_contracts, identified_contracts.as_ref().unwrap(), ) } else if func.is_fuzz_test() { @@ -299,8 +302,13 @@ impl<'a> ContractRunner<'a> { .collect::>(); let duration = start.elapsed(); - let suite_result = - SuiteResult::new(duration, test_results, warnings, self.contract.libraries.clone()); + let suite_result = SuiteResult::new( + duration, + test_results, + warnings, + self.contract.libraries.clone(), + known_contracts, + ); info!( duration=?suite_result.duration, "done. {}/{} successful", @@ -430,12 +438,10 @@ impl<'a> ContractRunner<'a> { setup: TestSetup, invariant_config: InvariantConfig, func: &Function, - known_contracts: Option<&ContractsByArtifact>, + known_contracts: &ContractsByArtifact, identified_contracts: &ContractsByAddress, ) -> TestResult { trace!(target: "forge::test::fuzz", "executing invariant test for {:?}", func.name); - let empty = ContractsByArtifact::default(); - let project_contracts = known_contracts.unwrap_or(&empty); let TestSetup { address, logs, traces, labeled_addresses, coverage, .. } = setup; // First, run the test normally to see if it needs to be skipped. @@ -466,7 +472,7 @@ impl<'a> ContractRunner<'a> { runner, invariant_config, identified_contracts, - project_contracts, + known_contracts, ); let invariant_contract = diff --git a/crates/forge/tests/it/repros.rs b/crates/forge/tests/it/repros.rs index b1f8efc49162..6f9576765385 100644 --- a/crates/forge/tests/it/repros.rs +++ b/crates/forge/tests/it/repros.rs @@ -1,5 +1,7 @@ //! Regression tests for previous issues. +use std::sync::Arc; + use crate::{ config::*, test_helpers::{ForgeTestData, TEST_DATA_DEFAULT}, @@ -8,7 +10,7 @@ use alloy_dyn_abi::{DecodedEvent, DynSolValue, EventExt}; use alloy_json_abi::Event; use alloy_primitives::{address, Address, U256}; use forge::result::TestStatus; -use foundry_config::{fs_permissions::PathPermission, FsPermissions}; +use foundry_config::{fs_permissions::PathPermission, Config, FsPermissions}; use foundry_evm::{ constants::HARDHAT_CONSOLE_ADDRESS, traces::{CallKind, CallTraceDecoder, DecodedCallData, TraceKind}, @@ -290,10 +292,12 @@ test_repro!(6538); // https://github.com/foundry-rs/foundry/issues/6554 test_repro!(6554; |config| { - let mut cheats_config = config.runner.cheats_config.as_ref().clone(); - let path = cheats_config.root.join("out/default/Issue6554.t.sol"); - cheats_config.fs_permissions.add(PathPermission::read_write(path)); - config.runner.cheats_config = std::sync::Arc::new(cheats_config); + let path = config.runner.config.__root.0.join("out/default/Issue6554.t.sol"); + + let mut prj_config = Config::clone(&config.runner.config); + prj_config.fs_permissions.add(PathPermission::read_write(path)); + config.runner.config = Arc::new(prj_config); + }); // https://github.com/foundry-rs/foundry/issues/6759 @@ -307,16 +311,18 @@ test_repro!(6616); // https://github.com/foundry-rs/foundry/issues/5529 test_repro!(5529; |config| { - let mut cheats_config = config.runner.cheats_config.as_ref().clone(); - cheats_config.always_use_create_2_factory = true; - config.runner.cheats_config = std::sync::Arc::new(cheats_config); + let mut prj_config = Config::clone(&config.runner.config); + prj_config.always_use_create_2_factory = true; + config.runner.evm_opts.always_use_create_2_factory = true; + config.runner.config = Arc::new(prj_config); }); // https://github.com/foundry-rs/foundry/issues/6634 test_repro!(6634; |config| { - let mut cheats_config = config.runner.cheats_config.as_ref().clone(); - cheats_config.always_use_create_2_factory = true; - config.runner.cheats_config = std::sync::Arc::new(cheats_config); + let mut prj_config = Config::clone(&config.runner.config); + prj_config.always_use_create_2_factory = true; + config.runner.evm_opts.always_use_create_2_factory = true; + config.runner.config = Arc::new(prj_config); }); test_repro!(7481); diff --git a/crates/forge/tests/it/test_helpers.rs b/crates/forge/tests/it/test_helpers.rs index 204df223b860..f65a3c116222 100644 --- a/crates/forge/tests/it/test_helpers.rs +++ b/crates/forge/tests/it/test_helpers.rs @@ -2,8 +2,8 @@ use alloy_primitives::U256; use forge::{ - inspectors::CheatsConfig, revm::primitives::SpecId, MultiContractRunner, - MultiContractRunnerBuilder, TestOptions, TestOptionsBuilder, + revm::primitives::SpecId, MultiContractRunner, MultiContractRunnerBuilder, TestOptions, + TestOptionsBuilder, }; use foundry_compilers::{ artifacts::{Libraries, Settings}, @@ -23,6 +23,7 @@ use std::{ env, fmt, io::Write, path::{Path, PathBuf}, + sync::Arc, }; pub const RE_PATH_SEPARATOR: &str = "/"; @@ -187,7 +188,7 @@ impl ForgeTestData { /// Builds a base runner pub fn base_runner(&self) -> MultiContractRunnerBuilder { init_tracing(); - let mut runner = MultiContractRunnerBuilder::default() + let mut runner = MultiContractRunnerBuilder::new(Arc::new(self.config.clone())) .sender(self.evm_opts.sender) .with_test_options(self.test_opts.clone()); if self.profile.is_cancun() { @@ -222,17 +223,14 @@ impl ForgeTestData { let env = opts.local_evm_env(); let output = self.output.clone(); - let artifact_ids = output.artifact_ids().map(|(id, _)| id).collect(); - self.base_runner() - .with_cheats_config(CheatsConfig::new( - &config, - opts.clone(), - Some(artifact_ids), - None, - None, - )) + + let sender = config.sender; + + let mut builder = self.base_runner(); + builder.config = Arc::new(config); + builder .enable_isolation(opts.isolate) - .sender(config.sender) + .sender(sender) .with_test_options(self.test_opts.clone()) .build(root, output, env, opts.clone()) .unwrap() diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index ea00dc5920ff..bdc940097a89 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -31,9 +31,6 @@ pub struct BuildData { pub output: ProjectCompileOutput, /// Id of target contract artifact. pub target: ArtifactId, - /// Artifact ids of the contracts. Passed to cheatcodes to enable usage of - /// `vm.getDeployedCode`. - pub artifact_ids: Vec, } impl BuildData { @@ -99,21 +96,8 @@ impl LinkedBuildData { &link_output.libraries, )?; - let known_contracts = ContractsByArtifact( - build_data - .get_linker() - .get_linked_artifacts(&link_output.libraries)? - .into_iter() - .filter_map(|(id, contract)| { - let name = id.name.clone(); - let bytecode = contract.bytecode.and_then(|b| b.into_bytes())?; - let deployed_bytecode = - contract.deployed_bytecode.and_then(|b| b.into_bytes())?; - let abi = contract.abi?; - - Some((id, ContractData { name, abi, bytecode, deployed_bytecode })) - }) - .collect(), + let known_contracts = ContractsByArtifact::new( + build_data.get_linker().get_linked_artifacts(&link_output.libraries)?, ); Ok(Self { @@ -169,8 +153,6 @@ impl PreprocessedState { let mut target_id: Option = None; - let artifact_ids = output.artifact_ids().map(|(id, _)| id).collect(); - // Find target artfifact id by name and path in compilation artifacts. for (id, contract) in output.artifact_ids().filter(|(id, _)| id.source == target_path) { if let Some(name) = &target_name { @@ -207,12 +189,7 @@ impl PreprocessedState { args, script_config, script_wallets, - build_data: BuildData { - output, - target, - project_root: project.root().clone(), - artifact_ids, - }, + build_data: BuildData { output, target, project_root: project.root().clone() }, }) } } diff --git a/crates/script/src/execute.rs b/crates/script/src/execute.rs index 9e76955914e4..1239cda1f3bc 100644 --- a/crates/script/src/execute.rs +++ b/crates/script/src/execute.rs @@ -94,7 +94,7 @@ impl PreExecutionState { let mut runner = self .script_config .get_runner_with_cheatcodes( - self.build_data.build_data.artifact_ids.clone(), + self.build_data.known_contracts.clone(), self.script_wallets.clone(), self.args.debug, self.build_data.build_data.target.clone(), diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index 17f70387acc5..dc4e05e1a0bf 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -45,7 +45,7 @@ use foundry_evm::{ }; use foundry_wallets::MultiWalletOpts; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; use yansi::Paint; mod broadcast; @@ -545,17 +545,17 @@ impl ScriptConfig { async fn get_runner_with_cheatcodes( &mut self, - artifact_ids: Vec, + known_contracts: ContractsByArtifact, script_wallets: ScriptWallets, debug: bool, target: ArtifactId, ) -> Result { - self._get_runner(Some((artifact_ids, script_wallets, target)), debug).await + self._get_runner(Some((known_contracts, script_wallets, target)), debug).await } async fn _get_runner( &mut self, - cheats_data: Option<(Vec, ScriptWallets, ArtifactId)>, + cheats_data: Option<(ContractsByArtifact, ScriptWallets, ArtifactId)>, debug: bool, ) -> Result { trace!("preparing script runner"); @@ -584,7 +584,7 @@ impl ScriptConfig { .spec(self.config.evm_spec_id()) .gas_limit(self.evm_opts.gas_limit()); - if let Some((artifact_ids, script_wallets, target)) = cheats_data { + if let Some((known_contracts, script_wallets, target)) = cheats_data { builder = builder.inspectors(|stack| { stack .debug(debug) @@ -592,7 +592,7 @@ impl ScriptConfig { CheatsConfig::new( &self.config, self.evm_opts.clone(), - Some(artifact_ids), + Some(Arc::new(known_contracts)), Some(script_wallets), Some(target.version), )