Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(forge): inline config for tests #9342

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
3 changes: 3 additions & 0 deletions crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ pub use fuzz::{FuzzConfig, FuzzDictionaryConfig};
mod invariant;
pub use invariant::InvariantConfig;

mod test;
pub use test::TestConfig;

mod inline;
pub use inline::{validate_profiles, InlineConfig, InlineConfigError, InlineConfigParser, NatSpec};

Expand Down
55 changes: 55 additions & 0 deletions crates/config/src/test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use std::str::FromStr;

use foundry_compilers::artifacts::EvmVersion;

use crate::{inline::InlineConfigParserError, InlineConfigParser};

/// Test configuration
///
/// Used to parse InlineConfig.
#[derive(Clone, Debug, Default)]
pub struct TestConfig {
pub evm_version: Option<EvmVersion>,
pub isolate: Option<bool>,
}

impl InlineConfigParser for TestConfig {
fn config_key() -> String {
"test".into()
}

fn try_merge(
&self,
configs: &[String],
) -> Result<Option<Self>, crate::inline::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() {
"evm-version" => {
conf_clone.evm_version =
Some(EvmVersion::from_str(value.as_str()).map_err(|_| {
InlineConfigParserError::InvalidConfigProperty(format!(
"evm-version {value}",
))
})?)
}
"isolate" => {
conf_clone.isolate = Some(bool::from_str(&value).map_err(|_| {
InlineConfigParserError::InvalidConfigProperty(format!("isolate {value}"))
})?)
}
_ => Err(InlineConfigParserError::InvalidConfigProperty(key))?,
}
}
Ok(Some(conf_clone))
}
}
5 changes: 5 additions & 0 deletions crates/evm/evm/src/executors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@ impl Executor {
self.env.spec_id()
}

/// Set the EVM spec ID.
pub fn set_spec_id(&mut self, spec_id: SpecId) {
self.env.handler_cfg.spec_id = spec_id;
}

