Skip to content

Commit bff4393

Browse files
committed
Remove proptest from fuzzed tests
1 parent 6a8410e commit bff4393

File tree

4 files changed

+99
-81
lines changed

4 files changed

+99
-81
lines changed

crates/evm/core/src/constants.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,6 @@ pub const MAGIC_ASSUME: &[u8] = b"FOUNDRY::ASSUME";
3737
/// Magic return value returned by the `skip` cheatcode. Optionally appended with a reason.
3838
pub const MAGIC_SKIP: &[u8] = b"FOUNDRY::SKIP";
3939

40-
/// Test timeout return value.
41-
pub const TEST_TIMEOUT: &str = "FOUNDRY::TEST_TIMEOUT";
42-
4340
/// The address that deploys the default CREATE2 deployer contract.
4441
pub const DEFAULT_CREATE2_DEPLOYER_DEPLOYER: Address =
4542
address!("0x3fAB184622Dc19b6109349B94811493BF2a45362");

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

Lines changed: 96 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use eyre::Result;
66
use foundry_common::evm::Breakpoints;
77
use foundry_config::FuzzConfig;
88
use foundry_evm_core::{
9-
constants::{CHEATCODE_ADDRESS, MAGIC_ASSUME, TEST_TIMEOUT},
9+
constants::{CHEATCODE_ADDRESS, MAGIC_ASSUME},
1010
decode::{RevertDecoder, SkipReason},
1111
};
1212
use foundry_evm_coverage::HitMaps;
@@ -16,7 +16,10 @@ use foundry_evm_fuzz::{
1616
};
1717
use foundry_evm_traces::SparsedTraceArena;
1818
use indicatif::ProgressBar;
19-
use proptest::test_runner::{TestCaseError, TestError, TestRunner};
19+
use proptest::{
20+
strategy::{Strategy, ValueTree},
21+
test_runner::{TestCaseError, TestRunner},
22+
};
2023
use std::{cell::RefCell, collections::BTreeMap};
2124

