Skip to content

Commit

Permalink
Changes after review:
Browse files Browse the repository at this point in the history
- do not unwrap func
- check if `beforeTestSelectors` exists
- move logic in prepare_unit_test fn
  • Loading branch information
grandizzy committed Jul 23, 2024
1 parent 0f41e8d commit 887ce66
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 60 deletions.
15 changes: 15 additions & 0 deletions crates/common/src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ pub trait TestFunctionExt {
matches!(self.test_function_kind(), TestFunctionKind::UnitTest { .. })
}

/// Returns `true` if this function is a `beforeTestSelectors` function.
fn is_before_test(&self) -> bool {
self.test_function_kind().is_before_test()
}

/// Returns `true` if this function is a fuzz test.
fn is_fuzz_test(&self) -> bool {
self.test_function_kind().is_fuzz_test()
Expand Down Expand Up @@ -115,6 +120,8 @@ pub enum TestFunctionKind {
AfterInvariant,
/// `fixture*`.
Fixture,
/// `beforeTestSelectors`.
BeforeTestSelectors,
/// Unknown kind.
Unknown,
}
Expand All @@ -138,6 +145,7 @@ impl TestFunctionKind {
_ if name.eq_ignore_ascii_case("setup") => Self::Setup,
_ if name.eq_ignore_ascii_case("afterinvariant") => Self::AfterInvariant,
_ if name.starts_with("fixture") => Self::Fixture,
_ if name.eq_ignore_ascii_case("beforeTestSelectors") => Self::BeforeTestSelectors,
_ => Self::Unknown,
}
}
Expand All @@ -153,6 +161,7 @@ impl TestFunctionKind {
Self::InvariantTest => "invariant",
Self::AfterInvariant => "afterInvariant",
Self::Fixture => "fixture",
Self::BeforeTestSelectors => "beforeTestSelectors",
Self::Unknown => "unknown",
}
}
Expand Down Expand Up @@ -181,6 +190,12 @@ impl TestFunctionKind {
matches!(self, Self::UnitTest { .. })
}

/// Returns `true` if this function is a `beforeTestSelectors` function.
#[inline]
pub const fn is_before_test(&self) -> bool {
matches!(self, Self::BeforeTestSelectors)
}

/// Returns `true` if this function is a fuzz test.
#[inline]
pub const fn is_fuzz_test(&self) -> bool {
Expand Down
156 changes: 96 additions & 60 deletions crates/forge/src/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,66 +426,12 @@ impl<'a> ContractRunner<'a> {
should_fail: bool,
setup: TestSetup,
) -> TestResult {
let address = setup.address;
let mut test_result = TestResult::new(setup);
let mut executor = self.executor.clone();

// Apply before test configured functions (if any).
for ITest::BeforeTestSelectors { test_selector, before_selectors } in
executor.call_sol_default(address, &ITest::beforeTestSelectorsCall {}).beforeSelectors
{
if test_selector == func.selector() {
for selector in before_selectors {
let before_func =
get_function(self.name, selector, &self.contract.abi).unwrap();

// Before test functions should be mutable without arguments and should not be
// invariant tests, fuzz tests or fixtures (other unit test within same contract
// or even current test are valid options).
if before_func.is_invariant_test() ||
before_func.is_fixture() ||
before_func.is_fuzz_test() ||
!before_func.inputs.is_empty() ||
matches!(
before_func.state_mutability,
alloy_json_abi::StateMutability::Pure |
alloy_json_abi::StateMutability::View
)
{
error!(
"function {} cannot be used as a before test selector",
before_func.signature()
);
} else {
// Apply before test call and collect results.
// To continue unit test execution the call should be success.
let (mut call_result, reason) = match executor.transact(
self.sender,
address,
before_func,
&[],
U256::ZERO,
Some(self.revert_decoder),
) {
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),
};

// Add before test call result traces to the unit test result.
test_result.merge_call_result(&call_result);

// Exit unit test if before test call is not success.
if !executor.is_raw_call_mut_success(address, &mut call_result, should_fail)
{
return test_result.single_result(false, reason, call_result);
}
}
}
break
}
}
// Prepare unit test execution.
let (executor, test_result, address) =
match self.prepare_unit_test(func, should_fail, setup) {
Ok(res) => res,
Err(res) => return res,
};

// Run current unit test.
let (mut raw_call_result, reason) = match executor.call(
Expand All @@ -506,6 +452,96 @@ impl<'a> ContractRunner<'a> {
test_result.single_result(success, reason, raw_call_result)
}

/// Prepares single unit test execution:
/// - set up the test result and executor
/// - check if before test functions are configured and apply them in order
///
/// A before test function is valid if it is mutable without arguments and is not
/// an invariant test, fuzz test or fixtures.
/// Unit tests within same contract (or even current test) are valid options.
///
/// Unit test execution stops if any of before test function fails.
fn prepare_unit_test(
&self,
func: &Function,
should_fail: bool,
setup: TestSetup,
) -> Result<(Executor, TestResult, Address), TestResult> {
let address = setup.address;
let mut executor = self.executor.clone();
let mut test_result = TestResult::new(setup);

// Apply before test configured functions (if any).
let before_test_fns: Vec<_> =
self.contract.abi.functions().filter(|func| func.name.is_before_test()).collect();
if before_test_fns.len() == 1 {
for ITest::BeforeTestSelectors { test_selector, before_selectors } in executor
.call_sol_default(address, &ITest::beforeTestSelectorsCall {})
.beforeSelectors
{
if test_selector == func.selector() {
for selector in before_selectors {
if let Ok(before_func) =
get_function(self.name, selector, &self.contract.abi)
{
if before_func.is_invariant_test() ||
before_func.is_fixture() ||
before_func.is_fuzz_test() ||
!before_func.inputs.is_empty() ||
matches!(
before_func.state_mutability,
alloy_json_abi::StateMutability::Pure |
alloy_json_abi::StateMutability::View
)
{
error!(
"function {} cannot be used as a before test selector",
before_func.signature()
);
} else {
// Apply before test call and collect results.
// To continue unit test execution the call should be success.
let (mut call_result, reason) = match executor.transact(
self.sender,
address,
before_func,
&[],
U256::ZERO,
Some(self.revert_decoder),
) {
Ok(res) => (res.raw, None),
Err(EvmError::Execution(err)) => (err.raw, Some(err.reason)),
Err(EvmError::SkipError) => {
return Err(test_result.single_skip())
}
Err(err) => return Err(test_result.single_fail(err)),
};

// Exit unit test if before test call is not success.
if !executor.is_raw_call_mut_success(
address,
&mut call_result,
should_fail,
) {
return Err(test_result.single_result(
false,
reason,
call_result,
));
}

// Add before test call result traces to the unit test result.
test_result.merge_call_result(&call_result);
}
}
}
break
}
}
}
Ok((executor, test_result, address))
}

#[allow(clippy::too_many_arguments)]
pub fn run_invariant_test(
&self,
Expand Down

0 comments on commit 887ce66

Please sign in to comment.