Skip to content

Commit

Permalink
Fixtures as storage arrays, remove inline config
Browse files Browse the repository at this point in the history
  • Loading branch information
grandizzy committed Apr 10, 2024
1 parent f9adb66 commit fb86084
Show file tree
Hide file tree
Showing 9 changed files with 130 additions and 170 deletions.
23 changes: 23 additions & 0 deletions crates/common/src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ pub trait TestFunctionExt {

/// Returns whether this function is a `setUp` function.
fn is_setup(&self) -> bool;

/// Returns whether this function is a invariant setup function.
fn is_invariant_target_setup(&self) -> bool;
}

impl TestFunctionExt for Function {
Expand All @@ -56,6 +59,10 @@ impl TestFunctionExt for Function {
fn is_setup(&self) -> bool {
self.name.is_setup()
}

fn is_invariant_target_setup(&self) -> bool {
self.name.is_invariant_target_setup()
}
}

impl TestFunctionExt for String {
Expand All @@ -78,6 +85,10 @@ impl TestFunctionExt for String {
fn is_setup(&self) -> bool {
self.as_str().is_setup()
}

fn is_invariant_target_setup(&self) -> bool {
self.as_str().is_invariant_target_setup()
}
}

impl TestFunctionExt for str {
Expand All @@ -100,6 +111,18 @@ impl TestFunctionExt for str {
fn is_setup(&self) -> bool {
self.eq_ignore_ascii_case("setup")
}

fn is_invariant_target_setup(&self) -> bool {
self.eq_ignore_ascii_case("excludeArtifacts") ||
self.eq_ignore_ascii_case("excludeContracts") ||
self.eq_ignore_ascii_case("excludeSenders") ||
self.eq_ignore_ascii_case("targetArtifacts") ||
self.eq_ignore_ascii_case("targetArtifactSelectors") ||
self.eq_ignore_ascii_case("targetContracts") ||
self.eq_ignore_ascii_case("targetSelectors") ||
self.eq_ignore_ascii_case("targetSenders") ||
self.eq_ignore_ascii_case("targetInterfaces")
}
}

/// An extension trait for `std::error::Error` for ABI encoding.
Expand Down
60 changes: 10 additions & 50 deletions crates/config/src/inline/conf_parser.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
use super::{remove_whitespaces, InlineConfigParserError};
use crate::{
inline::{INLINE_CONFIG_FIXTURE_KEY, INLINE_CONFIG_PREFIX},
InlineConfigError, NatSpec,
};
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 and fixtures just for the execution of a specific test.
/// providing special configs just for the execution of a specific test.
///
/// An example:
///
Expand All @@ -21,10 +18,6 @@ use regex::Regex;
/// /// forge-config: ci.fuzz.runs = 10000
/// function test_ImportantFuzzTest(uint256 x) public {...}
/// }
///
/// /// forge-config: fixture
/// function x() public returns (uint256[] memory) {...}
/// }
/// ```
pub trait InlineConfigParser
where
Expand Down Expand Up @@ -110,32 +103,16 @@ where
}
}

/// Type of inline config.
pub enum InlineConfigType {
/// Profile inline config.
Profile,
/// Fixture inline config.
Fixture,
}

/// Checks if all configuration lines specified in `natspec` use a valid profile
/// or are test fixture configurations.
/// 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.
/// A fixture can be set by using `forge-config: fixture` configuration.
pub fn validate_inline_config_type(
natspec: &NatSpec,
profiles: &[String],
) -> Result<InlineConfigType, InlineConfigError> {
pub fn validate_profiles(natspec: &NatSpec, profiles: &[String]) -> Result<(), InlineConfigError> {
for config in natspec.config_lines() {
if config.eq(&format!("{INLINE_CONFIG_PREFIX}:{INLINE_CONFIG_FIXTURE_KEY}")) {
return Ok(InlineConfigType::Fixture);
}
if !profiles.iter().any(|p| config.starts_with(&format!("{INLINE_CONFIG_PREFIX}:{p}."))) {
let err_line: String = natspec.debug_context();
let profiles = format!("{profiles:?}");
Expand All @@ -145,7 +122,7 @@ pub fn validate_inline_config_type(
})?
}
}
Ok(InlineConfigType::Profile)
Ok(())
}

/// Tries to parse a `u32` from `value`. The `key` argument is used to give details
Expand All @@ -162,7 +139,7 @@ pub fn parse_config_bool(key: String, value: String) -> Result<bool, InlineConfi

#[cfg(test)]
mod tests {
use crate::{inline::conf_parser::validate_inline_config_type, NatSpec};
use crate::{inline::conf_parser::validate_profiles, NatSpec};

#[test]
fn can_reject_invalid_profiles() {
Expand All @@ -172,13 +149,13 @@ mod tests {
function: Default::default(),
line: Default::default(),
docs: r"
forge-config: ciii.invariant.depth = 1
forge-config: ciii.invariant.depth = 1
forge-config: default.invariant.depth = 1
"
.into(),
};

let result = validate_inline_config_type(&natspec, &profiles);
let result = validate_profiles(&natspec, &profiles);
assert!(result.is_err());
}

Expand All @@ -190,30 +167,13 @@ mod tests {
function: Default::default(),
line: Default::default(),
docs: r"
forge-config: ci.invariant.depth = 1
forge-config: ci.invariant.depth = 1
forge-config: default.invariant.depth = 1
"
.into(),
};

let result = validate_inline_config_type(&natspec, &profiles);
assert!(result.is_ok());
}

#[test]
fn can_accept_fixtures() {
let profiles = ["ci".to_string(), "default".to_string()];
let natspec = NatSpec {
contract: Default::default(),
function: Default::default(),
line: Default::default(),
docs: r"
forge-config: fixture
"
.into(),
};

let result = validate_inline_config_type(&natspec, &profiles);
let result = validate_profiles(&natspec, &profiles);
assert!(result.is_ok());
}
}
32 changes: 3 additions & 29 deletions crates/config/src/inline/mod.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
use crate::Config;
pub use conf_parser::{
parse_config_bool, parse_config_u32, validate_inline_config_type, InlineConfigParser,
InlineConfigType,
};
pub use conf_parser::{parse_config_bool, parse_config_u32, validate_profiles, InlineConfigParser};
pub use error::{InlineConfigError, InlineConfigParserError};
pub use natspec::NatSpec;
use once_cell::sync::Lazy;
use std::collections::{HashMap, HashSet};
use std::collections::HashMap;

