Skip to content

Commit 4a41367

Browse files
grandizzymattsse
andauthored
feat(test): allow custom txes before unit and fuzz test (#8497)
* feat(test): allow performing txes before unit test * Changes after review: - do not unwrap func - check if `beforeTestSelectors` exists - move logic in prepare_unit_test fn - apply same logic to fuzz tests * Review: Before test is not a test kind * Changes after review: beforeTestSetup new fn signature * Remove obsolete struct from test * Update crates/forge/src/runner.rs Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de> * Changes after review: avoid executor clone * Fix Cow::Borrowed usage --------- Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
1 parent cc88da9 commit 4a41367

File tree

7 files changed

+232
-43
lines changed

7 files changed

+232
-43
lines changed

crates/common/src/traits.rs

+5
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ pub trait TestFunctionExt {
4444
matches!(self.test_function_kind(), TestFunctionKind::UnitTest { .. })
4545
}
4646

47+
/// Returns `true` if this function is a `beforeTestSetup` function.
48+
fn is_before_test_setup(&self) -> bool {
49+
self.tfe_as_str().eq_ignore_ascii_case("beforetestsetup")
50+
}
51+
4752
/// Returns `true` if this function is a fuzz test.
4853
fn is_fuzz_test(&self) -> bool {
4954
self.test_function_kind().is_fuzz_test()

crates/evm/evm/src/executors/invariant/mod.rs

+28-25
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,7 @@ impl<'a> InvariantExecutor<'a> {
532532
/// targetArtifactSelectors > excludeArtifacts > targetArtifacts
533533
pub fn select_contract_artifacts(&mut self, invariant_address: Address) -> Result<()> {
534534
let result = self
535+
.executor
535536
.call_sol_default(invariant_address, &IInvariantTest::targetArtifactSelectorsCall {});
536537

537538
// Insert them into the executor `targeted_abi`.
@@ -542,10 +543,12 @@ impl<'a> InvariantExecutor<'a> {
542543
self.artifact_filters.targeted.entry(identifier).or_default().extend(selectors);
543544
}
544545

545-
let selected =
546-
self.call_sol_default(invariant_address, &IInvariantTest::targetArtifactsCall {});
547-
let excluded =
548-
self.call_sol_default(invariant_address, &IInvariantTest::excludeArtifactsCall {});
546+
let selected = self
547+
.executor
548+
.call_sol_default(invariant_address, &IInvariantTest::targetArtifactsCall {});
549+
let excluded = self
550+
.executor
551+
.call_sol_default(invariant_address, &IInvariantTest::excludeArtifactsCall {});
549552

550553
// Insert `excludeArtifacts` into the executor `excluded_abi`.
551554
for contract in excluded.excludedArtifacts {
@@ -620,10 +623,14 @@ impl<'a> InvariantExecutor<'a> {
620623
&self,
621624
to: Address,
622625
) -> Result<(SenderFilters, FuzzRunIdentifiedContracts)> {
623-
let targeted_senders =
624-
self.call_sol_default(to, &IInvariantTest::targetSendersCall {}).targetedSenders;
625-
let mut excluded_senders =
626-
self.call_sol_default(to, &IInvariantTest::excludeSendersCall {}).excludedSenders;
626+
let targeted_senders = self
627+
.executor
628+
.call_sol_default(to, &IInvariantTest::targetSendersCall {})
629+
.targetedSenders;
630+
let mut excluded_senders = self
631+
.executor
632+
.call_sol_default(to, &IInvariantTest::excludeSendersCall {})
633+
.excludedSenders;
627634
// Extend with default excluded addresses - https://github.com/foundry-rs/foundry/issues/4163
628635
excluded_senders.extend([
629636
CHEATCODE_ADDRESS,
@@ -634,10 +641,14 @@ impl<'a> InvariantExecutor<'a> {
634641
excluded_senders.extend(PRECOMPILES);
635642
let sender_filters = SenderFilters::new(targeted_senders, excluded_senders);
636643

637-
let selected =
638-
self.call_sol_default(to, &IInvariantTest::targetContractsCall {}).targetedContracts;
639-
let excluded =
640-
self.call_sol_default(to, &IInvariantTest::excludeContractsCall {}).excludedContracts;
644+
let selected = self
645+
.executor
646+
.call_sol_default(to, &IInvariantTest::targetContractsCall {})
647+
.targetedContracts;
648+
let excluded = self
649+
.executor
650+
.call_sol_default(to, &IInvariantTest::excludeContractsCall {})
651+
.excludedContracts;
641652

642653
let contracts = self
643654
.setup_contracts
@@ -678,6 +689,7 @@ impl<'a> InvariantExecutor<'a> {
678689
targeted_contracts: &mut TargetedContracts,
679690
) -> Result<()> {
680691
let interfaces = self
692+
.executor
681693
.call_sol_default(invariant_address, &IInvariantTest::targetInterfacesCall {})
682694
.targetedInterfaces;
683695

@@ -735,13 +747,15 @@ impl<'a> InvariantExecutor<'a> {
735747
}
736748

737749
// Collect contract functions marked as target for fuzzing campaign.
738-
let selectors = self.call_sol_default(address, &IInvariantTest::targetSelectorsCall {});
750+
let selectors =
751+
self.executor.call_sol_default(address, &IInvariantTest::targetSelectorsCall {});
739752
for IInvariantTest::FuzzSelector { addr, selectors } in selectors.targetedSelectors {
740753
self.add_address_with_functions(addr, &selectors, false, targeted_contracts)?;
741754
}
742755

743756
// Collect contract functions excluded from fuzzing campaign.
744-
let selectors = self.call_sol_default(address, &IInvariantTest::excludeSelectorsCall {});
757+
let selectors =
758+
self.executor.call_sol_default(address, &IInvariantTest::excludeSelectorsCall {});
745759
for IInvariantTest::FuzzSelector { addr, selectors } in selectors.excludedSelectors {
746760
self.add_address_with_functions(addr, &selectors, true, targeted_contracts)?;
747761
}
@@ -773,17 +787,6 @@ impl<'a> InvariantExecutor<'a> {
773787
contract.add_selectors(selectors.iter().copied(), should_exclude)?;
774788
Ok(())
775789
}
776-
777-
fn call_sol_default<C: SolCall>(&self, to: Address, args: &C) -> C::Return
778-
where
779-
C::Return: Default,
780-
{
781-
self.executor
782-
.call_sol(CALLER, to, args, U256::ZERO, None)
783-
.map(|c| c.decoded_result)
784-
.inspect_err(|e| warn!(target: "forge::test", "failed calling {:?}: {e}", C::SIGNATURE))
785-
.unwrap_or_default()
786-
}
787790
}
788791

789792
/// Collects data from call for fuzzing. However, it first verifies that the sender is not an EOA

crates/evm/evm/src/executors/mod.rs

+13
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ sol! {
5050
interface ITest {
5151
function setUp() external;
5252
function failed() external view returns (bool failed);
53+
54+
#[derive(Default)]
55+
function beforeTestSetup(bytes4 testSelector) public view returns (bytes[] memory beforeTestCalldata);
5356
}
5457
}
5558

@@ -602,6 +605,16 @@ impl Executor {
602605

603606
EnvWithHandlerCfg::new_with_spec_id(Box::new(env), self.spec_id())
604607
}
608+
609+
pub fn call_sol_default<C: SolCall>(&self, to: Address, args: &C) -> C::Return
610+
where
611+
C::Return: Default,
612+
{
613+
self.call_sol(CALLER, to, args, U256::ZERO, None)
614+
.map(|c| c.decoded_result)
615+
.inspect_err(|e| warn!(target: "forge::test", "failed calling {:?}: {e}", C::SIGNATURE))
616+
.unwrap_or_default()
617+
}
605618
}
606619

607620
/// Represents the context after an execution error occurred.

crates/forge/src/result.rs

+10-2
Original file line numberDiff line numberDiff line change
@@ -449,9 +449,9 @@ impl TestResult {
449449
}
450450

451451
/// Returns the failed result with reason for single test.
452-
pub fn single_fail(mut self, err: EvmError) -> Self {
452+
pub fn single_fail(mut self, reason: Option<String>) -> Self {
453453
self.status = TestStatus::Failure;
454-
self.reason = Some(err.to_string());
454+
self.reason = reason;
455455
self
456456
}
457457

@@ -579,6 +579,14 @@ impl TestResult {
579579
format!("{self} {name} {}", self.kind.report())
580580
}
581581

582+
/// Function to merge logs, addresses, traces and coverage from a call result into test result.
583+
pub fn merge_call_result(&mut self, call_result: &RawCallResult) {
584+
self.logs.extend(call_result.logs.clone());
585+
self.labeled_addresses.extend(call_result.labels.clone());
586+
self.traces.extend(call_result.traces.clone().map(|traces| (TraceKind::Execution, traces)));
587+
self.merge_coverages(call_result.coverage.clone());
588+
}
589+
582590
/// Function to merge given coverage in current test result coverage.
583591
pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
584592
let old_coverage = std::mem::take(&mut self.coverage);

crates/forge/src/runner.rs

+82-16
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use foundry_evm::{
2424
invariant::{
2525
check_sequence, replay_error, replay_run, InvariantExecutor, InvariantFuzzError,
2626
},
27-
CallResult, EvmError, ExecutionErr, Executor, RawCallResult,
27+
CallResult, EvmError, ExecutionErr, Executor, ITest, RawCallResult,
2828
},
2929
fuzz::{
3030
fixture_name,
@@ -36,6 +36,7 @@ use foundry_evm::{
3636
use proptest::test_runner::TestRunner;
3737
use rayon::prelude::*;
3838
use std::{
39+
borrow::Cow,
3940
cmp::min,
4041
collections::{BTreeMap, HashMap},
4142
time::Instant,
@@ -270,6 +271,7 @@ impl<'a> ContractRunner<'a> {
270271
));
271272
}
272273
}
274+
273275
// There are multiple setUp function, so we return a single test result for `setUp`
274276
if setup_fns.len() > 1 {
275277
return SuiteResult::new(
@@ -412,21 +414,26 @@ impl<'a> ContractRunner<'a> {
412414

413415
/// Runs a single unit test.
414416
///
415-
/// Calls the given functions and returns the `TestResult`.
417+
/// Applies before test txes (if any), runs current test and returns the `TestResult`.
416418
///
417-
/// State modifications are not committed to the evm database but discarded after the call,
418-
/// similar to `eth_call`.
419+
/// Before test txes are applied in order and state modifications committed to the EVM database
420+
/// (therefore the unit test call will be made on modified state).
421+
/// State modifications of before test txes and unit test function call are discarded after
422+
/// test ends, similar to `eth_call`.
419423
pub fn run_unit_test(
420424
&self,
421425
func: &Function,
422426
should_fail: bool,
423427
setup: TestSetup,
424428
) -> TestResult {
425-
let address = setup.address;
426-
let test_result = TestResult::new(setup);
429+
// Prepare unit test execution.
430+
let (executor, test_result, address) = match self.prepare_test(func, setup) {
431+
Ok(res) => res,
432+
Err(res) => return res,
433+
};
427434

428-
// Run unit test
429-
let (mut raw_call_result, reason) = match self.executor.call(
435+
// Run current unit test.
436+
let (mut raw_call_result, reason) = match executor.call(
430437
self.sender,
431438
address,
432439
func,
@@ -437,11 +444,10 @@ impl<'a> ContractRunner<'a> {
437444
Ok(res) => (res.raw, None),
438445
Err(EvmError::Execution(err)) => (err.raw, Some(err.reason)),
439446
Err(EvmError::SkipError) => return test_result.single_skip(),
440-
Err(err) => return test_result.single_fail(err),
447+
Err(err) => return test_result.single_fail(Some(err.to_string())),
441448
};
442449

443-
let success =
444-
self.executor.is_raw_call_mut_success(address, &mut raw_call_result, should_fail);
450+
let success = executor.is_raw_call_mut_success(address, &mut raw_call_result, should_fail);
445451
test_result.single_result(success, reason, raw_call_result)
446452
}
447453

@@ -618,6 +624,15 @@ impl<'a> ContractRunner<'a> {
618624
)
619625
}
620626

627+
/// Runs a fuzzed test.
628+
///
629+
/// Applies the before test txes (if any), fuzzes the current function and returns the
630+
/// `TestResult`.
631+
///
632+
/// Before test txes are applied in order and state modifications committed to the EVM database
633+
/// (therefore the fuzz test will use the modified state).
634+
/// State modifications of before test txes and fuzz test are discarded after test ends,
635+
/// similar to `eth_call`.
621636
pub fn run_fuzz_test(
622637
&self,
623638
func: &Function,
@@ -626,14 +641,18 @@ impl<'a> ContractRunner<'a> {
626641
setup: TestSetup,
627642
fuzz_config: FuzzConfig,
628643
) -> TestResult {
629-
let address = setup.address;
644+
let progress = start_fuzz_progress(self.progress, self.name, &func.name, fuzz_config.runs);
645+
646+
// Prepare fuzz test execution.
630647
let fuzz_fixtures = setup.fuzz_fixtures.clone();
631-
let test_result = TestResult::new(setup);
648+
let (executor, test_result, address) = match self.prepare_test(func, setup) {
649+
Ok(res) => res,
650+
Err(res) => return res,
651+
};
632652

633-
// Run fuzz test
634-
let progress = start_fuzz_progress(self.progress, self.name, &func.name, fuzz_config.runs);
653+
// Run fuzz test.
635654
let fuzzed_executor =
636-
FuzzedExecutor::new(self.executor.clone(), runner, self.sender, fuzz_config);
655+
FuzzedExecutor::new(executor.into_owned(), runner, self.sender, fuzz_config);
637656
let result = fuzzed_executor.fuzz(
638657
func,
639658
&fuzz_fixtures,
@@ -650,4 +669,51 @@ impl<'a> ContractRunner<'a> {
650669
}
651670
test_result.fuzz_result(result)
652671
}
672+
673+
/// Prepares single unit test and fuzz test execution:
674+
/// - set up the test result and executor
675+
/// - check if before test txes are configured and apply them in order
676+
///
677+
/// Before test txes are arrays of arbitrary calldata obtained by calling the `beforeTest`
678+
/// function with test selector as a parameter.
679+
///
680+
/// Unit tests within same contract (or even current test) are valid options for before test tx
681+
/// configuration. Test execution stops if any of before test txes fails.
682+
fn prepare_test(
683+
&self,
684+
func: &Function,
685+
setup: TestSetup,
686+
) -> Result<(Cow<'_, Executor>, TestResult, Address), TestResult> {
687+
let address = setup.address;
688+
let mut executor = Cow::Borrowed(&self.executor);
689+
let mut test_result = TestResult::new(setup);
690+
691+
// Apply before test configured functions (if any).
692+
if self.contract.abi.functions().filter(|func| func.name.is_before_test_setup()).count() ==
693+
1
694+
{
695+
for calldata in executor
696+
.call_sol_default(
697+
address,
698+
&ITest::beforeTestSetupCall { testSelector: func.selector() },
699+
)
700+
.beforeTestCalldata
701+
{
702+
// Apply before test configured calldata.
703+
match executor.to_mut().transact_raw(self.sender, address, calldata, U256::ZERO) {
704+
Ok(call_result) => {
705+
// Merge tx result traces in unit test result.
706+
test_result.merge_call_result(&call_result);
707+
708+
// To continue unit test execution the call should not revert.
709+
if call_result.reverted {
710+
return Err(test_result.single_fail(None))
711+
}
712+
}
713+
Err(_) => return Err(test_result.single_fail(None)),
714+
}
715+
}
716+
}
717+
Ok((executor, test_result, address))
718+
}
653719
}

crates/forge/tests/it/repros.rs

+3
Original file line numberDiff line numberDiff line change
@@ -364,3 +364,6 @@ test_repro!(8168);
364364

365365
// https://github.com/foundry-rs/foundry/issues/8383
366366
test_repro!(8383);
367+
368+
// https://github.com/foundry-rs/foundry/issues/1543
369+
test_repro!(1543);

0 commit comments

Comments
 (0)