Skip to content

Commit

Permalink
test: integrate inline config in tests (#6473)
Browse files Browse the repository at this point in the history
  • Loading branch information
mattsse authored Nov 30, 2023
1 parent 7369a10 commit 91df94b
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 80 deletions.
22 changes: 2 additions & 20 deletions crates/config/src/inline/conf_parser.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,7 @@
use super::{remove_whitespaces, INLINE_CONFIG_PREFIX};
use crate::{InlineConfigError, NatSpec};
use super::{remove_whitespaces, InlineConfigParserError};
use crate::{inline::INLINE_CONFIG_PREFIX, InlineConfigError, NatSpec};
use regex::Regex;

/// Errors returned by the [`InlineConfigParser`] trait.
#[derive(Debug, Clone, 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),
}

/// 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.
Expand Down
45 changes: 45 additions & 0 deletions crates/config/src/inline/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/// Errors returned by the [`InlineConfigParser`] trait.
#[derive(Debug, Clone, 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 [`InlineConfigParserError`], enriching them with context information
/// reporting the misconfigured line.
#[derive(thiserror::Error, Debug)]
#[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);
}
}
80 changes: 35 additions & 45 deletions crates/config/src/inline/mod.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
use crate::Config;
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;
use std::{borrow::Cow, collections::HashMap};

mod conf_parser;
pub use conf_parser::{
parse_config_bool, parse_config_u32, validate_profiles, InlineConfigParser,
InlineConfigParserError,
};

mod error;
mod natspec;
pub use natspec::NatSpec;

pub const INLINE_CONFIG_FUZZ_KEY: &str = "fuzz";
pub const INLINE_CONFIG_INVARIANT_KEY: &str = "invariant";
Expand All @@ -20,62 +18,54 @@ static INLINE_CONFIG_PREFIX_SELECTED_PROFILE: Lazy<String> = Lazy::new(|| {
format!("{INLINE_CONFIG_PREFIX}:{selected_profile}.")
});

/// Wrapper error struct that catches config parsing
/// errors [`InlineConfigParserError`], enriching them with context information
/// reporting the misconfigured line.
#[derive(thiserror::Error, Debug)]
#[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,
}

/// Represents a (test-contract, test-function) pair
type InlineConfigKey = (String, String);

/// 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(Default, Debug, Clone)]
pub struct InlineConfig<T: 'static> {
pub struct InlineConfig<T> {
/// Maps a (test-contract, test-function) pair
/// to a specific configuration provided by the user.
configs: HashMap<InlineConfigKey, T>,
configs: HashMap<InlineConfigKey<'static>, T>,
}