mod conf_parser;
mod error;
mod natspec;

pub const INLINE_CONFIG_FUZZ_KEY: &str = "fuzz";
pub const INLINE_CONFIG_INVARIANT_KEY: &str = "invariant";
pub const INLINE_CONFIG_FIXTURE_KEY: &str = "fixture";
const INLINE_CONFIG_PREFIX: &str = "forge-config";

static INLINE_CONFIG_PREFIX_SELECTED_PROFILE: Lazy<String> = Lazy::new(|| {
Expand All @@ -41,7 +37,7 @@ impl<T> InlineConfig<T> {
}

/// Inserts an inline configuration, for a test function.
/// Configuration is identified by the pair "contract", "function".
/// Configuration is identified by the pair "contract", "function".
pub fn insert<C, F>(&mut self, contract_id: C, fn_name: F, config: T)
where
C: Into<String>,
Expand All @@ -52,28 +48,6 @@ impl<T> InlineConfig<T> {
}
}

/// Represents per-test fixtures, declared inline
/// as structured comments in Solidity test files. This allows
/// setting data sets for specific fuzzed parameters in a solidity test.
#[derive(Clone, Debug, Default)]
pub struct InlineFixturesConfig {
/// Maps a test-contract to a set of test-fixtures.
configs: HashMap<String, HashSet<String>>,
}

impl InlineFixturesConfig {
/// Records a function to be used as fixture for given contract.
/// The name of function should be the same as the name of fuzzed parameter.
pub fn add_fixture(&mut self, contract: String, fixture: String) {
self.configs.entry(contract).or_default().insert(fixture);
}

/// Returns functions to be used as fixtures for given contract.
pub fn get_fixtures(&mut self, contract: String) -> Option<&HashSet<String>> {
self.configs.get(&contract)
}
}

pub(crate) fn remove_whitespaces(s: &str) -> String {
s.chars().filter(|c| !c.is_whitespace()).collect()
}
5 changes: 1 addition & 4 deletions crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,7 @@ use providers::remappings::RemappingsProvider;

mod inline;
use crate::etherscan::EtherscanEnvProvider;
pub use inline::{
validate_inline_config_type, InlineConfig, InlineConfigError, InlineConfigParser,
InlineConfigType, InlineFixturesConfig, NatSpec,
};
pub use inline::{validate_profiles, InlineConfig, InlineConfigError, InlineConfigParser, NatSpec};

/// Foundry configuration
///
Expand Down
62 changes: 24 additions & 38 deletions crates/forge/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ extern crate tracing;

use foundry_compilers::ProjectCompileOutput;
use foundry_config::{
validate_inline_config_type, Config, FuzzConfig, InlineConfig, InlineConfigError,
InlineConfigParser, InlineConfigType, InlineFixturesConfig, InvariantConfig, NatSpec,
validate_profiles, Config, FuzzConfig, InlineConfig, InlineConfigError, InlineConfigParser,
InvariantConfig, NatSpec,
};
use proptest::test_runner::{
FailurePersistence, FileFailurePersistence, RngAlgorithm, TestRng, TestRunner,
Expand Down Expand Up @@ -40,8 +40,6 @@ pub struct TestOptions {
pub inline_fuzz: InlineConfig<FuzzConfig>,
/// Contains per-test specific "invariant" configurations.
pub inline_invariant: InlineConfig<InvariantConfig>,
/// Contains per-test specific "fixture" configurations.
pub inline_fixtures: InlineFixturesConfig,
}

impl TestOptions {
Expand All @@ -57,45 +55,33 @@ impl TestOptions {
let natspecs: Vec<NatSpec> = NatSpec::parse(output, root);
let mut inline_invariant = InlineConfig::<InvariantConfig>::default();
let mut inline_fuzz = InlineConfig::<FuzzConfig>::default();
let mut inline_fixtures = InlineFixturesConfig::default();

for natspec in natspecs {
match validate_inline_config_type(&natspec, &profiles)? {
InlineConfigType::Fixture => {
inline_fixtures.add_fixture(natspec.contract, natspec.function);
}
InlineConfigType::Profile => {
FuzzConfig::validate_configs(&natspec)?;
InvariantConfig::validate_configs(&natspec)?;

// Apply in-line configurations for the current profile
let configs: Vec<String> = natspec.current_profile_configs().collect();
let c: &str = &natspec.contract;
let f: &str = &natspec.function;
let line: String = natspec.debug_context();

match base_fuzz.try_merge(&configs) {
Ok(Some(conf)) => inline_fuzz.insert(c, f, conf),
Ok(None) => { /* No inline config found, do nothing */ }
Err(e) => Err(InlineConfigError { line: line.clone(), source: e })?,
}

match base_invariant.try_merge(&configs) {
Ok(Some(conf)) => inline_invariant.insert(c, f, conf),
Ok(None) => { /* No inline config found, do nothing */ }
Err(e) => Err(InlineConfigError { line: line.clone(), source: e })?,
}
}
// Perform general validation
validate_profiles(&natspec, &profiles)?;
FuzzConfig::validate_configs(&natspec)?;
InvariantConfig::validate_configs(&natspec)?;

// Apply in-line configurations for the current profile
let configs: Vec<String> = natspec.current_profile_configs().collect();
let c: &str = &natspec.contract;
let f: &str = &natspec.function;
let line: String = natspec.debug_context();

match base_fuzz.try_merge(&configs) {
Ok(Some(conf)) => inline_fuzz.insert(c, f, conf),
Ok(None) => { /* No inline config found, do nothing */ }
Err(e) => Err(InlineConfigError { line: line.clone(), source: e })?,
}

match base_invariant.try_merge(&configs) {
Ok(Some(conf)) => inline_invariant.insert(c, f, conf),
Ok(None) => { /* No inline config found, do nothing */ }
Err(e) => Err(InlineConfigError { line: line.clone(), source: e })?,
}
}

Ok(Self {
fuzz: base_fuzz,
invariant: base_invariant,
inline_fuzz,
inline_invariant,
inline_fixtures,
})
Ok(Self { fuzz: base_fuzz, invariant: base_invariant, inline_fuzz, inline_invariant })
}

/// Returns a "fuzz" test runner instance. Parameters are used to select tight scoped fuzz
Expand Down
Loading

0 comments on commit fb86084

Please sign in to comment.