diff --git a/crates/common/src/traits.rs b/crates/common/src/traits.rs index 606b4861af92..f5f3ea14ce46 100644 --- a/crates/common/src/traits.rs +++ b/crates/common/src/traits.rs @@ -44,6 +44,11 @@ pub trait TestFunctionExt { matches!(self.test_function_kind(), TestFunctionKind::UnitTest { .. }) } + /// Returns `true` if this function is a `beforeTestSetup` function. + fn is_before_test_setup(&self) -> bool { + self.tfe_as_str().eq_ignore_ascii_case("beforetestsetup") + } + /// Returns `true` if this function is a fuzz test. fn is_fuzz_test(&self) -> bool { self.test_function_kind().is_fuzz_test() diff --git a/crates/evm/evm/src/executors/invariant/mod.rs b/crates/evm/evm/src/executors/invariant/mod.rs index 7dcdc15680e1..91143f4b232e 100644 --- a/crates/evm/evm/src/executors/invariant/mod.rs +++ b/crates/evm/evm/src/executors/invariant/mod.rs @@ -532,6 +532,7 @@ impl<'a> InvariantExecutor<'a> { /// targetArtifactSelectors > excludeArtifacts > targetArtifacts pub fn select_contract_artifacts(&mut self, invariant_address: Address) -> Result<()> { let result = self + .executor .call_sol_default(invariant_address, &IInvariantTest::targetArtifactSelectorsCall {}); // Insert them into the executor `targeted_abi`. @@ -542,10 +543,12 @@ impl<'a> InvariantExecutor<'a> { self.artifact_filters.targeted.entry(identifier).or_default().extend(selectors); } - let selected = - self.call_sol_default(invariant_address, &IInvariantTest::targetArtifactsCall {}); - let excluded = - self.call_sol_default(invariant_address, &IInvariantTest::excludeArtifactsCall {}); + let selected = self + .executor + .call_sol_default(invariant_address, &IInvariantTest::targetArtifactsCall {}); + let excluded = self + .executor + .call_sol_default(invariant_address, &IInvariantTest::excludeArtifactsCall {}); // Insert `excludeArtifacts` into the executor `excluded_abi`. for contract in excluded.excludedArtifacts { @@ -620,10 +623,14 @@ impl<'a> InvariantExecutor<'a> { &self, to: Address, ) -> Result<(SenderFilters, FuzzRunIdentifiedContracts)> { - let targeted_senders = - self.call_sol_default(to, &IInvariantTest::targetSendersCall {}).targetedSenders; - let mut excluded_senders = - self.call_sol_default(to, &IInvariantTest::excludeSendersCall {}).excludedSenders; + let targeted_senders = self + .executor + .call_sol_default(to, &IInvariantTest::targetSendersCall {}) + .targetedSenders; + let mut excluded_senders = self + .executor + .call_sol_default(to, &IInvariantTest::excludeSendersCall {}) + .excludedSenders; // Extend with default excluded addresses - https://github.com/foundry-rs/foundry/issues/4163 excluded_senders.extend([ CHEATCODE_ADDRESS, @@ -634,10 +641,14 @@ impl<'a> InvariantExecutor<'a> { excluded_senders.extend(PRECOMPILES); let sender_filters = SenderFilters::new(targeted_senders, excluded_senders); - let selected = - self.call_sol_default(to, &IInvariantTest::targetContractsCall {}).targetedContracts; - let excluded = - self.call_sol_default(to, &IInvariantTest::excludeContractsCall {}).excludedContracts; + let selected = self + .executor + .call_sol_default(to, &IInvariantTest::targetContractsCall {}) + .targetedContracts; + let excluded = self + .executor + .call_sol_default(to, &IInvariantTest::excludeContractsCall {}) + .excludedContracts; let contracts = self .setup_contracts @@ -678,6 +689,7 @@ impl<'a> InvariantExecutor<'a> { targeted_contracts: &mut TargetedContracts, ) -> Result<()> { let interfaces = self + .executor .call_sol_default(invariant_address, &IInvariantTest::targetInterfacesCall {}) .targetedInterfaces; @@ -735,13 +747,15 @@ impl<'a> InvariantExecutor<'a> { } // Collect contract functions marked as target for fuzzing campaign. - let selectors = self.call_sol_default(address, &IInvariantTest::targetSelectorsCall {}); + let selectors = + self.executor.call_sol_default(address, &IInvariantTest::targetSelectorsCall {}); for IInvariantTest::FuzzSelector { addr, selectors } in selectors.targetedSelectors { self.add_address_with_functions(addr, &selectors, false, targeted_contracts)?; } // Collect contract functions excluded from fuzzing campaign. - let selectors = self.call_sol_default(address, &IInvariantTest::excludeSelectorsCall {}); + let selectors = + self.executor.call_sol_default(address, &IInvariantTest::excludeSelectorsCall {}); for IInvariantTest::FuzzSelector { addr, selectors } in selectors.excludedSelectors { self.add_address_with_functions(addr, &selectors, true, targeted_contracts)?; } @@ -773,17 +787,6 @@ impl<'a> InvariantExecutor<'a> { contract.add_selectors(selectors.iter().copied(), should_exclude)?; Ok(()) } - - fn call_sol_default(&self, to: Address, args: &C) -> C::Return - where - C::Return: Default, - { - self.executor - .call_sol(CALLER, to, args, U256::ZERO, None) - .map(|c| c.decoded_result) - .inspect_err(|e| warn!(target: "forge::test", "failed calling {:?}: {e}", C::SIGNATURE)) - .unwrap_or_default() - } } /// Collects data from call for fuzzing. However, it first verifies that the sender is not an EOA diff --git a/crates/evm/evm/src/executors/mod.rs b/crates/evm/evm/src/executors/mod.rs index 66010a33cfc7..2f08b2f42efd 100644 --- a/crates/evm/evm/src/executors/mod.rs +++ b/crates/evm/evm/src/executors/mod.rs @@ -50,6 +50,9 @@ sol! { interface ITest { function setUp() external; function failed() external view returns (bool failed); + + #[derive(Default)] + function beforeTestSetup(bytes4 testSelector) public view returns (bytes[] memory beforeTestCalldata); } } @@ -602,6 +605,16 @@ impl Executor { EnvWithHandlerCfg::new_with_spec_id(Box::new(env), self.spec_id()) } + + pub fn call_sol_default(&self, to: Address, args: &C) -> C::Return + where + C::Return: Default, + { + self.call_sol(CALLER, to, args, U256::ZERO, None) + .map(|c| c.decoded_result) + .inspect_err(|e| warn!(target: "forge::test", "failed calling {:?}: {e}", C::SIGNATURE)) + .unwrap_or_default() + } } /// Represents the context after an execution error occurred. diff --git a/crates/forge/src/result.rs b/crates/forge/src/result.rs index 8352432e4769..e7953575f3b5 100644 --- a/crates/forge/src/result.rs +++ b/crates/forge/src/result.rs @@ -449,9 +449,9 @@ impl TestResult { } /// Returns the failed result with reason for single test. - pub fn single_fail(mut self, err: EvmError) -> Self { + pub fn single_fail(mut self, reason: Option) -> Self { self.status = TestStatus::Failure; - self.reason = Some(err.to_string()); + self.reason = reason; self } @@ -579,6 +579,14 @@ impl TestResult { format!("{self} {name} {}", self.kind.report()) } + /// Function to merge logs, addresses, traces and coverage from a call result into test result. + pub fn merge_call_result(&mut self, call_result: &RawCallResult) { + self.logs.extend(call_result.logs.clone()); + self.labeled_addresses.extend(call_result.labels.clone()); + self.traces.extend(call_result.traces.clone().map(|traces| (TraceKind::Execution, traces))); + self.merge_coverages(call_result.coverage.clone()); + } + /// Function to merge given coverage in current test result coverage. pub fn merge_coverages(&mut self, other_coverage: Option) { let old_coverage = std::mem::take(&mut self.coverage); diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index 2198921be605..6ed06e525e39 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -24,7 +24,7 @@ use foundry_evm::{ invariant::{ check_sequence, replay_error, replay_run, InvariantExecutor, InvariantFuzzError, }, - CallResult, EvmError, ExecutionErr, Executor, RawCallResult, + CallResult, EvmError, ExecutionErr, Executor, ITest, RawCallResult, }, fuzz::{ fixture_name, @@ -36,6 +36,7 @@ use foundry_evm::{ use proptest::test_runner::TestRunner; use rayon::prelude::*; use std::{ + borrow::Cow, cmp::min, collections::{BTreeMap, HashMap}, time::Instant, @@ -270,6 +271,7 @@ impl<'a> ContractRunner<'a> { )); } } + // There are multiple setUp function, so we return a single test result for `setUp` if setup_fns.len() > 1 { return SuiteResult::new( @@ -412,21 +414,26 @@ impl<'a> ContractRunner<'a> { /// Runs a single unit test. /// - /// Calls the given functions and returns the `TestResult`. + /// Applies before test txes (if any), runs current test and returns the `TestResult`. /// - /// State modifications are not committed to the evm database but discarded after the call, - /// similar to `eth_call`. + /// Before test txes are applied in order and state modifications committed to the EVM database + /// (therefore the unit test call will be made on modified state). + /// State modifications of before test txes and unit test function call are discarded after + /// test ends, similar to `eth_call`. pub fn run_unit_test( &self, func: &Function, should_fail: bool, setup: TestSetup, ) -> TestResult { - let address = setup.address; - let test_result = TestResult::new(setup); + // Prepare unit test execution. + let (executor, test_result, address) = match self.prepare_test(func, setup) { + Ok(res) => res, + Err(res) => return res, + }; - // Run unit test - let (mut raw_call_result, reason) = match self.executor.call( + // Run current unit test. + let (mut raw_call_result, reason) = match executor.call( self.sender, address, func, @@ -437,11 +444,10 @@ impl<'a> ContractRunner<'a> { Ok(res) => (res.raw, None), Err(EvmError::Execution(err)) => (err.raw, Some(err.reason)), Err(EvmError::SkipError) => return test_result.single_skip(), - Err(err) => return test_result.single_fail(err), + Err(err) => return test_result.single_fail(Some(err.to_string())), }; - let success = - self.executor.is_raw_call_mut_success(address, &mut raw_call_result, should_fail); + let success = executor.is_raw_call_mut_success(address, &mut raw_call_result, should_fail); test_result.single_result(success, reason, raw_call_result) } @@ -618,6 +624,15 @@ impl<'a> ContractRunner<'a> { ) } + /// Runs a fuzzed test. + /// + /// Applies the before test txes (if any), fuzzes the current function and returns the + /// `TestResult`. + /// + /// Before test txes are applied in order and state modifications committed to the EVM database + /// (therefore the fuzz test will use the modified state). + /// State modifications of before test txes and fuzz test are discarded after test ends, + /// similar to `eth_call`. pub fn run_fuzz_test( &self, func: &Function, @@ -626,14 +641,18 @@ impl<'a> ContractRunner<'a> { setup: TestSetup, fuzz_config: FuzzConfig, ) -> TestResult { - let address = setup.address; + 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 test_result = TestResult::new(setup); + let (executor, test_result, address) = match self.prepare_test(func, setup) { + Ok(res) => res, + Err(res) => return res, + }; - // Run fuzz test - let progress = start_fuzz_progress(self.progress, self.name, &func.name, fuzz_config.runs); + // Run fuzz test. let fuzzed_executor = - FuzzedExecutor::new(self.executor.clone(), runner, self.sender, fuzz_config); + FuzzedExecutor::new(executor.into_owned(), runner, self.sender, fuzz_config); let result = fuzzed_executor.fuzz( func, &fuzz_fixtures, @@ -650,4 +669,51 @@ impl<'a> ContractRunner<'a> { } test_result.fuzz_result(result) } + + /// Prepares single unit test and fuzz test execution: + /// - set up the test result and executor + /// - check if before test txes are configured and apply them in order + /// + /// Before test txes are arrays of arbitrary calldata obtained by calling the `beforeTest` + /// function with test selector as a parameter. + /// + /// Unit tests within same contract (or even current test) are valid options for before test tx + /// configuration. Test execution stops if any of before test txes fails. + fn prepare_test( + &self, + func: &Function, + setup: TestSetup, + ) -> Result<(Cow<'_, Executor>, TestResult, Address), TestResult> { + let address = setup.address; + let mut executor = Cow::Borrowed(&self.executor); + let mut test_result = TestResult::new(setup); + + // Apply before test configured functions (if any). + if self.contract.abi.functions().filter(|func| func.name.is_before_test_setup()).count() == + 1 + { + for calldata in executor + .call_sol_default( + address, + &ITest::beforeTestSetupCall { testSelector: func.selector() }, + ) + .beforeTestCalldata + { + // Apply before test configured calldata. + match executor.to_mut().transact_raw(self.sender, address, calldata, U256::ZERO) { + Ok(call_result) => { + // Merge tx result traces in unit test result. + test_result.merge_call_result(&call_result); + + // To continue unit test execution the call should not revert. + if call_result.reverted { + return Err(test_result.single_fail(None)) + } + } + Err(_) => return Err(test_result.single_fail(None)), + } + } + } + Ok((executor, test_result, address)) + } } diff --git a/crates/forge/tests/it/repros.rs b/crates/forge/tests/it/repros.rs index 1268300e4427..a74933f6f9a1 100644 --- a/crates/forge/tests/it/repros.rs +++ b/crates/forge/tests/it/repros.rs @@ -364,3 +364,6 @@ test_repro!(8168); // https://github.com/foundry-rs/foundry/issues/8383 test_repro!(8383); + +// https://github.com/foundry-rs/foundry/issues/1543 +test_repro!(1543); diff --git a/testdata/default/repros/Issue1543.t.sol b/testdata/default/repros/Issue1543.t.sol new file mode 100644 index 000000000000..e8b4806ed120 --- /dev/null +++ b/testdata/default/repros/Issue1543.t.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity 0.8.18; + +import "ds-test/test.sol"; + +contract SelfDestructor { + function kill() external { + selfdestruct(payable(msg.sender)); + } +} + +// https://github.com/foundry-rs/foundry/issues/1543 +contract Issue1543Test is DSTest { + SelfDestructor killer; + uint256 a; + uint256 b; + + function setUp() public { + killer = new SelfDestructor(); + } + + function beforeTestSetup(bytes4 testSelector) public pure returns (bytes[] memory beforeTestCalldata) { + if (testSelector == this.testKill.selector) { + beforeTestCalldata = new bytes[](1); + beforeTestCalldata[0] = abi.encodePacked(this.kill_contract.selector); + } + + if (testSelector == this.testA.selector) { + beforeTestCalldata = new bytes[](3); + beforeTestCalldata[0] = abi.encodePacked(this.testA.selector); + beforeTestCalldata[1] = abi.encodePacked(this.testA.selector); + beforeTestCalldata[2] = abi.encodePacked(this.testA.selector); + } + + if (testSelector == this.testB.selector) { + beforeTestCalldata = new bytes[](1); + beforeTestCalldata[0] = abi.encodePacked(this.setB.selector); + } + + if (testSelector == this.testC.selector) { + beforeTestCalldata = new bytes[](2); + beforeTestCalldata[0] = abi.encodePacked(this.testA.selector); + beforeTestCalldata[1] = abi.encodeWithSignature("setBWithValue(uint256)", 111); + } + } + + function kill_contract() external { + uint256 killer_size = getSize(address(killer)); + require(killer_size == 106); + killer.kill(); + } + + function testKill() public view { + uint256 killer_size = getSize(address(killer)); + require(killer_size == 0); + } + + function getSize(address c) public view returns (uint32) { + uint32 size; + assembly { + size := extcodesize(c) + } + return size; + } + + function testA() public { + require(a <= 3); + a += 1; + } + + function testSimpleA() public view { + require(a == 0); + } + + function setB() public { + b = 100; + } + + function testB() public { + require(b == 100); + } + + function setBWithValue(uint256 value) public { + b = value; + } + + function testC(uint256 h) public { + assertEq(a, 1); + assertEq(b, 111); + } +}