From 56d0dd8745248e9cd029472eb0a8697d12677246 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Thu, 28 Nov 2024 14:59:27 +0100 Subject: [PATCH] feat: rewrite inline config using figment (#9414) * feat: rewrite inline config using figment * wip * wip * fix: use same GasLimit type * wip * fixes * tests * test fixes * fmt * test update --- Cargo.lock | 1 + crates/config/Cargo.toml | 5 +- crates/config/src/fuzz.rs | 108 +--------- crates/config/src/inline/conf_parser.rs | 169 --------------- crates/config/src/inline/error.rs | 44 ---- crates/config/src/inline/mod.rs | 185 +++++++++++++---- crates/config/src/inline/natspec.rs | 146 +++++++------ crates/config/src/invariant.rs | 93 +-------- crates/config/src/lib.rs | 194 ++++++++++++++---- crates/config/src/providers/mod.rs | 17 +- crates/config/src/providers/remappings.rs | 2 +- crates/config/src/utils.rs | 78 ------- crates/evm/core/src/opts.rs | 50 +---- crates/forge/bin/cmd/coverage.rs | 6 +- crates/forge/bin/cmd/test/mod.rs | 20 +- crates/forge/src/lib.rs | 239 +++++++--------------- crates/forge/src/runner.rs | 34 +-- crates/forge/tests/cli/alphanet.rs | 15 +- crates/forge/tests/cli/build.rs | 15 +- crates/forge/tests/cli/config.rs | 2 + crates/forge/tests/cli/inline_config.rs | 194 ++++++++++++++++++ crates/forge/tests/cli/main.rs | 1 + crates/forge/tests/it/cheats.rs | 18 +- crates/forge/tests/it/core.rs | 12 +- crates/forge/tests/it/fork.rs | 42 ++-- crates/forge/tests/it/fs.rs | 12 +- crates/forge/tests/it/fuzz.rs | 33 +-- crates/forge/tests/it/inline.rs | 36 +--- crates/forge/tests/it/invariant.rs | 131 +++++++----- crates/forge/tests/it/repros.rs | 38 ++-- crates/forge/tests/it/test_helpers.rs | 190 ++++++++--------- crates/test-utils/src/filter.rs | 1 + 32 files changed, 969 insertions(+), 1162 deletions(-) delete mode 100644 crates/config/src/inline/conf_parser.rs delete mode 100644 crates/config/src/inline/error.rs create mode 100644 crates/forge/tests/cli/inline_config.rs diff --git a/Cargo.lock b/Cargo.lock index d94d710ac07f..09fab74b7f0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3910,6 +3910,7 @@ dependencies = [ "foundry-compilers", "glob", "globset", + "itertools 0.13.0", "mesc", "number_prefix", "path-slash", diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index 3e6815d784e6..ba01a1afbf4c 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -27,11 +27,12 @@ dirs-next = "2" dunce.workspace = true eyre.workspace = true figment = { workspace = true, features = ["toml", "env"] } -globset = "0.4" glob = "0.3" +globset = "0.4" Inflector = "0.11" -number_prefix = "0.4" +itertools.workspace = true mesc.workspace = true +number_prefix = "0.4" regex.workspace = true reqwest.workspace = true semver = { workspace = true, features = ["serde"] } diff --git a/crates/config/src/fuzz.rs b/crates/config/src/fuzz.rs index fb04383228a9..ae3d3c796701 100644 --- a/crates/config/src/fuzz.rs +++ b/crates/config/src/fuzz.rs @@ -1,9 +1,5 @@ //! Configuration for fuzz testing. -use crate::inline::{ - parse_config_bool, parse_config_u32, InlineConfigParser, InlineConfigParserError, - INLINE_CONFIG_FUZZ_KEY, -}; use alloy_primitives::U256; use serde::{Deserialize, Serialize}; use std::path::PathBuf; @@ -53,50 +49,13 @@ impl FuzzConfig { /// Creates fuzz configuration to write failures in `{PROJECT_ROOT}/cache/fuzz` dir. pub fn new(cache_dir: PathBuf) -> Self { Self { - runs: 256, - max_test_rejects: 65536, - seed: None, - dictionary: FuzzDictionaryConfig::default(), - gas_report_samples: 256, failure_persist_dir: Some(cache_dir), failure_persist_file: Some("failures".to_string()), - show_logs: false, + ..Default::default() } } } -impl InlineConfigParser for FuzzConfig { - fn config_key() -> String { - INLINE_CONFIG_FUZZ_KEY.into() - } - - fn try_merge(&self, configs: &[String]) -> Result, InlineConfigParserError> { - let overrides: Vec<(String, String)> = Self::get_config_overrides(configs); - - if overrides.is_empty() { - return Ok(None) - } - - let mut conf_clone = self.clone(); - - for pair in overrides { - let key = pair.0; - let value = pair.1; - match key.as_str() { - "runs" => conf_clone.runs = parse_config_u32(key, value)?, - "max-test-rejects" => conf_clone.max_test_rejects = parse_config_u32(key, value)?, - "dictionary-weight" => { - conf_clone.dictionary.dictionary_weight = parse_config_u32(key, value)? - } - "failure-persist-file" => conf_clone.failure_persist_file = Some(value), - "show-logs" => conf_clone.show_logs = parse_config_bool(key, value)?, - _ => Err(InlineConfigParserError::InvalidConfigProperty(key))?, - } - } - Ok(Some(conf_clone)) - } -} - /// Contains for fuzz testing #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct FuzzDictionaryConfig { @@ -132,68 +91,3 @@ impl Default for FuzzDictionaryConfig { } } } - -#[cfg(test)] -mod tests { - use crate::{inline::InlineConfigParser, FuzzConfig}; - - #[test] - fn unrecognized_property() { - let configs = &["forge-config: default.fuzz.unknownprop = 200".to_string()]; - let base_config = FuzzConfig::default(); - if let Err(e) = base_config.try_merge(configs) { - assert_eq!(e.to_string(), "'unknownprop' is an invalid config property"); - } else { - unreachable!() - } - } - - #[test] - fn successful_merge() { - let configs = &[ - "forge-config: default.fuzz.runs = 42424242".to_string(), - "forge-config: default.fuzz.dictionary-weight = 42".to_string(), - "forge-config: default.fuzz.failure-persist-file = fuzz-failure".to_string(), - ]; - let base_config = FuzzConfig::default(); - let merged: FuzzConfig = base_config.try_merge(configs).expect("No errors").unwrap(); - assert_eq!(merged.runs, 42424242); - assert_eq!(merged.dictionary.dictionary_weight, 42); - assert_eq!(merged.failure_persist_file, Some("fuzz-failure".to_string())); - } - - #[test] - fn merge_is_none() { - let empty_config = &[]; - let base_config = FuzzConfig::default(); - let merged = base_config.try_merge(empty_config).expect("No errors"); - assert!(merged.is_none()); - } - - #[test] - fn merge_is_none_unrelated_property() { - let unrelated_configs = &["forge-config: default.invariant.runs = 2".to_string()]; - let base_config = FuzzConfig::default(); - let merged = base_config.try_merge(unrelated_configs).expect("No errors"); - assert!(merged.is_none()); - } - - #[test] - fn override_detection() { - let configs = &[ - "forge-config: default.fuzz.runs = 42424242".to_string(), - "forge-config: ci.fuzz.runs = 666666".to_string(), - "forge-config: default.invariant.runs = 2".to_string(), - "forge-config: default.fuzz.dictionary-weight = 42".to_string(), - ]; - let variables = FuzzConfig::get_config_overrides(configs); - assert_eq!( - variables, - vec![ - ("runs".into(), "42424242".into()), - ("runs".into(), "666666".into()), - ("dictionary-weight".into(), "42".into()) - ] - ); - } -} diff --git a/crates/config/src/inline/conf_parser.rs b/crates/config/src/inline/conf_parser.rs deleted file mode 100644 index e6944965499d..000000000000 --- a/crates/config/src/inline/conf_parser.rs +++ /dev/null @@ -1,169 +0,0 @@ -use super::{remove_whitespaces, InlineConfigParserError}; -use crate::{inline::INLINE_CONFIG_PREFIX, InlineConfigError, NatSpec}; -use regex::Regex; - -/// This trait is intended to parse configurations from -/// structured text. Foundry users can annotate Solidity test functions, -/// providing special configs just for the execution of a specific test. -/// -/// An example: -/// -/// ```solidity -/// contract MyTest is Test { -/// /// forge-config: default.fuzz.runs = 100 -/// /// forge-config: ci.fuzz.runs = 500 -/// function test_SimpleFuzzTest(uint256 x) public {...} -/// -/// /// forge-config: default.fuzz.runs = 500 -/// /// forge-config: ci.fuzz.runs = 10000 -/// function test_ImportantFuzzTest(uint256 x) public {...} -/// } -/// ``` -pub trait InlineConfigParser -where - Self: Clone + Default + Sized + 'static, -{ - /// Returns a config key that is common to all valid configuration lines - /// for the current impl. This helps to extract correct values out of a text. - /// - /// An example key would be `fuzz` of `invariant`. - fn config_key() -> String; - - /// Tries to override `self` properties with values specified in the `configs` parameter. - /// - /// Returns - /// - `Some(Self)` in case some configurations are merged into self. - /// - `None` in case there are no configurations that can be applied to self. - /// - `Err(InlineConfigParserError)` in case of wrong configuration. - fn try_merge(&self, configs: &[String]) -> Result, InlineConfigParserError>; - - /// Validates and merges the natspec configs for current profile into the current config. - fn merge(&self, natspec: &NatSpec) -> Result, InlineConfigError> { - let config_key = Self::config_key(); - - let configs = natspec - .current_profile_configs() - .filter(|l| l.contains(&config_key)) - .collect::>(); - - self.try_merge(&configs).map_err(|e| { - let line = natspec.debug_context(); - InlineConfigError { line, source: e } - }) - } - - /// Given a list of config lines, returns all available pairs (key, value) matching the current - /// config key. - /// - /// # Examples - /// - /// ```ignore - /// assert_eq!( - /// get_config_overrides(&[ - /// "forge-config: default.invariant.runs = 500", - /// "forge-config: default.invariant.depth = 500", - /// "forge-config: ci.invariant.depth = 500", - /// "forge-config: ci.fuzz.runs = 10", - /// ]), - /// [("runs", "500"), ("depth", "500"), ("depth", "500")] - /// ); - /// ``` - fn get_config_overrides(config_lines: &[String]) -> Vec<(String, String)> { - let mut result: Vec<(String, String)> = vec![]; - let config_key = Self::config_key(); - let profile = ".*"; - let prefix = format!("^{INLINE_CONFIG_PREFIX}:{profile}{config_key}\\."); - let re = Regex::new(&prefix).unwrap(); - - config_lines - .iter() - .map(|l| remove_whitespaces(l)) - .filter(|l| re.is_match(l)) - .map(|l| re.replace(&l, "").to_string()) - .for_each(|line| { - let key_value = line.split('=').collect::>(); // i.e. "['runs', '500']" - if let Some(key) = key_value.first() { - if let Some(value) = key_value.last() { - result.push((key.to_string(), value.to_string())); - } - } - }); - - result - } -} - -/// Checks if all configuration lines specified in `natspec` use a valid profile. -/// -/// i.e. Given available profiles -/// ```rust -/// let _profiles = vec!["ci", "default"]; -/// ``` -/// A configuration like `forge-config: ciii.invariant.depth = 1` would result -/// in an error. -pub fn validate_profiles(natspec: &NatSpec, profiles: &[String]) -> Result<(), InlineConfigError> { - for config in natspec.config_lines() { - if !profiles.iter().any(|p| config.starts_with(&format!("{INLINE_CONFIG_PREFIX}:{p}."))) { - let err_line: String = natspec.debug_context(); - let profiles = format!("{profiles:?}"); - Err(InlineConfigError { - source: InlineConfigParserError::InvalidProfile(config, profiles), - line: err_line, - })? - } - } - Ok(()) -} - -/// Tries to parse a `u32` from `value`. The `key` argument is used to give details -/// in the case of an error. -pub fn parse_config_u32(key: String, value: String) -> Result { - value.parse().map_err(|_| InlineConfigParserError::ParseInt(key, value)) -} - -/// Tries to parse a `bool` from `value`. The `key` argument is used to give details -/// in the case of an error. -pub fn parse_config_bool(key: String, value: String) -> Result { - value.parse().map_err(|_| InlineConfigParserError::ParseBool(key, value)) -} - -#[cfg(test)] -mod tests { - use crate::{inline::conf_parser::validate_profiles, NatSpec}; - - #[test] - fn can_reject_invalid_profiles() { - let profiles = ["ci".to_string(), "default".to_string()]; - let natspec = NatSpec { - contract: Default::default(), - function: Default::default(), - line: Default::default(), - docs: r" - forge-config: ciii.invariant.depth = 1 - forge-config: default.invariant.depth = 1 - " - .into(), - }; - - let result = validate_profiles(&natspec, &profiles); - assert!(result.is_err()); - } - - #[test] - fn can_accept_valid_profiles() { - let profiles = ["ci".to_string(), "default".to_string()]; - let natspec = NatSpec { - contract: Default::default(), - function: Default::default(), - line: Default::default(), - docs: r" - forge-config: ci.invariant.depth = 1 - forge-config: default.invariant.depth = 1 - " - .into(), - }; - - let result = validate_profiles(&natspec, &profiles); - assert!(result.is_ok()); - } -} diff --git a/crates/config/src/inline/error.rs b/crates/config/src/inline/error.rs deleted file mode 100644 index ddcb6a61bdb8..000000000000 --- a/crates/config/src/inline/error.rs +++ /dev/null @@ -1,44 +0,0 @@ -/// Errors returned by the [`InlineConfigParser`](crate::InlineConfigParser) trait. -#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] -pub enum InlineConfigParserError { - /// An invalid configuration property has been provided. - /// The property cannot be mapped to the configuration object - #[error("'{0}' is an invalid config property")] - InvalidConfigProperty(String), - /// An invalid profile has been provided - #[error("'{0}' specifies an invalid profile. Available profiles are: {1}")] - InvalidProfile(String, String), - /// An error occurred while trying to parse an integer configuration value - #[error("Invalid config value for key '{0}'. Unable to parse '{1}' into an integer value")] - ParseInt(String, String), - /// An error occurred while trying to parse a boolean configuration value - #[error("Invalid config value for key '{0}'. Unable to parse '{1}' into a boolean value")] - ParseBool(String, String), -} - -/// Wrapper error struct that catches config parsing errors, enriching them with context information -/// reporting the misconfigured line. -#[derive(Debug, thiserror::Error)] -#[error("Inline config error detected at {line}")] -pub struct InlineConfigError { - /// Specifies the misconfigured line. This is something of the form - /// `dir/TestContract.t.sol:FuzzContract:10:12:111` - pub line: String, - /// The inner error - pub source: InlineConfigParserError, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn can_format_inline_config_errors() { - let source = InlineConfigParserError::ParseBool("key".into(), "invalid-bool-value".into()); - let line = "dir/TestContract.t.sol:FuzzContract".to_string(); - let error = InlineConfigError { line: line.clone(), source }; - - let expected = format!("Inline config error detected at {line}"); - assert_eq!(error.to_string(), expected); - } -} diff --git a/crates/config/src/inline/mod.rs b/crates/config/src/inline/mod.rs index 8b5616a21afb..30b1c820ec9b 100644 --- a/crates/config/src/inline/mod.rs +++ b/crates/config/src/inline/mod.rs @@ -1,61 +1,168 @@ use crate::Config; use alloy_primitives::map::HashMap; -use std::sync::LazyLock; -mod conf_parser; -pub use conf_parser::*; - -mod error; -pub use error::*; +use figment::{ + value::{Dict, Map, Value}, + Figment, Profile, Provider, +}; +use itertools::Itertools; mod natspec; pub use natspec::*; -pub const INLINE_CONFIG_FUZZ_KEY: &str = "fuzz"; -pub const INLINE_CONFIG_INVARIANT_KEY: &str = "invariant"; -const INLINE_CONFIG_PREFIX: &str = "forge-config"; +const INLINE_CONFIG_PREFIX: &str = "forge-config:"; + +type DataMap = Map; -static INLINE_CONFIG_PREFIX_SELECTED_PROFILE: LazyLock = LazyLock::new(|| { - let selected_profile = Config::selected_profile().to_string(); - format!("{INLINE_CONFIG_PREFIX}:{selected_profile}.") -}); +/// Errors returned when parsing inline config. +#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] +pub enum InlineConfigErrorKind { + /// Failed to parse inline config as TOML. + #[error(transparent)] + Parse(#[from] toml::de::Error), + /// An invalid profile has been provided. + #[error("invalid profile `{0}`; valid profiles: {1}")] + InvalidProfile(String, String), +} + +/// Wrapper error struct that catches config parsing errors, enriching them with context information +/// reporting the misconfigured line. +#[derive(Debug, thiserror::Error)] +#[error("Inline config error at {location}: {kind}")] +pub struct InlineConfigError { + /// The span of the error in the format: + /// `dir/TestContract.t.sol:FuzzContract:10:12:111` + pub location: String, + /// The inner error + pub kind: InlineConfigErrorKind, +} /// Represents per-test configurations, declared inline /// as structured comments in Solidity test files. This allows /// to create configs directly bound to a solidity test. #[derive(Clone, Debug, Default)] -pub struct InlineConfig { - /// Contract-level configurations, used for functions that do not have a specific - /// configuration. - contract_level: HashMap, - /// Maps a (test-contract, test-function) pair - /// to a specific configuration provided by the user. - fn_level: HashMap<(String, String), T>, +pub struct InlineConfig { + /// Contract-level configuration. + contract_level: HashMap, + /// Function-level configuration. + fn_level: HashMap<(String, String), DataMap>, } -impl InlineConfig { - /// Returns an inline configuration, if any, for a test function. - /// Configuration is identified by the pair "contract", "function". - pub fn get(&self, contract_id: &str, fn_name: &str) -> Option<&T> { - let key = (contract_id.to_string(), fn_name.to_string()); - self.fn_level.get(&key).or_else(|| self.contract_level.get(contract_id)) +impl InlineConfig { + /// Creates a new, empty [`InlineConfig`]. + pub fn new() -> Self { + Self::default() + } + + /// Inserts a new [`NatSpec`] into the [`InlineConfig`]. + pub fn insert(&mut self, natspec: &NatSpec) -> Result<(), InlineConfigError> { + let map = if let Some(function) = &natspec.function { + self.fn_level.entry((natspec.contract.clone(), function.clone())).or_default() + } else { + self.contract_level.entry(natspec.contract.clone()).or_default() + }; + let joined = natspec + .config_values() + .map(|s| { + // Replace `-` with `_` for backwards compatibility with the old parser. + if let Some(idx) = s.find('=') { + s[..idx].replace('-', "_") + &s[idx..] + } else { + s.to_string() + } + }) + .format("\n") + .to_string(); + let data = toml::from_str::(&joined).map_err(|e| InlineConfigError { + location: natspec.location_string(), + kind: InlineConfigErrorKind::Parse(e), + })?; + extend_data_map(map, &data); + Ok(()) } - pub fn insert_contract(&mut self, contract_id: impl Into, config: T) { - self.contract_level.insert(contract_id.into(), config); + /// Returns a [`figment::Provider`] for this [`InlineConfig`] at the given contract and function + /// level. + pub fn provide<'a>(&'a self, contract: &'a str, function: &'a str) -> InlineConfigProvider<'a> { + InlineConfigProvider { inline: self, contract, function } } - /// Inserts an inline configuration, for a test function. - /// Configuration is identified by the pair "contract", "function". - pub fn insert_fn(&mut self, contract_id: C, fn_name: F, config: T) - where - C: Into, - F: Into, - { - let key = (contract_id.into(), fn_name.into()); - self.fn_level.insert(key, config); + /// Merges the inline configuration at the given contract and function level with the provided + /// base configuration. + pub fn merge(&self, contract: &str, function: &str, base: &Config) -> Figment { + 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()) + } + + fn get_contract(&self, contract: &str) -> Option<&DataMap> { + self.contract_level.get(contract) + } + + fn get_function(&self, contract: &str, function: &str) -> Option<&DataMap> { + let key = (contract.to_string(), function.to_string()); + self.fn_level.get(&key) } } -pub(crate) fn remove_whitespaces(s: &str) -> String { - s.chars().filter(|c| !c.is_whitespace()).collect() +/// [`figment::Provider`] for [`InlineConfig`] at a given contract and function level. +/// +/// Created by [`InlineConfig::provide`]. +#[derive(Clone, Debug)] +pub struct InlineConfigProvider<'a> { + inline: &'a InlineConfig, + contract: &'a str, + function: &'a str, +} + +impl Provider for InlineConfigProvider<'_> { + fn metadata(&self) -> figment::Metadata { + figment::Metadata::named("inline config") + } + + fn data(&self) -> figment::Result { + let mut map = DataMap::new(); + if let Some(new) = self.inline.get_contract(self.contract) { + extend_data_map(&mut map, new); + } + if let Some(new) = self.inline.get_function(self.contract, self.function) { + extend_data_map(&mut map, new); + } + Ok(map) + } +} + +fn extend_data_map(map: &mut DataMap, new: &DataMap) { + for (profile, data) in new { + extend_dict(map.entry(profile.clone()).or_default(), data); + } +} + +fn extend_dict(dict: &mut Dict, new: &Dict) { + for (k, v) in new { + match dict.entry(k.clone()) { + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert(v.clone()); + } + std::collections::btree_map::Entry::Occupied(entry) => { + extend_value(entry.into_mut(), v); + } + } + } +} + +fn extend_value(value: &mut Value, new: &Value) { + match (value, new) { + (Value::Dict(tag, dict), Value::Dict(new_tag, new_dict)) => { + *tag = *new_tag; + extend_dict(dict, new_dict); + } + (value, new) => *value = new.clone(), + } } diff --git a/crates/config/src/inline/natspec.rs b/crates/config/src/inline/natspec.rs index 6dd6b696c015..5774d9e193f1 100644 --- a/crates/config/src/inline/natspec.rs +++ b/crates/config/src/inline/natspec.rs @@ -1,8 +1,10 @@ -use super::{remove_whitespaces, INLINE_CONFIG_PREFIX, INLINE_CONFIG_PREFIX_SELECTED_PROFILE}; +use super::{InlineConfigError, InlineConfigErrorKind, INLINE_CONFIG_PREFIX}; +use figment::Profile; use foundry_compilers::{ artifacts::{ast::NodeType, Node}, ProjectCompileOutput, }; +use itertools::Itertools; use serde_json::Value; use solang_parser::{helpers::CodeLocation, pt}; use std::{collections::BTreeMap, path::Path}; @@ -10,15 +12,13 @@ use std::{collections::BTreeMap, path::Path}; /// Convenient struct to hold in-line per-test configurations #[derive(Clone, Debug, PartialEq, Eq)] pub struct NatSpec { - /// The parent contract of the natspec + /// The parent contract of the natspec. pub contract: String, - /// The function annotated with the natspec. None if the natspec is contract-level + /// The function annotated with the natspec. None if the natspec is contract-level. pub function: Option, - /// The line the natspec appears, in the form - /// `row:col:length` i.e. `10:21:122` + /// The line the natspec appears, in the form `row:col:length`, i.e. `10:21:122`. pub line: String, - /// The actual natspec comment, without slashes or block - /// punctuation + /// The actual natspec comment, without slashes or block punctuation. pub docs: String, } @@ -56,29 +56,52 @@ impl NatSpec { natspecs } - /// Returns a string describing the natspec - /// context, for debugging purposes 🐞 - /// i.e. `test/Counter.t.sol:CounterTest:testFuzz_SetNumber` - pub fn debug_context(&self) -> String { - format!("{}:{}", self.contract, self.function.as_deref().unwrap_or_default()) + /// Checks if all configuration lines use a valid profile. + /// + /// i.e. Given available profiles + /// ```rust + /// let _profiles = vec!["ci", "default"]; + /// ``` + /// A configuration like `forge-config: ciii.invariant.depth = 1` would result + /// in an error. + pub fn validate_profiles(&self, profiles: &[Profile]) -> eyre::Result<()> { + for config in self.config_values() { + if !profiles.iter().any(|p| { + config + .strip_prefix(p.as_str().as_str()) + .is_some_and(|rest| rest.trim_start().starts_with('.')) + }) { + Err(InlineConfigError { + location: self.location_string(), + kind: InlineConfigErrorKind::InvalidProfile( + config.to_string(), + profiles.iter().format(", ").to_string(), + ), + })? + } + } + Ok(()) } - /// Returns a list of configuration lines that match the current profile - pub fn current_profile_configs(&self) -> impl Iterator + '_ { - self.config_lines_with_prefix(INLINE_CONFIG_PREFIX_SELECTED_PROFILE.as_str()) + /// Returns the path of the contract. + pub fn path(&self) -> &str { + match self.contract.split_once(':') { + Some((path, _)) => path, + None => self.contract.as_str(), + } } - /// Returns a list of configuration lines that match a specific string prefix - pub fn config_lines_with_prefix<'a>( - &'a self, - prefix: &'a str, - ) -> impl Iterator + 'a { - self.config_lines().filter(move |l| l.starts_with(prefix)) + /// Returns the location of the natspec as a string. + pub fn location_string(&self) -> String { + format!("{}:{}", self.path(), self.line) } - /// Returns a list of all the configuration lines available in the natspec - pub fn config_lines(&self) -> impl Iterator + '_ { - self.docs.lines().filter(|line| line.contains(INLINE_CONFIG_PREFIX)).map(remove_whitespaces) + /// Returns a list of all the configuration values available in the natspec. + pub fn config_values(&self) -> impl Iterator { + self.docs.lines().filter_map(|line| { + line.find(INLINE_CONFIG_PREFIX) + .map(|idx| line[idx + INLINE_CONFIG_PREFIX.len()..].trim()) + }) } } @@ -258,6 +281,42 @@ mod tests { use super::*; use serde_json::json; + #[test] + fn can_reject_invalid_profiles() { + let profiles = ["ci".into(), "default".into()]; + let natspec = NatSpec { + contract: Default::default(), + function: Default::default(), + line: Default::default(), + docs: r" + forge-config: ciii.invariant.depth = 1 + forge-config: default.invariant.depth = 1 + " + .into(), + }; + + let result = natspec.validate_profiles(&profiles); + assert!(result.is_err()); + } + + #[test] + fn can_accept_valid_profiles() { + let profiles = ["ci".into(), "default".into()]; + let natspec = NatSpec { + contract: Default::default(), + function: Default::default(), + line: Default::default(), + docs: r" + forge-config: ci.invariant.depth = 1 + forge-config: default.invariant.depth = 1 + " + .into(), + }; + + let result = natspec.validate_profiles(&profiles); + assert!(result.is_ok()); + } + #[test] fn parse_solang() { let src = " @@ -355,42 +414,13 @@ contract FuzzInlineConf is DSTest { #[test] fn config_lines() { let natspec = natspec(); - let config_lines = natspec.config_lines(); - assert_eq!( - config_lines.collect::>(), - vec![ - "forge-config:default.fuzz.runs=600".to_string(), - "forge-config:ci.fuzz.runs=500".to_string(), - "forge-config:default.invariant.runs=1".to_string() - ] - ) - } - - #[test] - fn current_profile_configs() { - let natspec = natspec(); - let config_lines = natspec.current_profile_configs(); - - assert_eq!( - config_lines.collect::>(), - vec![ - "forge-config:default.fuzz.runs=600".to_string(), - "forge-config:default.invariant.runs=1".to_string() - ] - ); - } - - #[test] - fn config_lines_with_prefix() { - use super::INLINE_CONFIG_PREFIX; - let natspec = natspec(); - let prefix = format!("{INLINE_CONFIG_PREFIX}:default"); - let config_lines = natspec.config_lines_with_prefix(&prefix); + let config_lines = natspec.config_values(); assert_eq!( config_lines.collect::>(), - vec![ - "forge-config:default.fuzz.runs=600".to_string(), - "forge-config:default.invariant.runs=1".to_string() + [ + "default.fuzz.runs = 600".to_string(), + "ci.fuzz.runs = 500".to_string(), + "default.invariant.runs = 1".to_string() ] ) } diff --git a/crates/config/src/invariant.rs b/crates/config/src/invariant.rs index 70e9a2b85847..97f189b363d1 100644 --- a/crates/config/src/invariant.rs +++ b/crates/config/src/invariant.rs @@ -1,12 +1,6 @@ //! Configuration for invariant testing -use crate::{ - fuzz::FuzzDictionaryConfig, - inline::{ - parse_config_bool, parse_config_u32, InlineConfigParser, InlineConfigParserError, - INLINE_CONFIG_INVARIANT_KEY, - }, -}; +use crate::fuzz::FuzzDictionaryConfig; use serde::{Deserialize, Serialize}; use std::path::PathBuf; @@ -80,88 +74,3 @@ impl InvariantConfig { .join(contract_name.split(':').last().unwrap()) } } - -impl InlineConfigParser for InvariantConfig { - fn config_key() -> String { - INLINE_CONFIG_INVARIANT_KEY.into() - } - - fn try_merge(&self, configs: &[String]) -> Result, InlineConfigParserError> { - let overrides: Vec<(String, String)> = Self::get_config_overrides(configs); - - if overrides.is_empty() { - return Ok(None) - } - - let mut conf_clone = self.clone(); - - for pair in overrides { - let key = pair.0; - let value = pair.1; - match key.as_str() { - "runs" => conf_clone.runs = parse_config_u32(key, value)?, - "depth" => conf_clone.depth = parse_config_u32(key, value)?, - "fail-on-revert" => conf_clone.fail_on_revert = parse_config_bool(key, value)?, - "call-override" => conf_clone.call_override = parse_config_bool(key, value)?, - "failure-persist-dir" => { - conf_clone.failure_persist_dir = Some(PathBuf::from(value)) - } - "shrink-run-limit" => conf_clone.shrink_run_limit = parse_config_u32(key, value)?, - "show-metrics" => conf_clone.show_metrics = parse_config_bool(key, value)?, - _ => Err(InlineConfigParserError::InvalidConfigProperty(key.to_string()))?, - } - } - Ok(Some(conf_clone)) - } -} - -#[cfg(test)] -mod tests { - use crate::{inline::InlineConfigParser, InvariantConfig}; - - #[test] - fn unrecognized_property() { - let configs = &["forge-config: default.invariant.unknownprop = 200".to_string()]; - let base_config = InvariantConfig::default(); - if let Err(e) = base_config.try_merge(configs) { - assert_eq!(e.to_string(), "'unknownprop' is an invalid config property"); - } else { - unreachable!() - } - } - - #[test] - fn successful_merge() { - let configs = &["forge-config: default.invariant.runs = 42424242".to_string()]; - let base_config = InvariantConfig::default(); - let merged: InvariantConfig = base_config.try_merge(configs).expect("No errors").unwrap(); - assert_eq!(merged.runs, 42424242); - } - - #[test] - fn merge_is_none() { - let empty_config = &[]; - let base_config = InvariantConfig::default(); - let merged = base_config.try_merge(empty_config).expect("No errors"); - assert!(merged.is_none()); - } - - #[test] - fn can_merge_unrelated_properties_into_config() { - let unrelated_configs = &["forge-config: default.fuzz.runs = 2".to_string()]; - let base_config = InvariantConfig::default(); - let merged = base_config.try_merge(unrelated_configs).expect("No errors"); - assert!(merged.is_none()); - } - - #[test] - fn override_detection() { - let configs = &[ - "forge-config: default.fuzz.runs = 42424242".to_string(), - "forge-config: ci.fuzz.runs = 666666".to_string(), - "forge-config: default.invariant.runs = 2".to_string(), - ]; - let variables = InvariantConfig::get_config_overrides(configs); - assert_eq!(variables, vec![("runs".into(), "2".into())]); - } -} diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 5a159c3925b8..72be43ab18a9 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -108,7 +108,7 @@ mod invariant; pub use invariant::InvariantConfig; mod inline; -pub use inline::{validate_profiles, InlineConfig, InlineConfigError, InlineConfigParser, NatSpec}; +pub use inline::{InlineConfig, InlineConfigError, NatSpec}; pub mod soldeer; use soldeer::{SoldeerConfig, SoldeerDependencyConfig}; @@ -163,6 +163,11 @@ pub struct Config { /// set to the extracting Figment's selected `Profile`. #[serde(skip)] pub profile: Profile, + /// The list of all profiles defined in the config. + /// + /// See `profile`. + #[serde(skip)] + pub profiles: Vec, /// path of the source contracts dir, like `src` or `contracts` pub src: PathBuf, /// path of the test dir @@ -481,7 +486,7 @@ pub struct Config { /// Use EOF-enabled solc for compilation. pub eof: bool, - /// Warnings gathered when loading the Config. See [`WarningsProvider`] for more information + /// Warnings gathered when loading the Config. See [`WarningsProvider`] for more information. #[serde(rename = "__warnings", default, skip_serializing)] pub warnings: Vec, @@ -516,7 +521,7 @@ pub const DEPRECATIONS: &[(&str, &str)] = &[("cancun", "evm_version = Cancun")]; impl Config { /// The default profile: "default" - pub const DEFAULT_PROFILE: Profile = Profile::const_new("default"); + pub const DEFAULT_PROFILE: Profile = Profile::Default; /// The hardhat profile: "hardhat" pub const HARDHAT_PROFILE: Profile = Profile::const_new("hardhat"); @@ -569,7 +574,7 @@ impl Config { /// See [`figment`](Self::figment) for more details. #[track_caller] pub fn load_with_providers(providers: FigmentProviders) -> Self { - Self::default().to_figment(providers).extract().unwrap() + Self::from_provider(Self::default().to_figment(providers)) } /// Returns the current `Config` @@ -620,19 +625,47 @@ impl Config { /// let config = Config::try_from(figment); /// ``` pub fn try_from(provider: T) -> Result { - let figment = Figment::from(provider); + Self::try_from_figment(Figment::from(provider)) + } + + fn try_from_figment(figment: Figment) -> Result { let mut config = figment.extract::().map_err(ExtractConfigError::new)?; config.profile = figment.profile().clone(); + + // The `"profile"` profile contains all the profiles as keys. + let mut add_profile = |profile: &Profile| { + if !config.profiles.contains(profile) { + config.profiles.push(profile.clone()); + } + }; + let figment = figment.select(Self::PROFILE_SECTION); + if let Ok(data) = figment.data() { + if let Some(profiles) = data.get(&Profile::new(Self::PROFILE_SECTION)) { + for profile in profiles.keys() { + add_profile(&Profile::new(profile)); + } + } + } + add_profile(&Self::DEFAULT_PROFILE); + add_profile(&config.profile); + Ok(config) } /// Returns the populated [Figment] using the requested [FigmentProviders] preset. /// - /// This will merge various providers, such as env,toml,remappings into the figment. - pub fn to_figment(self, providers: FigmentProviders) -> Figment { - let mut c = self; + /// This will merge various providers, such as env,toml,remappings into the figment if + /// requested. + pub fn to_figment(&self, providers: FigmentProviders) -> Figment { + // Note that `Figment::from` here is a method on `Figment` rather than the `From` impl below + + if providers.is_none() { + return Figment::from(self); + } + + let root = self.root.0.as_path(); let profile = Self::selected_profile(); - let mut figment = Figment::default().merge(DappHardhatDirProvider(&c.root.0)); + let mut figment = Figment::default().merge(DappHardhatDirProvider(root)); // merge global foundry.toml file if let Some(global_toml) = Self::foundry_dir_toml().filter(|p| p.exists()) { @@ -645,7 +678,7 @@ impl Config { // merge local foundry.toml file figment = Self::merge_toml_provider( figment, - TomlFileProvider::new(Some("FOUNDRY_CONFIG"), c.root.0.join(Self::FILE_NAME)).cached(), + TomlFileProvider::new(Some("FOUNDRY_CONFIG"), root.join(Self::FILE_NAME)).cached(), profile.clone(), ); @@ -692,17 +725,17 @@ impl Config { lib_paths: figment .extract_inner::>("libs") .map(Cow::Owned) - .unwrap_or_else(|_| Cow::Borrowed(&c.libs)), - root: &c.root.0, + .unwrap_or_else(|_| Cow::Borrowed(&self.libs)), + root, remappings: figment.extract_inner::>("remappings"), }; figment = figment.merge(remappings); } // normalize defaults - figment = c.normalize_defaults(figment); + figment = self.normalize_defaults(figment); - Figment::from(c).merge(figment).select(profile) + Figment::from(self).merge(figment).select(profile) } /// The config supports relative paths and tracks the root path separately see @@ -1722,6 +1755,19 @@ impl Config { /// /// If the `FOUNDRY_PROFILE` env variable is not set, this returns the `DEFAULT_PROFILE`. pub fn selected_profile() -> Profile { + // Can't cache in tests because the env var can change. + #[cfg(test)] + { + Self::force_selected_profile() + } + #[cfg(not(test))] + { + static CACHE: std::sync::OnceLock = std::sync::OnceLock::new(); + CACHE.get_or_init(Self::force_selected_profile).clone() + } + } + + fn force_selected_profile() -> Profile { Profile::from_env_or("FOUNDRY_PROFILE", Self::DEFAULT_PROFILE) } @@ -2017,19 +2063,20 @@ impl Config { /// This normalizes the default `evm_version` if a `solc` was provided in the config. /// /// See also - fn normalize_defaults(&mut self, figment: Figment) -> Figment { + fn normalize_defaults(&self, mut figment: Figment) -> Figment { + // TODO: add a warning if evm_version is provided but incompatible + if figment.contains("evm_version") { + return figment; + } + + // Normalize `evm_version` based on the provided solc version. if let Ok(solc) = figment.extract_inner::("solc") { - // check if evm_version is set - // TODO: add a warning if evm_version is provided but incompatible - if figment.find_value("evm_version").is_err() { - if let Some(version) = solc - .try_version() - .ok() - .and_then(|version| self.evm_version.normalize_version_solc(&version)) - { - // normalize evm_version based on the provided solc version - self.evm_version = version; - } + if let Some(version) = solc + .try_version() + .ok() + .and_then(|version| self.evm_version.normalize_version_solc(&version)) + { + figment = figment.merge(("evm_version", version)); } } @@ -2039,36 +2086,53 @@ impl Config { impl From for Figment { fn from(c: Config) -> Self { + (&c).into() + } +} +impl From<&Config> for Figment { + fn from(c: &Config) -> Self { c.to_figment(FigmentProviders::All) } } -/// Determines what providers should be used when loading the [Figment] for a [Config] +/// Determines what providers should be used when loading the [`Figment`] for a [`Config`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum FigmentProviders { - /// Include all providers + /// Include all providers. #[default] All, - /// Only include necessary providers that are useful for cast commands + /// Only include necessary providers that are useful for cast commands. /// - /// This will exclude more expensive providers such as remappings + /// This will exclude more expensive providers such as remappings. Cast, - /// Only include necessary providers that are useful for anvil + /// Only include necessary providers that are useful for anvil. /// - /// This will exclude more expensive providers such as remappings + /// This will exclude more expensive providers such as remappings. Anvil, + /// Don't include any providers. + None, } impl FigmentProviders { - /// Returns true if all providers should be included + /// Returns true if all providers should be included. pub const fn is_all(&self) -> bool { matches!(self, Self::All) } - /// Returns true if this is the cast preset + /// Returns true if this is the cast preset. pub const fn is_cast(&self) -> bool { matches!(self, Self::Cast) } + + /// Returns true if this is the anvil preset. + pub const fn is_anvil(&self) -> bool { + matches!(self, Self::Anvil) + } + + /// Returns true if no providers should be included. + pub const fn is_none(&self) -> bool { + matches!(self, Self::None) + } } /// Wrapper type for `regex::Regex` that implements `PartialEq` @@ -2154,6 +2218,20 @@ impl AsRef for RootPath { } } +impl std::ops::Deref for RootPath { + type Target = PathBuf; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for RootPath { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + /// Parses a config profile /// /// All `Profile` date is ignored by serde, however the `Config::to_string_pretty` includes it and @@ -2202,11 +2280,9 @@ impl Default for Config { fn default() -> Self { Self { profile: Self::DEFAULT_PROFILE, + profiles: vec![Self::DEFAULT_PROFILE], fs_permissions: FsPermissions::new([PathPermission::read("out")]), - #[cfg(not(feature = "isolate-by-default"))] - isolate: false, - #[cfg(feature = "isolate-by-default")] - isolate: true, + isolate: cfg!(feature = "isolate-by-default"), root: Default::default(), src: "src".into(), test: "test".into(), @@ -2322,11 +2398,12 @@ impl Default for Config { } } -/// Wrapper for the config's `gas_limit` value necessary because toml-rs can't handle larger number because integers are stored signed: +/// Wrapper for the config's `gas_limit` value necessary because toml-rs can't handle larger number +/// because integers are stored signed: /// /// Due to this limitation this type will be serialized/deserialized as String if it's larger than /// `i64` -#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)] pub struct GasLimit(#[serde(deserialize_with = "crate::deserialize_u64_or_max")] pub u64); impl From for GasLimit { @@ -3052,9 +3129,39 @@ mod tests { #[test] fn test_figment_is_default() { figment::Jail::expect_with(|_| { - let mut default: Config = Config::figment().extract().unwrap(); - default.profile = Config::default().profile; - assert_eq!(default, Config::default()); + let mut default: Config = Config::figment().extract()?; + let default2 = Config::default(); + default.profile = default2.profile.clone(); + default.profiles = default2.profiles.clone(); + assert_eq!(default, default2); + Ok(()) + }); + } + + #[test] + fn figment_profiles() { + figment::Jail::expect_with(|jail| { + jail.create_file( + "foundry.toml", + r" + [foo.baz] + libs = ['node_modules', 'lib'] + + [profile.default] + libs = ['node_modules', 'lib'] + + [profile.ci] + libs = ['node_modules', 'lib'] + + [profile.local] + libs = ['node_modules', 'lib'] + ", + )?; + + let config = crate::Config::load(); + let expected: &[figment::Profile] = &["ci".into(), "default".into(), "local".into()]; + assert_eq!(config.profiles, expected); + Ok(()) }); } @@ -3163,7 +3270,6 @@ mod tests { jail.set_env("FOUNDRY_PROFILE", "custom"); let config = Config::load(); - assert_eq!(config.src, PathBuf::from("customsrc")); assert_eq!(config.test, PathBuf::from("defaulttest")); assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]); diff --git a/crates/config/src/providers/mod.rs b/crates/config/src/providers/mod.rs index 1f9f5c88ea89..9bd8a014f969 100644 --- a/crates/config/src/providers/mod.rs +++ b/crates/config/src/providers/mod.rs @@ -17,7 +17,7 @@ pub struct WarningsProvider

{ old_warnings: Result, Error>, } -impl

WarningsProvider

{ +impl WarningsProvider

{ const WARNINGS_KEY: &'static str = "__warnings"; /// Creates a new warnings provider. @@ -41,9 +41,7 @@ impl

WarningsProvider

{ }; Self::new(provider, figment.profile().clone(), old_warnings) } -} -impl WarningsProvider

{ /// Collects all warnings. pub fn collect_warnings(&self) -> Result, Error> { let data = self.provider.data().unwrap_or_default(); @@ -103,12 +101,10 @@ impl Provider for WarningsProvider

{ } fn data(&self) -> Result, Error> { + let warnings = self.collect_warnings()?; Ok(Map::from([( self.profile.clone(), - Dict::from([( - Self::WARNINGS_KEY.to_string(), - Value::serialize(self.collect_warnings()?)?, - )]), + Dict::from([(Self::WARNINGS_KEY.to_string(), Value::serialize(warnings)?)]), )])) } @@ -138,8 +134,9 @@ impl Provider for FallbackProfileProvider

{ } fn data(&self) -> Result, Error> { - if let Some(fallback) = self.provider.data()?.get(&self.fallback) { - let mut inner = self.provider.data()?.remove(&self.profile).unwrap_or_default(); + let data = self.provider.data()?; + if let Some(fallback) = data.get(&self.fallback) { + let mut inner = data.get(&self.profile).cloned().unwrap_or_default(); for (k, v) in fallback.iter() { if !inner.contains_key(k) { inner.insert(k.to_owned(), v.clone()); @@ -147,7 +144,7 @@ impl Provider for FallbackProfileProvider

{ } Ok(self.profile.collect(inner)) } else { - self.provider.data() + Ok(data) } } diff --git a/crates/config/src/providers/remappings.rs b/crates/config/src/providers/remappings.rs index 623234f94798..343fde697859 100644 --- a/crates/config/src/providers/remappings.rs +++ b/crates/config/src/providers/remappings.rs @@ -112,7 +112,7 @@ pub struct RemappingsProvider<'a> { pub lib_paths: Cow<'a, Vec>, /// the root path used to turn an absolute `Remapping`, as we're getting it from /// `Remapping::find_many` into a relative one. - pub root: &'a PathBuf, + pub root: &'a Path, /// This contains either: /// - previously set remappings /// - a `MissingField` error, which means previous provider didn't set the "remappings" field diff --git a/crates/config/src/utils.rs b/crates/config/src/utils.rs index 2117834f428a..e07d7dfbcb09 100644 --- a/crates/config/src/utils.rs +++ b/crates/config/src/utils.rs @@ -14,7 +14,6 @@ use std::{ path::{Path, PathBuf}, str::FromStr, }; -use toml_edit::{DocumentMut, Item}; /// Loads the config for the current project workspace pub fn load_config() -> Config { @@ -186,45 +185,6 @@ pub(crate) fn get_dir_remapping(dir: impl AsRef) -> Option { } } -/// Returns all available `profile` keys in a given `.toml` file -/// -/// i.e. The toml below would return would return `["default", "ci", "local"]` -/// ```toml -/// [profile.default] -/// ... -/// [profile.ci] -/// ... -/// [profile.local] -/// ``` -pub fn get_available_profiles(toml_path: impl AsRef) -> eyre::Result> { - let mut result = vec![Config::DEFAULT_PROFILE.to_string()]; - - if !toml_path.as_ref().exists() { - return Ok(result) - } - - let doc = read_toml(toml_path)?; - - if let Some(Item::Table(profiles)) = doc.as_table().get(Config::PROFILE_SECTION) { - for (profile, _) in profiles { - let p = profile.to_string(); - if !result.contains(&p) { - result.push(p); - } - } - } - - Ok(result) -} - -/// Returns a [`toml_edit::Document`] loaded from the provided `path`. -/// Can raise an error in case of I/O or parsing errors. -fn read_toml(path: impl AsRef) -> eyre::Result { - let path = path.as_ref().to_owned(); - let doc: DocumentMut = std::fs::read_to_string(path)?.parse()?; - Ok(doc) -} - /// Deserialize stringified percent. The value must be between 0 and 100 inclusive. pub(crate) fn deserialize_stringified_percent<'de, D>(deserializer: D) -> Result where @@ -319,41 +279,3 @@ pub fn evm_spec_id(evm_version: &EvmVersion, alphanet: bool) -> SpecId { EvmVersion::Prague => SpecId::OSAKA, // Osaka enables EOF } } - -#[cfg(test)] -mod tests { - use crate::get_available_profiles; - use std::path::Path; - - #[test] - fn get_profiles_from_toml() { - figment::Jail::expect_with(|jail| { - jail.create_file( - "foundry.toml", - r" - [foo.baz] - libs = ['node_modules', 'lib'] - - [profile.default] - libs = ['node_modules', 'lib'] - - [profile.ci] - libs = ['node_modules', 'lib'] - - [profile.local] - libs = ['node_modules', 'lib'] - ", - )?; - - let path = Path::new("./foundry.toml"); - let profiles = get_available_profiles(path).unwrap(); - - assert_eq!( - profiles, - vec!["default".to_string(), "ci".to_string(), "local".to_string()] - ); - - Ok(()) - }); - } -} diff --git a/crates/evm/core/src/opts.rs b/crates/evm/core/src/opts.rs index 9849fd1cef9c..6f4448ae482c 100644 --- a/crates/evm/core/src/opts.rs +++ b/crates/evm/core/src/opts.rs @@ -4,9 +4,9 @@ use alloy_primitives::{Address, B256, U256}; use alloy_provider::{network::AnyRpcBlock, Provider}; use eyre::WrapErr; use foundry_common::{provider::ProviderBuilder, ALCHEMY_FREE_TIER_CUPS}; -use foundry_config::{Chain, Config}; +use foundry_config::{Chain, Config, GasLimit}; use revm::primitives::{BlockEnv, CfgEnv, TxEnv}; -use serde::{Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Serialize}; use url::Url; #[derive(Clone, Debug, Default, Serialize, Deserialize)] @@ -166,7 +166,7 @@ impl EvmOpts { /// Returns the gas limit to use pub fn gas_limit(&self) -> u64 { - self.env.block_gas_limit.unwrap_or(self.env.gas_limit) + self.env.block_gas_limit.unwrap_or(self.env.gas_limit).0 } /// Returns the configured chain id, which will be @@ -225,8 +225,7 @@ impl EvmOpts { #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct Env { /// The block gas limit. - #[serde(deserialize_with = "string_or_number")] - pub gas_limit: u64, + pub gas_limit: GasLimit, /// The `CHAINID` opcode value. pub chain_id: Option, @@ -260,47 +259,10 @@ pub struct Env { pub block_prevrandao: B256, /// the block.gaslimit value during EVM execution - #[serde( - default, - skip_serializing_if = "Option::is_none", - deserialize_with = "string_or_number_opt" - )] - pub block_gas_limit: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub block_gas_limit: Option, /// EIP-170: Contract code size limit in bytes. Useful to increase this because of tests. #[serde(default, skip_serializing_if = "Option::is_none")] pub code_size_limit: Option, } - -#[derive(Deserialize)] -#[serde(untagged)] -enum Gas { - Number(u64), - Text(String), -} - -fn string_or_number<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - use serde::de::Error; - match Gas::deserialize(deserializer)? { - Gas::Number(num) => Ok(num), - Gas::Text(s) => s.parse().map_err(D::Error::custom), - } -} - -fn string_or_number_opt<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - use serde::de::Error; - - match Option::::deserialize(deserializer)? { - Some(gas) => match gas { - Gas::Number(num) => Ok(Some(num)), - Gas::Text(s) => s.parse().map(Some).map_err(D::Error::custom), - }, - _ => Ok(None), - } -} diff --git a/crates/forge/bin/cmd/coverage.rs b/crates/forge/bin/cmd/coverage.rs index 7e60a5451efc..4ca111a56740 100644 --- a/crates/forge/bin/cmd/coverage.rs +++ b/crates/forge/bin/cmd/coverage.rs @@ -230,11 +230,7 @@ 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 { - fuzz: config.fuzz.clone(), - invariant: config.invariant.clone(), - ..Default::default() - }) + .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 ad2e52df6725..6e19c96d08d7 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, TestOptionsBuilder, + MultiContractRunner, MultiContractRunnerBuilder, TestFilter, TestOptions, }; use foundry_cli::{ opts::{CoreBuildArgs, GlobalOpts}, @@ -34,7 +34,7 @@ use foundry_config::{ Metadata, Profile, Provider, }, filter::GlobMatcher, - get_available_profiles, Config, + Config, }; use foundry_debugger::Debugger; use foundry_evm::traces::identifier::TraceIdentifiers; @@ -301,25 +301,20 @@ impl TestArgs { // Create test options from general project settings and compiler output. let project_root = &project.paths.root; - let toml = config.get_config_path(); - let profiles = get_available_profiles(toml)?; // Remove the snapshots directory if it exists. // This is to ensure that we don't have any stale snapshots. // If `FORGE_SNAPSHOT_CHECK` is set, we don't remove the snapshots directory as it is // required for comparison. - if std::env::var("FORGE_SNAPSHOT_CHECK").is_err() { + if std::env::var_os("FORGE_SNAPSHOT_CHECK").is_none() { let snapshot_dir = project_root.join(&config.snapshots); if snapshot_dir.exists() { let _ = fs::remove_dir_all(project_root.join(&config.snapshots)); } } - let test_options: TestOptions = TestOptionsBuilder::default() - .fuzz(config.fuzz.clone()) - .invariant(config.invariant.clone()) - .profiles(profiles) - .build(&output, project_root)?; + 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; @@ -347,7 +342,6 @@ 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) @@ -1067,9 +1061,9 @@ contract FooBarTest is DSTest { &prj.root().to_string_lossy(), ]); let outcome = args.run().await.unwrap(); - let gas_report = outcome.gas_report.unwrap(); + let gas_report = outcome.gas_report.as_ref().unwrap(); - assert_eq!(gas_report.contracts.len(), 3); + assert_eq!(gas_report.contracts.len(), 3, "{}", outcome.summary(Default::default())); let call_cnts = gas_report .contracts .values() diff --git a/crates/forge/src/lib.rs b/crates/forge/src/lib.rs index 0bec55153099..257760c4e94b 100644 --- a/crates/forge/src/lib.rs +++ b/crates/forge/src/lib.rs @@ -7,15 +7,16 @@ extern crate foundry_common; #[macro_use] extern crate tracing; +use alloy_primitives::U256; +use eyre::Result; use foundry_compilers::ProjectCompileOutput; use foundry_config::{ - validate_profiles, Config, FuzzConfig, InlineConfig, InlineConfigError, InlineConfigParser, - InvariantConfig, NatSpec, + figment::Figment, Config, FuzzConfig, InlineConfig, InvariantConfig, NatSpec, }; use proptest::test_runner::{ FailurePersistence, FileFailurePersistence, RngAlgorithm, TestRng, TestRunner, }; -use std::path::Path; +use std::sync::Arc; pub mod coverage; @@ -34,200 +35,108 @@ pub mod result; pub use foundry_common::traits::TestFilter; pub use foundry_evm::*; -/// Metadata on how to run fuzz/invariant tests +/// Test configuration. #[derive(Clone, Debug, Default)] pub struct TestOptions { - /// The base "fuzz" test configuration. To be used as a fallback in case - /// no more specific configs are found for a given run. - pub fuzz: FuzzConfig, - /// The base "invariant" test configuration. To be used as a fallback in case - /// no more specific configs are found for a given run. - pub invariant: InvariantConfig, - /// Contains per-test specific "fuzz" configurations. - pub inline_fuzz: InlineConfig, - /// Contains per-test specific "invariant" configurations. - pub inline_invariant: InlineConfig, + /// 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, - root: &Path, - profiles: Vec, - base_fuzz: FuzzConfig, - base_invariant: InvariantConfig, - ) -> Result { - let natspecs: Vec = NatSpec::parse(output, root); - let mut inline_invariant = InlineConfig::::default(); - let mut inline_fuzz = InlineConfig::::default(); - - // Validate all natspecs + 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 { - validate_profiles(natspec, &profiles)?; + inline.insert(natspec)?; + // Validate after parsing as TOML. + natspec.validate_profiles(profiles)?; } + Ok(Self { config: base_config, inline }) + } - // Firstly, apply contract-level configurations - for natspec in natspecs.iter().filter(|n| n.function.is_none()) { - if let Some(fuzz) = base_fuzz.merge(natspec)? { - inline_fuzz.insert_contract(&natspec.contract, fuzz); - } - - if let Some(invariant) = base_invariant.merge(natspec)? { - inline_invariant.insert_contract(&natspec.contract, invariant); - } - } - - for (natspec, f) in natspecs.iter().filter_map(|n| n.function.as_ref().map(|f| (n, f))) { - // Apply in-line configurations for the current profile - let c = &natspec.contract; - - // We might already have inserted contract-level configs above, so respect data already - // present in inline configs. - let base_fuzz = inline_fuzz.get(c, f).unwrap_or(&base_fuzz); - let base_invariant = inline_invariant.get(c, f).unwrap_or(&base_invariant); - - if let Some(fuzz) = base_fuzz.merge(natspec)? { - inline_fuzz.insert_fn(c, f, fuzz); - } - - if let Some(invariant) = base_invariant.merge(natspec)? { - inline_invariant.insert_fn(c, f, invariant); - } - } + /// Creates a new instance without parsing inline configuration. + pub fn new_unparsed(base_config: Arc) -> Self { + Self { config: base_config, inline: InlineConfig::new() } + } - Ok(Self { fuzz: base_fuzz, invariant: base_invariant, inline_fuzz, inline_invariant }) + /// 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_id` is the id of the test contract, expressed as a relative path from the - /// project root. - /// - `test_fn` is the name of the test function declared inside the test contract. - pub fn fuzz_runner(&self, contract_id: &str, test_fn: &str) -> TestRunner { - let fuzz_config = self.fuzz_config(contract_id, test_fn).clone(); - let failure_persist_path = fuzz_config + /// - `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(fuzz_config.failure_persist_file.unwrap()) + .join(config.failure_persist_file.as_ref().unwrap()) .into_os_string() .into_string() .unwrap(); - self.fuzzer_with_cases( - fuzz_config.runs, - fuzz_config.max_test_rejects, + 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_id` is the id of the test contract, expressed as a relative path from the - /// project root. - /// - `test_fn` is the name of the test function declared inside the test contract. - pub fn invariant_runner(&self, contract_id: &str, test_fn: &str) -> TestRunner { - let invariant = self.invariant_config(contract_id, test_fn); - self.fuzzer_with_cases(invariant.runs, invariant.max_assume_rejects, None) - } - - /// Returns a "fuzz" configuration setup. 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_id` is the id of the test contract, expressed as a relative path from the - /// project root. - /// - `test_fn` is the name of the test function declared inside the test contract. - pub fn fuzz_config(&self, contract_id: &str, test_fn: &str) -> &FuzzConfig { - self.inline_fuzz.get(contract_id, test_fn).unwrap_or(&self.fuzz) - } - - /// Returns an "invariant" configuration setup. Parameters are used to select tight scoped - /// invariant configs that apply for a contract-function pair. A fallback configuration is - /// applied if no specific setup is found for a given input. - /// - /// - `contract_id` is the id of the test contract, expressed as a relative path from the - /// project root. - /// - `test_fn` is the name of the test function declared inside the test contract. - pub fn invariant_config(&self, contract_id: &str, test_fn: &str) -> &InvariantConfig { - self.inline_invariant.get(contract_id, test_fn).unwrap_or(&self.invariant) - } - - pub fn fuzzer_with_cases( + /// - `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, - 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) = &self.fuzz.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) - } + 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)) } } -/// Builder utility to create a [`TestOptions`] instance. -#[derive(Default)] -#[must_use = "builders do nothing unless you call `build` on them"] -pub struct TestOptionsBuilder { - fuzz: Option, - invariant: Option, - profiles: Option>, -} - -impl TestOptionsBuilder { - /// Sets a [`FuzzConfig`] to be used as base "fuzz" configuration. - pub fn fuzz(mut self, conf: FuzzConfig) -> Self { - self.fuzz = Some(conf); - self - } - - /// Sets a [`InvariantConfig`] to be used as base "invariant" configuration. - pub fn invariant(mut self, conf: InvariantConfig) -> Self { - self.invariant = Some(conf); - self - } - - /// Sets available configuration profiles. Profiles are useful to validate existing in-line - /// configurations. This argument is necessary in case a `compile_output`is provided. - pub fn profiles(mut self, p: Vec) -> Self { - self.profiles = Some(p); - self - } - - /// Creates an instance of [`TestOptions`]. This takes care of creating "fuzz" and - /// "invariant" fallbacks, and extracting all inline test configs, if available. - /// - /// `root` is a reference to the user's project root dir. This is essential - /// to determine the base path of generated contract identifiers. This is to provide correct - /// matchers for inline test configs. - pub fn build( - self, - output: &ProjectCompileOutput, - root: &Path, - ) -> Result { - let profiles: Vec = - self.profiles.unwrap_or_else(|| vec![Config::selected_profile().into()]); - let base_fuzz = self.fuzz.unwrap_or_default(); - let base_invariant = self.invariant.unwrap_or_default(); - TestOptions::new(output, root, profiles, base_fuzz, base_invariant) +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/runner.rs b/crates/forge/src/runner.rs index 4b66a482c27c..fc2b89cb0815 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -341,24 +341,26 @@ impl ContractRunner<'_> { self.run_unit_test(func, should_fail, setup) } TestFunctionKind::FuzzTest { should_fail } => { - let runner = test_options.fuzz_runner(self.name, &func.name); - let fuzz_config = test_options.fuzz_config(self.name, &func.name); - - self.run_fuzz_test(func, should_fail, runner, setup, fuzz_config.clone()) + 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 => { - let runner = test_options.invariant_runner(self.name, &func.name); - let invariant_config = test_options.invariant_config(self.name, &func.name); - - self.run_invariant_test( - runner, - setup, - invariant_config.clone(), - func, - call_after_invariant, - &known_contracts, - identified_contracts.as_ref().unwrap(), - ) + 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!(), }; diff --git a/crates/forge/tests/cli/alphanet.rs b/crates/forge/tests/cli/alphanet.rs index 6e41551ac890..49b8c01fc7d2 100644 --- a/crates/forge/tests/cli/alphanet.rs +++ b/crates/forge/tests/cli/alphanet.rs @@ -1,6 +1,10 @@ // Ensure we can run basic counter tests with EOF support. -#[cfg(target_os = "linux")] forgetest_init!(test_eof_flag, |prj, cmd| { + if !has_docker() { + println!("skipping because no docker is available"); + return; + } + cmd.forge_fuse().args(["test", "--eof"]).assert_success().stdout_eq(str![[r#" [COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] @@ -17,3 +21,12 @@ Ran 1 test suite [ELAPSED]: 2 tests passed, 0 failed, 0 skipped (2 total tests) "#]]); }); + +fn has_docker() -> bool { + if !cfg!(target_os = "linux") { + return false; + } + + // `images` will also check for the daemon. + std::process::Command::new("docker").arg("images").output().is_ok_and(|o| o.status.success()) +} diff --git a/crates/forge/tests/cli/build.rs b/crates/forge/tests/cli/build.rs index 9585b216b159..ca54ae0d0352 100644 --- a/crates/forge/tests/cli/build.rs +++ b/crates/forge/tests/cli/build.rs @@ -140,6 +140,11 @@ Compiler run successful! // tests build output is as expected forgetest_init!(build_sizes_no_forge_std, |prj, cmd| { + prj.write_config(Config { + solc: Some(foundry_config::SolcReq::Version(semver::Version::new(0, 8, 27))), + ..Default::default() + }); + cmd.args(["build", "--sizes"]).assert_success().stdout_eq(str![ r#" ... @@ -154,12 +159,12 @@ forgetest_init!(build_sizes_no_forge_std, |prj, cmd| { str![[r#" { "Counter": { - "runtime_size": 247, - "init_size": 277, - "runtime_margin": 24329, - "init_margin": 48875 + "runtime_size": 236, + "init_size": 263, + "runtime_margin": 24340, + "init_margin": 48889 } -} +} "#]] .is_json(), ); diff --git a/crates/forge/tests/cli/config.rs b/crates/forge/tests/cli/config.rs index db87a85ba0d1..72aacff49eb6 100644 --- a/crates/forge/tests/cli/config.rs +++ b/crates/forge/tests/cli/config.rs @@ -29,6 +29,8 @@ forgetest!(can_extract_config_values, |prj, cmd| { // explicitly set all values let input = Config { profile: Config::DEFAULT_PROFILE, + // `profiles` is not serialized. + profiles: vec![], root: Default::default(), src: "test-src".into(), test: "test-test".into(), diff --git a/crates/forge/tests/cli/inline_config.rs b/crates/forge/tests/cli/inline_config.rs new file mode 100644 index 000000000000..de585a48ce17 --- /dev/null +++ b/crates/forge/tests/cli/inline_config.rs @@ -0,0 +1,194 @@ +forgetest!(runs, |prj, cmd| { + prj.add_test( + "inline.sol", + " + contract Inline { + /** forge-config: default.fuzz.runs = 2 */ + function test1(bool) public {} + + \t///\t forge-config:\tdefault.fuzz.runs=\t3 \t + + function test2(bool) public {} + } + ", + ) + .unwrap(); + + cmd.arg("test").assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +Ran 2 tests for test/inline.sol:Inline +[PASS] test1(bool) (runs: 2, [AVG_GAS]) +[PASS] test2(bool) (runs: 3, [AVG_GAS]) +Suite result: ok. 2 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 1 test suite [ELAPSED]: 2 tests passed, 0 failed, 0 skipped (2 total tests) + +"#]]); + + // Make sure inline config is parsed in coverage too. + cmd.forge_fuse().arg("coverage").assert_success().stdout_eq(str![[r#" +... +Ran 2 tests for test/inline.sol:Inline +[PASS] test1(bool) (runs: 2, [AVG_GAS]) +[PASS] test2(bool) (runs: 3, [AVG_GAS]) +Suite result: ok. 2 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 1 test suite [ELAPSED]: 2 tests passed, 0 failed, 0 skipped (2 total tests) +| File | % Lines | % Statements | % Branches | % Funcs | +|-------|---------------|---------------|---------------|---------------| +| Total | 100.00% (0/0) | 100.00% (0/0) | 100.00% (0/0) | 100.00% (0/0) | + +"#]]); +}); + +forgetest!(invalid_profile, |prj, cmd| { + prj.add_test( + "inline.sol", + " + /** forge-config: unknown.fuzz.runs = 2 */ + contract Inline { + function test(bool) public {} + } + ", + ) + .unwrap(); + + cmd.arg("test").assert_failure().stderr_eq(str![[r#" +Error: Inline config error at test/inline.sol:0:0:0: invalid profile `unknown.fuzz.runs = 2`; valid profiles: default + +"#]]); +}); + +// TODO: Uncomment once this done for normal config too. +/* +forgetest!(invalid_key, |prj, cmd| { + prj.add_test( + "inline.sol", + " + /** forge-config: default.fuzzz.runs = 2 */ + contract Inline { + function test(bool) public {} + } + ", + ) + .unwrap(); + + cmd.arg("test").assert_failure().stderr_eq(str![[]]).stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +Ran 1 test for test/inline.sol:Inline +[FAIL: failed to get inline configuration: unknown config section `default`] test(bool) ([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: failed to get inline configuration: unknown config section `default`] test(bool) ([GAS]) + +Encountered a total of 1 failing tests, 0 tests succeeded + +"#]]); +}); + +forgetest!(invalid_key_2, |prj, cmd| { + prj.add_test( + "inline.sol", + " +/** forge-config: default.fuzz.runss = 2 */ + contract Inline { + function test(bool) public {} + } + ", + ) + .unwrap(); + + cmd.arg("test").assert_failure().stderr_eq(str![[]]).stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +Ran 1 test for test/inline.sol:Inline +[FAIL: failed to get inline configuration: unknown config section `default`] test(bool) ([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: failed to get inline configuration: unknown config section `default`] test(bool) ([GAS]) + +Encountered a total of 1 failing tests, 0 tests succeeded + +"#]]); +}); +*/ + +forgetest!(invalid_value, |prj, cmd| { + prj.add_test( + "inline.sol", + " + /** forge-config: default.fuzz.runs = [2] */ + contract Inline { + function test(bool) public {} + } + ", + ) + .unwrap(); + + cmd.arg("test").assert_failure().stderr_eq(str![[]]).stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +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]) +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]) + +Encountered a total of 1 failing tests, 0 tests succeeded + +"#]]); +}); + +forgetest!(invalid_value_2, |prj, cmd| { + prj.add_test( + "inline.sol", + " + /** forge-config: default.fuzz.runs = '2' */ + contract Inline { + function test(bool) public {} + } + ", + ) + .unwrap(); + + cmd.arg("test").assert_failure().stderr_eq(str![[]]).stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +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]) +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]) + +Encountered a total of 1 failing tests, 0 tests succeeded + +"#]]); +}); diff --git a/crates/forge/tests/cli/main.rs b/crates/forge/tests/cli/main.rs index 5838fa853777..d59dbc6bedd6 100644 --- a/crates/forge/tests/cli/main.rs +++ b/crates/forge/tests/cli/main.rs @@ -18,6 +18,7 @@ mod debug; mod doc; mod eip712; mod geiger; +mod inline_config; mod multi_script; mod script; mod soldeer; diff --git a/crates/forge/tests/it/cheats.rs b/crates/forge/tests/it/cheats.rs index 871cda045fa7..11fcdbcfd5c1 100644 --- a/crates/forge/tests/it/cheats.rs +++ b/crates/forge/tests/it/cheats.rs @@ -27,9 +27,9 @@ async fn test_cheats_local(test_data: &ForgeTestData) { filter = filter.exclude_contracts("(LastCallGasDefaultTest|MockFunctionTest|WithSeed)"); } - let mut config = test_data.config.clone(); - config.fs_permissions = FsPermissions::new(vec![PathPermission::read_write("./")]); - let runner = test_data.runner_with_config(config); + let runner = test_data.runner_with(|config| { + config.fs_permissions = FsPermissions::new(vec![PathPermission::read_write("./")]); + }); TestConfig::with_filter(runner, filter).run().await; } @@ -38,9 +38,9 @@ async fn test_cheats_local(test_data: &ForgeTestData) { async fn test_cheats_local_isolated(test_data: &ForgeTestData) { let filter = Filter::new(".*", ".*(Isolated)", &format!(".*cheats{RE_PATH_SEPARATOR}*")); - let mut config = test_data.config.clone(); - config.isolate = true; - let runner = test_data.runner_with_config(config); + let runner = test_data.runner_with(|config| { + config.isolate = true; + }); TestConfig::with_filter(runner, filter).run().await; } @@ -49,9 +49,9 @@ async fn test_cheats_local_isolated(test_data: &ForgeTestData) { async fn test_cheats_local_with_seed(test_data: &ForgeTestData) { let filter = Filter::new(".*", ".*(WithSeed)", &format!(".*cheats{RE_PATH_SEPARATOR}*")); - let mut config = test_data.config.clone(); - config.fuzz.seed = Some(U256::from(100)); - let runner = test_data.runner_with_config(config); + let runner = test_data.runner_with(|config| { + config.fuzz.seed = Some(U256::from(100)); + }); TestConfig::with_filter(runner, filter).run().await; } diff --git a/crates/forge/tests/it/core.rs b/crates/forge/tests/it/core.rs index c8a599195441..a2b4916d3e7c 100644 --- a/crates/forge/tests/it/core.rs +++ b/crates/forge/tests/it/core.rs @@ -758,9 +758,9 @@ async fn test_trace() { #[tokio::test(flavor = "multi_thread")] async fn test_assertions_revert_false() { let filter = Filter::new(".*", ".*NoAssertionsRevertTest", ".*"); - let mut config = TEST_DATA_DEFAULT.config.clone(); - config.assertions_revert = false; - let mut runner = TEST_DATA_DEFAULT.runner_with_config(config); + let mut runner = TEST_DATA_DEFAULT.runner_with(|config| { + config.assertions_revert = false; + }); let results = runner.test_collect(&filter); assert_multiple( @@ -784,9 +784,9 @@ async fn test_assertions_revert_false() { #[tokio::test(flavor = "multi_thread")] async fn test_legacy_assertions() { let filter = Filter::new(".*", ".*LegacyAssertions", ".*"); - let mut config = TEST_DATA_DEFAULT.config.clone(); - config.legacy_assertions = true; - let mut runner = TEST_DATA_DEFAULT.runner_with_config(config); + let mut runner = TEST_DATA_DEFAULT.runner_with(|config| { + config.legacy_assertions = true; + }); let results = runner.test_collect(&filter); assert_multiple( diff --git a/crates/forge/tests/it/fork.rs b/crates/forge/tests/it/fork.rs index 8dc637528ddd..5974a12ed644 100644 --- a/crates/forge/tests/it/fork.rs +++ b/crates/forge/tests/it/fork.rs @@ -35,9 +35,9 @@ async fn test_cheats_fork_revert() { /// Executes all non-reverting fork cheatcodes #[tokio::test(flavor = "multi_thread")] async fn test_cheats_fork() { - let mut config = TEST_DATA_PARIS.config.clone(); - config.fs_permissions = FsPermissions::new(vec![PathPermission::read("./fixtures")]); - let runner = TEST_DATA_PARIS.runner_with_config(config); + let runner = TEST_DATA_PARIS.runner_with(|config| { + config.fs_permissions = FsPermissions::new(vec![PathPermission::read("./fixtures")]); + }); let filter = Filter::new(".*", ".*", &format!(".*cheats{RE_PATH_SEPARATOR}Fork")) .exclude_tests(".*Revert"); TestConfig::with_filter(runner, filter).run().await; @@ -46,9 +46,9 @@ async fn test_cheats_fork() { /// Executes eth_getLogs cheatcode #[tokio::test(flavor = "multi_thread")] async fn test_get_logs_fork() { - let mut config = TEST_DATA_DEFAULT.config.clone(); - config.fs_permissions = FsPermissions::new(vec![PathPermission::read("./fixtures")]); - let runner = TEST_DATA_DEFAULT.runner_with_config(config); + let runner = TEST_DATA_DEFAULT.runner_with(|config| { + config.fs_permissions = FsPermissions::new(vec![PathPermission::read("./fixtures")]); + }); let filter = Filter::new("testEthGetLogs", ".*", &format!(".*cheats{RE_PATH_SEPARATOR}Fork")) .exclude_tests(".*Revert"); TestConfig::with_filter(runner, filter).run().await; @@ -57,9 +57,9 @@ async fn test_get_logs_fork() { /// Executes rpc cheatcode #[tokio::test(flavor = "multi_thread")] async fn test_rpc_fork() { - let mut config = TEST_DATA_DEFAULT.config.clone(); - config.fs_permissions = FsPermissions::new(vec![PathPermission::read("./fixtures")]); - let runner = TEST_DATA_DEFAULT.runner_with_config(config); + let runner = TEST_DATA_DEFAULT.runner_with(|config| { + config.fs_permissions = FsPermissions::new(vec![PathPermission::read("./fixtures")]); + }); let filter = Filter::new("testRpc", ".*", &format!(".*cheats{RE_PATH_SEPARATOR}Fork")) .exclude_tests(".*Revert"); TestConfig::with_filter(runner, filter).run().await; @@ -102,25 +102,25 @@ async fn test_create_same_fork() { /// Test that `no_storage_caching` config is properly applied #[tokio::test(flavor = "multi_thread")] async fn test_storage_caching_config() { - // no_storage_caching set to true: storage should not be cached - let mut config = TEST_DATA_DEFAULT.config.clone(); - config.no_storage_caching = true; - let runner = TEST_DATA_DEFAULT.runner_with_config(config); let filter = Filter::new("testStorageCaching", ".*", &format!(".*cheats{RE_PATH_SEPARATOR}Fork")) .exclude_tests(".*Revert"); - TestConfig::with_filter(runner, filter).run().await; + + let runner = TEST_DATA_DEFAULT.runner_with(|config| { + config.no_storage_caching = true; + }); + + // no_storage_caching set to true: storage should not be cached + TestConfig::with_filter(runner, filter.clone()).run().await; let cache_dir = Config::foundry_block_cache_dir(Chain::mainnet(), 19800000).unwrap(); let _ = fs::remove_file(cache_dir); - // no_storage_caching set to false: storage should be cached - let mut config = TEST_DATA_DEFAULT.config.clone(); - config.no_storage_caching = false; - let runner = TEST_DATA_DEFAULT.runner_with_config(config); - let filter = - Filter::new("testStorageCaching", ".*", &format!(".*cheats{RE_PATH_SEPARATOR}Fork")) - .exclude_tests(".*Revert"); + let runner = TEST_DATA_DEFAULT.runner_with(|config| { + config.no_storage_caching = false; + }); TestConfig::with_filter(runner, filter).run().await; + + // no_storage_caching set to false: storage should be cached let cache_dir = Config::foundry_block_cache_dir(Chain::mainnet(), 19800000).unwrap(); assert!(cache_dir.exists()); diff --git a/crates/forge/tests/it/fs.rs b/crates/forge/tests/it/fs.rs index 5bb0b59fb24b..5733ec5849b9 100644 --- a/crates/forge/tests/it/fs.rs +++ b/crates/forge/tests/it/fs.rs @@ -6,18 +6,18 @@ use foundry_test_utils::Filter; #[tokio::test(flavor = "multi_thread")] async fn test_fs_disabled() { - let mut config = TEST_DATA_DEFAULT.config.clone(); - config.fs_permissions = FsPermissions::new(vec![PathPermission::none("./")]); - let runner = TEST_DATA_DEFAULT.runner_with_config(config); + let runner = TEST_DATA_DEFAULT.runner_with(|config| { + config.fs_permissions = FsPermissions::new(vec![PathPermission::none("./")]); + }); let filter = Filter::new(".*", ".*", ".*fs/Disabled"); TestConfig::with_filter(runner, filter).run().await; } #[tokio::test(flavor = "multi_thread")] async fn test_fs_default() { - let mut config = TEST_DATA_DEFAULT.config.clone(); - config.fs_permissions = FsPermissions::new(vec![PathPermission::read("./fixtures")]); - let runner = TEST_DATA_DEFAULT.runner_with_config(config); + let runner = TEST_DATA_DEFAULT.runner_with(|config| { + config.fs_permissions = FsPermissions::new(vec![PathPermission::read("./fixtures")]); + }); let filter = Filter::new(".*", ".*", ".*fs/Default"); TestConfig::with_filter(runner, filter).run().await; } diff --git a/crates/forge/tests/it/fuzz.rs b/crates/forge/tests/it/fuzz.rs index 8972c9bd98f1..eaa627b9652f 100644 --- a/crates/forge/tests/it/fuzz.rs +++ b/crates/forge/tests/it/fuzz.rs @@ -82,11 +82,12 @@ async fn test_successful_fuzz_cases() { #[ignore] async fn test_fuzz_collection() { let filter = Filter::new(".*", ".*", ".*fuzz/FuzzCollection.t.sol"); - let mut runner = TEST_DATA_DEFAULT.runner(); - runner.test_options.invariant.depth = 100; - runner.test_options.invariant.runs = 1000; - runner.test_options.fuzz.runs = 1000; - runner.test_options.fuzz.seed = Some(U256::from(6u32)); + let mut runner = TEST_DATA_DEFAULT.runner_with(|config| { + config.invariant.depth = 100; + config.invariant.runs = 1000; + config.fuzz.runs = 1000; + config.fuzz.seed = Some(U256::from(6u32)); + }); let results = runner.test_collect(&filter); assert_multiple( @@ -111,11 +112,14 @@ async fn test_fuzz_collection() { #[tokio::test(flavor = "multi_thread")] async fn test_persist_fuzz_failure() { let filter = Filter::new(".*", ".*", ".*fuzz/FuzzFailurePersist.t.sol"); - let mut runner = TEST_DATA_DEFAULT.runner(); - runner.test_options.fuzz.runs = 1000; - macro_rules! get_failure_result { - () => { + macro_rules! run_fail { + () => { run_fail!(|config| {}) }; + (|$config:ident| $e:expr) => {{ + let mut runner = TEST_DATA_DEFAULT.runner_with(|$config| { + $config.fuzz.runs = 1000; + $e + }); runner .test_collect(&filter) .get("default/fuzz/FuzzFailurePersist.t.sol:FuzzFailurePersistTest") @@ -125,11 +129,11 @@ async fn test_persist_fuzz_failure() { .unwrap() .counterexample .clone() - }; + }}; } // record initial counterexample calldata - let initial_counterexample = get_failure_result!(); + let initial_counterexample = run_fail!(); let initial_calldata = match initial_counterexample { Some(CounterExample::Single(counterexample)) => counterexample.calldata, _ => Bytes::new(), @@ -137,7 +141,7 @@ async fn test_persist_fuzz_failure() { // run several times and compare counterexamples calldata for i in 0..10 { - let new_calldata = match get_failure_result!() { + let new_calldata = match run_fail!() { Some(CounterExample::Single(counterexample)) => counterexample.calldata, _ => Bytes::new(), }; @@ -146,8 +150,9 @@ async fn test_persist_fuzz_failure() { } // write new failure in different file - runner.test_options.fuzz.failure_persist_file = Some("failure1".to_string()); - let new_calldata = match get_failure_result!() { + let new_calldata = match run_fail!(|config| { + config.fuzz.failure_persist_file = Some("failure1".to_string()); + }) { Some(CounterExample::Single(counterexample)) => counterexample.calldata, _ => Bytes::new(), }; diff --git a/crates/forge/tests/it/inline.rs b/crates/forge/tests/it/inline.rs index 4448f982dcd3..eab7f9ec1bb1 100644 --- a/crates/forge/tests/it/inline.rs +++ b/crates/forge/tests/it/inline.rs @@ -1,15 +1,13 @@ //! Inline configuration tests. -use crate::test_helpers::{ForgeTestData, ForgeTestProfile, TEST_DATA_DEFAULT}; -use forge::{result::TestKind, TestOptionsBuilder}; -use foundry_config::{FuzzConfig, InvariantConfig}; +use crate::test_helpers::TEST_DATA_DEFAULT; +use forge::result::TestKind; use foundry_test_utils::Filter; #[tokio::test(flavor = "multi_thread")] async fn inline_config_run_fuzz() { let filter = Filter::new(".*", ".*", ".*inline/FuzzInlineConf.t.sol"); - // Fresh runner to make sure there's no persisted failure from previous tests. - let mut runner = ForgeTestData::new(ForgeTestProfile::Default).runner(); + let mut runner = TEST_DATA_DEFAULT.runner(); let result = runner.test_collect(&filter); let results = result .into_iter() @@ -70,31 +68,3 @@ async fn inline_config_run_invariant() { _ => unreachable!(), } } - -#[test] -fn build_test_options() { - let root = &TEST_DATA_DEFAULT.project.paths.root; - let profiles = vec!["default".to_string(), "ci".to_string()]; - let build_result = TestOptionsBuilder::default() - .fuzz(FuzzConfig::default()) - .invariant(InvariantConfig::default()) - .profiles(profiles) - .build(&TEST_DATA_DEFAULT.output, root); - - assert!(build_result.is_ok()); -} - -#[test] -fn build_test_options_just_one_valid_profile() { - let root = &TEST_DATA_DEFAULT.project.root(); - let valid_profiles = vec!["profile-sheldon-cooper".to_string()]; - let build_result = TestOptionsBuilder::default() - .fuzz(FuzzConfig::default()) - .invariant(InvariantConfig::default()) - .profiles(valid_profiles) - .build(&TEST_DATA_DEFAULT.output, root); - - // We expect an error, since COMPILED contains in-line - // per-test configs for "default" and "ci" profiles - assert!(build_result.is_err()); -} diff --git a/crates/forge/tests/it/invariant.rs b/crates/forge/tests/it/invariant.rs index 3e09cd465a33..2f4da4054245 100644 --- a/crates/forge/tests/it/invariant.rs +++ b/crates/forge/tests/it/invariant.rs @@ -48,8 +48,9 @@ async fn test_invariant_with_alias() { #[tokio::test(flavor = "multi_thread")] async fn test_invariant_filters() { - let mut runner = TEST_DATA_DEFAULT.runner(); - runner.test_options.invariant.runs = 10; + let mut runner = TEST_DATA_DEFAULT.runner_with(|config| { + config.invariant.runs = 10; + }); // Contracts filter tests. assert_multiple( @@ -173,9 +174,10 @@ async fn test_invariant_filters() { #[tokio::test(flavor = "multi_thread")] async fn test_invariant_override() { let filter = Filter::new(".*", ".*", ".*fuzz/invariant/common/InvariantReentrancy.t.sol"); - let mut runner = TEST_DATA_DEFAULT.runner(); - runner.test_options.invariant.fail_on_revert = false; - runner.test_options.invariant.call_override = true; + let mut runner = TEST_DATA_DEFAULT.runner_with(|config| { + config.invariant.fail_on_revert = false; + config.invariant.call_override = true; + }); let results = runner.test_collect(&filter); assert_multiple( &results, @@ -189,10 +191,11 @@ async fn test_invariant_override() { #[tokio::test(flavor = "multi_thread")] async fn test_invariant_fail_on_revert() { let filter = Filter::new(".*", ".*", ".*fuzz/invariant/common/InvariantHandlerFailure.t.sol"); - let mut runner = TEST_DATA_DEFAULT.runner(); - runner.test_options.invariant.fail_on_revert = true; - runner.test_options.invariant.runs = 1; - runner.test_options.invariant.depth = 10; + let mut runner = TEST_DATA_DEFAULT.runner_with(|config| { + config.invariant.fail_on_revert = true; + config.invariant.runs = 1; + config.invariant.depth = 10; + }); let results = runner.test_collect(&filter); assert_multiple( &results, @@ -213,9 +216,13 @@ async fn test_invariant_fail_on_revert() { #[ignore] async fn test_invariant_storage() { let filter = Filter::new(".*", ".*", ".*fuzz/invariant/storage/InvariantStorageTest.t.sol"); - let mut runner = TEST_DATA_DEFAULT.runner(); - runner.test_options.invariant.depth = 100 + (50 * cfg!(windows) as u32); - runner.test_options.fuzz.seed = Some(U256::from(6u32)); + let mut runner = TEST_DATA_DEFAULT.runner_with(|config| { + config.invariant.depth = 100; + if cfg!(windows) { + config.invariant.depth += 50; + } + config.fuzz.seed = Some(U256::from(6u32)); + }); let results = runner.test_collect(&filter); assert_multiple( &results, @@ -254,8 +261,9 @@ async fn test_invariant_inner_contract() { #[cfg_attr(windows, ignore = "for some reason there's different rng")] async fn test_invariant_shrink() { let filter = Filter::new(".*", ".*", ".*fuzz/invariant/common/InvariantInnerContract.t.sol"); - let mut runner = TEST_DATA_DEFAULT.runner(); - runner.test_options.fuzz.seed = Some(U256::from(119u32)); + let mut runner = TEST_DATA_DEFAULT.runner_with(|config| { + config.fuzz.seed = Some(U256::from(119u32)); + }); match get_counterexample!(runner, &filter) { CounterExample::Single(_) => panic!("CounterExample should be a sequence."), @@ -300,10 +308,11 @@ async fn test_invariant_require_shrink() { async fn check_shrink_sequence(test_pattern: &str, expected_len: usize) { let filter = Filter::new(test_pattern, ".*", ".*fuzz/invariant/common/InvariantShrinkWithAssert.t.sol"); - let mut runner = TEST_DATA_DEFAULT.runner(); - runner.test_options.fuzz.seed = Some(U256::from(100u32)); - runner.test_options.invariant.runs = 1; - runner.test_options.invariant.depth = 15; + let mut runner = TEST_DATA_DEFAULT.runner_with(|config| { + config.fuzz.seed = Some(U256::from(100u32)); + config.invariant.runs = 1; + config.invariant.depth = 15; + }); match get_counterexample!(runner, &filter) { CounterExample::Single(_) => panic!("CounterExample should be a sequence."), @@ -318,10 +327,11 @@ async fn check_shrink_sequence(test_pattern: &str, expected_len: usize) { async fn test_shrink_big_sequence() { let filter = Filter::new(".*", ".*", ".*fuzz/invariant/common/InvariantShrinkBigSequence.t.sol"); - let mut runner = TEST_DATA_DEFAULT.runner(); - runner.test_options.fuzz.seed = Some(U256::from(119u32)); - runner.test_options.invariant.runs = 1; - runner.test_options.invariant.depth = 1000; + let mut runner = TEST_DATA_DEFAULT.runner_with(|config| { + config.fuzz.seed = Some(U256::from(119u32)); + config.invariant.runs = 1; + config.invariant.depth = 1000; + }); let initial_counterexample = runner .test_collect(&filter) @@ -390,11 +400,12 @@ async fn test_shrink_big_sequence() { async fn test_shrink_fail_on_revert() { let filter = Filter::new(".*", ".*", ".*fuzz/invariant/common/InvariantShrinkFailOnRevert.t.sol"); - let mut runner = TEST_DATA_DEFAULT.runner(); - runner.test_options.fuzz.seed = Some(U256::from(119u32)); - runner.test_options.invariant.fail_on_revert = true; - runner.test_options.invariant.runs = 1; - runner.test_options.invariant.depth = 200; + let mut runner = TEST_DATA_DEFAULT.runner_with(|config| { + config.fuzz.seed = Some(U256::from(119u32)); + config.invariant.fail_on_revert = true; + config.invariant.runs = 1; + config.invariant.depth = 200; + }); match get_counterexample!(runner, &filter) { CounterExample::Single(_) => panic!("CounterExample should be a sequence."), @@ -408,8 +419,9 @@ async fn test_shrink_fail_on_revert() { #[tokio::test(flavor = "multi_thread")] async fn test_invariant_preserve_state() { let filter = Filter::new(".*", ".*", ".*fuzz/invariant/common/InvariantPreserveState.t.sol"); - let mut runner = TEST_DATA_DEFAULT.runner(); - runner.test_options.invariant.fail_on_revert = true; + let mut runner = TEST_DATA_DEFAULT.runner_with(|config| { + config.invariant.fail_on_revert = true; + }); let results = runner.test_collect(&filter); assert_multiple( &results, @@ -452,9 +464,10 @@ async fn test_invariant_with_address_fixture() { #[tokio::test(flavor = "multi_thread")] async fn test_invariant_assume_does_not_revert() { let filter = Filter::new(".*", ".*", ".*fuzz/invariant/common/InvariantAssume.t.sol"); - let mut runner = TEST_DATA_DEFAULT.runner(); - // Should not treat vm.assume as revert. - runner.test_options.invariant.fail_on_revert = true; + let mut runner = TEST_DATA_DEFAULT.runner_with(|config| { + // Should not treat vm.assume as revert. + config.invariant.fail_on_revert = true; + }); let results = runner.test_collect(&filter); assert_multiple( &results, @@ -468,10 +481,11 @@ async fn test_invariant_assume_does_not_revert() { #[tokio::test(flavor = "multi_thread")] async fn test_invariant_assume_respects_restrictions() { let filter = Filter::new(".*", ".*", ".*fuzz/invariant/common/InvariantAssume.t.sol"); - let mut runner = TEST_DATA_DEFAULT.runner(); - runner.test_options.invariant.runs = 1; - runner.test_options.invariant.depth = 10; - runner.test_options.invariant.max_assume_rejects = 1; + let mut runner = TEST_DATA_DEFAULT.runner_with(|config| { + config.invariant.runs = 1; + config.invariant.depth = 10; + config.invariant.max_assume_rejects = 1; + }); let results = runner.test_collect(&filter); assert_multiple( &results, @@ -491,8 +505,9 @@ async fn test_invariant_assume_respects_restrictions() { #[tokio::test(flavor = "multi_thread")] async fn test_invariant_decode_custom_error() { let filter = Filter::new(".*", ".*", ".*fuzz/invariant/common/InvariantCustomError.t.sol"); - let mut runner = TEST_DATA_DEFAULT.runner(); - runner.test_options.invariant.fail_on_revert = true; + let mut runner = TEST_DATA_DEFAULT.runner_with(|config| { + config.invariant.fail_on_revert = true; + }); let results = runner.test_collect(&filter); assert_multiple( &results, @@ -512,8 +527,9 @@ async fn test_invariant_decode_custom_error() { #[tokio::test(flavor = "multi_thread")] async fn test_invariant_fuzzed_selected_targets() { let filter = Filter::new(".*", ".*", ".*fuzz/invariant/target/FuzzedTargetContracts.t.sol"); - let mut runner = TEST_DATA_DEFAULT.runner(); - runner.test_options.invariant.fail_on_revert = true; + let mut runner = TEST_DATA_DEFAULT.runner_with(|config| { + config.invariant.fail_on_revert = true; + }); let results = runner.test_collect(&filter); assert_multiple( &results, @@ -539,9 +555,10 @@ async fn test_invariant_fuzzed_selected_targets() { #[tokio::test(flavor = "multi_thread")] async fn test_invariant_fixtures() { let filter = Filter::new(".*", ".*", ".*fuzz/invariant/common/InvariantFixtures.t.sol"); - let mut runner = TEST_DATA_DEFAULT.runner(); - runner.test_options.invariant.runs = 1; - runner.test_options.invariant.depth = 100; + let mut runner = TEST_DATA_DEFAULT.runner_with(|config| { + config.invariant.runs = 1; + config.invariant.depth = 100; + }); let results = runner.test_collect(&filter); assert_multiple( &results, @@ -592,8 +609,9 @@ async fn test_invariant_scrape_values() { #[tokio::test(flavor = "multi_thread")] async fn test_invariant_roll_fork_handler() { let filter = Filter::new(".*", ".*", ".*fuzz/invariant/common/InvariantRollFork.t.sol"); - let mut runner = TEST_DATA_DEFAULT.runner(); - runner.test_options.fuzz.seed = Some(U256::from(119u32)); + let mut runner = TEST_DATA_DEFAULT.runner_with(|config| { + config.fuzz.seed = Some(U256::from(119u32)); + }); let results = runner.test_collect(&filter); assert_multiple( &results, @@ -625,8 +643,9 @@ async fn test_invariant_roll_fork_handler() { #[tokio::test(flavor = "multi_thread")] async fn test_invariant_excluded_senders() { let filter = Filter::new(".*", ".*", ".*fuzz/invariant/common/InvariantExcludedSenders.t.sol"); - let mut runner = TEST_DATA_DEFAULT.runner(); - runner.test_options.invariant.fail_on_revert = true; + let mut runner = TEST_DATA_DEFAULT.runner_with(|config| { + config.invariant.fail_on_revert = true; + }); let results = runner.test_collect(&filter); assert_multiple( &results, @@ -670,10 +689,11 @@ async fn test_invariant_after_invariant() { #[tokio::test(flavor = "multi_thread")] async fn test_invariant_selectors_weight() { let filter = Filter::new(".*", ".*", ".*fuzz/invariant/common/InvariantSelectorsWeight.t.sol"); - let mut runner = TEST_DATA_DEFAULT.runner(); - runner.test_options.fuzz.seed = Some(U256::from(119u32)); - runner.test_options.invariant.runs = 1; - runner.test_options.invariant.depth = 10; + let mut runner = TEST_DATA_DEFAULT.runner_with(|config| { + config.fuzz.seed = Some(U256::from(119u32)); + config.invariant.runs = 1; + config.invariant.depth = 10; + }); let results = runner.test_collect(&filter); assert_multiple( &results, @@ -688,10 +708,11 @@ async fn test_invariant_selectors_weight() { async fn test_no_reverts_in_counterexample() { let filter = Filter::new(".*", ".*", ".*fuzz/invariant/common/InvariantSequenceNoReverts.t.sol"); - let mut runner = TEST_DATA_DEFAULT.runner(); - runner.test_options.invariant.fail_on_revert = false; - // Use original counterexample to test sequence len. - runner.test_options.invariant.shrink_run_limit = 0; + let mut runner = TEST_DATA_DEFAULT.runner_with(|config| { + config.invariant.fail_on_revert = false; + // Use original counterexample to test sequence len. + config.invariant.shrink_run_limit = 0; + }); match get_counterexample!(runner, &filter) { CounterExample::Single(_) => panic!("CounterExample should be a sequence."), diff --git a/crates/forge/tests/it/repros.rs b/crates/forge/tests/it/repros.rs index 53185cf97856..69c3a0fb35f7 100644 --- a/crates/forge/tests/it/repros.rs +++ b/crates/forge/tests/it/repros.rs @@ -1,11 +1,6 @@ //! Regression tests for previous issues. -use std::sync::Arc; - -use crate::{ - config::*, - test_helpers::{ForgeTestData, TEST_DATA_DEFAULT}, -}; +use crate::{config::*, test_helpers::TEST_DATA_DEFAULT}; use alloy_dyn_abi::{DecodedEvent, DynSolValue, EventExt}; use alloy_json_abi::Event; use alloy_primitives::{address, b256, Address, U256}; @@ -19,6 +14,7 @@ use foundry_evm::{ traces::{CallKind, CallTraceDecoder, DecodedCallData, TraceKind}, }; use foundry_test_utils::Filter; +use std::sync::Arc; /// Creates a test that runs `testdata/repros/Issue{issue}.t.sol`. macro_rules! test_repro { @@ -33,7 +29,7 @@ macro_rules! test_repro { #[tokio::test(flavor = "multi_thread")] $(#[$attr])* async fn [< issue_ $issue_number >]() { - repro_config($issue_number, $should_fail, $sender.into(), &*TEST_DATA_DEFAULT).await.run().await; + repro_config($issue_number, $should_fail, $sender.into()).await.run().await; } } }; @@ -42,7 +38,7 @@ macro_rules! test_repro { #[tokio::test(flavor = "multi_thread")] $(#[$attr])* async fn [< issue_ $issue_number >]() { - let mut $res = repro_config($issue_number, $should_fail, $sender.into(), &*TEST_DATA_DEFAULT).await.test(); + let mut $res = repro_config($issue_number, $should_fail, $sender.into()).await.test(); $e } } @@ -52,7 +48,7 @@ macro_rules! test_repro { #[tokio::test(flavor = "multi_thread")] $(#[$attr])* async fn [< issue_ $issue_number >]() { - let mut $config = repro_config($issue_number, false, None, &*TEST_DATA_DEFAULT).await; + let mut $config = repro_config($issue_number, false, None).await; $e $config.run().await; } @@ -60,23 +56,19 @@ macro_rules! test_repro { }; } -async fn repro_config( - issue: usize, - should_fail: bool, - sender: Option

, - test_data: &ForgeTestData, -) -> TestConfig { +async fn repro_config(issue: usize, should_fail: bool, sender: Option
) -> TestConfig { foundry_test_utils::init_tracing(); let filter = Filter::path(&format!(".*repros/Issue{issue}.t.sol")); - let mut config = test_data.config.clone(); - config.fs_permissions = - FsPermissions::new(vec![PathPermission::read("./fixtures"), PathPermission::read("out")]); - if let Some(sender) = sender { - config.sender = sender; - } - - let runner = TEST_DATA_DEFAULT.runner_with_config(config); + let runner = TEST_DATA_DEFAULT.runner_with(|config| { + config.fs_permissions = FsPermissions::new(vec![ + PathPermission::read("./fixtures"), + PathPermission::read("out"), + ]); + if let Some(sender) = sender { + config.sender = sender; + } + }); TestConfig::with_filter(runner, filter).set_should_fail(should_fail) } diff --git a/crates/forge/tests/it/test_helpers.rs b/crates/forge/tests/it/test_helpers.rs index 5e540d8c67aa..298bbae2971d 100644 --- a/crates/forge/tests/it/test_helpers.rs +++ b/crates/forge/tests/it/test_helpers.rs @@ -4,7 +4,6 @@ use alloy_chains::NamedChain; use alloy_primitives::U256; use forge::{ revm::primitives::SpecId, MultiContractRunner, MultiContractRunnerBuilder, TestOptions, - TestOptionsBuilder, }; use foundry_compilers::{ artifacts::{EvmVersion, Libraries, Settings}, @@ -15,10 +14,7 @@ use foundry_config::{ fs_permissions::PathPermission, Config, FsPermissions, FuzzConfig, FuzzDictionaryConfig, InvariantConfig, RpcEndpoint, RpcEndpoints, }; -use foundry_evm::{ - constants::CALLER, - opts::{Env, EvmOpts}, -}; +use foundry_evm::{constants::CALLER, opts::EvmOpts}; use foundry_test_utils::{fd_lock, init_tracing, rpc::next_rpc_endpoint}; use std::{ env, fmt, @@ -74,69 +70,6 @@ impl ForgeTestProfile { SolcConfig { settings } } - pub fn project(&self) -> Project { - self.config().project().expect("Failed to build project") - } - - pub fn test_opts(&self, output: &ProjectCompileOutput) -> TestOptions { - TestOptionsBuilder::default() - .fuzz(FuzzConfig { - runs: 256, - max_test_rejects: 65536, - seed: None, - dictionary: FuzzDictionaryConfig { - include_storage: true, - include_push_bytes: true, - dictionary_weight: 40, - max_fuzz_dictionary_addresses: 10_000, - max_fuzz_dictionary_values: 10_000, - }, - gas_report_samples: 256, - failure_persist_dir: Some(tempfile::tempdir().unwrap().into_path()), - failure_persist_file: Some("testfailure".to_string()), - show_logs: false, - }) - .invariant(InvariantConfig { - runs: 256, - depth: 15, - fail_on_revert: false, - call_override: false, - dictionary: FuzzDictionaryConfig { - dictionary_weight: 80, - include_storage: true, - include_push_bytes: true, - max_fuzz_dictionary_addresses: 10_000, - max_fuzz_dictionary_values: 10_000, - }, - shrink_run_limit: 5000, - max_assume_rejects: 65536, - gas_report_samples: 256, - failure_persist_dir: Some(tempfile::tempdir().unwrap().into_path()), - show_metrics: false, - }) - .build(output, Path::new(self.project().root())) - .expect("Config loaded") - } - - pub fn evm_opts(&self) -> EvmOpts { - EvmOpts { - env: Env { - gas_limit: u64::MAX, - chain_id: None, - tx_origin: CALLER, - block_number: 1, - block_timestamp: 1, - ..Default::default() - }, - sender: CALLER, - initial_balance: U256::MAX, - ffi: true, - verbosity: 3, - memory_limit: 1 << 26, - ..Default::default() - } - } - /// Build [Config] for test profile. /// /// Project source files are read from testdata/{profile_name} @@ -155,11 +88,66 @@ impl ForgeTestProfile { "fork/Fork.t.sol:DssExecLib:0xfD88CeE74f7D78697775aBDAE53f9Da1559728E4".to_string(), ]; + config.prompt_timeout = 0; + + config.gas_limit = u64::MAX.into(); + config.chain = None; + config.tx_origin = CALLER; + config.block_number = 1; + config.block_timestamp = 1; + + config.sender = CALLER; + config.initial_balance = U256::MAX; + config.ffi = true; + config.verbosity = 3; + config.memory_limit = 1 << 26; + if self.is_paris() { config.evm_version = EvmVersion::Paris; } - config + config.fuzz = FuzzConfig { + runs: 256, + max_test_rejects: 65536, + seed: None, + dictionary: FuzzDictionaryConfig { + include_storage: true, + include_push_bytes: true, + dictionary_weight: 40, + max_fuzz_dictionary_addresses: 10_000, + max_fuzz_dictionary_values: 10_000, + }, + gas_report_samples: 256, + failure_persist_dir: Some(tempfile::tempdir().unwrap().into_path()), + failure_persist_file: Some("testfailure".to_string()), + show_logs: false, + }; + config.invariant = InvariantConfig { + runs: 256, + depth: 15, + fail_on_revert: false, + call_override: false, + dictionary: FuzzDictionaryConfig { + dictionary_weight: 80, + include_storage: true, + include_push_bytes: true, + max_fuzz_dictionary_addresses: 10_000, + max_fuzz_dictionary_values: 10_000, + }, + shrink_run_limit: 5000, + max_assume_rejects: 65536, + gas_report_samples: 256, + failure_persist_dir: Some( + tempfile::Builder::new() + .prefix(&format!("foundry-{self}")) + .tempdir() + .unwrap() + .into_path(), + ), + show_metrics: false, + }; + + config.sanitized() } } @@ -167,9 +155,7 @@ impl ForgeTestProfile { pub struct ForgeTestData { pub project: Project, pub output: ProjectCompileOutput, - pub test_opts: TestOptions, - pub evm_opts: EvmOpts, - pub config: Config, + pub config: Arc, pub profile: ForgeTestProfile, } @@ -179,67 +165,63 @@ impl ForgeTestData { /// Uses [get_compiled] to lazily compile the project. pub fn new(profile: ForgeTestProfile) -> Self { init_tracing(); - - let mut project = profile.project(); + let config = Arc::new(profile.config()); + let mut project = config.project().unwrap(); let output = get_compiled(&mut project); - let test_opts = profile.test_opts(&output); - let config = profile.config(); - let evm_opts = profile.evm_opts(); - - Self { project, output, test_opts, evm_opts, config, profile } + Self { project, output, config, profile } } /// Builds a base runner pub fn base_runner(&self) -> MultiContractRunnerBuilder { init_tracing(); - let mut runner = MultiContractRunnerBuilder::new(Arc::new(self.config.clone())) - .sender(self.evm_opts.sender) - .with_test_options(self.test_opts.clone()); + let config = self.config.clone(); + let mut runner = MultiContractRunnerBuilder::new(config.clone()) + .sender(self.config.sender) + .with_test_options(TestOptions::new_unparsed(config)); if self.profile.is_paris() { runner = runner.evm_spec(SpecId::MERGE); } - runner } /// Builds a non-tracing runner pub fn runner(&self) -> MultiContractRunner { - let mut config = self.config.clone(); - config.fs_permissions = - FsPermissions::new(vec![PathPermission::read_write(manifest_root())]); - self.runner_with_config(config) + self.runner_with(|_| {}) } /// Builds a non-tracing runner - pub fn runner_with_config(&self, mut config: Config) -> MultiContractRunner { + pub fn runner_with(&self, modify: impl FnOnce(&mut Config)) -> MultiContractRunner { + let mut config = (*self.config).clone(); + modify(&mut config); + self.runner_with_config(config) + } + + fn runner_with_config(&self, mut config: Config) -> MultiContractRunner { config.rpc_endpoints = rpc_endpoints(); config.allow_paths.push(manifest_root().to_path_buf()); - // no prompt testing - config.prompt_timeout = 0; - - let root = self.project.root(); - let mut opts = self.evm_opts.clone(); - - if config.isolate { - opts.isolate = true; + if config.fs_permissions.is_empty() { + config.fs_permissions = + FsPermissions::new(vec![PathPermission::read_write(manifest_root())]); } - let sender = config.sender; + let opts = config_evm_opts(&config); let mut builder = self.base_runner(); - builder.config = Arc::new(config); + let config = Arc::new(config); + let root = self.project.root(); + builder.config = config.clone(); builder .enable_isolation(opts.isolate) - .sender(sender) - .with_test_options(self.test_opts.clone()) + .sender(config.sender) + .with_test_options(TestOptions::new(&self.output, config.clone()).unwrap()) .build(root, &self.output, opts.local_evm_env(), opts) .unwrap() } /// Builds a tracing runner pub fn tracing_runner(&self) -> MultiContractRunner { - let mut opts = self.evm_opts.clone(); + let mut opts = config_evm_opts(&self.config); opts.verbosity = 5; self.base_runner() .build(self.project.root(), &self.output, opts.local_evm_env(), opts) @@ -248,7 +230,7 @@ impl ForgeTestData { /// Builds a runner that runs against forked state pub async fn forked_runner(&self, rpc: &str) -> MultiContractRunner { - let mut opts = self.evm_opts.clone(); + let mut opts = config_evm_opts(&self.config); opts.env.chain_id = None; // clear chain id so the correct one gets fetched from the RPC opts.fork_url = Some(rpc.to_string()); @@ -369,3 +351,7 @@ pub fn rpc_endpoints() -> RpcEndpoints { ("rpcEnvAlias", RpcEndpoint::Env("${RPC_ENV_ALIAS}".into())), ]) } + +fn config_evm_opts(config: &Config) -> EvmOpts { + config.to_figment(foundry_config::FigmentProviders::None).extract().unwrap() +} diff --git a/crates/test-utils/src/filter.rs b/crates/test-utils/src/filter.rs index 003b0170fca8..1ba905d27d8c 100644 --- a/crates/test-utils/src/filter.rs +++ b/crates/test-utils/src/filter.rs @@ -2,6 +2,7 @@ use foundry_common::TestFilter; use regex::Regex; use std::path::Path; +#[derive(Clone, Debug)] pub struct Filter { test_regex: Regex, contract_regex: Regex,