/// Creates the default CREATE2 Contract Deployer for local tests and scripts.
pub fn deploy_create2_deployer(&mut self) -> eyre::Result<()> {
trace!("deploying local create2 deployer");
Expand Down
49 changes: 45 additions & 4 deletions crates/forge/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ extern crate tracing;
use foundry_compilers::ProjectCompileOutput;
use foundry_config::{
validate_profiles, Config, FuzzConfig, InlineConfig, InlineConfigError, InlineConfigParser,
InvariantConfig, NatSpec,
InvariantConfig, NatSpec, TestConfig,
};
use proptest::test_runner::{
FailurePersistence, FileFailurePersistence, RngAlgorithm, TestRng, TestRunner,
Expand Down Expand Up @@ -43,10 +43,15 @@ pub struct TestOptions {
/// 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,
/// The base "test" configuration. To be used as a fallback in case no more specific configs
/// are found for a given run.
pub test: TestConfig,
/// Contains per-test specific "fuzz" configurations.
pub inline_fuzz: InlineConfig<FuzzConfig>,
/// Contains per-test specific "invariant" configurations.
pub inline_invariant: InlineConfig<InvariantConfig>,
/// Contains per-test specific configurations.
pub inline_test: InlineConfig<TestConfig>,
}

impl TestOptions {
Expand All @@ -58,11 +63,12 @@ impl TestOptions {
profiles: Vec<String>,
base_fuzz: FuzzConfig,
base_invariant: InvariantConfig,
base_test: TestConfig,
) -> Result<Self, InlineConfigError> {
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_test = InlineConfig::<TestConfig>::default();
// Validate all natspecs
for natspec in &natspecs {
validate_profiles(natspec, &profiles)?;
Expand All @@ -77,6 +83,10 @@ impl TestOptions {
if let Some(invariant) = base_invariant.merge(natspec)? {
inline_invariant.insert_contract(&natspec.contract, invariant);
}

if let Some(test) = base_test.merge(natspec)? {
inline_test.insert_contract(&natspec.contract, test);
}
}

for (natspec, f) in natspecs.iter().filter_map(|n| n.function.as_ref().map(|f| (n, f))) {
Expand All @@ -87,6 +97,7 @@ impl TestOptions {
// 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);
let base_test = inline_test.get(c, f).unwrap_or(&base_test);

if let Some(fuzz) = base_fuzz.merge(natspec)? {
inline_fuzz.insert_fn(c, f, fuzz);
Expand All @@ -95,9 +106,20 @@ impl TestOptions {
if let Some(invariant) = base_invariant.merge(natspec)? {
inline_invariant.insert_fn(c, f, invariant);
}

if let Some(test) = base_test.merge(natspec)? {
inline_test.insert_fn(c, f, test);
}
}

Ok(Self { fuzz: base_fuzz, invariant: base_invariant, inline_fuzz, inline_invariant })
Ok(Self {
fuzz: base_fuzz,
invariant: base_invariant,
test: base_test,
inline_fuzz,
inline_invariant,
inline_test,
})
}

/// Returns a "fuzz" test runner instance. Parameters are used to select tight scoped fuzz
Expand Down Expand Up @@ -157,6 +179,17 @@ impl TestOptions {
self.inline_invariant.get(contract_id, test_fn).unwrap_or(&self.invariant)
}

/// Returns a "test" configuration setup. Parameters are used to select tight scoped test
/// 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 test_config(&self, contract_id: &str, test_fn: &str) -> &TestConfig {
self.inline_test.get(contract_id, test_fn).unwrap_or(&self.test)
}

pub fn fuzzer_with_cases(
&self,
cases: u32,
Expand Down Expand Up @@ -190,6 +223,7 @@ impl TestOptions {
pub struct TestOptionsBuilder {
fuzz: Option<FuzzConfig>,
invariant: Option<InvariantConfig>,
test: Option<TestConfig>,
profiles: Option<Vec<String>>,
}

Expand All @@ -206,6 +240,12 @@ impl TestOptionsBuilder {
self
}

/// Sets a [`TestConfig`] to be used as base "test" configuration.
pub fn test(mut self, conf: TestConfig) -> Self {
self.test = 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<String>) -> Self {
Expand All @@ -228,6 +268,7 @@ impl TestOptionsBuilder {
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)
let base_test = self.test.unwrap_or_default();
TestOptions::new(output, root, profiles, base_fuzz, base_invariant, base_test)
}
}
82 changes: 64 additions & 18 deletions crates/forge/src/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use foundry_common::{
contracts::{ContractsByAddress, ContractsByArtifact},
TestFunctionExt, TestFunctionKind,
};
use foundry_config::{FuzzConfig, InvariantConfig};
use foundry_config::{evm_spec_id, FuzzConfig, InvariantConfig, TestConfig};
use foundry_evm::{
constants::CALLER,
decode::RevertDecoder,
Expand Down Expand Up @@ -336,15 +336,23 @@ impl ContractRunner<'_> {
.entered();

let setup = setup.clone();
let test_config = test_options.test_config(self.name, &func.name);
let mut res = match kind {
TestFunctionKind::UnitTest { should_fail } => {
self.run_unit_test(func, should_fail, setup)
self.run_unit_test(func, should_fail, setup, test_config)
}
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())
self.run_fuzz_test(
func,
should_fail,
runner,
setup,
fuzz_config.clone(),
test_config,
)
}
TestFunctionKind::InvariantTest => {
let runner = test_options.invariant_runner(self.name, &func.name);
Expand All @@ -358,6 +366,7 @@ impl ContractRunner<'_> {
call_after_invariant,
&known_contracts,
identified_contracts.as_ref().unwrap(),
test_config,
)
}
_ => unreachable!(),
Expand Down Expand Up @@ -386,9 +395,10 @@ impl ContractRunner<'_> {
func: &Function,
should_fail: bool,
setup: TestSetup,
test_config: &TestConfig,
) -> TestResult {
// Prepare unit test execution.
let (executor, test_result, address) = match self.prepare_test(func, setup) {
let (executor, test_result, address) = match self.prepare_test(func, setup, test_config) {
Ok(res) => res,
Err(res) => return res,
};
Expand Down Expand Up @@ -422,25 +432,40 @@ impl ContractRunner<'_> {
call_after_invariant: bool,
known_contracts: &ContractsByArtifact,
identified_contracts: &ContractsByAddress,
config: &TestConfig,
) -> TestResult {
let address = setup.address;
let fuzz_fixtures = setup.fuzz_fixtures.clone();
let mut test_result = TestResult::new(setup);

let mut executor = self.executor.clone();

let change_isolate =
config.isolate.is_some_and(|isolate| executor.inspector().enable_isolation != isolate);
let change_evm_verion = config
.evm_version
.is_some_and(|evm_version| executor.spec_id() != evm_spec_id(&evm_version, false));

if change_isolate || change_evm_verion {
if change_evm_verion {
let spec_id = evm_spec_id(&config.evm_version.unwrap(), false);
executor.set_spec_id(spec_id);
}

if change_isolate {
executor.inspector_mut().enable_isolation(config.isolate.unwrap());
}
}

// First, run the test normally to see if it needs to be skipped.
if let Err(EvmError::Skip(reason)) = self.executor.call(
self.sender,
address,
func,
&[],
U256::ZERO,
Some(self.revert_decoder),
) {
if let Err(EvmError::Skip(reason)) =
executor.call(self.sender, address, func, &[], U256::ZERO, Some(self.revert_decoder))
{
return test_result.invariant_skip(reason);
};

let mut evm = InvariantExecutor::new(
self.executor.clone(),
executor.clone(),
runner,
invariant_config.clone(),
identified_contracts,
Expand Down Expand Up @@ -472,7 +497,7 @@ impl ContractRunner<'_> {
})
.collect::<Vec<BasicTxDetails>>();
if let Ok((success, replayed_entirely)) = check_sequence(
self.executor.clone(),
executor.clone(),
&txes,
(0..min(txes.len(), invariant_config.depth as usize)).collect(),
invariant_contract.address,
Expand All @@ -490,7 +515,7 @@ impl ContractRunner<'_> {
// exit without executing new runs.
let _ = replay_run(
&invariant_contract,
self.executor.clone(),
executor.clone(),
known_contracts,
identified_contracts.clone(),
&mut test_result.logs,
Expand Down Expand Up @@ -533,7 +558,7 @@ impl ContractRunner<'_> {
match replay_error(
&case_data,
&invariant_contract,
self.executor.clone(),
executor.clone(),
known_contracts,
identified_contracts.clone(),
&mut test_result.logs,
Expand Down Expand Up @@ -569,7 +594,7 @@ impl ContractRunner<'_> {
_ => {
if let Err(err) = replay_run(
&invariant_contract,
self.executor.clone(),
executor.clone(),
known_contracts,
identified_contracts.clone(),
&mut test_result.logs,
Expand Down Expand Up @@ -610,12 +635,13 @@ impl ContractRunner<'_> {
runner: TestRunner,
setup: TestSetup,
fuzz_config: FuzzConfig,
test_config: &TestConfig,
) -> TestResult {
let progress = start_fuzz_progress(self.progress, self.name, &func.name, fuzz_config.runs);

// Prepare fuzz test execution.
let fuzz_fixtures = setup.fuzz_fixtures.clone();
let (executor, test_result, address) = match self.prepare_test(func, setup) {
let (executor, test_result, address) = match self.prepare_test(func, setup, test_config) {
Ok(res) => res,
Err(res) => return res,
};
Expand Down Expand Up @@ -647,11 +673,31 @@ impl ContractRunner<'_> {
&self,
func: &Function,
setup: TestSetup,
config: &TestConfig,
) -> Result<(Cow<'_, Executor>, TestResult, Address), TestResult> {
let address = setup.address;
let mut executor = Cow::Borrowed(&self.executor);
let mut test_result = TestResult::new(setup);

let change_isolate =
config.isolate.is_some_and(|isolate| executor.inspector().enable_isolation != isolate);
let change_evm_verion = config
.evm_version
.is_some_and(|evm_version| executor.spec_id() != evm_spec_id(&evm_version, false));

if change_isolate || change_evm_verion {
let executor = executor.to_mut();

if change_evm_verion {
let spec_id = evm_spec_id(&config.evm_version.unwrap(), false);
executor.set_spec_id(spec_id);
}

if change_isolate {
executor.inspector_mut().enable_isolation(config.isolate.unwrap());
}
}

// Apply before test configured functions (if any).
if self.contract.abi.functions().filter(|func| func.name.is_before_test_setup()).count() ==
1
Expand Down
Loading
Loading