Skip to content

Commit 8c2ae1d

Browse files
authored
feat(forge): exit test run gracefully if show progress (#12290)
feat(forge): exit test gracefully if show progress
1 parent d0f0422 commit 8c2ae1d

File tree

8 files changed

+54
-35
lines changed

8 files changed

+54
-35
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::executors::{
2-
DURATION_BETWEEN_METRICS_REPORT, Executor, FailFast, FuzzTestTimer, RawCallResult,
2+
DURATION_BETWEEN_METRICS_REPORT, EarlyExit, Executor, FuzzTestTimer, RawCallResult,
33
};
44
use alloy_dyn_abi::JsonAbiExt;
55
use alloy_json_abi::Function;
@@ -102,7 +102,7 @@ impl FuzzedExecutor {
102102
address: Address,
103103
rd: &RevertDecoder,
104104
progress: Option<&ProgressBar>,
105-
fail_fast: &FailFast,
105+
early_exit: &EarlyExit,
106106
) -> Result<FuzzTestResult> {
107107
// Stores the fuzz test execution data.
108108
let mut test_data = FuzzTestData::default();
@@ -132,7 +132,7 @@ impl FuzzedExecutor {
132132
let mut last_metrics_report = Instant::now();
133133
let max_runs = self.config.runs;
134134
let continue_campaign = |runs: u32| {
135-
if fail_fast.should_stop() {
135+
if early_exit.should_stop() {
136136
return false;
137137
}
138138

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ use serde_json::json;
5252

5353
mod shrink;
5454
use crate::executors::{
55-
DURATION_BETWEEN_METRICS_REPORT, EvmError, FailFast, FuzzTestTimer, corpus::CorpusManager,
55+
DURATION_BETWEEN_METRICS_REPORT, EarlyExit, EvmError, FuzzTestTimer, corpus::CorpusManager,
5656
};
5757
pub use shrink::check_sequence;
5858

@@ -330,7 +330,7 @@ impl<'a> InvariantExecutor<'a> {
330330
fuzz_fixtures: &FuzzFixtures,
331331
deployed_libs: &[Address],
332332
progress: Option<&ProgressBar>,
333-
fail_fast: &FailFast,
333+
early_exit: &EarlyExit,
334334
) -> Result<InvariantFuzzTestResult> {
335335
// Throw an error to abort test run if the invariant function accepts input params
336336
if !invariant_contract.invariant_function.inputs.is_empty() {
@@ -345,7 +345,7 @@ impl<'a> InvariantExecutor<'a> {
345345
let timer = FuzzTestTimer::new(self.config.timeout);
346346
let mut last_metrics_report = Instant::now();
347347
let continue_campaign = |runs: u32| {
348-
if fail_fast.should_stop() {
348+
if early_exit.should_stop() {
349349
return false;
350350
}
351351

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use super::{
22
call_after_invariant_function, call_invariant_function, error::FailedInvariantCaseData,
33
shrink_sequence,
44
};
5-
use crate::executors::Executor;
5+
use crate::executors::{EarlyExit, Executor};
66
use alloy_dyn_abi::JsonAbiExt;
77
use alloy_primitives::{Log, U256, map::HashMap};
88
use eyre::Result;
@@ -109,6 +109,7 @@ pub fn replay_error(
109109
deprecated_cheatcodes: &mut HashMap<&'static str, Option<&'static str>>,
110110
progress: Option<&ProgressBar>,
111111
show_solidity: bool,
112+
early_exit: &EarlyExit,
112113
) -> Result<Vec<BaseCounterExample>> {
113114
match failed_case.test_error {
114115
// Don't use at the moment.
@@ -121,6 +122,7 @@ pub fn replay_error(
121122
&executor,
122123
invariant_contract.call_after_invariant,
123124
progress,
125+
early_exit,
124126
)?;
125127

126128
set_up_inner_replay(&mut executor, &failed_case.inner_sequence);

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::executors::{
2-
Executor,
2+
EarlyExit, Executor,
33
invariant::{
44
call_after_invariant_function, call_invariant_function, error::FailedInvariantCaseData,
55
},
@@ -43,6 +43,7 @@ pub(crate) fn shrink_sequence(
4343
executor: &Executor,
4444
call_after_invariant: bool,
4545
progress: Option<&ProgressBar>,
46+
early_exit: &EarlyExit,
4647
) -> eyre::Result<Vec<BasicTxDetails>> {
4748
trace!(target: "forge::test", "Shrinking sequence of {} calls.", calls.len());
4849

@@ -65,6 +66,10 @@ pub(crate) fn shrink_sequence(
6566

6667
let mut shrinker = CallSequenceShrinker::new(calls.len());
6768
for _ in 0..failed_case.shrink_run_limit {
69+
if early_exit.should_stop() {
70+
break;
71+
}
72+
6873
// Remove call at current index.
6974
shrinker.included_calls.clear(call_idx);
7075

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

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,32 +1130,33 @@ impl FuzzTestTimer {
11301130
}
11311131
}
11321132

1133-
/// Helper struct to enable fail fast behavior: when one test fails, all other tests stop early.
1133+
/// Helper struct to enable early exit behavior: when one test fails or run is interrupted,
1134+
/// all other tests stop early.
11341135
#[derive(Clone, Debug)]
1135-
pub struct FailFast {
1136-
/// Shared atomic flag set to `true` when a failure occurs.
1137-
/// None if fail-fast is disabled.
1136+
pub struct EarlyExit {
1137+
/// Shared atomic flag set to `true` when a failure occurs or ctrl-c received.
1138+
/// None if running without fail-fast or show-progress.
11381139
inner: Option<Arc<AtomicBool>>,
11391140
}
11401141

1141-
impl FailFast {
1142-
pub fn new(fail_fast: bool) -> Self {
1143-
Self { inner: fail_fast.then_some(Arc::new(AtomicBool::new(false))) }
1142+
impl EarlyExit {
1143+
pub fn new(early_exit: bool) -> Self {
1144+
Self { inner: early_exit.then_some(Arc::new(AtomicBool::new(false))) }
11441145
}
11451146

11461147
/// Returns `true` if fail-fast is enabled.
11471148
pub fn is_enabled(&self) -> bool {
11481149
self.inner.is_some()
11491150
}
11501151

1151-
/// Sets the failure flag. Used by other tests to stop early.
1152-
pub fn record_fail(&self) {
1153-
if let Some(fail_fast) = &self.inner {
1154-
fail_fast.store(true, Ordering::Relaxed);
1152+
/// Sets the exit flag. Used by other tests to stop early.
1153+
pub fn record_exit(&self) {
1154+
if let Some(early_exit) = &self.inner {
1155+
early_exit.store(true, Ordering::Relaxed);
11551156
}
11561157
}
11571158

1158-
/// Whether a failure has been recorded and test should stop.
1159+
/// Whether tests should stop and exit early.
11591160
pub fn should_stop(&self) -> bool {
11601161
self.inner.as_ref().map(|flag| flag.load(Ordering::Relaxed)).unwrap_or(false)
11611162
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,7 @@ impl TestArgs {
566566
let mut backtrace_builder = None;
567567
for (contract_name, mut suite_result) in rx {
568568
let tests = &mut suite_result.test_results;
569+
let has_tests = !tests.is_empty();
569570

570571
// Clear the addresses and labels from previous test.
571572
decoder.clear_addresses();
@@ -583,7 +584,7 @@ impl TestArgs {
583584
for warning in &suite_result.warnings {
584585
sh_warn!("{warning}")?;
585586
}
586-
if !tests.is_empty() {
587+
if has_tests {
587588
let len = tests.len();
588589
let tests = if len > 1 { "tests" } else { "test" };
589590
sh_println!("Ran {len} {tests} for {contract_name}")?;
@@ -808,7 +809,7 @@ impl TestArgs {
808809
}
809810

810811
// Print suite summary.
811-
if !silent {
812+
if !silent && has_tests {
812813
sh_println!("{}", suite_result.summary())?;
813814
}
814815

crates/forge/src/multi_runner.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use foundry_evm::{
2121
Env,
2222
backend::Backend,
2323
decode::RevertDecoder,
24-
executors::{Executor, ExecutorBuilder, FailFast},
24+
executors::{EarlyExit, Executor, ExecutorBuilder},
2525
fork::CreateFork,
2626
inspectors::CheatsConfig,
2727
opts::EvmOpts,
@@ -307,8 +307,8 @@ pub struct TestRunnerConfig {
307307
pub isolation: bool,
308308
/// Networks with enabled features.
309309
pub networks: NetworkConfigs,
310-
/// Whether to exit early on test failure.
311-
pub fail_fast: FailFast,
310+
/// Whether to exit early on test failure or if test run interrupted.
311+
pub early_exit: EarlyExit,
312312
}
313313

314314
impl TestRunnerConfig {
@@ -601,8 +601,8 @@ impl MultiContractRunnerBuilder {
601601
inline_config: Arc::new(InlineConfig::new_parsed(output, &self.config)?),
602602
isolation: self.isolation,
603603
networks: self.networks,
604+
early_exit: EarlyExit::new(self.fail_fast || self.config.show_progress),
604605
config: self.config,
605-
fail_fast: FailFast::new(self.fail_fast),
606606
},
607607

608608
fork: self.fork,

crates/forge/src/runner.rs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ use std::{
4343
sync::Arc,
4444
time::Instant,
4545
};
46+
use tokio::signal;
4647
use tracing::Span;
4748

4849
/// When running tests, we deploy all external libraries present in the project. To avoid additional
@@ -395,14 +396,22 @@ impl<'a> ContractRunner<'a> {
395396
return SuiteResult::new(start.elapsed(), test_results, warnings);
396397
}
397398

398-
let fail_fast = &self.tcfg.fail_fast;
399+
let early_exit = &self.tcfg.early_exit;
400+
401+
if self.progress.is_some() {
402+
let interrupt = early_exit.clone();
403+
self.tokio_handle.spawn(async move {
404+
signal::ctrl_c().await.expect("Failed to listen for Ctrl+C");
405+
interrupt.record_exit();
406+
});
407+
}
399408

400409
let test_results = functions
401410
.par_iter()
402-
.map(|&func| {
411+
.filter_map(|&func| {
403412
// Early exit if we're running with fail-fast and a test already failed.
404-
if fail_fast.should_stop() {
405-
return (func.signature(), TestResult::setup_result(setup.clone()));
413+
if early_exit.should_stop() {
414+
return None;
406415
}
407416

408417
let start = Instant::now();
@@ -435,10 +444,10 @@ impl<'a> ContractRunner<'a> {
435444

436445
// Set fail fast flag if current test failed.
437446
if res.status.is_failure() {
438-
fail_fast.record_fail();
447+
early_exit.record_exit();
439448
}
440449

441-
(sig, res)
450+
Some((sig, res))
442451
})
443452
.collect::<BTreeMap<_, _>>();
444453

@@ -637,7 +646,7 @@ impl<'a> FunctionRunner<'a> {
637646
let mut result = FuzzTestResult::default();
638647

639648
for i in 0..fixtures_len {
640-
if self.tcfg.fail_fast.should_stop() {
649+
if self.tcfg.early_exit.should_stop() {
641650
return self.result;
642651
}
643652

@@ -822,7 +831,7 @@ impl<'a> FunctionRunner<'a> {
822831
&self.setup.fuzz_fixtures,
823832
&self.setup.deployed_libs,
824833
progress.as_ref(),
825-
&self.tcfg.fail_fast,
834+
&self.tcfg.early_exit,
826835
) {
827836
Ok(x) => x,
828837
Err(e) => {
@@ -856,6 +865,7 @@ impl<'a> FunctionRunner<'a> {
856865
&mut self.result.deprecated_cheatcodes,
857866
progress.as_ref(),
858867
show_solidity,
868+
&self.tcfg.early_exit,
859869
) {
860870
Ok(call_sequence) => {
861871
if !call_sequence.is_empty() {
@@ -973,7 +983,7 @@ impl<'a> FunctionRunner<'a> {
973983
self.address,
974984
&self.cr.mcr.revert_decoder,
975985
progress.as_ref(),
976-
&self.tcfg.fail_fast,
986+
&self.tcfg.early_exit,
977987
) {
978988
Ok(x) => x,
979989
Err(e) => {

0 commit comments

Comments
 (0)