use crate::{
fuzz::{invariant::BasicTxDetails, BaseCounterExample},
multi_runner::{is_matching_test, TestContract, TestRunnerConfig},
progress::{start_fuzz_progress, TestsProgress},
result::{SuiteResult, TestResult, TestSetup},
MultiContractRunner, TestFilter,
};
use alloy_dyn_abi::DynSolValue;
use alloy_json_abi::Function;
use alloy_primitives::{address, map::HashMap, Address, U256};
use eyre::Result;
use foundry_common::{contracts::ContractsByAddress, TestFunctionExt, TestFunctionKind};
use foundry_config::Config;
use foundry_evm::{
constants::CALLER,
decode::RevertDecoder,
executors::{
fuzz::FuzzedExecutor,
invariant::{
check_sequence, replay_error, replay_run, InvariantExecutor, InvariantFuzzError,
},
CallResult, EvmError, Executor, ITest, RawCallResult,
},
fuzz::{
fixture_name,
invariant::{CallDetails, InvariantContract},
CounterExample, FuzzFixtures,
},
traces::{load_contracts, TraceKind, TraceMode},
};
use proptest::test_runner::{
FailurePersistence, FileFailurePersistence, RngAlgorithm, TestRng, TestRunner,
};
use rayon::prelude::*;
use std::{borrow::Cow, cmp::min, collections::BTreeMap, sync::Arc, time::Instant};
use tracing::Span;
pub const LIBRARY_DEPLOYER: Address = address!("1F95D37F27EA0dEA9C252FC09D5A6eaA97647353");
pub struct ContractRunner<'a> {
name: &'a str,
contract: &'a TestContract,
executor: Executor,
progress: Option<&'a TestsProgress>,
tokio_handle: &'a tokio::runtime::Handle,
span: tracing::Span,
tcfg: Cow<'a, TestRunnerConfig>,
mcr: &'a MultiContractRunner,
}
impl<'a> std::ops::Deref for ContractRunner<'a> {
type Target = Cow<'a, TestRunnerConfig>;
#[inline(always)]
fn deref(&self) -> &Self::Target {
&self.tcfg
}
}
impl<'a> ContractRunner<'a> {
pub fn new(
name: &'a str,
contract: &'a TestContract,
executor: Executor,
progress: Option<&'a TestsProgress>,
tokio_handle: &'a tokio::runtime::Handle,
span: Span,
mcr: &'a MultiContractRunner,
) -> Self {
Self {
name,
contract,
executor,
progress,
tokio_handle,
span,
tcfg: Cow::Borrowed(&mcr.tcfg),
mcr,
}
}
pub fn setup(&mut self, call_setup: bool) -> TestSetup {
self._setup(call_setup).unwrap_or_else(|err| {
if err.to_string().contains("skipped") {
TestSetup::skipped(err.to_string())
} else {
TestSetup::failed(err.to_string())
}
})
}
fn _setup(&mut self, call_setup: bool) -> Result<TestSetup> {
trace!(call_setup, "setting up");
self.apply_contract_inline_config()?;
self.executor.set_balance(self.sender, U256::MAX)?;
self.executor.set_balance(CALLER, U256::MAX)?;
self.executor.set_nonce(self.sender, 1)?;
self.executor.set_balance(LIBRARY_DEPLOYER, U256::MAX)?;
let mut result = TestSetup::default();
for code in self.mcr.libs_to_deploy.iter() {
let deploy_result = self.executor.deploy(
LIBRARY_DEPLOYER,
code.clone(),
U256::ZERO,
Some(&self.mcr.revert_decoder),
);
if let Ok(deployed) = &deploy_result {
result.deployed_libs.push(deployed.address);
}
let (raw, reason) = RawCallResult::from_evm_result(deploy_result.map(Into::into))?;
result.extend(raw, TraceKind::Deployment);
if reason.is_some() {
result.reason = reason;
return Ok(result);
}
}
let address = self.sender.create(self.executor.get_nonce(self.sender)?);
result.address = address;
self.executor.set_balance(address, self.initial_balance())?;
let deploy_result = self.executor.deploy(
self.sender,
self.contract.bytecode.clone(),
U256::ZERO,
Some(&self.mcr.revert_decoder),
);
if let Ok(dr) = &deploy_result {
debug_assert_eq!(dr.address, address);
}
let (raw, reason) = RawCallResult::from_evm_result(deploy_result.map(Into::into))?;
result.extend(raw, TraceKind::Deployment);
if reason.is_some() {
result.reason = reason;
return Ok(result);
}
self.executor.set_balance(self.sender, self.initial_balance())?;
self.executor.set_balance(CALLER, self.initial_balance())?;
self.executor.set_balance(LIBRARY_DEPLOYER, self.initial_balance())?;
self.executor.deploy_create2_deployer()?;
if call_setup {
trace!("calling setUp");
let res = self.executor.setup(None, address, Some(&self.mcr.revert_decoder));
let (raw, reason) = RawCallResult::from_evm_result(res)?;
result.extend(raw, TraceKind::Setup);
result.reason = reason;
}
result.fuzz_fixtures = self.fuzz_fixtures(address);
Ok(result)
}
fn initial_balance(&self) -> U256 {
self.evm_opts.initial_balance
}
fn apply_contract_inline_config(&mut self) -> Result<()> {
if self.inline_config.contains_contract(self.name) {
let new_config = Arc::new(self.inline_config(None)?);
self.tcfg.to_mut().reconfigure_with(new_config);
let prev_tracer = self.executor.inspector_mut().tracer.take();
self.tcfg.configure_executor(&mut self.executor);
self.executor.inspector_mut().tracer = prev_tracer;
}
Ok(())
}
fn inline_config(&self, func: Option<&Function>) -> Result<Config> {
let function = func.map(|f| f.name.as_str()).unwrap_or("");
let config =
self.mcr.inline_config.merge(self.name, function, &self.config).extract::<Config>()?;
Ok(config)
}
fn fuzz_fixtures(&mut self, address: Address) -> FuzzFixtures {
let mut fixtures = HashMap::default();
let fixture_functions = self.contract.abi.functions().filter(|func| func.is_fixture());
for func in fixture_functions {
if func.inputs.is_empty() {
if let Ok(CallResult { raw: _, decoded_result }) =
self.executor.call(CALLER, address, func, &[], U256::ZERO, None)
{
fixtures.insert(fixture_name(func.name.clone()), decoded_result);
}
} else {
let mut vals = Vec::new();
let mut index = 0;
loop {
if let Ok(CallResult { raw: _, decoded_result }) = self.executor.call(
CALLER,
address,
func,
&[DynSolValue::Uint(U256::from(index), 256)],
U256::ZERO,
None,
) {
vals.push(decoded_result);
} else {
break;
}
index += 1;
}
fixtures.insert(fixture_name(func.name.clone()), DynSolValue::Array(vals));
};
}
FuzzFixtures::new(fixtures)
}
pub fn run_tests(mut self, filter: &dyn TestFilter) -> SuiteResult {
let start = Instant::now();
let mut warnings = Vec::new();
let setup_fns: Vec<_> =
self.contract.abi.functions().filter(|func| func.name.is_setup()).collect();
let call_setup = setup_fns.len() == 1 && setup_fns[0].name == "setUp";
for &setup_fn in setup_fns.iter() {
if setup_fn.name != "setUp" {
warnings.push(format!(
"Found invalid setup function \"{}\" did you mean \"setUp()\"?",
setup_fn.signature()
));
}
}
if setup_fns.len() > 1 {
return SuiteResult::new(
start.elapsed(),
[("setUp()".to_string(), TestResult::fail("multiple setUp functions".to_string()))]
.into(),
warnings,
)
}
let after_invariant_fns: Vec<_> =
self.contract.abi.functions().filter(|func| func.name.is_after_invariant()).collect();
if after_invariant_fns.len() > 1 {
return SuiteResult::new(
start.elapsed(),
[(
"afterInvariant()".to_string(),
TestResult::fail("multiple afterInvariant functions".to_string()),
)]
.into(),
warnings,
)
}
let call_after_invariant = after_invariant_fns.first().is_some_and(|after_invariant_fn| {
let match_sig = after_invariant_fn.name == "afterInvariant";
if !match_sig {
warnings.push(format!(
"Found invalid afterInvariant function \"{}\" did you mean \"afterInvariant()\"?",
after_invariant_fn.signature()
));
}
match_sig
});
let has_invariants = self.contract.abi.functions().any(|func| func.is_invariant_test());
let prev_tracer = self.executor.inspector_mut().tracer.take();
if prev_tracer.is_some() || has_invariants {
self.executor.set_tracing(TraceMode::Call);
}
let setup_time = Instant::now();
let setup = self.setup(call_setup);
debug!("finished setting up in {:?}", setup_time.elapsed());
self.executor.inspector_mut().tracer = prev_tracer;
if setup.reason.is_some() {
return SuiteResult::new(
start.elapsed(),
[("setUp()".to_string(), TestResult::setup_result(setup))].into(),
warnings,
)
}
let find_timer = Instant::now();
let functions = self
.contract
.abi
.functions()
.filter(|func| is_matching_test(func, filter))
.collect::<Vec<_>>();
debug!(
"Found {} test functions out of {} in {:?}",
functions.len(),
self.contract.abi.functions().count(),
find_timer.elapsed(),
);
let identified_contracts = has_invariants.then(|| {
load_contracts(setup.traces.iter().map(|(_, t)| &t.arena), &self.mcr.known_contracts)
});
let test_results = functions
.par_iter()
.map(|&func| {
let start = Instant::now();
let _guard = self.tokio_handle.enter();
let _guard;
let current_span = tracing::Span::current();
if current_span.is_none() || current_span.id() != self.span.id() {
_guard = self.span.enter();
}
let sig = func.signature();
let kind = func.test_function_kind();
let _guard = debug_span!(
"test",
%kind,
name = %if enabled!(tracing::Level::TRACE) { &sig } else { &func.name },
)
.entered();
let mut res = FunctionRunner::new(&self, &setup).run(
func,
kind,
call_after_invariant,
identified_contracts.as_ref(),
);
res.duration = start.elapsed();
(sig, res)
})
.collect::<BTreeMap<_, _>>();
let duration = start.elapsed();
let test_fail_deprecations = self
.contract
.abi
.functions()
.filter_map(|func| {
TestFunctionKind::classify(&func.name, !func.inputs.is_empty())
.is_any_test_fail()
.then_some(func.name.clone())
})
.collect::<Vec<_>>()
.join(", ");
if !test_fail_deprecations.is_empty() {
warnings.push(format!(
"`testFail*` has been deprecated and will be removed in the next release. Consider changing to test_Revert[If|When]_Condition and expecting a revert. Found deprecated testFail* function(s): {test_fail_deprecations}.",
));
}
SuiteResult::new(duration, test_results, warnings)
}
}
struct FunctionRunner<'a> {
tcfg: Cow<'a, TestRunnerConfig>,
executor: Cow<'a, Executor>,
cr: &'a ContractRunner<'a>,
address: Address,
setup: &'a TestSetup,
result: TestResult,
}
impl<'a> std::ops::Deref for FunctionRunner<'a> {
type Target = Cow<'a, TestRunnerConfig>;
#[inline(always)]
fn deref(&self) -> &Self::Target {
&self.tcfg
}
}
impl<'a> FunctionRunner<'a> {
fn new(cr: &'a ContractRunner<'a>, setup: &'a TestSetup) -> Self {
Self {
tcfg: match &cr.tcfg {
Cow::Borrowed(tcfg) => Cow::Borrowed(tcfg),
Cow::Owned(tcfg) => Cow::Owned(tcfg.clone()),
},
executor: Cow::Borrowed(&cr.executor),
cr,
address: setup.address,
setup,
result: TestResult::new(setup),
}
}
fn revert_decoder(&self) -> &'a RevertDecoder {
&self.cr.mcr.revert_decoder
}
fn apply_function_inline_config(&mut self, func: &Function) -> Result<()> {
if self.inline_config.contains_function(self.cr.name, &func.name) {
let new_config = Arc::new(self.cr.inline_config(Some(func))?);
self.tcfg.to_mut().reconfigure_with(new_config);
self.tcfg.configure_executor(self.executor.to_mut());
}
Ok(())
}
fn run(
mut self,
func: &Function,
kind: TestFunctionKind,
call_after_invariant: bool,
identified_contracts: Option<&ContractsByAddress>,
) -> TestResult {
if let Err(e) = self.apply_function_inline_config(func) {
self.result.single_fail(Some(e.to_string()));
return self.result;
}
match kind {
TestFunctionKind::UnitTest { should_fail } => self.run_unit_test(func, should_fail),
TestFunctionKind::FuzzTest { should_fail } => self.run_fuzz_test(func, should_fail),
TestFunctionKind::InvariantTest => {
self.run_invariant_test(func, call_after_invariant, identified_contracts.unwrap())
}
_ => unreachable!(),
}
}
fn run_unit_test(mut self, func: &Function, should_fail: bool) -> TestResult {
if self.prepare_test(func).is_err() {
return self.result;
}
let (mut raw_call_result, reason) = match self.executor.call(
self.sender,
self.address,
func,
&[],
U256::ZERO,
Some(self.revert_decoder()),
) {
Ok(res) => (res.raw, None),
Err(EvmError::Execution(err)) => (err.raw, Some(err.reason)),
Err(EvmError::Skip(reason)) => {
self.result.single_skip(reason);
return self.result;
}
Err(err) => {
self.result.single_fail(Some(err.to_string()));
return self.result;
}
};
let success =
self.executor.is_raw_call_mut_success(self.address, &mut raw_call_result, should_fail);
self.result.single_result(success, reason, raw_call_result);
self.result
}
fn run_invariant_test(
mut self,
func: &Function,
call_after_invariant: bool,
identified_contracts: &ContractsByAddress,
) -> TestResult {
if let Err(EvmError::Skip(reason)) = self.executor.call(
self.sender,
self.address,
func,
&[],
U256::ZERO,
Some(self.revert_decoder()),
) {
self.result.invariant_skip(reason);
return self.result;
};
let runner = self.invariant_runner();
let invariant_config = &self.config.invariant;
let mut evm = InvariantExecutor::new(
self.clone_executor(),
runner,
invariant_config.clone(),
identified_contracts,
&self.cr.mcr.known_contracts,
);
let invariant_contract = InvariantContract {
address: self.address,
invariant_function: func,
call_after_invariant,
abi: &self.cr.contract.abi,
};
let failure_dir = invariant_config.clone().failure_dir(self.cr.name);
let failure_file = failure_dir.join(&invariant_contract.invariant_function.name);
if let Ok(call_sequence) =
foundry_common::fs::read_json_file::<Vec<BaseCounterExample>>(failure_file.as_path())
{
let txes = call_sequence
.iter()
.map(|seq| BasicTxDetails {
sender: seq.sender.unwrap_or_default(),
call_details: CallDetails {
target: seq.addr.unwrap_or_default(),
calldata: seq.calldata.clone(),
},
})
.collect::<Vec<BasicTxDetails>>();
if let Ok((success, replayed_entirely)) = check_sequence(
self.clone_executor(),
&txes,
(0..min(txes.len(), invariant_config.depth as usize)).collect(),
invariant_contract.address,
invariant_contract.invariant_function.selector().to_vec().into(),
invariant_config.fail_on_revert,
invariant_contract.call_after_invariant,
) {
if !success {
let _= sh_warn!("\
Replayed invariant failure from {:?} file. \
Run `forge clean` or remove file to ignore failure and to continue invariant test campaign.",
failure_file.as_path()
);
let _ = replay_run(
&invariant_contract,
self.clone_executor(),
&self.cr.mcr.known_contracts,
identified_contracts.clone(),
&mut self.result.logs,
&mut self.result.traces,
&mut self.result.coverage,
&mut self.result.deprecated_cheatcodes,
&txes,
);
self.result.invariant_replay_fail(
replayed_entirely,
&invariant_contract.invariant_function.name,
call_sequence,
);
return self.result;
}
}
}
let progress =
start_fuzz_progress(self.cr.progress, self.cr.name, &func.name, invariant_config.runs);
let invariant_result = match evm.invariant_fuzz(
invariant_contract.clone(),
&self.setup.fuzz_fixtures,
&self.setup.deployed_libs,
progress.as_ref(),
) {
Ok(x) => x,
Err(e) => {
self.result.invariant_setup_fail(e);
return self.result;
}
};
self.result.merge_coverages(invariant_result.coverage);
let mut counterexample = None;
let success = invariant_result.error.is_none();
let reason = invariant_result.error.as_ref().and_then(|err| err.revert_reason());
match invariant_result.error {
Some(error) => match error {
InvariantFuzzError::BrokenInvariant(case_data) |
InvariantFuzzError::Revert(case_data) => {
match replay_error(
&case_data,
&invariant_contract,
self.clone_executor(),
&self.cr.mcr.known_contracts,
identified_contracts.clone(),
&mut self.result.logs,
&mut self.result.traces,
&mut self.result.coverage,
&mut self.result.deprecated_cheatcodes,
progress.as_ref(),
) {
Ok(call_sequence) => {
if !call_sequence.is_empty() {
if let Err(err) = foundry_common::fs::create_dir_all(failure_dir) {
error!(%err, "Failed to create invariant failure dir");
} else if let Err(err) = foundry_common::fs::write_json_file(
failure_file.as_path(),
&call_sequence,
) {
error!(%err, "Failed to record call sequence");
}
counterexample = Some(CounterExample::Sequence(call_sequence))
}
}
Err(err) => {
error!(%err, "Failed to replay invariant error");
}
};
}
InvariantFuzzError::MaxAssumeRejects(_) => {}
},
_ => {
if let Err(err) = replay_run(
&invariant_contract,
self.clone_executor(),
&self.cr.mcr.known_contracts,
identified_contracts.clone(),
&mut self.result.logs,
&mut self.result.traces,
&mut self.result.coverage,
&mut self.result.deprecated_cheatcodes,
&invariant_result.last_run_inputs,
) {
error!(%err, "Failed to replay last invariant run");
}
}
}
self.result.invariant_result(
invariant_result.gas_report_traces,
success,
reason,
counterexample,
invariant_result.cases,
invariant_result.reverts,
invariant_result.metrics,
);
self.result
}
fn run_fuzz_test(mut self, func: &Function, should_fail: bool) -> TestResult {
if self.prepare_test(func).is_err() {
return self.result;
}
let runner = self.fuzz_runner();
let fuzz_config = self.config.fuzz.clone();
let progress =
start_fuzz_progress(self.cr.progress, self.cr.name, &func.name, fuzz_config.runs);
let fuzzed_executor =
FuzzedExecutor::new(self.executor.into_owned(), runner, self.tcfg.sender, fuzz_config);
let result = fuzzed_executor.fuzz(
func,
&self.setup.fuzz_fixtures,
&self.setup.deployed_libs,
self.address,
should_fail,
&self.cr.mcr.revert_decoder,
progress.as_ref(),
);
self.result.fuzz_result(result);
self.result
}
fn prepare_test(&mut self, func: &Function) -> Result<(), ()> {
let address = self.setup.address;
if self.cr.contract.abi.functions().filter(|func| func.name.is_before_test_setup()).count() ==
1
{
for calldata in self
.executor
.call_sol_default(
address,
&ITest::beforeTestSetupCall { testSelector: func.selector() },
)
.beforeTestCalldata
{
match self.executor.to_mut().transact_raw(
self.tcfg.sender,
address,
calldata,
U256::ZERO,
) {
Ok(call_result) => {
let reverted = call_result.reverted;
self.result.extend(call_result);
if reverted {
self.result.single_fail(None);
return Err(());
}
}
Err(_) => {
self.result.single_fail(None);
return Err(());
}
}
}
}
Ok(())
}
fn fuzz_runner(&self) -> TestRunner {
let config = &self.config.fuzz;
let failure_persist_path = config
.failure_persist_dir
.as_ref()
.unwrap()
.join(config.failure_persist_file.as_ref().unwrap())
.into_os_string()
.into_string()
.unwrap();
fuzzer_with_cases(
config.seed,
config.runs,
config.max_test_rejects,
Some(Box::new(FileFailurePersistence::Direct(failure_persist_path.leak()))),
)
}
fn invariant_runner(&self) -> TestRunner {
let config = &self.config.invariant;
fuzzer_with_cases(self.config.fuzz.seed, config.runs, config.max_assume_rejects, None)
}
fn clone_executor(&self) -> Executor {
self.executor.clone().into_owned()
}
}
fn fuzzer_with_cases(
seed: Option<U256>,
cases: u32,
max_global_rejects: u32,
file_failure_persistence: Option<Box<dyn FailurePersistence>>,
) -> TestRunner {
let config = proptest::test_runner::Config {
failure_persistence: file_failure_persistence,
cases,
max_global_rejects,
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)
}
}