Skip to content

Commit 0e9875f

Browse files
committed
fix(forge): enable fail fast flag
1 parent 2347fc4 commit 0e9875f

File tree

7 files changed

+156
-14
lines changed

7 files changed

+156
-14
lines changed

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use crate::executors::{DURATION_BETWEEN_METRICS_REPORT, Executor, FuzzTestTimer, RawCallResult};
1+
use crate::executors::{
2+
DURATION_BETWEEN_METRICS_REPORT, Executor, FailFast, FuzzTestTimer, RawCallResult,
3+
};
24
use alloy_dyn_abi::JsonAbiExt;
35
use alloy_json_abi::Function;
46
use alloy_primitives::{Address, Bytes, Log, U256, map::HashMap};
@@ -90,6 +92,7 @@ impl FuzzedExecutor {
9092
/// test case.
9193
///
9294
/// Returns a list of all the consumed gas and calldata of every fuzz case.
95+
#[allow(clippy::too_many_arguments)]
9396
pub fn fuzz(
9497
&mut self,
9598
func: &Function,
@@ -98,6 +101,7 @@ impl FuzzedExecutor {
98101
address: Address,
99102
rd: &RevertDecoder,
100103
progress: Option<&ProgressBar>,
104+
fail_fast: &FailFast,
101105
) -> Result<FuzzTestResult> {
102106
// Stores the fuzz test execution data.
103107
let mut test_data = FuzzTestData::default();
@@ -128,6 +132,10 @@ impl FuzzedExecutor {
128132
let mut last_metrics_report = Instant::now();
129133
let max_runs = self.config.runs;
130134
let continue_campaign = |runs: u32| {
135+
if fail_fast.should_stop() {
136+
return false;
137+
}
138+
131139
if timer.is_enabled() { !timer.is_timed_out() } else { runs < max_runs }
132140
};
133141

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ use serde_json::json;
4949

5050
mod shrink;
5151
use crate::executors::{
52-
DURATION_BETWEEN_METRICS_REPORT, EvmError, FuzzTestTimer, corpus::CorpusManager,
52+
DURATION_BETWEEN_METRICS_REPORT, EvmError, FailFast, FuzzTestTimer, corpus::CorpusManager,
5353
};
5454
pub use shrink::check_sequence;
5555

@@ -327,6 +327,7 @@ impl<'a> InvariantExecutor<'a> {
327327
fuzz_fixtures: &FuzzFixtures,
328328
deployed_libs: &[Address],
329329
progress: Option<&ProgressBar>,
330+
fail_fast: &FailFast,
330331
) -> Result<InvariantFuzzTestResult> {
331332
// Throw an error to abort test run if the invariant function accepts input params
332333
if !invariant_contract.invariant_function.inputs.is_empty() {
@@ -341,6 +342,10 @@ impl<'a> InvariantExecutor<'a> {
341342
let timer = FuzzTestTimer::new(self.config.timeout);
342343
let mut last_metrics_report = Instant::now();
343344
let continue_campaign = |runs: u32| {
345+
if fail_fast.should_stop() {
346+
return false;
347+
}
348+
344349
if timer.is_enabled() { !timer.is_timed_out() } else { runs < self.config.runs }
345350
};
346351

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ use revm::{
4444
};
4545
use std::{
4646
borrow::Cow,
47+
sync::{
48+
Arc,
49+
atomic::{AtomicBool, Ordering},
50+
},
4751
time::{Duration, Instant},
4852
};
4953

@@ -1109,3 +1113,33 @@ impl FuzzTestTimer {
11091113
self.inner.is_some_and(|(start, duration)| start.elapsed() > duration)
11101114
}
11111115
}
1116+
1117+
/// Helper struct to enable fail fast behavior: when one test fails, all other tests stop early.
1118+
pub struct FailFast {
1119+
/// Shared atomic flag set to `true` when a failure occurs.
1120+
/// None if fail-fast is disabled.
1121+
inner: Option<Arc<AtomicBool>>,
1122+
}
1123+
1124+
impl FailFast {
1125+
pub fn new(fail_fast: bool) -> Self {
1126+
Self { inner: fail_fast.then_some(Arc::new(AtomicBool::new(false))) }
1127+
}
1128+
1129+
/// Returns `true` if fail-fast is enabled.
1130+
pub fn is_enabled(&self) -> bool {
1131+
self.inner.is_some()
1132+
}
1133+
1134+
/// Sets the failure flag. Used by other tests to stop early.
1135+
pub fn record_fail(&self) {
1136+
if let Some(fail_fast) = &self.inner {
1137+
fail_fast.store(true, Ordering::Relaxed);
1138+
}
1139+
}
1140+
1141+
/// Whether a failure has been recorded and test should stop.
1142+
pub fn should_stop(&self) -> bool {
1143+
self.inner.as_ref().map(|flag| flag.load(Ordering::Relaxed)).unwrap_or(false)
1144+
}
1145+
}

crates/forge/src/cmd/test/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -509,9 +509,10 @@ impl TestArgs {
509509
let (tx, rx) = channel::<(String, SuiteResult)>();
510510
let timer = Instant::now();
511511
let show_progress = config.show_progress;
512+
let fail_fast = self.fail_fast;
512513
let handle = tokio::task::spawn_blocking({
513514
let filter = filter.clone();
514-
move || runner.test(&filter, tx, show_progress)
515+
move || runner.test(&filter, tx, show_progress, fail_fast)
515516
});
516517

517518
// Set up trace identifiers.

crates/forge/src/multi_runner.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use foundry_evm::{
1818
Env,
1919
backend::Backend,
2020
decode::RevertDecoder,
21-
executors::{Executor, ExecutorBuilder},
21+
executors::{Executor, ExecutorBuilder, FailFast},
2222
fork::CreateFork,
2323
inspectors::CheatsConfig,
2424
opts::EvmOpts,
@@ -153,7 +153,7 @@ impl MultiContractRunner {
153153
filter: &dyn TestFilter,
154154
) -> Result<impl Iterator<Item = (String, SuiteResult)>> {
155155
let (tx, rx) = mpsc::channel();
156-
self.test(filter, tx, false)?;
156+
self.test(filter, tx, false, false)?;
157157
Ok(rx.into_iter())
158158
}
159159

@@ -168,6 +168,7 @@ impl MultiContractRunner {
168168
filter: &dyn TestFilter,
169169
tx: mpsc::Sender<(String, SuiteResult)>,
170170
show_progress: bool,
171+
fail_fast: bool,
171172
) -> Result<()> {
172173
let tokio_handle = tokio::runtime::Handle::current();
173174
trace!("running all tests");
@@ -185,6 +186,8 @@ impl MultiContractRunner {
185186
find_time,
186187
);
187188

189+
let fail_fast = FailFast::new(fail_fast);
190+
188191
if show_progress {
189192
let tests_progress = TestsProgress::new(contracts.len(), rayon::current_num_threads());
190193
// Collect test suite results to stream at the end of test run.
@@ -201,6 +204,7 @@ impl MultiContractRunner {
201204
filter,
202205
&tokio_handle,
203206
Some(&tests_progress),
207+
&fail_fast,
204208
);
205209

206210
tests_progress
@@ -220,14 +224,16 @@ impl MultiContractRunner {
220224
} else {
221225
contracts.par_iter().for_each(|&(id, contract)| {
222226
let _guard = tokio_handle.enter();
223-
let result = self.run_test_suite(id, contract, &db, filter, &tokio_handle, None);
227+
let result =
228+
self.run_test_suite(id, contract, &db, filter, &tokio_handle, None, &fail_fast);
224229
let _ = tx.send((id.identifier(), result));
225230
})
226231
}
227232

228233
Ok(())
229234
}
230235

236+
#[allow(clippy::too_many_arguments)]
231237
fn run_test_suite(
232238
&self,
233239
artifact_id: &ArtifactId,
@@ -236,6 +242,7 @@ impl MultiContractRunner {
236242
filter: &dyn TestFilter,
237243
tokio_handle: &tokio::runtime::Handle,
238244
progress: Option<&TestsProgress>,
245+
fail_fast: &FailFast,
239246
) -> SuiteResult {
240247
let identifier = artifact_id.identifier();
241248
let mut span_name = identifier.as_str();
@@ -259,7 +266,7 @@ impl MultiContractRunner {
259266
span,
260267
self,
261268
);
262-
let r = runner.run_tests(filter);
269+
let r = runner.run_tests(filter, fail_fast);
263270

264271
debug!(duration=?r.duration, "executed all tests in contract");
265272

crates/forge/src/runner.rs

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::{
55
fuzz::BaseCounterExample,
66
multi_runner::{TestContract, TestRunnerConfig, is_matching_test},
77
progress::{TestsProgress, start_fuzz_progress},
8-
result::{SuiteResult, TestResult, TestSetup},
8+
result::{SuiteResult, TestResult, TestSetup, TestStatus},
99
};
1010
use alloy_dyn_abi::{DynSolValue, JsonAbiExt};
1111
use alloy_json_abi::Function;
@@ -18,7 +18,7 @@ use foundry_evm::{
1818
constants::CALLER,
1919
decode::RevertDecoder,
2020
executors::{
21-
CallResult, EvmError, Executor, ITest, RawCallResult,
21+
CallResult, EvmError, Executor, FailFast, ITest, RawCallResult,
2222
fuzz::FuzzedExecutor,
2323
invariant::{
2424
InvariantExecutor, InvariantFuzzError, check_sequence, replay_error, replay_run,
@@ -279,7 +279,7 @@ impl<'a> ContractRunner<'a> {
279279
}
280280

281281
/// Runs all tests for a contract whose names match the provided regular expression
282-
pub fn run_tests(mut self, filter: &dyn TestFilter) -> SuiteResult {
282+
pub fn run_tests(mut self, filter: &dyn TestFilter, fail_fast: &FailFast) -> SuiteResult {
283283
let start = Instant::now();
284284
let mut warnings = Vec::new();
285285

@@ -404,6 +404,11 @@ impl<'a> ContractRunner<'a> {
404404
let test_results = functions
405405
.par_iter()
406406
.map(|&func| {
407+
// Early exit if we're running with fail-fast and a test already failed.
408+
if fail_fast.should_stop() {
409+
return (func.signature(), TestResult::setup_result(setup.clone()));
410+
}
411+
407412
let start = Instant::now();
408413

409414
let _guard = self.tokio_handle.enter();
@@ -429,9 +434,15 @@ impl<'a> ContractRunner<'a> {
429434
kind,
430435
call_after_invariant,
431436
identified_contracts.as_ref(),
437+
fail_fast,
432438
);
433439
res.duration = start.elapsed();
434440

441+
// Set fail fast flag if current test failed.
442+
if res.status.is_failure() {
443+
fail_fast.record_fail();
444+
}
445+
435446
(sig, res)
436447
})
437448
.collect::<BTreeMap<_, _>>();
@@ -501,6 +512,7 @@ impl<'a> FunctionRunner<'a> {
501512
kind: TestFunctionKind,
502513
call_after_invariant: bool,
503514
identified_contracts: Option<&ContractsByAddress>,
515+
fail_fast: &FailFast,
504516
) -> TestResult {
505517
if let Err(e) = self.apply_function_inline_config(func) {
506518
self.result.single_fail(Some(e.to_string()));
@@ -509,15 +521,16 @@ impl<'a> FunctionRunner<'a> {
509521

510522
match kind {
511523
TestFunctionKind::UnitTest { .. } => self.run_unit_test(func),
512-
TestFunctionKind::FuzzTest { .. } => self.run_fuzz_test(func),
513-
TestFunctionKind::TableTest => self.run_table_test(func),
524+
TestFunctionKind::FuzzTest { .. } => self.run_fuzz_test(func, fail_fast),
525+
TestFunctionKind::TableTest => self.run_table_test(func, fail_fast),
514526
TestFunctionKind::InvariantTest => {
515527
let test_bytecode = &self.cr.contract.bytecode;
516528
self.run_invariant_test(
517529
func,
518530
call_after_invariant,
519531
identified_contracts.unwrap(),
520532
test_bytecode,
533+
fail_fast,
521534
)
522535
}
523536
_ => unreachable!(),
@@ -573,7 +586,7 @@ impl<'a> FunctionRunner<'a> {
573586
/// - `uint256[] public fixtureAmount = [2, 5]`
574587
/// - `bool[] public fixtureSwap = [true, false]` The `table_test` is then called with the pair
575588
/// of args `(2, true)` and `(5, false)`.
576-
fn run_table_test(mut self, func: &Function) -> TestResult {
589+
fn run_table_test(mut self, func: &Function, fail_fast: &FailFast) -> TestResult {
577590
// Prepare unit test execution.
578591
if self.prepare_test(func).is_err() {
579592
return self.result;
@@ -628,7 +641,12 @@ impl<'a> FunctionRunner<'a> {
628641
fixtures_len as u32,
629642
);
630643

644+
self.result.status = TestStatus::Success;
631645
for i in 0..fixtures_len {
646+
if fail_fast.should_stop() {
647+
return self.result;
648+
}
649+
632650
// Increment progress bar.
633651
if let Some(progress) = progress.as_ref() {
634652
progress.inc(1);
@@ -686,6 +704,7 @@ impl<'a> FunctionRunner<'a> {
686704
call_after_invariant: bool,
687705
identified_contracts: &ContractsByAddress,
688706
test_bytecode: &Bytes,
707+
fail_fast: &FailFast,
689708
) -> TestResult {
690709
// First, run the test normally to see if it needs to be skipped.
691710
if let Err(EvmError::Skip(reason)) = self.executor.call(
@@ -800,6 +819,7 @@ impl<'a> FunctionRunner<'a> {
800819
&self.setup.fuzz_fixtures,
801820
&self.setup.deployed_libs,
802821
progress.as_ref(),
822+
fail_fast,
803823
) {
804824
Ok(x) => x,
805825
Err(e) => {
@@ -910,7 +930,7 @@ impl<'a> FunctionRunner<'a> {
910930
/// (therefore the fuzz test will use the modified state).
911931
/// State modifications of before test txes and fuzz test are discarded after test ends,
912932
/// similar to `eth_call`.
913-
fn run_fuzz_test(mut self, func: &Function) -> TestResult {
933+
fn run_fuzz_test(mut self, func: &Function, fail_fast: &FailFast) -> TestResult {
914934
// Prepare fuzz test execution.
915935
if self.prepare_test(func).is_err() {
916936
return self.result;
@@ -950,6 +970,7 @@ impl<'a> FunctionRunner<'a> {
950970
self.address,
951971
&self.cr.mcr.revert_decoder,
952972
progress.as_ref(),
973+
fail_fast,
953974
) {
954975
Ok(x) => x,
955976
Err(e) => {

0 commit comments

Comments
 (0)