diff --git a/crates/cheatcodes/src/config.rs b/crates/cheatcodes/src/config.rs index 2279e24359a5..e0463dfc4535 100644 --- a/crates/cheatcodes/src/config.rs +++ b/crates/cheatcodes/src/config.rs @@ -69,8 +69,8 @@ impl CheatsConfig { running_version: Option, ) -> Self { let mut allowed_paths = vec![config.root.clone()]; - allowed_paths.extend(config.libs.clone()); - allowed_paths.extend(config.allow_paths.clone()); + allowed_paths.extend(config.libs.iter().cloned()); + allowed_paths.extend(config.allow_paths.iter().cloned()); let rpc_endpoints = config.rpc_endpoints.clone().resolved(); trace!(?rpc_endpoints, "using resolved rpc endpoints"); @@ -101,6 +101,17 @@ impl CheatsConfig { } } + /// Returns a new `CheatsConfig` configured with the given `Config` and `EvmOpts`. + pub fn clone_with(&self, config: &Config, evm_opts: EvmOpts) -> Self { + Self::new( + config, + evm_opts, + self.available_artifacts.clone(), + self.running_contract.clone(), + self.running_version.clone(), + ) + } + /// Attempts to canonicalize (see [std::fs::canonicalize]) the path. /// /// Canonicalization fails for non-existing paths, in which case we just normalize the path. diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index 447a9e7473ec..ac8058dd9f99 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -439,7 +439,7 @@ pub struct Cheatcodes { /// Scripting based transactions pub broadcastable_transactions: BroadcastableTransactions, - /// Additional, user configurable context this Inspector has access to when inspecting a call + /// Additional, user configurable context this Inspector has access to when inspecting a call. pub config: Arc, /// Test-scoped context holding data that needs to be reset every test run @@ -540,7 +540,7 @@ impl Cheatcodes { /// Returns the configured wallets if available, else creates a new instance. pub fn wallets(&mut self) -> &Wallets { - self.wallets.get_or_insert(Wallets::new(MultiWallet::default(), None)) + self.wallets.get_or_insert_with(|| Wallets::new(MultiWallet::default(), None)) } /// Sets the unlocked wallets. diff --git a/crates/chisel/src/executor.rs b/crates/chisel/src/executor.rs index 09c00f6ad5b3..71bf18e1ad6a 100644 --- a/crates/chisel/src/executor.rs +++ b/crates/chisel/src/executor.rs @@ -341,7 +341,7 @@ impl SessionSource { ) }) .gas_limit(self.config.evm_opts.gas_limit()) - .spec(self.config.foundry_config.evm_spec_id()) + .spec_id(self.config.foundry_config.evm_spec_id()) .legacy_assertions(self.config.foundry_config.legacy_assertions) .build(env, backend); diff --git a/crates/config/src/inline/mod.rs b/crates/config/src/inline/mod.rs index 30b1c820ec9b..fa67b2426cf0 100644 --- a/crates/config/src/inline/mod.rs +++ b/crates/config/src/inline/mod.rs @@ -4,6 +4,7 @@ use figment::{ value::{Dict, Map, Value}, Figment, Profile, Provider, }; +use foundry_compilers::ProjectCompileOutput; use itertools::Itertools; mod natspec; @@ -53,6 +54,20 @@ impl InlineConfig { Self::default() } + /// Tries to create a new instance by detecting inline configurations from the project compile + /// output. + pub fn new_parsed(output: &ProjectCompileOutput, config: &Config) -> eyre::Result { + let natspecs: Vec = NatSpec::parse(output, &config.root); + let profiles = &config.profiles; + let mut inline = Self::new(); + for natspec in &natspecs { + inline.insert(natspec)?; + // Validate after parsing as TOML. + natspec.validate_profiles(profiles)?; + } + Ok(inline) + } + /// Inserts a new [`NatSpec`] into the [`InlineConfig`]. pub fn insert(&mut self, natspec: &NatSpec) -> Result<(), InlineConfigError> { let map = if let Some(function) = &natspec.function { @@ -92,13 +107,16 @@ impl InlineConfig { Figment::from(base).merge(self.provide(contract, function)) } - /// Returns `true` if a configuration is present at the given contract and function level. - pub fn contains(&self, contract: &str, function: &str) -> bool { - // Order swapped to avoid allocation in `get_function` since order doesn't matter here. - self.get_contract(contract) - .filter(|map| !map.is_empty()) - .or_else(|| self.get_function(contract, function)) - .is_some_and(|map| !map.is_empty()) + /// Returns `true` if a configuration is present at the given contract level. + pub fn contains_contract(&self, contract: &str) -> bool { + self.get_contract(contract).is_some_and(|map| !map.is_empty()) + } + + /// Returns `true` if a configuration is present at the function level. + /// + /// Does not include contract-level configurations. + pub fn contains_function(&self, contract: &str, function: &str) -> bool { + self.get_function(contract, function).is_some_and(|map| !map.is_empty()) } fn get_contract(&self, contract: &str) -> Option<&DataMap> { diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index e1c3fa5ee2d0..f2d25461dfaf 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -1125,7 +1125,7 @@ impl Config { /// Returns the [SpecId] derived from the configured [EvmVersion] #[inline] pub fn evm_spec_id(&self) -> SpecId { - evm_spec_id(&self.evm_version, self.alphanet) + evm_spec_id(self.evm_version, self.alphanet) } /// Returns whether the compiler version should be auto-detected diff --git a/crates/config/src/utils.rs b/crates/config/src/utils.rs index e07d7dfbcb09..19f70a939e02 100644 --- a/crates/config/src/utils.rs +++ b/crates/config/src/utils.rs @@ -259,7 +259,7 @@ impl FromStr for Numeric { /// Returns the [SpecId] derived from [EvmVersion] #[inline] -pub fn evm_spec_id(evm_version: &EvmVersion, alphanet: bool) -> SpecId { +pub fn evm_spec_id(evm_version: EvmVersion, alphanet: bool) -> SpecId { if alphanet { return SpecId::OSAKA; } diff --git a/crates/evm/core/src/opts.rs b/crates/evm/core/src/opts.rs index c5817e483c25..aec0d78a05cf 100644 --- a/crates/evm/core/src/opts.rs +++ b/crates/evm/core/src/opts.rs @@ -113,9 +113,8 @@ impl EvmOpts { /// And the block that was used to configure the environment. pub async fn fork_evm_env( &self, - fork_url: impl AsRef, + fork_url: &str, ) -> eyre::Result<(revm::primitives::Env, AnyRpcBlock)> { - let fork_url = fork_url.as_ref(); let provider = ProviderBuilder::new(fork_url) .compute_units_per_second(self.get_compute_units_per_second()) .build()?; diff --git a/crates/evm/core/src/utils.rs b/crates/evm/core/src/utils.rs index 2257709e5e91..0841d9340147 100644 --- a/crates/evm/core/src/utils.rs +++ b/crates/evm/core/src/utils.rs @@ -46,10 +46,7 @@ pub fn apply_chain_and_block_specific_env_changes( return; } - NamedChain::Arbitrum | - NamedChain::ArbitrumGoerli | - NamedChain::ArbitrumNova | - NamedChain::ArbitrumTestnet => { + c if c.is_arbitrum() => { // on arbitrum `block.number` is the L1 block which is included in the // `l1BlockNumber` field if let Some(l1_block_number) = block diff --git a/crates/evm/evm/src/executors/builder.rs b/crates/evm/evm/src/executors/builder.rs index fee3c249ad90..c371a6550b87 100644 --- a/crates/evm/evm/src/executors/builder.rs +++ b/crates/evm/evm/src/executors/builder.rs @@ -52,7 +52,7 @@ impl ExecutorBuilder { /// Sets the EVM spec to use. #[inline] - pub fn spec(mut self, spec: SpecId) -> Self { + pub fn spec_id(mut self, spec: SpecId) -> Self { self.spec_id = spec; self } diff --git a/crates/evm/evm/src/executors/mod.rs b/crates/evm/evm/src/executors/mod.rs index 8146cec82d44..8560c3e10f95 100644 --- a/crates/evm/evm/src/executors/mod.rs +++ b/crates/evm/evm/src/executors/mod.rs @@ -86,9 +86,7 @@ pub struct Executor { env: EnvWithHandlerCfg, /// The Revm inspector stack. inspector: InspectorStack, - /// The gas limit for calls and deployments. This is different from the gas limit imposed by - /// the passed in environment, as those limits are used by the EVM for certain opcodes like - /// `gaslimit`. + /// The gas limit for calls and deployments. gas_limit: u64, /// Whether `failed()` should be called on the test contract to determine if the test failed. legacy_assertions: bool, @@ -166,6 +164,36 @@ impl Executor { self.env.spec_id() } + /// Sets the EVM spec ID. + pub fn set_spec_id(&mut self, spec_id: SpecId) { + self.env.handler_cfg.spec_id = spec_id; + } + + /// Returns the gas limit for calls and deployments. + /// + /// This is different from the gas limit imposed by the passed in environment, as those limits + /// are used by the EVM for certain opcodes like `gaslimit`. + pub fn gas_limit(&self) -> u64 { + self.gas_limit + } + + /// Sets the gas limit for calls and deployments. + pub fn set_gas_limit(&mut self, gas_limit: u64) { + self.gas_limit = gas_limit; + } + + /// Returns whether `failed()` should be called on the test contract to determine if the test + /// failed. + pub fn legacy_assertions(&self) -> bool { + self.legacy_assertions + } + + /// Sets whether `failed()` should be called on the test contract to determine if the test + /// failed. + pub fn set_legacy_assertions(&mut self, legacy_assertions: bool) { + self.legacy_assertions = legacy_assertions; + } + /// Creates the default CREATE2 Contract Deployer for local tests and scripts. pub fn deploy_create2_deployer(&mut self) -> eyre::Result<()> { trace!("deploying local create2 deployer"); @@ -235,12 +263,6 @@ impl Executor { self } - #[inline] - pub fn set_gas_limit(&mut self, gas_limit: u64) -> &mut Self { - self.gas_limit = gas_limit; - self - } - #[inline] pub fn create2_deployer(&self) -> Address { self.inspector().create2_deployer() diff --git a/crates/evm/evm/src/executors/trace.rs b/crates/evm/evm/src/executors/trace.rs index 214e7c28ae16..d9c0d74f6e97 100644 --- a/crates/evm/evm/src/executors/trace.rs +++ b/crates/evm/evm/src/executors/trace.rs @@ -32,7 +32,7 @@ impl TracingExecutor { .alphanet(alphanet) .create2_deployer(create2_deployer) }) - .spec(evm_spec_id(&version.unwrap_or_default(), alphanet)) + .spec_id(evm_spec_id(version.unwrap_or_default(), alphanet)) .build(env, db), } } diff --git a/crates/evm/traces/src/lib.rs b/crates/evm/traces/src/lib.rs index a0fa7e1fca49..f88efbb1b195 100644 --- a/crates/evm/traces/src/lib.rs +++ b/crates/evm/traces/src/lib.rs @@ -349,8 +349,8 @@ impl TraceMode { } } - pub fn with_verbosity(self, verbosiy: u8) -> Self { - if verbosiy >= 3 { + pub fn with_verbosity(self, verbosity: u8) -> Self { + if verbosity >= 3 { std::cmp::max(self, Self::Call) } else { self diff --git a/crates/forge/bin/cmd/coverage.rs b/crates/forge/bin/cmd/coverage.rs index 3a7d43436999..f1c862d81a52 100644 --- a/crates/forge/bin/cmd/coverage.rs +++ b/crates/forge/bin/cmd/coverage.rs @@ -11,7 +11,7 @@ use forge::{ }, opts::EvmOpts, utils::IcPcMap, - MultiContractRunnerBuilder, TestOptions, + MultiContractRunnerBuilder, }; use foundry_cli::utils::{LoadConfig, STATIC_FUZZ_SEED}; use foundry_common::{compile::ProjectCompiler, fs}; @@ -233,7 +233,6 @@ impl CoverageArgs { .evm_spec(config.evm_spec_id()) .sender(evm_opts.sender) .with_fork(evm_opts.get_fork(&config, env.clone())) - .with_test_options(TestOptions::new(output, config.clone())?) .set_coverage(true) .build(&root, output, env, evm_opts)?; diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index e5a058c0d72d..dd6d04a98cde 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -14,7 +14,7 @@ use forge::{ identifier::SignaturesIdentifier, CallTraceDecoderBuilder, InternalTraceMode, TraceKind, }, - MultiContractRunner, MultiContractRunnerBuilder, TestFilter, TestOptions, + MultiContractRunner, MultiContractRunnerBuilder, TestFilter, }; use foundry_cli::{ opts::{CoreBuildArgs, GlobalOpts}, @@ -317,9 +317,6 @@ impl TestArgs { } } - let config = Arc::new(config); - let test_options = TestOptions::new(&output, config.clone())?; - let should_debug = self.debug.is_some(); let should_draw = self.flamegraph || self.flamechart; @@ -346,6 +343,7 @@ impl TestArgs { }; // Prepare the test builder. + let config = Arc::new(config); let runner = MultiContractRunnerBuilder::new(config.clone()) .set_debug(should_debug) .set_decode_internal(decode_internal) @@ -353,7 +351,6 @@ impl TestArgs { .evm_spec(config.evm_spec_id()) .sender(evm_opts.sender) .with_fork(evm_opts.get_fork(&config, env.clone())) - .with_test_options(test_options) .enable_isolation(evm_opts.isolate) .alphanet(evm_opts.alphanet) .build(project_root, &output, env, evm_opts)?; diff --git a/crates/forge/src/lib.rs b/crates/forge/src/lib.rs index 257760c4e94b..ddeada0a69c2 100644 --- a/crates/forge/src/lib.rs +++ b/crates/forge/src/lib.rs @@ -7,17 +7,6 @@ extern crate foundry_common; #[macro_use] extern crate tracing; -use alloy_primitives::U256; -use eyre::Result; -use foundry_compilers::ProjectCompileOutput; -use foundry_config::{ - figment::Figment, Config, FuzzConfig, InlineConfig, InvariantConfig, NatSpec, -}; -use proptest::test_runner::{ - FailurePersistence, FileFailurePersistence, RngAlgorithm, TestRng, TestRunner, -}; -use std::sync::Arc; - pub mod coverage; pub mod gas_report; @@ -34,109 +23,3 @@ pub mod result; // TODO: remove pub use foundry_common::traits::TestFilter; pub use foundry_evm::*; - -/// Test configuration. -#[derive(Clone, Debug, Default)] -pub struct TestOptions { - /// The base configuration. - pub config: Arc, - /// Per-test configuration. Merged onto `base_config`. - pub inline: InlineConfig, -} - -impl TestOptions { - /// Tries to create a new instance by detecting inline configurations from the project compile - /// output. - pub fn new(output: &ProjectCompileOutput, base_config: Arc) -> eyre::Result { - let natspecs: Vec = NatSpec::parse(output, &base_config.root); - let profiles = &base_config.profiles; - let mut inline = InlineConfig::new(); - for natspec in &natspecs { - inline.insert(natspec)?; - // Validate after parsing as TOML. - natspec.validate_profiles(profiles)?; - } - Ok(Self { config: base_config, inline }) - } - - /// Creates a new instance without parsing inline configuration. - pub fn new_unparsed(base_config: Arc) -> Self { - Self { config: base_config, inline: InlineConfig::new() } - } - - /// Returns the [`Figment`] for the configuration. - pub fn figment(&self, contract: &str, function: &str) -> Result { - Ok(self.inline.merge(contract, function, &self.config)) - } - - /// Returns a "fuzz" test runner instance. Parameters are used to select tight scoped fuzz - /// configs that apply for a contract-function pair. A fallback configuration is applied - /// if no specific setup is found for a given input. - /// - /// - `contract` is the id of the test contract, expressed as a relative path from the project - /// root. - /// - `function` is the name of the test function declared inside the test contract. - pub fn fuzz_runner(&self, contract: &str, function: &str) -> Result<(FuzzConfig, TestRunner)> { - let config: FuzzConfig = self.figment(contract, function)?.extract_inner("fuzz")?; - let failure_persist_path = config - .failure_persist_dir - .as_ref() - .unwrap() - .join(config.failure_persist_file.as_ref().unwrap()) - .into_os_string() - .into_string() - .unwrap(); - let runner = fuzzer_with_cases( - config.seed, - config.runs, - config.max_test_rejects, - Some(Box::new(FileFailurePersistence::Direct(failure_persist_path.leak()))), - ); - Ok((config, runner)) - } - - /// Returns an "invariant" test runner instance. Parameters are used to select tight scoped fuzz - /// configs that apply for a contract-function pair. A fallback configuration is applied - /// if no specific setup is found for a given input. - /// - /// - `contract` is the id of the test contract, expressed as a relative path from the project - /// root. - /// - `function` is the name of the test function declared inside the test contract. - pub fn invariant_runner( - &self, - contract: &str, - function: &str, - ) -> Result<(InvariantConfig, TestRunner)> { - let figment = self.figment(contract, function)?; - let config: InvariantConfig = figment.extract_inner("invariant")?; - let seed: Option = figment.extract_inner("fuzz.seed").ok(); - let runner = fuzzer_with_cases(seed, config.runs, config.max_assume_rejects, None); - Ok((config, runner)) - } -} - -fn fuzzer_with_cases( - seed: Option, - cases: u32, - max_global_rejects: u32, - file_failure_persistence: Option>, -) -> TestRunner { - let config = proptest::test_runner::Config { - failure_persistence: file_failure_persistence, - cases, - max_global_rejects, - // Disable proptest shrink: for fuzz tests we provide single counterexample, - // for invariant tests we shrink outside proptest. - max_shrink_iters: 0, - ..Default::default() - }; - - if let Some(seed) = seed { - trace!(target: "forge::test", %seed, "building deterministic fuzzer"); - let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &seed.to_be_bytes::<32>()); - TestRunner::new_with_rng(config, rng) - } else { - trace!(target: "forge::test", "building stochastic fuzzer"); - TestRunner::new(config) - } -} diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index 572c3d4fe984..0372becb208a 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -2,20 +2,18 @@ use crate::{ progress::TestsProgress, result::SuiteResult, runner::LIBRARY_DEPLOYER, ContractRunner, - TestFilter, TestOptions, + TestFilter, }; use alloy_json_abi::{Function, JsonAbi}; use alloy_primitives::{Address, Bytes, U256}; use eyre::Result; use foundry_common::{get_contract_name, shell::verbosity, ContractsByArtifact, TestFunctionExt}; -use foundry_compilers::{ - artifacts::Libraries, compilers::Compiler, Artifact, ArtifactId, ProjectCompileOutput, -}; -use foundry_config::Config; +use foundry_compilers::{artifacts::Libraries, Artifact, ArtifactId, ProjectCompileOutput}; +use foundry_config::{Config, InlineConfig}; use foundry_evm::{ backend::Backend, decode::RevertDecoder, - executors::ExecutorBuilder, + executors::{Executor, ExecutorBuilder}, fork::CreateFork, inspectors::CheatsConfig, opts::EvmOpts, @@ -48,38 +46,34 @@ pub struct MultiContractRunner { /// Mapping of contract name to JsonAbi, creation bytecode and library bytecode which /// needs to be deployed & linked against pub contracts: DeployableContracts, - /// The EVM instance used in the test runner - pub evm_opts: EvmOpts, - /// The configured evm - pub env: revm::primitives::Env, - /// The EVM spec - pub evm_spec: SpecId, - /// Revert decoder. Contains all known errors and their selectors. - pub revert_decoder: RevertDecoder, - /// The address which will be used as the `from` field in all EVM calls - pub sender: Option
, - /// The fork to use at launch - pub fork: Option, - /// Project config. - pub config: Arc, - /// Whether to collect coverage info - pub coverage: bool, - /// Whether to collect debug info - pub debug: bool, - /// Whether to enable steps tracking in the tracer. - pub decode_internal: InternalTraceMode, - /// Settings related to fuzz and/or invariant tests - pub test_options: TestOptions, - /// Whether to enable call isolation - pub isolation: bool, - /// Whether to enable Alphanet features. - pub alphanet: bool, /// Known contracts linked with computed library addresses. pub known_contracts: ContractsByArtifact, + /// Revert decoder. Contains all known errors and their selectors. + pub revert_decoder: RevertDecoder, /// Libraries to deploy. pub libs_to_deploy: Vec, /// Library addresses used to link contracts. pub libraries: Libraries, + + /// The fork to use at launch + pub fork: Option, + + /// The base configuration for the test runner. + pub tcfg: TestRunnerConfig, +} + +impl std::ops::Deref for MultiContractRunner { + type Target = TestRunnerConfig; + + fn deref(&self) -> &Self::Target { + &self.tcfg + } +} + +impl std::ops::DerefMut for MultiContractRunner { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.tcfg + } } impl MultiContractRunner { @@ -196,7 +190,7 @@ impl MultiContractRunner { let result = self.run_test_suite( id, contract, - db.clone(), + &db, filter, &tokio_handle, Some(&tests_progress), @@ -219,8 +213,7 @@ impl MultiContractRunner { } else { contracts.par_iter().for_each(|&(id, contract)| { let _guard = tokio_handle.enter(); - let result = - self.run_test_suite(id, contract, db.clone(), filter, &tokio_handle, None); + let result = self.run_test_suite(id, contract, &db, filter, &tokio_handle, None); let _ = tx.send((id.identifier(), result)); }) } @@ -230,7 +223,7 @@ impl MultiContractRunner { &self, artifact_id: &ArtifactId, contract: &TestContract, - db: Backend, + db: &Backend, filter: &dyn TestFilter, tokio_handle: &tokio::runtime::Handle, progress: Option<&TestsProgress>, @@ -238,35 +231,6 @@ impl MultiContractRunner { let identifier = artifact_id.identifier(); let mut span_name = identifier.as_str(); - let cheats_config = CheatsConfig::new( - &self.config, - self.evm_opts.clone(), - Some(self.known_contracts.clone()), - Some(artifact_id.name.clone()), - Some(artifact_id.version.clone()), - ); - - let trace_mode = TraceMode::default() - .with_debug(self.debug) - .with_decode_internal(self.decode_internal) - .with_verbosity(self.evm_opts.verbosity) - .with_state_changes(verbosity() > 4); - - let executor = ExecutorBuilder::new() - .inspectors(|stack| { - stack - .cheatcodes(Arc::new(cheats_config)) - .trace_mode(trace_mode) - .coverage(self.coverage) - .enable_isolation(self.isolation) - .alphanet(self.alphanet) - .create2_deployer(self.evm_opts.create2_deployer) - }) - .spec(self.evm_spec) - .gas_limit(self.evm_opts.gas_limit()) - .legacy_assertions(self.config.legacy_assertions) - .build(self.env.clone(), db); - if !enabled!(tracing::Level::TRACE) { span_name = get_contract_name(&identifier); } @@ -276,20 +240,16 @@ impl MultiContractRunner { debug!("start executing all tests in contract"); - let runner = ContractRunner { - name: &identifier, + let runner = ContractRunner::new( + &identifier, contract, - libs_to_deploy: &self.libs_to_deploy, - executor, - revert_decoder: &self.revert_decoder, - initial_balance: self.evm_opts.initial_balance, - sender: self.sender.unwrap_or_default(), - debug: self.debug, + self.tcfg.executor(self.known_contracts.clone(), artifact_id, db.clone()), progress, tokio_handle, span, - }; - let r = runner.run_tests(filter, &self.test_options, self.known_contracts.clone()); + self, + ); + let r = runner.run_tests(filter); debug!(duration=?r.duration, "executed all tests in contract"); @@ -297,6 +257,116 @@ impl MultiContractRunner { } } +/// Configuration for the test runner. +/// +/// This is modified after instantiation through inline config. +#[derive(Clone)] +pub struct TestRunnerConfig { + /// Project config. + pub config: Arc, + /// Inline configuration. + pub inline_config: Arc, + + /// EVM configuration. + pub evm_opts: EvmOpts, + /// EVM environment. + pub env: revm::primitives::Env, + /// EVM version. + pub spec_id: SpecId, + /// The address which will be used to deploy the initial contracts and send all transactions. + pub sender: Address, + + /// Whether to collect coverage info + pub coverage: bool, + /// Whether to collect debug info + pub debug: bool, + /// Whether to enable steps tracking in the tracer. + pub decode_internal: InternalTraceMode, + /// Whether to enable call isolation. + pub isolation: bool, + /// Whether to enable Alphanet features. + pub alphanet: bool, +} + +impl TestRunnerConfig { + /// Reconfigures all fields using the given `config`. + pub fn reconfigure_with(&mut self, config: Arc) { + debug_assert!(!Arc::ptr_eq(&self.config, &config)); + + // TODO: self.evm_opts + // TODO: self.env + self.spec_id = config.evm_spec_id(); + self.sender = config.sender; + // self.coverage = N/A; + // self.debug = N/A; + // self.decode_internal = N/A; + // self.isolation = N/A; + self.alphanet = config.alphanet; + + self.config = config; + } + + /// Configures the given executor with this configuration. + pub fn configure_executor(&self, executor: &mut Executor) { + // TODO: See above + + let inspector = executor.inspector_mut(); + // inspector.set_env(&self.env); + if let Some(cheatcodes) = inspector.cheatcodes.as_mut() { + cheatcodes.config = + Arc::new(cheatcodes.config.clone_with(&self.config, self.evm_opts.clone())); + } + inspector.tracing(self.trace_mode()); + inspector.collect_coverage(self.coverage); + inspector.enable_isolation(self.isolation); + inspector.alphanet(self.alphanet); + // inspector.set_create2_deployer(self.evm_opts.create2_deployer); + + // executor.env_mut().clone_from(&self.env); + executor.set_spec_id(self.spec_id); + // executor.set_gas_limit(self.evm_opts.gas_limit()); + executor.set_legacy_assertions(self.config.legacy_assertions); + } + + /// Creates a new executor with this configuration. + pub fn executor( + &self, + known_contracts: ContractsByArtifact, + artifact_id: &ArtifactId, + db: Backend, + ) -> Executor { + let cheats_config = Arc::new(CheatsConfig::new( + &self.config, + self.evm_opts.clone(), + Some(known_contracts), + Some(artifact_id.name.clone()), + Some(artifact_id.version.clone()), + )); + ExecutorBuilder::new() + .inspectors(|stack| { + stack + .cheatcodes(cheats_config) + .trace_mode(self.trace_mode()) + .coverage(self.coverage) + .enable_isolation(self.isolation) + .alphanet(self.alphanet) + .create2_deployer(self.evm_opts.create2_deployer) + }) + .spec_id(self.spec_id) + .gas_limit(self.evm_opts.gas_limit()) + .legacy_assertions(self.config.legacy_assertions) + .build(self.env.clone(), db) + } + + fn trace_mode(&self) -> TraceMode { + TraceMode::default() + .with_debug(self.debug) + .with_decode_internal(self.decode_internal) + .with_verbosity(self.evm_opts.verbosity) + .with_state_changes(verbosity() > 4) + } +} + /// Builder used for instantiating the multi-contract runner #[derive(Clone, Debug)] #[must_use = "builders do nothing unless you call `build` on them"] @@ -322,8 +392,6 @@ pub struct MultiContractRunnerBuilder { pub isolation: bool, /// Whether to enable Alphanet features. pub alphanet: bool, - /// Settings related to fuzz and/or invariant tests - pub test_options: Option, } impl MultiContractRunnerBuilder { @@ -337,7 +405,6 @@ impl MultiContractRunnerBuilder { coverage: Default::default(), debug: Default::default(), isolation: Default::default(), - test_options: Default::default(), decode_internal: Default::default(), alphanet: Default::default(), } @@ -363,11 +430,6 @@ impl MultiContractRunnerBuilder { self } - pub fn with_test_options(mut self, test_options: TestOptions) -> Self { - self.test_options = Some(test_options); - self - } - pub fn set_coverage(mut self, enable: bool) -> Self { self.coverage = enable; self @@ -395,10 +457,10 @@ impl MultiContractRunnerBuilder { /// Given an EVM, proceeds to return a runner which is able to execute all tests /// against that evm - pub fn build( + pub fn build( self, root: &Path, - output: &ProjectCompileOutput, + output: &ProjectCompileOutput, env: revm::primitives::Env, evm_opts: EvmOpts, ) -> Result { @@ -449,22 +511,28 @@ impl MultiContractRunnerBuilder { Ok(MultiContractRunner { contracts: deployable_contracts, - evm_opts, - env, - evm_spec: self.evm_spec.unwrap_or(SpecId::CANCUN), - sender: self.sender, revert_decoder, - fork: self.fork, - config: self.config, - coverage: self.coverage, - debug: self.debug, - decode_internal: self.decode_internal, - test_options: self.test_options.unwrap_or_default(), - isolation: self.isolation, - alphanet: self.alphanet, known_contracts, libs_to_deploy, libraries, + + fork: self.fork, + + tcfg: TestRunnerConfig { + evm_opts, + env, + spec_id: self.evm_spec.unwrap_or_else(|| self.config.evm_spec_id()), + sender: self.sender.unwrap_or(self.config.sender), + + coverage: self.coverage, + debug: self.debug, + decode_internal: self.decode_internal, + inline_config: Arc::new(InlineConfig::new_parsed(output, &self.config)?), + isolation: self.isolation, + alphanet: self.alphanet, + + config: self.config, + }, }) } } diff --git a/crates/forge/src/result.rs b/crates/forge/src/result.rs index 7f02db577c0d..28b52d74c1f0 100644 --- a/crates/forge/src/result.rs +++ b/crates/forge/src/result.rs @@ -467,12 +467,12 @@ impl fmt::Display for TestResult { impl TestResult { /// Creates a new test result starting from test setup results. - pub fn new(setup: TestSetup) -> Self { + pub fn new(setup: &TestSetup) -> Self { Self { - labeled_addresses: setup.labels, - logs: setup.logs, - traces: setup.traces, - coverage: setup.coverage, + labeled_addresses: setup.labels.clone(), + logs: setup.logs.clone(), + traces: setup.traces.clone(), + coverage: setup.coverage.clone(), ..Default::default() } } @@ -496,27 +496,25 @@ impl TestResult { } /// Returns the skipped result for single test (used in skipped fuzz test too). - pub fn single_skip(mut self, reason: SkipReason) -> Self { + pub fn single_skip(&mut self, reason: SkipReason) { self.status = TestStatus::Skipped; self.reason = reason.0; - self } /// Returns the failed result with reason for single test. - pub fn single_fail(mut self, reason: Option) -> Self { + pub fn single_fail(&mut self, reason: Option) { self.status = TestStatus::Failure; self.reason = reason; - self } /// Returns the result for single test. Merges execution results (logs, labeled addresses, /// traces and coverages) in initial setup results. pub fn single_result( - mut self, + &mut self, success: bool, reason: Option, raw_call_result: RawCallResult, - ) -> Self { + ) { self.kind = TestKind::Unit { gas: raw_call_result.gas_used.wrapping_sub(raw_call_result.stipend) }; @@ -539,13 +537,11 @@ impl TestResult { self.gas_snapshots = cheatcodes.gas_snapshots; self.deprecated_cheatcodes = cheatcodes.deprecated; } - - self } /// Returns the result for a fuzzed test. Merges fuzz execution results (logs, labeled /// addresses, traces and coverages) in initial setup results. - pub fn fuzz_result(mut self, result: FuzzTestResult) -> Self { + pub fn fuzz_result(&mut self, result: FuzzTestResult) { self.kind = TestKind::Fuzz { median_gas: result.median_gas(false), mean_gas: result.mean_gas(false), @@ -572,26 +568,23 @@ impl TestResult { self.gas_report_traces = result.gas_report_traces.into_iter().map(|t| vec![t]).collect(); self.breakpoints = result.breakpoints.unwrap_or_default(); self.deprecated_cheatcodes = result.deprecated_cheatcodes; - - self } /// Returns the skipped result for invariant test. - pub fn invariant_skip(mut self, reason: SkipReason) -> Self { + pub fn invariant_skip(&mut self, reason: SkipReason) { self.kind = TestKind::Invariant { runs: 1, calls: 1, reverts: 1, metrics: HashMap::default() }; self.status = TestStatus::Skipped; self.reason = reason.0; - self } /// Returns the fail result for replayed invariant test. pub fn invariant_replay_fail( - mut self, + &mut self, replayed_entirely: bool, invariant_name: &String, call_sequence: Vec, - ) -> Self { + ) { self.kind = TestKind::Invariant { runs: 1, calls: 1, reverts: 1, metrics: HashMap::default() }; self.status = TestStatus::Failure; @@ -601,22 +594,20 @@ impl TestResult { Some(format!("{invariant_name} persisted failure revert")) }; self.counterexample = Some(CounterExample::Sequence(call_sequence)); - self } /// Returns the fail result for invariant test setup. - pub fn invariant_setup_fail(mut self, e: Report) -> Self { + pub fn invariant_setup_fail(&mut self, e: Report) { self.kind = TestKind::Invariant { runs: 0, calls: 0, reverts: 0, metrics: HashMap::default() }; self.status = TestStatus::Failure; self.reason = Some(format!("failed to set up invariant testing environment: {e}")); - self } /// Returns the invariant test result. #[allow(clippy::too_many_arguments)] pub fn invariant_result( - mut self, + &mut self, gas_report_traces: Vec>, success: bool, reason: Option, @@ -624,7 +615,7 @@ impl TestResult { cases: Vec, reverts: usize, metrics: Map, - ) -> Self { + ) { self.kind = TestKind::Invariant { runs: cases.len(), calls: cases.iter().map(|sequence| sequence.cases().len()).sum(), @@ -638,7 +629,6 @@ impl TestResult { self.reason = reason; self.counterexample = counterexample; self.gas_report_traces = gas_report_traces; - self } /// Returns `true` if this is the result of a fuzz test diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index fc2b89cb0815..0948df6d1b0d 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -2,20 +2,17 @@ use crate::{ fuzz::{invariant::BasicTxDetails, BaseCounterExample}, - multi_runner::{is_matching_test, TestContract}, + multi_runner::{is_matching_test, TestContract, TestRunnerConfig}, progress::{start_fuzz_progress, TestsProgress}, result::{SuiteResult, TestResult, TestSetup}, - TestFilter, TestOptions, + MultiContractRunner, TestFilter, }; use alloy_dyn_abi::DynSolValue; use alloy_json_abi::Function; -use alloy_primitives::{address, map::HashMap, Address, Bytes, U256}; +use alloy_primitives::{address, map::HashMap, Address, U256}; use eyre::Result; -use foundry_common::{ - contracts::{ContractsByAddress, ContractsByArtifact}, - TestFunctionExt, TestFunctionKind, -}; -use foundry_config::{FuzzConfig, InvariantConfig}; +use foundry_common::{contracts::ContractsByAddress, TestFunctionExt, TestFunctionKind}; +use foundry_config::Config; use foundry_evm::{ constants::CALLER, decode::RevertDecoder, @@ -33,9 +30,12 @@ use foundry_evm::{ }, traces::{load_contracts, TraceKind, TraceMode}, }; -use proptest::test_runner::TestRunner; +use proptest::test_runner::{ + FailurePersistence, FileFailurePersistence, RngAlgorithm, TestRng, TestRunner, +}; use rayon::prelude::*; -use std::{borrow::Cow, cmp::min, collections::BTreeMap, time::Instant}; +use std::{borrow::Cow, cmp::min, collections::BTreeMap, sync::Arc, time::Instant}; +use tracing::Span; /// When running tests, we deploy all external libraries present in the project. To avoid additional /// libraries affecting nonces of senders used in tests, we are using separate address to @@ -45,33 +45,56 @@ use std::{borrow::Cow, cmp::min, collections::BTreeMap, time::Instant}; pub const LIBRARY_DEPLOYER: Address = address!("1F95D37F27EA0dEA9C252FC09D5A6eaA97647353"); /// A type that executes all tests of a contract -#[derive(Clone, Debug)] pub struct ContractRunner<'a> { /// The name of the contract. - pub name: &'a str, + name: &'a str, /// The data of the contract. - pub contract: &'a TestContract, - /// The libraries that need to be deployed before the contract. - pub libs_to_deploy: &'a Vec, - /// The executor used by the runner. - pub executor: Executor, - /// Revert decoder. Contains all known errors. - pub revert_decoder: &'a RevertDecoder, - /// The initial balance of the test contract. - pub initial_balance: U256, - /// The address which will be used as the `from` field in all EVM calls. - pub sender: Address, - /// Whether debug traces should be generated. - pub debug: bool, + contract: &'a TestContract, + /// The EVM executor. + executor: Executor, /// Overall test run progress. - pub progress: Option<&'a TestsProgress>, + progress: Option<&'a TestsProgress>, /// The handle to the tokio runtime. - pub tokio_handle: &'a tokio::runtime::Handle, + tokio_handle: &'a tokio::runtime::Handle, /// The span of the contract. - pub span: tracing::Span, + span: tracing::Span, + /// The contract-level configuration. + tcfg: Cow<'a, TestRunnerConfig>, + /// The parent runner. + mcr: &'a MultiContractRunner, +} + +impl<'a> std::ops::Deref for ContractRunner<'a> { + type Target = Cow<'a, TestRunnerConfig>; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + &self.tcfg + } } -impl ContractRunner<'_> { +impl<'a> ContractRunner<'a> { + pub fn new( + name: &'a str, + contract: &'a TestContract, + executor: Executor, + progress: Option<&'a TestsProgress>, + tokio_handle: &'a tokio::runtime::Handle, + span: Span, + mcr: &'a MultiContractRunner, + ) -> Self { + Self { + name, + contract, + executor, + progress, + tokio_handle, + span, + tcfg: Cow::Borrowed(&mcr.tcfg), + mcr, + } + } + /// Deploys the test contract inside the runner from the sending account, and optionally runs /// the `setUp` function on the test contract. pub fn setup(&mut self, call_setup: bool) -> TestSetup { @@ -84,6 +107,8 @@ impl ContractRunner<'_> { fn _setup(&mut self, call_setup: bool) -> Result { trace!(call_setup, "setting up"); + self.apply_contract_inline_config()?; + // We max out their balance so that they can deploy and make calls. self.executor.set_balance(self.sender, U256::MAX)?; self.executor.set_balance(CALLER, U256::MAX)?; @@ -95,12 +120,12 @@ impl ContractRunner<'_> { self.executor.set_balance(LIBRARY_DEPLOYER, U256::MAX)?; let mut result = TestSetup::default(); - for code in self.libs_to_deploy.iter() { + for code in self.mcr.libs_to_deploy.iter() { let deploy_result = self.executor.deploy( LIBRARY_DEPLOYER, code.clone(), U256::ZERO, - Some(self.revert_decoder), + Some(&self.mcr.revert_decoder), ); let (raw, reason) = RawCallResult::from_evm_result(deploy_result.map(Into::into))?; result.extend(raw, TraceKind::Deployment); @@ -115,14 +140,14 @@ impl ContractRunner<'_> { // Set the contracts initial balance before deployment, so it is available during // construction - self.executor.set_balance(address, self.initial_balance)?; + self.executor.set_balance(address, self.initial_balance())?; // Deploy the test contract let deploy_result = self.executor.deploy( self.sender, self.contract.bytecode.clone(), U256::ZERO, - Some(self.revert_decoder), + Some(&self.mcr.revert_decoder), ); if let Ok(dr) = &deploy_result { debug_assert_eq!(dr.address, address); @@ -135,16 +160,16 @@ impl ContractRunner<'_> { } // Reset `self.sender`s, `CALLER`s and `LIBRARY_DEPLOYER`'s balance to the initial balance. - self.executor.set_balance(self.sender, self.initial_balance)?; - self.executor.set_balance(CALLER, self.initial_balance)?; - self.executor.set_balance(LIBRARY_DEPLOYER, self.initial_balance)?; + self.executor.set_balance(self.sender, self.initial_balance())?; + self.executor.set_balance(CALLER, self.initial_balance())?; + self.executor.set_balance(LIBRARY_DEPLOYER, self.initial_balance())?; self.executor.deploy_create2_deployer()?; // Optionally call the `setUp` function if call_setup { trace!("calling setUp"); - let res = self.executor.setup(None, address, Some(self.revert_decoder)); + let res = self.executor.setup(None, address, Some(&self.mcr.revert_decoder)); let (raw, reason) = RawCallResult::from_evm_result(res)?; result.extend(raw, TraceKind::Setup); result.reason = reason; @@ -155,6 +180,31 @@ impl ContractRunner<'_> { Ok(result) } + fn initial_balance(&self) -> U256 { + self.evm_opts.initial_balance + } + + /// Configures this runner with the inline configuration for the contract. + fn apply_contract_inline_config(&mut self) -> Result<()> { + if self.inline_config.contains_contract(self.name) { + let new_config = Arc::new(self.inline_config(None)?); + self.tcfg.to_mut().reconfigure_with(new_config); + let prev_tracer = self.executor.inspector_mut().tracer.take(); + self.tcfg.configure_executor(&mut self.executor); + // Don't set tracer here. + self.executor.inspector_mut().tracer = prev_tracer; + } + Ok(()) + } + + /// Returns the configuration for a contract or function. + fn inline_config(&self, func: Option<&Function>) -> Result { + let function = func.map(|f| f.name.as_str()).unwrap_or(""); + let config = + self.mcr.inline_config.merge(self.name, function, &self.config).extract::()?; + Ok(config) + } + /// Collect fixtures from test contract. /// /// Fixtures can be defined: @@ -210,12 +260,7 @@ impl ContractRunner<'_> { } /// Runs all tests for a contract whose names match the provided regular expression - pub fn run_tests( - mut self, - filter: &dyn TestFilter, - test_options: &TestOptions, - known_contracts: ContractsByArtifact, - ) -> SuiteResult { + pub fn run_tests(mut self, filter: &dyn TestFilter) -> SuiteResult { let start = Instant::now(); let mut warnings = Vec::new(); @@ -302,16 +347,16 @@ impl ContractRunner<'_> { .functions() .filter(|func| is_matching_test(func, filter)) .collect::>(); - let find_time = find_timer.elapsed(); debug!( "Found {} test functions out of {} in {:?}", functions.len(), self.contract.abi.functions().count(), - find_time, + find_timer.elapsed(), ); - let identified_contracts = has_invariants - .then(|| load_contracts(setup.traces.iter().map(|(_, t)| &t.arena), &known_contracts)); + let identified_contracts = has_invariants.then(|| { + load_contracts(setup.traces.iter().map(|(_, t)| &t.arena), &self.mcr.known_contracts) + }); let test_results = functions .par_iter() .map(|&func| { @@ -335,36 +380,12 @@ impl ContractRunner<'_> { ) .entered(); - let setup = setup.clone(); - let mut res = match kind { - TestFunctionKind::UnitTest { should_fail } => { - self.run_unit_test(func, should_fail, setup) - } - TestFunctionKind::FuzzTest { should_fail } => { - match test_options.fuzz_runner(self.name, &func.name) { - Ok((fuzz_config, runner)) => { - self.run_fuzz_test(func, should_fail, runner, setup, fuzz_config) - } - Err(err) => TestResult::fail(err.to_string()), - } - } - TestFunctionKind::InvariantTest => { - match test_options.invariant_runner(self.name, &func.name) { - Ok((invariant_config, runner)) => self.run_invariant_test( - runner, - setup, - invariant_config, - func, - call_after_invariant, - &known_contracts, - identified_contracts.as_ref().unwrap(), - ), - Err(err) => TestResult::fail(err.to_string()), - } - } - _ => unreachable!(), - }; - + let mut res = FunctionRunner::new(&self, &setup).run( + func, + kind, + call_after_invariant, + identified_contracts.as_ref(), + ); res.duration = start.elapsed(); (sig, res) @@ -374,6 +395,83 @@ impl ContractRunner<'_> { let duration = start.elapsed(); SuiteResult::new(duration, test_results, warnings) } +} + +/// Executes a single test function, returning a [`TestResult`]. +struct FunctionRunner<'a> { + /// The function-level configuration. + tcfg: Cow<'a, TestRunnerConfig>, + /// The EVM executor. + executor: Cow<'a, Executor>, + /// The parent runner. + cr: &'a ContractRunner<'a>, + /// The address of the test contract. + address: Address, + /// The test setup result. + setup: &'a TestSetup, + /// The test result. Returned after running the test. + result: TestResult, +} + +impl<'a> std::ops::Deref for FunctionRunner<'a> { + type Target = Cow<'a, TestRunnerConfig>; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + &self.tcfg + } +} + +impl<'a> FunctionRunner<'a> { + fn new(cr: &'a ContractRunner<'a>, setup: &'a TestSetup) -> Self { + Self { + tcfg: match &cr.tcfg { + Cow::Borrowed(tcfg) => Cow::Borrowed(tcfg), + Cow::Owned(tcfg) => Cow::Owned(tcfg.clone()), + }, + executor: Cow::Borrowed(&cr.executor), + cr, + address: setup.address, + setup, + result: TestResult::new(setup), + } + } + + fn revert_decoder(&self) -> &'a RevertDecoder { + &self.cr.mcr.revert_decoder + } + + /// Configures this runner with the inline configuration for the contract. + fn apply_function_inline_config(&mut self, func: &Function) -> Result<()> { + if self.inline_config.contains_function(self.cr.name, &func.name) { + let new_config = Arc::new(self.cr.inline_config(Some(func))?); + self.tcfg.to_mut().reconfigure_with(new_config); + self.tcfg.configure_executor(self.executor.to_mut()); + } + Ok(()) + } + + fn run( + mut self, + func: &Function, + kind: TestFunctionKind, + call_after_invariant: bool, + identified_contracts: Option<&ContractsByAddress>, + ) -> TestResult { + if let Err(e) = self.apply_function_inline_config(func) { + self.result.single_fail(Some(e.to_string())); + return self.result; + } + + match kind { + TestFunctionKind::UnitTest { should_fail } => self.run_unit_test(func, should_fail), + TestFunctionKind::FuzzTest { should_fail } => self.run_fuzz_test(func, should_fail), + TestFunctionKind::InvariantTest => { + self.run_invariant_test(func, call_after_invariant, identified_contracts.unwrap()) + } + _ => unreachable!(), + } + } /// Runs a single unit test. /// @@ -383,80 +481,77 @@ impl ContractRunner<'_> { /// (therefore the unit test call will be made on modified state). /// State modifications of before test txes and unit test function call are discarded after /// test ends, similar to `eth_call`. - pub fn run_unit_test( - &self, - func: &Function, - should_fail: bool, - setup: TestSetup, - ) -> TestResult { + fn run_unit_test(mut self, func: &Function, should_fail: bool) -> TestResult { // Prepare unit test execution. - let (executor, test_result, address) = match self.prepare_test(func, setup) { - Ok(res) => res, - Err(res) => return res, - }; + if self.prepare_test(func).is_err() { + return self.result; + } // Run current unit test. - let (mut raw_call_result, reason) = match executor.call( + let (mut raw_call_result, reason) = match self.executor.call( self.sender, - address, + self.address, func, &[], U256::ZERO, - Some(self.revert_decoder), + Some(self.revert_decoder()), ) { Ok(res) => (res.raw, None), Err(EvmError::Execution(err)) => (err.raw, Some(err.reason)), - Err(EvmError::Skip(reason)) => return test_result.single_skip(reason), - Err(err) => return test_result.single_fail(Some(err.to_string())), + Err(EvmError::Skip(reason)) => { + self.result.single_skip(reason); + return self.result; + } + Err(err) => { + self.result.single_fail(Some(err.to_string())); + return self.result; + } }; - let success = executor.is_raw_call_mut_success(address, &mut raw_call_result, should_fail); - test_result.single_result(success, reason, raw_call_result) + let success = + self.executor.is_raw_call_mut_success(self.address, &mut raw_call_result, should_fail); + self.result.single_result(success, reason, raw_call_result); + self.result } - #[allow(clippy::too_many_arguments)] - pub fn run_invariant_test( - &self, - runner: TestRunner, - setup: TestSetup, - invariant_config: InvariantConfig, + fn run_invariant_test( + mut self, func: &Function, call_after_invariant: bool, - known_contracts: &ContractsByArtifact, identified_contracts: &ContractsByAddress, ) -> TestResult { - let address = setup.address; - let fuzz_fixtures = setup.fuzz_fixtures.clone(); - let mut test_result = TestResult::new(setup); - // First, run the test normally to see if it needs to be skipped. if let Err(EvmError::Skip(reason)) = self.executor.call( self.sender, - address, + self.address, func, &[], U256::ZERO, - Some(self.revert_decoder), + Some(self.revert_decoder()), ) { - return test_result.invariant_skip(reason); + self.result.invariant_skip(reason); + return self.result; }; + let runner = self.invariant_runner(); + let invariant_config = &self.config.invariant; + let mut evm = InvariantExecutor::new( - self.executor.clone(), + self.clone_executor(), runner, invariant_config.clone(), identified_contracts, - known_contracts, + &self.cr.mcr.known_contracts, ); let invariant_contract = InvariantContract { - address, + address: self.address, invariant_function: func, call_after_invariant, - abi: &self.contract.abi, + abi: &self.cr.contract.abi, }; - let failure_dir = invariant_config.clone().failure_dir(self.name); - let failure_file = failure_dir.join(invariant_contract.invariant_function.clone().name); + let failure_dir = invariant_config.clone().failure_dir(self.cr.name); + let failure_file = failure_dir.join(&invariant_contract.invariant_function.name); // Try to replay recorded failure if any. if let Ok(call_sequence) = @@ -474,7 +569,7 @@ impl ContractRunner<'_> { }) .collect::>(); if let Ok((success, replayed_entirely)) = check_sequence( - self.executor.clone(), + self.clone_executor(), &txes, (0..min(txes.len(), invariant_config.depth as usize)).collect(), invariant_contract.address, @@ -492,34 +587,40 @@ impl ContractRunner<'_> { // exit without executing new runs. let _ = replay_run( &invariant_contract, - self.executor.clone(), - known_contracts, + self.clone_executor(), + &self.cr.mcr.known_contracts, identified_contracts.clone(), - &mut test_result.logs, - &mut test_result.traces, - &mut test_result.coverage, - &mut test_result.deprecated_cheatcodes, + &mut self.result.logs, + &mut self.result.traces, + &mut self.result.coverage, + &mut self.result.deprecated_cheatcodes, &txes, ); - return test_result.invariant_replay_fail( + self.result.invariant_replay_fail( replayed_entirely, &invariant_contract.invariant_function.name, call_sequence, - ) + ); + return self.result; } } } let progress = - start_fuzz_progress(self.progress, self.name, &func.name, invariant_config.runs); - let invariant_result = - match evm.invariant_fuzz(invariant_contract.clone(), &fuzz_fixtures, progress.as_ref()) - { - Ok(x) => x, - Err(e) => return test_result.invariant_setup_fail(e), - }; + start_fuzz_progress(self.cr.progress, self.cr.name, &func.name, invariant_config.runs); + let invariant_result = match evm.invariant_fuzz( + invariant_contract.clone(), + &self.setup.fuzz_fixtures, + progress.as_ref(), + ) { + Ok(x) => x, + Err(e) => { + self.result.invariant_setup_fail(e); + return self.result; + } + }; // Merge coverage collected during invariant run with test setup coverage. - test_result.merge_coverages(invariant_result.coverage); + self.result.merge_coverages(invariant_result.coverage); let mut counterexample = None; let success = invariant_result.error.is_none(); @@ -535,13 +636,13 @@ impl ContractRunner<'_> { match replay_error( &case_data, &invariant_contract, - self.executor.clone(), - known_contracts, + self.clone_executor(), + &self.cr.mcr.known_contracts, identified_contracts.clone(), - &mut test_result.logs, - &mut test_result.traces, - &mut test_result.coverage, - &mut test_result.deprecated_cheatcodes, + &mut self.result.logs, + &mut self.result.traces, + &mut self.result.coverage, + &mut self.result.deprecated_cheatcodes, progress.as_ref(), ) { Ok(call_sequence) => { @@ -571,13 +672,13 @@ impl ContractRunner<'_> { _ => { if let Err(err) = replay_run( &invariant_contract, - self.executor.clone(), - known_contracts, + self.clone_executor(), + &self.cr.mcr.known_contracts, identified_contracts.clone(), - &mut test_result.logs, - &mut test_result.traces, - &mut test_result.coverage, - &mut test_result.deprecated_cheatcodes, + &mut self.result.logs, + &mut self.result.traces, + &mut self.result.coverage, + &mut self.result.deprecated_cheatcodes, &invariant_result.last_run_inputs, ) { error!(%err, "Failed to replay last invariant run"); @@ -585,7 +686,7 @@ impl ContractRunner<'_> { } } - test_result.invariant_result( + self.result.invariant_result( invariant_result.gas_report_traces, success, reason, @@ -593,7 +694,8 @@ impl ContractRunner<'_> { invariant_result.cases, invariant_result.reverts, invariant_result.metrics, - ) + ); + self.result } /// Runs a fuzzed test. @@ -605,35 +707,31 @@ impl ContractRunner<'_> { /// (therefore the fuzz test will use the modified state). /// State modifications of before test txes and fuzz test are discarded after test ends, /// similar to `eth_call`. - pub fn run_fuzz_test( - &self, - func: &Function, - should_fail: bool, - runner: TestRunner, - setup: TestSetup, - fuzz_config: FuzzConfig, - ) -> TestResult { - let progress = start_fuzz_progress(self.progress, self.name, &func.name, fuzz_config.runs); - + fn run_fuzz_test(mut self, func: &Function, should_fail: bool) -> TestResult { // Prepare fuzz test execution. - let fuzz_fixtures = setup.fuzz_fixtures.clone(); - let (executor, test_result, address) = match self.prepare_test(func, setup) { - Ok(res) => res, - Err(res) => return res, - }; + if self.prepare_test(func).is_err() { + return self.result; + } + + let runner = self.fuzz_runner(); + let fuzz_config = self.config.fuzz.clone(); + + let progress = + start_fuzz_progress(self.cr.progress, self.cr.name, &func.name, fuzz_config.runs); // Run fuzz test. let fuzzed_executor = - FuzzedExecutor::new(executor.into_owned(), runner, self.sender, fuzz_config); + FuzzedExecutor::new(self.executor.into_owned(), runner, self.tcfg.sender, fuzz_config); let result = fuzzed_executor.fuzz( func, - &fuzz_fixtures, - address, + &self.setup.fuzz_fixtures, + self.address, should_fail, - self.revert_decoder, + &self.cr.mcr.revert_decoder, progress.as_ref(), ); - test_result.fuzz_result(result) + self.result.fuzz_result(result); + self.result } /// Prepares single unit test and fuzz test execution: @@ -645,20 +743,15 @@ impl ContractRunner<'_> { /// /// Unit tests within same contract (or even current test) are valid options for before test tx /// configuration. Test execution stops if any of before test txes fails. - fn prepare_test( - &self, - func: &Function, - setup: TestSetup, - ) -> Result<(Cow<'_, Executor>, TestResult, Address), TestResult> { - let address = setup.address; - let mut executor = Cow::Borrowed(&self.executor); - let mut test_result = TestResult::new(setup); + fn prepare_test(&mut self, func: &Function) -> Result<(), ()> { + let address = self.setup.address; // Apply before test configured functions (if any). - if self.contract.abi.functions().filter(|func| func.name.is_before_test_setup()).count() == + if self.cr.contract.abi.functions().filter(|func| func.name.is_before_test_setup()).count() == 1 { - for calldata in executor + for calldata in self + .executor .call_sol_default( address, &ITest::beforeTestSetupCall { testSelector: func.selector() }, @@ -666,22 +759,84 @@ impl ContractRunner<'_> { .beforeTestCalldata { // Apply before test configured calldata. - match executor.to_mut().transact_raw(self.sender, address, calldata, U256::ZERO) { + match self.executor.to_mut().transact_raw( + self.tcfg.sender, + address, + calldata, + U256::ZERO, + ) { Ok(call_result) => { let reverted = call_result.reverted; // Merge tx result traces in unit test result. - test_result.extend(call_result); + self.result.extend(call_result); // To continue unit test execution the call should not revert. if reverted { - return Err(test_result.single_fail(None)) + self.result.single_fail(None); + return Err(()); } } - Err(_) => return Err(test_result.single_fail(None)), + Err(_) => { + self.result.single_fail(None); + return Err(()); + } } } } - Ok((executor, test_result, address)) + Ok(()) + } + + fn fuzz_runner(&self) -> TestRunner { + let config = &self.config.fuzz; + let failure_persist_path = config + .failure_persist_dir + .as_ref() + .unwrap() + .join(config.failure_persist_file.as_ref().unwrap()) + .into_os_string() + .into_string() + .unwrap(); + fuzzer_with_cases( + config.seed, + config.runs, + config.max_test_rejects, + Some(Box::new(FileFailurePersistence::Direct(failure_persist_path.leak()))), + ) + } + + fn invariant_runner(&self) -> TestRunner { + let config = &self.config.invariant; + fuzzer_with_cases(self.config.fuzz.seed, config.runs, config.max_assume_rejects, None) + } + + fn clone_executor(&self) -> Executor { + self.executor.clone().into_owned() + } +} + +fn fuzzer_with_cases( + seed: Option, + cases: u32, + max_global_rejects: u32, + file_failure_persistence: Option>, +) -> TestRunner { + let config = proptest::test_runner::Config { + failure_persistence: file_failure_persistence, + cases, + max_global_rejects, + // Disable proptest shrink: for fuzz tests we provide single counterexample, + // for invariant tests we shrink outside proptest. + max_shrink_iters: 0, + ..Default::default() + }; + + if let Some(seed) = seed { + trace!(target: "forge::test", %seed, "building deterministic fuzzer"); + let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &seed.to_be_bytes::<32>()); + TestRunner::new_with_rng(config, rng) + } else { + trace!(target: "forge::test", "building stochastic fuzzer"); + TestRunner::new(config) } } diff --git a/crates/forge/tests/cli/inline_config.rs b/crates/forge/tests/cli/inline_config.rs index de585a48ce17..31da29d21ea8 100644 --- a/crates/forge/tests/cli/inline_config.rs +++ b/crates/forge/tests/cli/inline_config.rs @@ -147,14 +147,14 @@ forgetest!(invalid_value, |prj, cmd| { Compiler run successful! Ran 1 test for test/inline.sol:Inline -[FAIL: invalid type: found sequence, expected u32 for key "default.runs.fuzz" in inline config] test(bool) ([GAS]) +[FAIL: invalid type: found sequence, expected u32 for key "default.fuzz.runs" in inline config] setUp() ([GAS]) Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED] Ran 1 test suite [ELAPSED]: 0 tests passed, 1 failed, 0 skipped (1 total tests) Failing tests: Encountered 1 failing test in test/inline.sol:Inline -[FAIL: invalid type: found sequence, expected u32 for key "default.runs.fuzz" in inline config] test(bool) ([GAS]) +[FAIL: invalid type: found sequence, expected u32 for key "default.fuzz.runs" in inline config] setUp() ([GAS]) Encountered a total of 1 failing tests, 0 tests succeeded @@ -179,16 +179,86 @@ forgetest!(invalid_value_2, |prj, cmd| { Compiler run successful! Ran 1 test for test/inline.sol:Inline -[FAIL: invalid type: found string "2", expected u32 for key "default.runs.fuzz" in inline config] test(bool) ([GAS]) +[FAIL: invalid type: found string "2", expected u32 for key "default.fuzz.runs" in inline config] setUp() ([GAS]) Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED] Ran 1 test suite [ELAPSED]: 0 tests passed, 1 failed, 0 skipped (1 total tests) Failing tests: Encountered 1 failing test in test/inline.sol:Inline -[FAIL: invalid type: found string "2", expected u32 for key "default.runs.fuzz" in inline config] test(bool) ([GAS]) +[FAIL: invalid type: found string "2", expected u32 for key "default.fuzz.runs" in inline config] setUp() ([GAS]) Encountered a total of 1 failing tests, 0 tests succeeded "#]]); }); + +forgetest_init!(evm_version, |prj, cmd| { + prj.wipe_contracts(); + prj.add_test( + "inline.sol", + r#" + import {Test} from "forge-std/Test.sol"; + + contract Dummy { + function getBlobBaseFee() public returns (uint256) { + return block.blobbasefee; + } + } + + contract FunctionConfig is Test { + Dummy dummy; + + function setUp() public { + dummy = new Dummy(); + } + + /// forge-config: default.evm_version = "shanghai" + function test_old() public { + vm.expectRevert(); + dummy.getBlobBaseFee(); + } + + function test_new() public { + dummy.getBlobBaseFee(); + } + } + + /// forge-config: default.evm_version = "shanghai" + contract ContractConfig is Test { + Dummy dummy; + + function setUp() public { + dummy = new Dummy(); + } + + function test_old() public { + vm.expectRevert(); + dummy.getBlobBaseFee(); + } + + /// forge-config: default.evm_version = "cancun" + function test_new() public { + dummy.getBlobBaseFee(); + } + } + "#, + ) + .unwrap(); + + cmd.arg("test").arg("--evm-version=cancun").assert_success().stdout_eq(str![[r#" +... +Ran 2 tests for test/inline.sol:FunctionConfig +[PASS] test_new() ([GAS]) +[PASS] test_old() ([GAS]) +Suite result: ok. 2 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 2 tests for test/inline.sol:ContractConfig +[PASS] test_new() ([GAS]) +[PASS] test_old() ([GAS]) +Suite result: ok. 2 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 2 test suites [ELAPSED]: 4 tests passed, 0 failed, 0 skipped (4 total tests) + +"#]]); +}); diff --git a/crates/forge/tests/it/config.rs b/crates/forge/tests/it/config.rs index 9cabd998a01a..655fae4db254 100644 --- a/crates/forge/tests/it/config.rs +++ b/crates/forge/tests/it/config.rs @@ -31,8 +31,8 @@ impl TestConfig { Self { runner, should_fail: false, filter } } - pub fn evm_spec(mut self, spec: SpecId) -> Self { - self.runner.evm_spec = spec; + pub fn spec_id(mut self, spec: SpecId) -> Self { + self.runner.spec_id = spec; self } diff --git a/crates/forge/tests/it/spec.rs b/crates/forge/tests/it/spec.rs index aed2063a0fba..52e581c33c92 100644 --- a/crates/forge/tests/it/spec.rs +++ b/crates/forge/tests/it/spec.rs @@ -7,8 +7,5 @@ use foundry_test_utils::Filter; #[tokio::test(flavor = "multi_thread")] async fn test_shanghai_compat() { let filter = Filter::new("", "ShanghaiCompat", ".*spec"); - TestConfig::with_filter(TEST_DATA_PARIS.runner(), filter) - .evm_spec(SpecId::SHANGHAI) - .run() - .await; + TestConfig::with_filter(TEST_DATA_PARIS.runner(), filter).spec_id(SpecId::SHANGHAI).run().await; } diff --git a/crates/forge/tests/it/test_helpers.rs b/crates/forge/tests/it/test_helpers.rs index 54985b9b6154..937f582f4e4f 100644 --- a/crates/forge/tests/it/test_helpers.rs +++ b/crates/forge/tests/it/test_helpers.rs @@ -2,9 +2,7 @@ use alloy_chains::NamedChain; use alloy_primitives::U256; -use forge::{ - revm::primitives::SpecId, MultiContractRunner, MultiContractRunnerBuilder, TestOptions, -}; +use forge::{revm::primitives::SpecId, MultiContractRunner, MultiContractRunnerBuilder}; use foundry_compilers::{ artifacts::{EvmVersion, Libraries, Settings}, utils::RuntimeOrHandle, @@ -177,9 +175,7 @@ impl ForgeTestData { pub fn base_runner(&self) -> MultiContractRunnerBuilder { init_tracing(); let config = self.config.clone(); - let mut runner = MultiContractRunnerBuilder::new(config.clone()) - .sender(self.config.sender) - .with_test_options(TestOptions::new_unparsed(config)); + let mut runner = MultiContractRunnerBuilder::new(config).sender(self.config.sender); if self.profile.is_paris() { runner = runner.evm_spec(SpecId::MERGE); } @@ -216,7 +212,6 @@ impl ForgeTestData { builder .enable_isolation(opts.isolate) .sender(config.sender) - .with_test_options(TestOptions::new(&self.output, config.clone()).unwrap()) .build(root, &self.output, opts.local_evm_env(), opts) .unwrap() } diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index ccf5eab2ad0d..5f5543912fe2 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -618,7 +618,7 @@ impl ScriptConfig { .alphanet(self.evm_opts.alphanet) .create2_deployer(self.evm_opts.create2_deployer) }) - .spec(self.config.evm_spec_id()) + .spec_id(self.config.evm_spec_id()) .gas_limit(self.evm_opts.gas_limit()) .legacy_assertions(self.config.legacy_assertions);