impl<T> InlineConfig<T> {
/// Returns an inline configuration, if any, for a test function.
/// Configuration is identified by the pair "contract", "function".
pub fn get<S: Into<String>>(&self, contract_id: S, fn_name: S) -> Option<&T> {
self.configs.get(&(contract_id.into(), fn_name.into()))
pub fn get<C, F>(&self, contract_id: C, fn_name: F) -> Option<&T>
where
C: Into<String>,
F: Into<String>,
{
// TODO use borrow
let key = InlineConfigKey {
contract: Cow::Owned(contract_id.into()),
function: Cow::Owned(fn_name.into()),
};
self.configs.get(&key)
}

/// Inserts an inline configuration, for a test function.
/// Configuration is identified by the pair "contract", "function".
pub fn insert<S: Into<String>>(&mut self, contract_id: S, fn_name: S, config: T) {
self.configs.insert((contract_id.into(), fn_name.into()), config);
pub fn insert<C, F>(&mut self, contract_id: C, fn_name: F, config: T)
where
C: Into<String>,
F: Into<String>,
{
let key = InlineConfigKey {
contract: Cow::Owned(contract_id.into()),
function: Cow::Owned(fn_name.into()),
};
self.configs.insert(key, config);
}
}

fn remove_whitespaces(s: &str) -> String {
s.chars().filter(|c| !c.is_whitespace()).collect()
/// Represents a (test-contract, test-function) pair
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct InlineConfigKey<'a> {
contract: Cow<'a, str>,
function: Cow<'a, str>,
}

#[cfg(test)]
mod tests {
use super::InlineConfigParserError;
use crate::InlineConfigError;

#[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);
}
pub(crate) fn remove_whitespaces(s: &str) -> String {
s.chars().filter(|c| !c.is_whitespace()).collect()
}
19 changes: 10 additions & 9 deletions crates/forge/tests/it/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use crate::test_helpers::{COMPILED, EVM_OPTS, PROJECT};
use forge::{
result::{SuiteResult, TestStatus},
MultiContractRunner, MultiContractRunnerBuilder, TestOptions,
MultiContractRunner, MultiContractRunnerBuilder, TestOptions, TestOptionsBuilder,
};
use foundry_config::{
fs_permissions::PathPermission, Config, FsPermissions, FuzzConfig, FuzzDictionaryConfig,
Expand Down Expand Up @@ -95,9 +95,10 @@ impl TestConfig {
}
}

/// Returns the [`TestOptions`] used by the tests.
pub fn test_opts() -> TestOptions {
TestOptions {
fuzz: FuzzConfig {
TestOptionsBuilder::default()
.fuzz(FuzzConfig {
runs: 256,
max_test_rejects: 65536,
seed: None,
Expand All @@ -108,8 +109,8 @@ pub fn test_opts() -> TestOptions {
max_fuzz_dictionary_addresses: 10_000,
max_fuzz_dictionary_values: 10_000,
},
},
invariant: InvariantConfig {
})
.invariant(InvariantConfig {
runs: 256,
depth: 15,
fail_on_revert: false,
Expand All @@ -122,10 +123,9 @@ pub fn test_opts() -> TestOptions {
max_fuzz_dictionary_values: 10_000,
},
shrink_sequence: true,
},
inline_fuzz: Default::default(),
inline_invariant: Default::default(),
}
})
.build(&COMPILED, &PROJECT.paths.root)
.expect("Config loaded")
}

pub fn manifest_root() -> &'static Path {
Expand Down Expand Up @@ -161,6 +161,7 @@ pub async fn runner_with_config(mut config: Config) -> MultiContractRunner {
let env = opts.evm_env().await.expect("could not instantiate fork environment");
let output = COMPILED.clone();
base_runner()
.with_test_options(test_opts())
.with_cheats_config(CheatsConfig::new(&config, opts.clone()))
.sender(config.sender)
.build(root, output, env, opts.clone())
Expand Down
7 changes: 4 additions & 3 deletions crates/forge/tests/it/inline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use foundry_test_utils::Filter;

#[tokio::test(flavor = "multi_thread")]
async fn inline_config_run_fuzz() {
let opts = test_options();
let opts = default_test_options();

let filter = Filter::new(".*", ".*", ".*inline/FuzzInlineConf.t.sol");

Expand All @@ -37,7 +37,7 @@ async fn inline_config_run_fuzz() {
async fn inline_config_run_invariant() {
const ROOT: &str = "inline/InvariantInlineConf.t.sol";

let opts = test_options();
let opts = default_test_options();
let filter = Filter::new(".*", ".*", ".*inline/InvariantInlineConf.t.sol");
let mut runner = runner().await;
runner.test_options = opts.clone();
Expand Down Expand Up @@ -98,7 +98,8 @@ fn build_test_options_just_one_valid_profile() {
assert!(build_result.is_err());
}

fn test_options() -> TestOptions {
/// Returns the [TestOptions] for the testing [PROJECT].
pub fn default_test_options() -> TestOptions {
let root = &PROJECT.paths.root;
TestOptionsBuilder::default()
.fuzz(FuzzConfig::default())
Expand Down
3 changes: 3 additions & 0 deletions crates/forge/tests/it/repros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ test_repro!(5808);
// <https://github.com/foundry-rs/foundry/issues/5935>
test_repro!(5935);

// <https://github.com/foundry-rs/foundry/issues/5948>
test_repro!(5948);

// https://github.com/foundry-rs/foundry/issues/6006
test_repro!(6006);

Expand Down
4 changes: 1 addition & 3 deletions testdata/cheats/Sleep.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ contract SleepTest is DSTest {
assertGe(end - start, milliseconds / 1000 * 1000, "sleep failed");
}

/*
/// forge-config: default.fuzz.runs = 10
/// forge-config: default.fuzz.runs = 2
function testSleepFuzzed(uint256 _milliseconds) public {
// Limit sleep time to 2 seconds to decrease test time
uint256 milliseconds = _milliseconds % 2000;
Expand All @@ -49,5 +48,4 @@ contract SleepTest is DSTest {
// Limit precision to 1000 ms
assertGe(end - start, milliseconds / 1000 * 1000, "sleep failed");
}
*/
}
32 changes: 32 additions & 0 deletions testdata/repros/Issue5948.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity 0.8.18;

import "ds-test/test.sol";
import "../cheats/Vm.sol";

// https://github.com/foundry-rs/foundry/issues/5948
contract Issue5948Test is DSTest {
Vm constant vm = Vm(HEVM_ADDRESS);

/// forge-config: default.fuzz.runs = 2
function testSleepFuzzed(uint256 _milliseconds) public {
// Limit sleep time to 2 seconds to decrease test time
uint256 milliseconds = _milliseconds % 2000;

string[] memory inputs = new string[](2);
inputs[0] = "date";
// OS X does not support precision more than 1 second
inputs[1] = "+%s000";

bytes memory res = vm.ffi(inputs);
uint256 start = vm.parseUint(string(res));

vm.sleep(milliseconds);

res = vm.ffi(inputs);
uint256 end = vm.parseUint(string(res));

// Limit precision to 1000 ms
assertGe(end - start, milliseconds / 1000 * 1000, "sleep failed");
}
}

0 comments on commit 91df94b

Please sign in to comment.