2225
mod types;
@@ -78,7 +81,7 @@ impl FuzzedExecutor {
7881
///
7982
/// Returns a list of all the consumed gas and calldata of every fuzz case
8083
pub fn fuzz(
81-
&self,
84+
&mut self,
8285
func: &Function,
8386
fuzz_fixtures: &FuzzFixtures,
8487
deployed_libs: &[Address],
@@ -101,69 +104,99 @@ impl FuzzedExecutor {
101104
// Start timer for this fuzz test.
102105
let timer = FuzzTestTimer::new(self.config.timeout);
103106

104-
let run_result = self.runner.clone().run(&strategy, |calldata| {
105-
// Check if the timeout has been reached.
106-
if timer.is_timed_out() {
107-
return Err(TestCaseError::fail(TEST_TIMEOUT));
107+
let continue_campaign = |runs: u32| {
108+
// If timeout is configured, then perform invariant runs until expires.
109+
if self.config.timeout.is_some() {
110+
return !timer.is_timed_out();
108111
}
112+
// If no timeout configured then loop until configured runs.
113+
runs < self.config.runs
114+
};
109115

110-
let fuzz_res = self.single_fuzz(address, calldata)?;
116+
let mut runs = 0;
117+
let mut rejects = 0;
118+
let mut run_failure = None;
119+
'stop: while continue_campaign(runs) {
120+
let Ok(strategy) = strategy.new_tree(&mut self.runner) else {
121+
run_failure = Some(TestCaseError::fail("no input generated to call fuzzed target"));
122+
break 'stop;
123+
};
111124

112125
// If running with progress then increment current run.
113126
if let Some(progress) = progress {
114127
progress.inc(1);
115128
};
116129

117-
match fuzz_res {
118-
FuzzOutcome::Case(case) => {
119-
let mut data = execution_data.borrow_mut();
120-
data.gas_by_case.push((case.case.gas, case.case.stipend));
130+
match self.single_fuzz(address, strategy.current()) {
131+
Ok(fuzz_outcome) => {
132+
match fuzz_outcome {
133+
FuzzOutcome::Case(case) => {
134+
let mut data = execution_data.borrow_mut();
135+
data.gas_by_case.push((case.case.gas, case.case.stipend));
121136

122-
if data.first_case.is_none() {
123-
data.first_case.replace(case.case);
124-
}
137+
if data.first_case.is_none() {
138+
data.first_case.replace(case.case);
139+
}
125140

126-
if let Some(call_traces) = case.traces {
127-
if data.traces.len() == max_traces_to_collect {
128-
data.traces.pop();
129-
}
130-
data.traces.push(call_traces);
131-
data.breakpoints.replace(case.breakpoints);
132-
}
133-
134-
if show_logs {
135-
data.logs.extend(case.logs);
136-
}
141+
if let Some(call_traces) = case.traces {
142+
if data.traces.len() == max_traces_to_collect {
143+
data.traces.pop();
144+
}
145+
data.traces.push(call_traces);
146+
data.breakpoints.replace(case.breakpoints);
147+
}
137148

138-
HitMaps::merge_opt(&mut data.coverage, case.coverage);
149+
if show_logs {
150+
data.logs.extend(case.logs);
151+
}
139152

140-
data.deprecated_cheatcodes = case.deprecated_cheatcodes;
141-
142-
Ok(())
153+
HitMaps::merge_opt(&mut data.coverage, case.coverage);
154+
data.deprecated_cheatcodes = case.deprecated_cheatcodes;
155+
}
156+
FuzzOutcome::CounterExample(CounterExampleOutcome {
157+
exit_reason: status,
158+
counterexample: outcome,
159+
..
160+
}) => {
161+
let reason = rd.maybe_decode(&outcome.1.result, status);
162+
execution_data.borrow_mut().logs.extend(outcome.1.logs.clone());
163+
execution_data.borrow_mut().counterexample = outcome;
164+
// HACK: we have to use an empty string here to denote `None`.
165+
run_failure = Some(TestCaseError::fail(reason.unwrap_or_default()));
166+
break 'stop;
167+
}
168+
}
143169
}
144-
FuzzOutcome::CounterExample(CounterExampleOutcome {
145-
exit_reason: status,
146-
counterexample: outcome,
147-
..
148-
}) => {
149-
// We cannot use the calldata returned by the test runner in `TestError::Fail`,
150-
// since that input represents the last run case, which may not correspond with
151-
// our failure - when a fuzz case fails, proptest will try to run at least one
152-
// more case to find a minimal failure case.
153-
let reason = rd.maybe_decode(&outcome.1.result, status);
154-
execution_data.borrow_mut().logs.extend(outcome.1.logs.clone());
155-
execution_data.borrow_mut().counterexample = outcome;
156-
// HACK: we have to use an empty string here to denote `None`.
157-
Err(TestCaseError::fail(reason.unwrap_or_default()))
170+
Err(err) => {
171+
match err {
172+
TestCaseError::Fail(_) => {
173+
run_failure = Some(err);
174+
break 'stop;
175+
}
176+
TestCaseError::Reject(_) => {
177+
// Apply max rejects only if configured, otherwise silently discard run.
178+
if self.config.max_test_rejects > 0 {
179+
rejects += 1;
180+
if rejects >= self.config.max_test_rejects {
181+
run_failure = Some(err);
182+
break 'stop;
183+
}
184+
} else {
185+
continue 'stop;
186+
}
187+
}
188+
}
158189
}
159190
}
160-
});
191+
192+
runs += 1;
193+
}
161194

162195
let fuzz_result = execution_data.into_inner();
163196
let (calldata, call) = fuzz_result.counterexample;
164197

165198
let mut traces = fuzz_result.traces;
166-
let (last_run_traces, last_run_breakpoints) = if run_result.is_ok() {
199+
let (last_run_traces, last_run_breakpoints) = if run_failure.is_none() {
167200
(traces.pop(), fuzz_result.breakpoints)
168201
} else {
169202
(call.traces.clone(), call.cheatcodes.map(|c| c.breakpoints))
@@ -172,7 +205,7 @@ impl FuzzedExecutor {
172205
let mut result = FuzzTestResult {
173206
first_case: fuzz_result.first_case.unwrap_or_default(),
174207
gas_by_case: fuzz_result.gas_by_case,
175-
success: run_result.is_ok(),
208+
success: run_failure.is_none(),
176209
skipped: false,
177210
reason: None,
178211
counterexample: None,
@@ -185,38 +218,24 @@ impl FuzzedExecutor {
185218
deprecated_cheatcodes: fuzz_result.deprecated_cheatcodes,
186219
};
187220

188-
match run_result {
189-
Ok(()) => {}
190-
Err(TestError::Abort(reason)) => {
191-
let msg = reason.message();
192-
// Currently the only operation that can trigger proptest global rejects is the
193-
// `vm.assume` cheatcode, thus we surface this info to the user when the fuzz test
194-
// aborts due to too many global rejects, making the error message more actionable.
195-
result.reason = if msg == "Too many global rejects" {
196-
let error = FuzzError::TooManyRejects(self.runner.config().max_global_rejects);
197-
Some(error.to_string())
221+
match run_failure {
222+
Some(TestCaseError::Fail(reason)) => {
223+
let reason = reason.to_string();
224+
result.reason = (!reason.is_empty()).then_some(reason);
225+
let args = if let Some(data) = calldata.get(4..) {
226+
func.abi_decode_input(data).unwrap_or_default()
198227
} else {
199-
Some(msg.to_string())
228+
vec![]
200229
};
230+
result.counterexample = Some(CounterExample::Single(
231+
BaseCounterExample::from_fuzz_call(calldata, args, call.traces),
232+
));
201233
}
202-
Err(TestError::Fail(reason, _)) => {
234+
Some(TestCaseError::Reject(reason)) => {
203235
let reason = reason.to_string();
204-
if reason == TEST_TIMEOUT {
205-
// If the reason is a timeout, we consider the fuzz test successful.
206-
result.success = true;
207-
} else {
208-
result.reason = (!reason.is_empty()).then_some(reason);
209-
let args = if let Some(data) = calldata.get(4..) {
210-
func.abi_decode_input(data).unwrap_or_default()
211-
} else {
212-
vec![]
213-
};
214-
215-
result.counterexample = Some(CounterExample::Single(
216-
BaseCounterExample::from_fuzz_call(calldata, args, call.traces),
217-
));
218-
}
236+
result.reason = (!reason.is_empty()).then_some(reason);
219237
}
238+
None => {}
220239
}
221240

222241
if let Some(reason) = &result.reason
@@ -245,7 +264,9 @@ impl FuzzedExecutor {
245264

246265
// Handle `vm.assume`.
247266
if call.result.as_ref() == MAGIC_ASSUME {
248-
return Err(TestCaseError::reject(FuzzError::AssumeReject));
267+
return Err(TestCaseError::reject(FuzzError::TooManyRejects(
268+
self.config.max_test_rejects,
269+
)));
249270
}
250271

251272
let (breakpoints, deprecated_cheatcodes) =

crates/forge/src/runner.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -930,7 +930,7 @@ impl<'a> FunctionRunner<'a> {
930930
);
931931

932932
// Run fuzz test.
933-
let fuzzed_executor =
933+
let mut fuzzed_executor =
934934
FuzzedExecutor::new(self.executor.into_owned(), runner, self.tcfg.sender, fuzz_config);
935935
let result = fuzzed_executor.fuzz(
936936
func,

crates/forge/tests/cli/test_cmd.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -793,14 +793,14 @@ contract CounterTest is Test {
793793
Compiler run successful!
794794
795795
Ran 1 test for test/CounterFuzz.t.sol:CounterTest
796-
[FAIL: panic: arithmetic underflow or overflow (0x11); counterexample: calldata=0xa76d58f5fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd args=[115792089237316195423570985008687907853269984665640564039457584007913129639933 [1.157e77]]] testAddOne(uint256) (runs: 84, [AVG_GAS])
796+
[FAIL: panic: arithmetic underflow or overflow (0x11); counterexample: calldata=0xa76d58f5fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe args=[115792089237316195423570985008687907853269984665640564039457584007913129639934 [1.157e77]]] testAddOne(uint256) (runs: 27, [AVG_GAS])
797797
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED]
798798
799799
Ran 1 test suite [ELAPSED]: 0 tests passed, 1 failed, 0 skipped (1 total tests)
800800
801801
Failing tests:
802802
Encountered 1 failing test in test/CounterFuzz.t.sol:CounterTest
803-
[FAIL: panic: arithmetic underflow or overflow (0x11); counterexample: calldata=0xa76d58f5fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd args=[115792089237316195423570985008687907853269984665640564039457584007913129639933 [1.157e77]]] testAddOne(uint256) (runs: 84, [AVG_GAS])
803+
[FAIL: panic: arithmetic underflow or overflow (0x11); counterexample: calldata=0xa76d58f5fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe args=[115792089237316195423570985008687907853269984665640564039457584007913129639934 [1.157e77]]] testAddOne(uint256) (runs: 27, [AVG_GAS])
804804
805805
Encountered a total of 1 failing tests, 0 tests succeeded
806806

0 commit comments

Comments
 (0)