Skip to main content

forge/
runner.rs

1//! The Forge test runner.
2
3use crate::{
4    MultiContractRunner, TestFilter,
5    coverage::HitMaps,
6    fuzz::{BaseCounterExample, FuzzTestResult},
7    multi_runner::{TestContract, TestRunnerConfig},
8    progress::{TestsProgress, start_fuzz_progress},
9    result::{
10        InvariantFailure, InvariantPredicateResult, SuiteResult, TestResult, TestSetup, TestStatus,
11        invariant_campaign_display_name,
12    },
13};
14use alloy_dyn_abi::{DynSolValue, JsonAbiExt};
15use alloy_json_abi::{Function, JsonAbi};
16use alloy_primitives::{Address, Bytes, Selector, U256, address, map::HashMap};
17use eyre::Result;
18use foundry_common::{TestFunctionExt, TestFunctionKind, contracts::ContractsByAddress};
19use foundry_compilers::utils::canonicalized;
20use foundry_config::{Config, FuzzCorpusConfig, InlineConfig, InvariantConfig};
21use foundry_evm::{
22    constants::CALLER,
23    core::evm::FoundryEvmNetwork,
24    decode::{RevertDecoder, SkipReason},
25    executors::{
26        CallResult, EvmError, Executor, ITest, RawCallResult, ShowmapOpts,
27        fuzz::FuzzedExecutor,
28        invariant::{
29            CheckSequenceOptions, HandlerAssertionFailure, InvariantExecutor, InvariantFuzzError,
30            check_sequence, replay_error, replay_handler_failure_sequence, replay_run,
31        },
32        replay_corpus_to_showmap,
33    },
34    fuzz::{
35        BasicTxDetails, CallDetails, CounterExample, FuzzFixtures, fixture_name,
36        invariant::{InvariantContract, InvariantSettings, is_optimization_invariant},
37        strategies::EvmFuzzState,
38    },
39    revm::primitives::hardfork::SpecId,
40    traces::{TraceKind, TraceMode, load_contracts},
41};
42use foundry_evm_networks::NetworkVariant;
43use foundry_evm_symbolic::{SymbolicExecutor, SymbolicRunInput, SymbolicRunResult};
44use itertools::Itertools;
45use proptest::test_runner::{RngAlgorithm, TestError, TestRng, TestRunner};
46use rayon::prelude::*;
47use serde::{Deserialize, Serialize};
48use std::{
49    borrow::Cow,
50    cmp::min,
51    collections::{BTreeMap, BTreeSet},
52    ops::Deref,
53    path::{Path, PathBuf},
54    sync::Arc,
55    time::Instant,
56};
57use tokio::signal;
58use tracing::Span;
59
60/// When running tests, we deploy all external libraries present in the project. To avoid additional
61/// libraries affecting nonces of senders used in tests, we are using separate address to
62/// predeploy libraries.
63///
64/// `address(uint160(uint256(keccak256("foundry library deployer"))))`
65pub const LIBRARY_DEPLOYER: Address = address!("0x1F95D37F27EA0dEA9C252FC09D5A6eaA97647353");
66
67pub(crate) fn is_symbolic_entrypoint(func: &Function) -> bool {
68    func.name.starts_with("check") || func.name.starts_with("prove")
69}
70
71pub(crate) struct InvariantCampaignScope<'a> {
72    pub config: &'a Config,
73    pub inline_config: &'a InlineConfig,
74    pub contract_name: &'a str,
75    pub all_override_networks: &'a [NetworkVariant],
76    pub pass_network: Option<&'a NetworkVariant>,
77}
78
79struct InvariantCampaignSelection<'a> {
80    matched_boolean_invariant_fns: Vec<&'a Function>,
81    merge_boolean_suite: bool,
82    boolean_suite_anchor: Option<&'a Function>,
83    optimization_anchors: usize,
84}
85
86impl InvariantCampaignSelection<'_> {
87    const fn anchor_count(&self) -> usize {
88        self.optimization_anchors
89            + if self.matched_boolean_invariant_fns.is_empty() {
90                0
91            } else if self.merge_boolean_suite {
92                1
93            } else {
94                self.matched_boolean_invariant_fns.len()
95            }
96    }
97}
98
99pub(crate) fn count_runnable_invariant_campaign_anchors(
100    abi: &JsonAbi,
101    filter: &dyn TestFilter,
102    scope: InvariantCampaignScope<'_>,
103) -> usize {
104    let invariant_fns = abi.functions().filter(|func| func.is_invariant_test()).collect::<Vec<_>>();
105    if invariant_fns.iter().any(|func| !func.inputs.is_empty()) {
106        return 0;
107    }
108
109    let functions = abi
110        .functions()
111        .filter(|func| filter.matches_test_function(func))
112        .filter(|func| {
113            function_matches_network_pass(
114                scope.all_override_networks,
115                scope.pass_network,
116                scope.inline_config.network_for(
117                    &scope.config.profile,
118                    scope.contract_name,
119                    &func.name,
120                ),
121            )
122        })
123        .collect::<Vec<_>>();
124
125    select_invariant_campaigns(
126        &invariant_fns,
127        &functions,
128        scope.config,
129        scope.inline_config,
130        scope.contract_name,
131    )
132    .anchor_count()
133}
134
135fn function_matches_network_pass(
136    all_override_networks: &[NetworkVariant],
137    pass_network: Option<&NetworkVariant>,
138    func_network: Option<NetworkVariant>,
139) -> bool {
140    if all_override_networks.is_empty() {
141        return true;
142    }
143    match pass_network {
144        None => func_network.is_none_or(|network| !all_override_networks.contains(&network)),
145        Some(target) => func_network.as_ref() == Some(target),
146    }
147}
148
149fn inline_config_for(
150    config: &Config,
151    inline_config: &InlineConfig,
152    contract_name: &str,
153    func: Option<&Function>,
154) -> Result<Config> {
155    let function = func.map(|f| f.name.as_str()).unwrap_or("");
156    Ok(config.merge_inline_provider(inline_config.provide(contract_name, function))?)
157}
158
159fn invariant_suite_configs_match(
160    config: &Config,
161    inline_config: &InlineConfig,
162    contract_name: &str,
163    funcs: &[&Function],
164) -> bool {
165    let Some((anchor, rest)) = funcs.split_first() else {
166        return true;
167    };
168    let anchor_config = match inline_config_for(config, inline_config, contract_name, Some(anchor))
169    {
170        Ok(config) => config.invariant,
171        Err(_) => return false,
172    };
173    rest.iter().all(|func| {
174        inline_config_for(config, inline_config, contract_name, Some(func))
175            .map(|config| config.invariant == anchor_config)
176            .unwrap_or(false)
177    })
178}
179
180fn select_invariant_campaigns<'a>(
181    invariant_fns: &[&'a Function],
182    functions: &[&'a Function],
183    config: &Config,
184    inline_config: &InlineConfig,
185    contract_name: &str,
186) -> InvariantCampaignSelection<'a> {
187    let boolean_invariant_fns =
188        invariant_fns.iter().copied().filter(|func| !is_optimization_invariant(func));
189    let matched_boolean_invariant_fns = functions
190        .iter()
191        .copied()
192        .filter(|func| func.is_invariant_test() && !is_optimization_invariant(func))
193        .collect::<Vec<_>>();
194    let optimization_anchors = functions
195        .iter()
196        .filter(|func| func.is_invariant_test() && is_optimization_invariant(func))
197        .count();
198
199    // The boolean invariant campaign is contract-level. Test filters only select which predicates
200    // are evaluated/reported inside that campaign; they must not decide the corpus/failure
201    // namespace. Use the canonical anchor when it is part of the filtered set, but preserve
202    // `--mt`/`--nmt` isolation when the filter deliberately excludes it.
203    let canonical_boolean_anchor = boolean_invariant_fns.into_iter().next();
204    let merge_boolean_suite = !matched_boolean_invariant_fns.is_empty()
205        && invariant_suite_configs_match(
206            config,
207            inline_config,
208            contract_name,
209            &matched_boolean_invariant_fns,
210        );
211    let boolean_suite_anchor = merge_boolean_suite
212        .then(|| {
213            canonical_boolean_anchor
214                .filter(|anchor| matched_boolean_invariant_fns.contains(anchor))
215                .or_else(|| matched_boolean_invariant_fns.first().copied())
216        })
217        .flatten();
218
219    InvariantCampaignSelection {
220        matched_boolean_invariant_fns,
221        merge_boolean_suite,
222        boolean_suite_anchor,
223        optimization_anchors,
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230    use foundry_common::EmptyTestFilter;
231    use foundry_config::NatSpec;
232
233    const CONTRACT_NAME: &str = "src/Test.t.sol:InvariantTest";
234
235    fn count_anchors(abi: &JsonAbi, inline_config: &InlineConfig) -> usize {
236        let config = Config::default();
237        count_runnable_invariant_campaign_anchors(
238            abi,
239            &EmptyTestFilter::default(),
240            InvariantCampaignScope {
241                config: &config,
242                inline_config,
243                contract_name: CONTRACT_NAME,
244                all_override_networks: &[],
245                pass_network: None,
246            },
247        )
248    }
249
250    #[test]
251    fn runnable_campaign_anchor_count_merges_boolean_suite_and_counts_optimizations() {
252        let abi = JsonAbi::parse([
253            "function invariantOne() external",
254            "function invariantTwo() external",
255            "function invariantOptimizeA() external returns (int256)",
256            "function invariantOptimizeB() external returns (int256)",
257        ])
258        .unwrap();
259
260        assert_eq!(count_anchors(&abi, &InlineConfig::new()), 3);
261    }
262
263    #[test]
264    fn runnable_campaign_anchor_count_splits_boolean_suite_when_configs_differ() {
265        let abi = JsonAbi::parse([
266            "function invariantOne() external",
267            "function invariantTwo() external",
268        ])
269        .unwrap();
270        let mut inline_config = InlineConfig::new();
271        inline_config
272            .insert(&NatSpec {
273                contract: CONTRACT_NAME.to_string(),
274                function: Some("invariantTwo".to_string()),
275                line: "1:1".to_string(),
276                docs: "forge-config: default.invariant.depth = 1".to_string(),
277            })
278            .unwrap();
279
280        assert_eq!(count_anchors(&abi, &inline_config), 2);
281    }
282
283    #[test]
284    fn runnable_campaign_anchor_count_respects_network_pass() {
285        let abi = JsonAbi::parse(["function invariantTempoOnly() external"]).unwrap();
286        let mut inline_config = InlineConfig::new();
287        inline_config
288            .insert(&NatSpec {
289                contract: CONTRACT_NAME.to_string(),
290                function: Some("invariantTempoOnly".to_string()),
291                line: "1:1".to_string(),
292                docs: r#"forge-config: default.networks.network = "tempo""#.to_string(),
293            })
294            .unwrap();
295        let config = Config::default();
296        let override_networks = [NetworkVariant::Tempo];
297
298        let default_pass = count_runnable_invariant_campaign_anchors(
299            &abi,
300            &EmptyTestFilter::default(),
301            InvariantCampaignScope {
302                config: &config,
303                inline_config: &inline_config,
304                contract_name: CONTRACT_NAME,
305                all_override_networks: &override_networks,
306                pass_network: None,
307            },
308        );
309        let tempo_pass = count_runnable_invariant_campaign_anchors(
310            &abi,
311            &EmptyTestFilter::default(),
312            InvariantCampaignScope {
313                config: &config,
314                inline_config: &inline_config,
315                contract_name: CONTRACT_NAME,
316                all_override_networks: &override_networks,
317                pass_network: Some(&NetworkVariant::Tempo),
318            },
319        );
320
321        assert_eq!(default_pass, 0);
322        assert_eq!(tempo_pass, 1);
323    }
324}
325
326/// A type that executes all tests of a contract
327pub struct ContractRunner<'a, FEN: FoundryEvmNetwork> {
328    /// The name of the contract.
329    name: &'a str,
330    /// The data of the contract.
331    contract: &'a TestContract,
332    /// The EVM executor.
333    executor: Executor<FEN>,
334    /// Overall test run progress.
335    progress: Option<&'a TestsProgress>,
336    /// The handle to the tokio runtime.
337    tokio_handle: tokio::runtime::Handle,
338    /// The span of the contract.
339    span: tracing::Span,
340    /// The contract-level configuration.
341    tcfg: Cow<'a, TestRunnerConfig<FEN>>,
342    /// The parent runner.
343    mcr: &'a MultiContractRunner<FEN>,
344    /// Number of matching invariant campaign anchors in the current test pass.
345    num_invariant_campaign_anchors: usize,
346}
347
348pub(crate) struct ContractRunnerContext<'a> {
349    pub(crate) progress: Option<&'a TestsProgress>,
350    pub(crate) tokio_handle: tokio::runtime::Handle,
351    pub(crate) num_invariant_campaign_anchors: usize,
352}
353
354impl<'a, FEN: FoundryEvmNetwork> Deref for ContractRunner<'a, FEN> {
355    type Target = Cow<'a, TestRunnerConfig<FEN>>;
356
357    #[inline(always)]
358    fn deref(&self) -> &Self::Target {
359        &self.tcfg
360    }
361}
362
363impl<'a, FEN: FoundryEvmNetwork> ContractRunner<'a, FEN> {
364    pub(crate) fn new(
365        name: &'a str,
366        contract: &'a TestContract,
367        executor: Executor<FEN>,
368        span: Span,
369        mcr: &'a MultiContractRunner<FEN>,
370        context: ContractRunnerContext<'a>,
371    ) -> Self {
372        Self {
373            name,
374            contract,
375            executor,
376            progress: context.progress,
377            tokio_handle: context.tokio_handle,
378            span,
379            tcfg: Cow::Borrowed(&mcr.tcfg),
380            mcr,
381            num_invariant_campaign_anchors: context.num_invariant_campaign_anchors,
382        }
383    }
384
385    /// Returns `true` if `func` should run in the current multi-network pass.
386    ///
387    /// In single-pass mode (no inline network overrides) every function passes.
388    /// In multi-pass mode:
389    /// - Default pass (`pass_network = None`): includes functions *without* an override annotation.
390    /// - Override pass (`pass_network = Some(v)`): includes only functions annotated with `v`.
391    fn function_matches_network_pass(&self, func: &Function) -> bool {
392        function_matches_network_pass(
393            &self.mcr.tcfg.multi_network.all_override_networks,
394            self.mcr.tcfg.multi_network.pass_network.as_ref(),
395            self.mcr.inline_config.network_for(&self.tcfg.config.profile, self.name, &func.name),
396        )
397    }
398
399    /// Deploys the test contract inside the runner from the sending account, and optionally runs
400    /// the `setUp` function on the test contract.
401    pub fn setup(&mut self, call_setup: bool) -> TestSetup {
402        self._setup(call_setup).unwrap_or_else(|err| {
403            if err.to_string().contains("skipped") {
404                TestSetup::skipped(err.to_string())
405            } else {
406                TestSetup::failed(err.to_string())
407            }
408        })
409    }
410
411    fn _setup(&mut self, call_setup: bool) -> Result<TestSetup> {
412        trace!(call_setup, "setting up");
413
414        self.apply_contract_inline_config()?;
415
416        // We max out their balance so that they can deploy and make calls.
417        self.executor.set_balance(self.sender, U256::MAX)?;
418        self.executor.set_balance(CALLER, U256::MAX)?;
419
420        // We set the nonce of the deployer accounts to 1 to get the same addresses as DappTools.
421        self.executor.set_nonce(self.sender, 1)?;
422
423        // Deploy libraries.
424        self.executor.set_balance(LIBRARY_DEPLOYER, U256::MAX)?;
425
426        let mut result = TestSetup::default();
427        for code in &self.mcr.libs_to_deploy {
428            let deploy_result = self.executor.deploy(
429                LIBRARY_DEPLOYER,
430                code.clone(),
431                U256::ZERO,
432                Some(&self.mcr.revert_decoder),
433            );
434
435            // Record deployed library address.
436            if let Ok(deployed) = &deploy_result {
437                result.deployed_libs.push(deployed.address);
438            }
439
440            let (raw, reason) = RawCallResult::from_evm_result(deploy_result.map(Into::into))?;
441            result.extend(raw, TraceKind::Deployment);
442            if reason.is_some() {
443                debug!(?reason, "deployment of library failed");
444                result.reason = reason;
445                return Ok(result);
446            }
447        }
448
449        let address = self.sender.create(self.executor.get_nonce(self.sender)?);
450        result.address = address;
451
452        // Set the contracts initial balance before deployment, so it is available during
453        // construction
454        self.executor.set_balance(address, self.initial_balance())?;
455
456        // Deploy the test contract
457        let deploy_result = self.executor.deploy(
458            self.sender,
459            self.contract.bytecode.clone(),
460            U256::ZERO,
461            Some(&self.mcr.revert_decoder),
462        );
463
464        result.deployment_failure = deploy_result.is_err();
465
466        if let Ok(dr) = &deploy_result {
467            debug_assert_eq!(dr.address, address);
468        }
469        let (raw, reason) = RawCallResult::from_evm_result(deploy_result.map(Into::into))?;
470        result.extend(raw, TraceKind::Deployment);
471        if reason.is_some() {
472            debug!(?reason, "deployment of test contract failed");
473            result.reason = reason;
474            return Ok(result);
475        }
476
477        // Reset `self.sender`s, `CALLER`s and `LIBRARY_DEPLOYER`'s balance to the initial balance.
478        self.executor.set_balance(self.sender, self.initial_balance())?;
479        self.executor.set_balance(CALLER, self.initial_balance())?;
480        self.executor.set_balance(LIBRARY_DEPLOYER, self.initial_balance())?;
481
482        self.executor.deploy_create2_deployer()?;
483
484        // Optionally call the `setUp` function
485        if call_setup {
486            trace!("calling setUp");
487            let res = self.executor.setup(None, address, Some(&self.mcr.revert_decoder));
488            let (raw, reason) = RawCallResult::from_evm_result(res)?;
489            result.extend(raw, TraceKind::Setup);
490            result.reason = reason;
491        }
492
493        result.fuzz_fixtures = self.fuzz_fixtures(address);
494
495        Ok(result)
496    }
497
498    fn initial_balance(&self) -> U256 {
499        self.evm_opts.initial_balance
500    }
501
502    /// Configures this runner with the inline configuration for the contract.
503    fn apply_contract_inline_config(&mut self) -> Result<()> {
504        if self.inline_config.contains_contract(self.name) {
505            let new_config = Arc::new(self.inline_config(None)?);
506            self.tcfg.to_mut().reconfigure_with(new_config);
507            let prev_tracer = self.executor.inspector_mut().tracer.take();
508            self.tcfg.configure_executor(&mut self.executor);
509            // Don't set tracer here.
510            self.executor.inspector_mut().tracer = prev_tracer;
511        }
512        Ok(())
513    }
514
515    /// Returns the configuration for a contract or function.
516    fn inline_config(&self, func: Option<&Function>) -> Result<Config> {
517        inline_config_for(&self.config, &self.mcr.inline_config, self.name, func)
518    }
519
520    /// Collect fixtures from test contract.
521    ///
522    /// Fixtures can be defined:
523    /// - as storage arrays in test contract, prefixed with `fixture`
524    /// - as functions prefixed with `fixture` and followed by parameter name to be fuzzed
525    ///
526    /// Storage array fixtures:
527    /// `uint256[] public fixture_amount = [1, 2, 3];`
528    /// define an array of uint256 values to be used for fuzzing `amount` named parameter in scope
529    /// of the current test.
530    ///
531    /// Function fixtures:
532    /// `function fixture_owner() public returns (address[] memory){}`
533    /// returns an array of addresses to be used for fuzzing `owner` named parameter in scope of the
534    /// current test.
535    fn fuzz_fixtures(&mut self, address: Address) -> FuzzFixtures {
536        let mut fixtures = HashMap::default();
537        let fixture_functions = self.contract.abi.functions().filter(|func| func.is_fixture());
538        for func in fixture_functions {
539            if func.inputs.is_empty() {
540                // Read fixtures declared as functions.
541                if let Ok(CallResult { raw: _, decoded_result }) =
542                    self.executor.call(CALLER, address, func, &[], U256::ZERO, None)
543                {
544                    fixtures.insert(fixture_name(func.name.clone()), decoded_result);
545                }
546            } else {
547                // For reading fixtures from storage arrays we collect values by calling the
548                // function with incremented indexes until there's an error.
549                let mut vals = Vec::new();
550                let mut index = 0;
551                loop {
552                    if let Ok(CallResult { raw: _, decoded_result }) = self.executor.call(
553                        CALLER,
554                        address,
555                        func,
556                        &[DynSolValue::Uint(U256::from(index), 256)],
557                        U256::ZERO,
558                        None,
559                    ) {
560                        vals.push(decoded_result);
561                    } else {
562                        // No result returned for this index, we reached the end of storage
563                        // array or the function is not a valid fixture.
564                        break;
565                    }
566                    index += 1;
567                }
568                fixtures.insert(fixture_name(func.name.clone()), DynSolValue::Array(vals));
569            };
570        }
571        FuzzFixtures::new(fixtures)
572    }
573
574    /// Runs all tests for a contract whose names match the provided regular expression
575    pub fn run_tests(mut self, filter: &dyn TestFilter) -> SuiteResult {
576        let start = Instant::now();
577        let mut warnings = Vec::new();
578
579        // Check if `setUp` function with valid signature declared.
580        let setup_fns: Vec<_> =
581            self.contract.abi.functions().filter(|func| func.name.is_setup()).collect();
582        let call_setup = setup_fns.len() == 1 && setup_fns[0].name == "setUp";
583        // There is a single miss-cased `setUp` function, so we add a warning
584        for &setup_fn in &setup_fns {
585            if setup_fn.name != "setUp" {
586                warnings.push(format!(
587                    "Found invalid setup function \"{}\" did you mean \"setUp()\"?",
588                    setup_fn.signature()
589                ));
590            }
591        }
592
593        // There are multiple setUp function, so we return a single test result for `setUp`
594        if setup_fns.len() > 1 {
595            // Trip the global fail-fast flag so sibling parallel suites (notably long-running
596            // invariant campaigns) observe `should_stop()` and exit at their next run boundary
597            // instead of running to their timeout.
598            self.tcfg.early_exit.record_failure();
599            return SuiteResult::new(
600                start.elapsed(),
601                [("setUp()".to_string(), TestResult::fail("multiple setUp functions".to_string()))]
602                    .into(),
603                warnings,
604            );
605        }
606
607        // Check if `afterInvariant` function with valid signature declared.
608        let after_invariant_fns: Vec<_> =
609            self.contract.abi.functions().filter(|func| func.name.is_after_invariant()).collect();
610        if after_invariant_fns.len() > 1 {
611            // Return a single test result failure if multiple functions declared.
612            self.tcfg.early_exit.record_failure();
613            return SuiteResult::new(
614                start.elapsed(),
615                [(
616                    "afterInvariant()".to_string(),
617                    TestResult::fail("multiple afterInvariant functions".to_string()),
618                )]
619                .into(),
620                warnings,
621            );
622        }
623        let call_after_invariant = after_invariant_fns.first().is_some_and(|after_invariant_fn| {
624            let match_sig = after_invariant_fn.name == "afterInvariant";
625            if !match_sig {
626                warnings.push(format!(
627                    "Found invalid afterInvariant function \"{}\" did you mean \"afterInvariant()\"?",
628                    after_invariant_fn.signature()
629                ));
630            }
631            match_sig
632        });
633
634        let invariant_fns: Vec<_> =
635            self.contract.abi.functions().filter(|func| func.is_invariant_test()).collect();
636
637        // Validate signatures up front: invariant functions must take no parameters. Without
638        // this, parameterized `invariant_*` functions would slip into contract-level campaigns
639        // and fail with a confusing "selector not found" / decode error mid-campaign. Reject
640        // here with a per-function result so the failure is obvious to the user.
641        let invalid_invariants: Vec<_> = invariant_fns
642            .iter()
643            .filter(|f| !f.inputs.is_empty())
644            .map(|f| {
645                (
646                    f.signature(),
647                    TestResult::fail(format!(
648                        "invariant `{}` must take no parameters",
649                        f.signature()
650                    )),
651                )
652            })
653            .collect();
654        if !invalid_invariants.is_empty() {
655            self.tcfg.early_exit.record_failure();
656            return SuiteResult::new(
657                start.elapsed(),
658                invalid_invariants.into_iter().collect(),
659                warnings,
660            );
661        }
662
663        // Invariant testing requires tracing to figure out what contracts were created.
664        // For regular test runs we disable debug-level setup traces as an optimization.
665        // In `forge test --debug`, keep setup traces in debug mode so setup failures are
666        // inspectable in the debugger.
667        let has_invariants = !invariant_fns.is_empty();
668
669        let should_override_setup_tracing =
670            !self.tcfg.debug && (self.executor.inspector().tracer.is_some() || has_invariants);
671
672        let prev_tracer = should_override_setup_tracing.then(|| {
673            let prev_tracer = self.executor.inspector_mut().tracer.take();
674            self.executor.set_tracing(TraceMode::Call);
675            prev_tracer
676        });
677
678        let setup_time = Instant::now();
679        let setup = self.setup(call_setup);
680        debug!("finished setting up in {:?}", setup_time.elapsed());
681
682        if let Some(prev_tracer) = prev_tracer {
683            self.executor.inspector_mut().tracer = prev_tracer;
684        }
685
686        if setup.reason.is_some() {
687            // The setup failed, so we return a single test result for `setUp`
688            let fail_msg = if setup.deployment_failure {
689                "constructor()".to_string()
690            } else {
691                "setUp()".to_string()
692            };
693            self.tcfg.early_exit.record_failure();
694            return SuiteResult::new(
695                start.elapsed(),
696                [(fail_msg, TestResult::setup_result(setup))].into(),
697                warnings,
698            );
699        }
700
701        // Filter out functions sequentially since it's very fast and there is no need to do it
702        // in parallel.
703        let find_timer = Instant::now();
704        let symbolic_enabled = self.config.symbolic.enabled;
705        let functions = self
706            .contract
707            .abi
708            .functions()
709            .filter(|func| {
710                if symbolic_enabled && is_symbolic_entrypoint(func) {
711                    filter.matches_test(&func.signature())
712                } else {
713                    filter.matches_test_function_in_contract(self.name, func)
714                }
715            })
716            .filter(|func| self.function_matches_network_pass(func))
717            .collect::<Vec<_>>();
718        debug!(
719            "Found {} test functions out of {} in {:?}",
720            functions.len(),
721            self.contract.abi.functions().count(),
722            find_timer.elapsed(),
723        );
724
725        let identified_contracts = has_invariants.then(|| {
726            load_contracts(setup.traces.iter().map(|(_, t)| &t.arena), &self.mcr.known_contracts)
727        });
728
729        let test_fail_functions =
730            functions.iter().filter(|func| func.test_function_kind().is_any_test_fail());
731        if test_fail_functions.clone().next().is_some() {
732            let fail = || {
733                TestResult::fail("`testFail*` has been removed. Consider changing to test_Revert[If|When]_Condition and expecting a revert".to_string())
734            };
735            let test_results = test_fail_functions.map(|func| (func.signature(), fail())).collect();
736            self.tcfg.early_exit.record_failure();
737            return SuiteResult::new(start.elapsed(), test_results, warnings);
738        }
739
740        let early_exit = &self.tcfg.early_exit;
741
742        if self.progress.is_some() {
743            let interrupt = early_exit.clone();
744            self.tokio_handle.spawn(async move {
745                signal::ctrl_c().await.expect("Failed to listen for Ctrl+C");
746                interrupt.record_ctrl_c();
747            });
748        }
749
750        let invariant_campaigns = select_invariant_campaigns(
751            &invariant_fns,
752            &functions,
753            &self.config,
754            &self.mcr.inline_config,
755            self.name,
756        );
757        let InvariantCampaignSelection {
758            matched_boolean_invariant_fns,
759            merge_boolean_suite: merge_invariant_suite,
760            boolean_suite_anchor: invariant_suite_anchor,
761            optimization_anchors: _,
762        } = invariant_campaigns;
763
764        let test_results = functions
765            .par_iter()
766            .filter_map(|&func| {
767                // Early exit if we're running with fail-fast and a test already failed.
768                if early_exit.should_stop() {
769                    return None;
770                }
771                // Invariant tests run either as a shared boolean suite or as a single
772                // optimization campaign; other test kinds keep their original invariant set.
773                let invariants: &[&Function] = if func.is_invariant_test() {
774                    if is_optimization_invariant(func) {
775                        std::slice::from_ref(&func)
776                    } else if merge_invariant_suite {
777                        // Only the suite anchor runs the merged boolean campaign.
778                        if invariant_suite_anchor != Some(func) {
779                            return None;
780                        }
781                        matched_boolean_invariant_fns.as_slice()
782                    } else {
783                        std::slice::from_ref(&func)
784                    }
785                } else {
786                    invariant_fns.as_slice()
787                };
788
789                // Skip invariant anchors that have no predicates to execute.
790                if func.is_invariant_test() && invariants.is_empty() {
791                    return None;
792                }
793
794                let start = Instant::now();
795
796                let _guard = self.tokio_handle.enter();
797
798                let _guard;
799                let current_span = tracing::Span::current();
800                if current_span.is_none() || current_span.id() != self.span.id() {
801                    _guard = self.span.enter();
802                }
803
804                let sig = func.signature();
805                let kind = if self.config.symbolic.enabled && is_symbolic_entrypoint(func) {
806                    TestFunctionKind::SymbolicTest
807                } else {
808                    func.test_function_kind()
809                };
810
811                let _guard = debug_span!(
812                    "test",
813                    %kind,
814                    name = %if enabled!(tracing::Level::TRACE) { &sig } else { &func.name },
815                )
816                .entered();
817
818                let mut res = FunctionRunner::new(&self, &setup).run(
819                    func,
820                    invariants,
821                    kind,
822                    call_after_invariant,
823                    identified_contracts.as_ref(),
824                );
825                res.duration = start.elapsed();
826
827                // Record test failure for early exit (only triggers if fail-fast is enabled).
828                if res.status.is_failure() {
829                    early_exit.record_failure();
830                }
831
832                Some((sig, res))
833            })
834            .collect::<BTreeMap<_, _>>();
835
836        let duration = start.elapsed();
837        SuiteResult::new(duration, test_results, warnings)
838    }
839}
840
841/// Executes a single test function, returning a [`TestResult`].
842struct FunctionRunner<'a, FEN: FoundryEvmNetwork> {
843    /// The function-level configuration.
844    tcfg: Cow<'a, TestRunnerConfig<FEN>>,
845    /// The EVM executor.
846    executor: Cow<'a, Executor<FEN>>,
847    /// The parent runner.
848    cr: &'a ContractRunner<'a, FEN>,
849    /// The address of the test contract.
850    address: Address,
851    /// The test setup result.
852    setup: &'a TestSetup,
853    /// The test result. Returned after running the test.
854    result: TestResult,
855}
856
857impl<'a, FEN: FoundryEvmNetwork> Deref for FunctionRunner<'a, FEN> {
858    type Target = Cow<'a, TestRunnerConfig<FEN>>;
859
860    #[inline(always)]
861    fn deref(&self) -> &Self::Target {
862        &self.tcfg
863    }
864}
865
866impl<'a, FEN: FoundryEvmNetwork> FunctionRunner<'a, FEN> {
867    fn new(cr: &'a ContractRunner<'a, FEN>, setup: &'a TestSetup) -> Self {
868        Self {
869            tcfg: match &cr.tcfg {
870                Cow::Borrowed(tcfg) => Cow::Borrowed(tcfg),
871                Cow::Owned(tcfg) => Cow::Owned(tcfg.clone()),
872            },
873            executor: Cow::Borrowed(&cr.executor),
874            cr,
875            address: setup.address,
876            setup,
877            result: TestResult::new(setup),
878        }
879    }
880
881    const fn revert_decoder(&self) -> &'a RevertDecoder {
882        &self.cr.mcr.revert_decoder
883    }
884
885    /// Returns whether verbose symbolic diagnostics should be rendered after progress clears.
886    fn should_defer_symbolic_diagnostics(&self) -> bool {
887        self.cr.progress.is_some() && self.config.symbolic.dump_smt
888    }
889
890    /// Configures this runner with the inline configuration for the contract.
891    fn apply_function_inline_config(&mut self, func: &Function) -> Result<()> {
892        if self.inline_config.contains_function(self.cr.name, &func.name) {
893            let new_config = Arc::new(self.cr.inline_config(Some(func))?);
894            self.tcfg.to_mut().reconfigure_with(new_config);
895            self.tcfg.configure_executor(self.executor.to_mut());
896        }
897        Ok(())
898    }
899
900    fn run(
901        mut self,
902        func: &Function,
903        invariants: &[&Function],
904        kind: TestFunctionKind,
905        call_after_invariant: bool,
906        identified_contracts: Option<&ContractsByAddress>,
907    ) -> TestResult {
908        if let Err(e) = self.apply_function_inline_config(func) {
909            self.result.single_fail(Some(e.to_string()));
910            return self.result;
911        }
912
913        // In showmap replay mode, only fuzz/invariant tests are runnable.
914        if self.cr.mcr.tcfg.showmap.is_some()
915            && matches!(kind, TestFunctionKind::UnitTest { .. } | TestFunctionKind::TableTest)
916        {
917            self.result.replay_skip("not runnable in showmap mode");
918            return self.result;
919        }
920
921        match kind {
922            TestFunctionKind::UnitTest { .. } => self.run_unit_test(func),
923            TestFunctionKind::FuzzTest { .. } => self.run_fuzz_test(func),
924            TestFunctionKind::TableTest => self.run_table_test(func),
925            TestFunctionKind::SymbolicTest => self.run_symbolic_test(func),
926            TestFunctionKind::InvariantTest => {
927                let fail_on_revert_for = |f: &Function| {
928                    if self.inline_config.contains_function(self.cr.name, &f.name)
929                        && let Ok(config) = self.cr.inline_config(Some(f))
930                    {
931                        return config.invariant.fail_on_revert;
932                    }
933                    self.config.invariant.fail_on_revert
934                };
935                let invariant_fns: Vec<_> =
936                    invariants.iter().copied().map(|f| (f, fail_on_revert_for(f))).collect();
937                self.run_invariant_test(
938                    func,
939                    invariant_fns,
940                    call_after_invariant,
941                    identified_contracts.unwrap(),
942                )
943            }
944            _ => unreachable!(),
945        }
946    }
947
948    /// Runs a single unit test.
949    ///
950    /// Applies before test txes (if any), runs current test and returns the `TestResult`.
951    ///
952    /// Before test txes are applied in order and state modifications committed to the EVM database
953    /// (therefore the unit test call will be made on modified state).
954    /// State modifications of before test txes and unit test function call are discarded after
955    /// test ends, similar to `eth_call`.
956    fn run_unit_test(mut self, func: &Function) -> TestResult {
957        // Prepare unit test execution.
958        if self.prepare_test(func).is_err() {
959            return self.result;
960        }
961
962        // Run current unit test.
963        let (mut raw_call_result, reason) = match self.executor.call(
964            self.sender,
965            self.address,
966            func,
967            &[],
968            U256::ZERO,
969            Some(self.revert_decoder()),
970        ) {
971            Ok(res) => (res.raw, None),
972            Err(EvmError::Execution(err)) => (err.raw, Some(err.reason)),
973            Err(EvmError::Skip(reason)) => {
974                self.result.single_skip(reason);
975                return self.result;
976            }
977            Err(err) => {
978                self.result.single_fail(Some(err.to_string()));
979                return self.result;
980            }
981        };
982
983        let success =
984            self.executor.is_raw_call_mut_success(self.address, &mut raw_call_result, false);
985        self.result.single_result(success, reason, raw_call_result);
986        self.result
987    }
988
989    /// Runs a symbolic test and replays any discovered counterexample concretely.
990    fn run_symbolic_test(mut self, func: &Function) -> TestResult {
991        if self.prepare_test(func).is_err() {
992            return self.result;
993        }
994
995        let mut symbolic = SymbolicExecutor::new(self.config.symbolic.clone());
996        if self.should_defer_symbolic_diagnostics() {
997            symbolic.capture_diagnostics();
998        }
999        let result = symbolic.run(SymbolicRunInput {
1000            executor: self.executor.as_ref(),
1001            target: self.address,
1002            sender: self.sender,
1003            function: func,
1004            value: U256::ZERO,
1005            ffi_enabled: self.config.ffi,
1006        });
1007        let portfolio_diagnostics = symbolic.portfolio_diagnostics();
1008        let symbolic_diagnostics = symbolic.take_diagnostics();
1009
1010        match result {
1011            SymbolicRunResult::Safe(stats) => {
1012                self.result.symbolic_result(true, None, None, stats);
1013            }
1014            SymbolicRunResult::Incomplete { kind, reason, stats } => {
1015                self.result.symbolic_result(
1016                    false,
1017                    Some(format!("incomplete symbolic execution ({kind:?}): {reason}")),
1018                    None,
1019                    stats,
1020                );
1021            }
1022            SymbolicRunResult::Counterexample { args, calldata, stats } => {
1023                let (mut raw_call_result, reason) = match self.executor.call(
1024                    self.sender,
1025                    self.address,
1026                    func,
1027                    &args,
1028                    U256::ZERO,
1029                    Some(self.revert_decoder()),
1030                ) {
1031                    Ok(res) => (res.raw, None),
1032                    Err(EvmError::Execution(err)) => (err.raw, Some(err.reason)),
1033                    Err(EvmError::Skip(reason)) => {
1034                        self.result.single_skip(reason);
1035                        self.result.symbolic_portfolio_diagnostics = portfolio_diagnostics;
1036                        self.result.symbolic_diagnostics = symbolic_diagnostics;
1037                        return self.result;
1038                    }
1039                    Err(err) => {
1040                        self.result.symbolic_result(false, Some(err.to_string()), None, stats);
1041                        self.result.symbolic_portfolio_diagnostics = portfolio_diagnostics;
1042                        self.result.symbolic_diagnostics = symbolic_diagnostics;
1043                        return self.result;
1044                    }
1045                };
1046
1047                let success = self.executor.is_raw_call_mut_success(
1048                    self.address,
1049                    &mut raw_call_result,
1050                    false,
1051                );
1052                let counterexample = CounterExample::Single(BaseCounterExample::from_fuzz_call(
1053                    calldata,
1054                    args,
1055                    raw_call_result.traces.clone(),
1056                ));
1057                self.result.extend(raw_call_result);
1058                self.result.symbolic_result(
1059                    false,
1060                    if success {
1061                        Some("symbolic counterexample did not replay".to_string())
1062                    } else {
1063                        reason
1064                    },
1065                    Some(counterexample),
1066                    stats,
1067                );
1068            }
1069        }
1070
1071        self.result.symbolic_portfolio_diagnostics = portfolio_diagnostics;
1072        self.result.symbolic_diagnostics = symbolic_diagnostics;
1073        self.result
1074    }
1075
1076    /// Runs a table test.
1077    /// The parameters dataset (table) is created from defined parameter fixtures, therefore each
1078    /// test table parameter should have the same number of fixtures defined.
1079    /// E.g. for table test
1080    /// - `table_test(uint256 amount, bool swap)` fixtures are defined as
1081    /// - `uint256[] public fixtureAmount = [2, 5]`
1082    /// - `bool[] public fixtureSwap = [true, false]` The `table_test` is then called with the pair
1083    ///   of args `(2, true)` and `(5, false)`.
1084    fn run_table_test(mut self, func: &Function) -> TestResult {
1085        // Prepare unit test execution.
1086        if self.prepare_test(func).is_err() {
1087            return self.result;
1088        }
1089
1090        // Extract and validate fixtures for the first table test parameter.
1091        let Some(first_param) = func.inputs.first() else {
1092            self.result.single_fail(Some("Table test should have at least one parameter".into()));
1093            return self.result;
1094        };
1095
1096        let Some(first_param_fixtures) =
1097            &self.setup.fuzz_fixtures.param_fixtures(first_param.name())
1098        else {
1099            self.result.single_fail(Some("Table test should have fixtures defined".into()));
1100            return self.result;
1101        };
1102
1103        if first_param_fixtures.is_empty() {
1104            self.result.single_fail(Some("Table test should have at least one fixture".into()));
1105            return self.result;
1106        }
1107
1108        let fixtures_len = first_param_fixtures.len();
1109        let mut table_fixtures = vec![&first_param_fixtures[..]];
1110
1111        // Collect fixtures for remaining parameters.
1112        for param in &func.inputs[1..] {
1113            let param_name = param.name();
1114            let Some(fixtures) = &self.setup.fuzz_fixtures.param_fixtures(param.name()) else {
1115                self.result.single_fail(Some(format!("No fixture defined for param {param_name}")));
1116                return self.result;
1117            };
1118
1119            if fixtures.len() != fixtures_len {
1120                self.result.single_fail(Some(format!(
1121                    "{} fixtures defined for {param_name} (expected {})",
1122                    fixtures.len(),
1123                    fixtures_len
1124                )));
1125                return self.result;
1126            }
1127
1128            table_fixtures.push(&fixtures[..]);
1129        }
1130
1131        let progress = start_fuzz_progress(
1132            self.cr.progress,
1133            self.cr.name,
1134            &func.name,
1135            None,
1136            fixtures_len as u32,
1137        );
1138
1139        let mut result = FuzzTestResult::default();
1140
1141        for i in 0..fixtures_len {
1142            if self.tcfg.early_exit.should_stop() {
1143                return self.result;
1144            }
1145
1146            // Increment progress bar.
1147            if let Some(progress) = progress.as_ref() {
1148                progress.inc(1);
1149            }
1150
1151            let args = table_fixtures.iter().map(|row| row[i].clone()).collect_vec();
1152            let (mut raw_call_result, reason) = match self.executor.call(
1153                self.sender,
1154                self.address,
1155                func,
1156                &args,
1157                U256::ZERO,
1158                Some(self.revert_decoder()),
1159            ) {
1160                Ok(res) => (res.raw, None),
1161                Err(EvmError::Execution(err)) => (err.raw, Some(err.reason)),
1162                Err(EvmError::Skip(reason)) => {
1163                    self.result.single_skip(reason);
1164                    return self.result;
1165                }
1166                Err(err) => {
1167                    self.result.single_fail(Some(err.to_string()));
1168                    return self.result;
1169                }
1170            };
1171
1172            result.gas_by_case.push((raw_call_result.gas_used, raw_call_result.stipend));
1173            result.logs.extend(raw_call_result.logs.clone());
1174            result.labels.extend(raw_call_result.labels.clone());
1175            HitMaps::merge_opt(&mut result.line_coverage, raw_call_result.line_coverage.clone());
1176
1177            let is_success =
1178                self.executor.is_raw_call_mut_success(self.address, &mut raw_call_result, false);
1179            // Record counterexample if test fails.
1180            if !is_success {
1181                result.counterexample =
1182                    Some(CounterExample::Single(BaseCounterExample::from_fuzz_call(
1183                        Bytes::from(func.abi_encode_input(&args).unwrap()),
1184                        args,
1185                        raw_call_result.traces.clone(),
1186                    )));
1187                result.reason = reason;
1188                result.traces = raw_call_result.traces;
1189                result.debug_bytecodes = raw_call_result.debug_bytecodes;
1190                self.result.table_result(result);
1191                return self.result;
1192            }
1193
1194            // If it's the last iteration and all other runs succeeded, then use last call result
1195            // for logs and traces.
1196            if i == fixtures_len - 1 {
1197                result.success = true;
1198                result.traces = raw_call_result.traces;
1199                result.debug_bytecodes = raw_call_result.debug_bytecodes;
1200                self.result.table_result(result);
1201                return self.result;
1202            }
1203        }
1204
1205        self.result
1206    }
1207
1208    fn run_invariant_test(
1209        mut self,
1210        func: &Function,
1211        invariants: Vec<(&Function, bool)>,
1212        call_after_invariant: bool,
1213        identified_contracts: &ContractsByAddress,
1214    ) -> TestResult {
1215        let runner = self.invariant_runner();
1216        let invariant_config = self.config.invariant.clone();
1217        let invariant_config = &invariant_config;
1218        let is_optimization = is_optimization_invariant(func);
1219
1220        let mut live_invariants = Vec::new();
1221        let mut skipped_predicate_results = Vec::new();
1222        for (invariant, fail_on_revert) in invariants {
1223            if let Some(reason) = self.invariant_skip_reason(invariant) {
1224                skipped_predicate_results.push(InvariantPredicateResult {
1225                    name: invariant.name.clone(),
1226                    status: TestStatus::Skipped,
1227                    reason: reason.0,
1228                });
1229            } else {
1230                live_invariants.push((invariant, fail_on_revert));
1231            }
1232        }
1233
1234        if live_invariants.is_empty() {
1235            let skip_reason = skipped_predicate_results
1236                .iter()
1237                .find(|predicate| predicate.name == func.name)
1238                .and_then(|predicate| predicate.reason.clone());
1239            self.result
1240                .invariant_skip_with_predicates(SkipReason(skip_reason), skipped_predicate_results);
1241            return self.result;
1242        }
1243        let campaign_anchor = live_invariants
1244            .iter()
1245            .find(|(invariant_fn, _)| *invariant_fn == func)
1246            .map(|(invariant_fn, _)| *invariant_fn)
1247            .unwrap_or_else(|| live_invariants[0].0);
1248
1249        let mut executor = self.clone_executor();
1250        // Enable edge coverage if running with coverage guided fuzzing or with edge coverage
1251        // metrics (useful for benchmarking the fuzzer).
1252        executor.inspector_mut().collect_edge_coverage_with_config(&invariant_config.corpus);
1253        executor.inspector_mut().collect_evm_cmp_log(invariant_config.corpus.collect_evm_cmp_log());
1254        executor
1255            .inspector_mut()
1256            .collect_sancov_edges(invariant_config.corpus.collect_sancov_edges());
1257        executor
1258            .inspector_mut()
1259            .collect_sancov_trace_cmp(invariant_config.corpus.collect_sancov_trace_cmp());
1260        let mut config = invariant_config.clone();
1261        let failure_dir = invariant_suite_paths(
1262            &mut config.corpus,
1263            invariant_config.failure_persist_dir.clone().unwrap(),
1264            self.cr.name,
1265            func.name.as_str(),
1266            is_optimization,
1267        );
1268        // Snapshot the per-test corpus dir before `config` is moved into `InvariantExecutor`.
1269        let resolved_corpus_dir = config.corpus.corpus_dir.clone();
1270
1271        let mut evm = InvariantExecutor::new_with_fuzz_seed(
1272            executor,
1273            runner,
1274            self.config.fuzz.seed,
1275            config,
1276            identified_contracts,
1277            &self.cr.mcr.known_contracts,
1278            self.cr.num_invariant_campaign_anchors,
1279        );
1280
1281        // Showmap replay mode: replay the persisted corpus and emit coverage
1282        // files instead of running the invariant campaign.
1283        if let Some(showmap) = self.cr.mcr.tcfg.showmap.clone() {
1284            let corpus_dir = showmap.corpus_dir.clone().or(resolved_corpus_dir);
1285
1286            // Reconstruct the per-test target selection that the campaign loop normally builds.
1287            if let Err(e) = evm.select_contract_artifacts(self.address) {
1288                self.result.invariant_setup_fail(e);
1289                return self.result;
1290            }
1291            let targeted = match evm.select_contracts_and_senders(self.address) {
1292                Ok((_, t)) => t,
1293                Err(e) => {
1294                    self.result.invariant_setup_fail(e);
1295                    return self.result;
1296                }
1297            };
1298            let dynamic = evm.dynamic_target_ctx();
1299            return self.run_showmap(
1300                func,
1301                corpus_dir,
1302                &showmap,
1303                None,
1304                Some(&targeted),
1305                Some(&dynamic),
1306            );
1307        }
1308
1309        // Compute current invariant settings up front so secondary persisted-failure handling
1310        // can use the same compatibility check as the primary replay path below.
1311        let current_settings = match evm.compute_settings(self.address) {
1312            Ok(s) => s,
1313            Err(e) => {
1314                self.result.invariant_setup_fail(e);
1315                return self.result;
1316            }
1317        };
1318        // A non-anchor predicate's persisted failure is only honored when its embedded settings
1319        // still match the current run; stale caches fall back to a fresh campaign.
1320        let persisted_invariants = if is_optimization {
1321            BTreeSet::new()
1322        } else {
1323            live_invariants
1324                .iter()
1325                .filter(|(invariant_fn, _)| *invariant_fn != campaign_anchor)
1326                .filter_map(|(invariant_fn, _)| {
1327                    persisted_invariant_failure(&failure_dir, invariant_fn, &current_settings)
1328                        .is_some()
1329                        .then_some(invariant_fn.name.as_str())
1330                })
1331                .collect::<BTreeSet<_>>()
1332        };
1333        // Warn when predicates are dropped because they already have persisted failures from a
1334        // previous campaign. Symmetric with the primary's persisted-replay warning so users
1335        // aren't surprised when fewer invariants appear in the report than their contract
1336        // defines (Echidna/Medusa never skip properties between runs).
1337        if !is_optimization {
1338            let persisted_skipped: Vec<&str> = live_invariants
1339                .iter()
1340                .filter(|(invariant_fn, _)| {
1341                    *invariant_fn != campaign_anchor
1342                        && persisted_invariants.contains(invariant_fn.name.as_str())
1343                })
1344                .map(|(invariant_fn, _)| invariant_fn.name.as_str())
1345                .collect();
1346            if !persisted_skipped.is_empty() {
1347                let _ = sh_warn!(
1348                    "{}: {} invariant(s) skipped due to persisted failures: {}. \
1349                     Run `forge clean` or delete files in {} to re-include.",
1350                    self.cr.name,
1351                    persisted_skipped.len(),
1352                    persisted_skipped.join(", "),
1353                    failure_dir.display(),
1354                );
1355            }
1356        }
1357        // Build the invariant list in source declaration order, retaining the anchor (`func`)
1358        // and every other selected predicate that doesn't already have a compatible persisted
1359        // failure. Track the anchor's index so downstream consumers can resolve the campaign
1360        // anchor without searching by name.
1361        let invariant_fns: Vec<(&Function, bool)> = live_invariants
1362            .into_iter()
1363            .filter(|(invariant_fn, _)| {
1364                *invariant_fn == campaign_anchor
1365                    || (!is_optimization
1366                        && !persisted_invariants.contains(invariant_fn.name.as_str()))
1367            })
1368            .collect();
1369        let anchor_idx = invariant_fns
1370            .iter()
1371            .position(|(invariant_fn, _)| *invariant_fn == campaign_anchor)
1372            .expect("campaign anchor must be present in invariant_fns");
1373        let predicate_count = invariant_fns.len() + skipped_predicate_results.len();
1374        let invariant_contract = InvariantContract::new(
1375            self.address,
1376            self.cr.name,
1377            invariant_fns,
1378            anchor_idx,
1379            call_after_invariant,
1380            &self.cr.contract.abi,
1381        );
1382        let show_solidity = invariant_config.show_solidity;
1383        let is_campaign = predicate_count > 1;
1384        let invariant_count = is_campaign.then_some(predicate_count);
1385        let invariant_display_name = if is_campaign {
1386            Cow::Owned(invariant_campaign_display_name(self.cr.name))
1387        } else {
1388            Cow::Borrowed(func.name.as_str())
1389        };
1390
1391        let progress = start_fuzz_progress(
1392            self.cr.progress,
1393            self.cr.name,
1394            invariant_display_name.as_ref(),
1395            invariant_config.timeout,
1396            invariant_config.runs,
1397        );
1398
1399        let replay_ctx = ReplayContext {
1400            invariant_contract: &invariant_contract,
1401            invariant_config,
1402            revert_decoder: self.revert_decoder(),
1403            show_solidity,
1404        };
1405
1406        // Try to replay recorded failure if any.
1407        let primary_failure_file =
1408            invariant_failure_file(&failure_dir, invariant_contract.anchor());
1409        let persisted_primary = persisted_invariant_failure(
1410            &failure_dir,
1411            invariant_contract.anchor(),
1412            &current_settings,
1413        );
1414        if let Some(InvariantPersistedFailure { mut call_sequence, assertion_failure, .. }) =
1415            persisted_primary
1416        {
1417            let (txes, replay) = replay_persisted_call_sequence(
1418                &replay_ctx,
1419                self.clone_executor(),
1420                &mut call_sequence,
1421                assertion_failure,
1422            );
1423            if let Ok((success, replayed_entirely, replay_reason)) = replay
1424                && !success
1425            {
1426                let warn =
1427                    "Replayed invariant failure from persisted file. \nRun `forge clean` or remove file to ignore failure and to continue invariant test campaign."
1428                        .to_string();
1429
1430                if let Some(ref progress) = progress {
1431                    progress.set_prefix(format!("{invariant_display_name}\n{warn}\n"));
1432                } else {
1433                    let _ = sh_warn!("{warn}");
1434                }
1435
1436                // If sequence still fails then replay error to collect traces and exit without
1437                // executing new runs.
1438                match replay_error(
1439                    evm.config(),
1440                    self.clone_executor(),
1441                    &txes,
1442                    None,
1443                    assertion_failure,
1444                    None, // check mode
1445                    &invariant_contract,
1446                    invariant_contract.anchor(),
1447                    &self.cr.mcr.known_contracts,
1448                    identified_contracts.clone(),
1449                    &mut self.result.logs,
1450                    &mut self.result.traces,
1451                    &mut self.result.debug_bytecodes,
1452                    &mut self.result.line_coverage,
1453                    &mut self.result.deprecated_cheatcodes,
1454                    progress.as_ref(),
1455                    &self.tcfg.early_exit,
1456                    None, // single-invariant replay path; no [i/N] counter
1457                ) {
1458                    Ok(replayed_call_sequence) if !replayed_call_sequence.is_empty() => {
1459                        call_sequence = replayed_call_sequence;
1460                        // Persist error in invariant failure dir.
1461                        record_invariant_failure(
1462                            failure_dir.as_path(),
1463                            primary_failure_file.as_path(),
1464                            &call_sequence,
1465                            &current_settings,
1466                            assertion_failure,
1467                        );
1468                    }
1469                    Ok(_) => {}
1470                    Err(err) => {
1471                        error!(%err, "Failed to replay invariant error");
1472                    }
1473                }
1474
1475                self.result.invariant_replay_fail(
1476                    replayed_entirely,
1477                    &invariant_contract.anchor().name,
1478                    replay_reason,
1479                    call_sequence,
1480                );
1481                return self.result;
1482            }
1483        }
1484
1485        // Replay persisted handler bugs; feed still-reproducing ones into the campaign,
1486        // delete stale files in place.
1487        let persisted_handler_failures = replay_persisted_handler_failures(
1488            &failure_dir.join("handlers"),
1489            &current_settings,
1490            self.clone_executor(),
1491            &replay_ctx,
1492        );
1493
1494        let invariant_result = match evm.invariant_fuzz(
1495            invariant_contract.clone(),
1496            &self.setup.fuzz_fixtures,
1497            self.build_fuzz_state(true),
1498            progress.as_ref(),
1499            &self.tcfg.early_exit,
1500            persisted_handler_failures,
1501        ) {
1502            Ok(x) => x,
1503            Err(e) => {
1504                self.result.invariant_setup_fail(e);
1505                return self.result;
1506            }
1507        };
1508        // Merge coverage collected during invariant run with test setup coverage.
1509        self.result.merge_coverages(invariant_result.line_coverage);
1510
1511        let mut counterexample = None;
1512        // Success requires zero predicate breaks *and* zero handler-side assertion bugs.
1513        let success =
1514            invariant_result.errors.is_empty() && invariant_result.handler_errors.is_empty();
1515        let mut invariant_failures: Vec<InvariantFailure> = vec![];
1516        let mut any_failure_persisted = false;
1517
1518        if success {
1519            if let Some(best_value) = invariant_result.optimization_best_value {
1520                // Optimization mode: replay and shrink to find shortest best sequence.
1521                match replay_error(
1522                    evm.config(),
1523                    self.clone_executor(),
1524                    &invariant_result.optimization_best_sequence,
1525                    None,
1526                    false,
1527                    Some(best_value),
1528                    &invariant_contract,
1529                    invariant_contract.anchor(),
1530                    &self.cr.mcr.known_contracts,
1531                    identified_contracts.clone(),
1532                    &mut self.result.logs,
1533                    &mut self.result.traces,
1534                    &mut self.result.debug_bytecodes,
1535                    &mut self.result.line_coverage,
1536                    &mut self.result.deprecated_cheatcodes,
1537                    progress.as_ref(),
1538                    &self.tcfg.early_exit,
1539                    None, // optimization mode is single-invariant; no [i/N] counter
1540                ) {
1541                    Ok(best_sequence) if !best_sequence.is_empty() => {
1542                        counterexample = Some(CounterExample::Sequence(
1543                            invariant_result.optimization_best_sequence.len(),
1544                            best_sequence,
1545                        ));
1546                    }
1547                    Err(err) => {
1548                        error!(%err, "Failed to replay optimization best sequence");
1549                    }
1550                    _ => {}
1551                }
1552            } else {
1553                // Standard check mode: replay last run for traces.
1554                if let Err(err) = replay_run(
1555                    &invariant_contract,
1556                    invariant_contract.anchor(),
1557                    self.clone_executor(),
1558                    &self.cr.mcr.known_contracts,
1559                    identified_contracts.clone(),
1560                    &mut self.result.logs,
1561                    &mut self.result.traces,
1562                    &mut self.result.debug_bytecodes,
1563                    &mut self.result.line_coverage,
1564                    &mut self.result.deprecated_cheatcodes,
1565                    &invariant_result.last_run_inputs,
1566                    show_solidity,
1567                ) {
1568                    error!(%err, "Failed to replay last invariant run");
1569                }
1570            }
1571        } else {
1572            // Total broken invariants in this campaign — used to decorate the shrink progress
1573            // bar with `[i/N]` so users see how many shrinkers are queued behind the current
1574            // one. `errors` keys cover both the anchor and any broken secondaries.
1575            let total_broken = invariant_result.errors.len();
1576            // Replay-and-shrink the anchor's failure first (gets [1/N] on the progress bar),
1577            // then push it into `invariant_failures` as the first entry. Non-replayable error
1578            // variants (e.g. `MaxAssumeRejects`) still get an entry — without a counterexample
1579            // — so the reason is rendered.
1580            if let Some(error) = invariant_result.errors.get(&invariant_contract.anchor().name) {
1581                let anchor_counterexample = match error {
1582                    InvariantFuzzError::BrokenInvariant(case_data)
1583                    | InvariantFuzzError::Revert(case_data) => {
1584                        let TestError::Fail(_, ref calls) = case_data.test_error else {
1585                            unreachable!("FailedInvariantCaseData::new always sets TestError::Fail")
1586                        };
1587                        match replay_error(
1588                            evm.config(),
1589                            self.clone_executor(),
1590                            calls,
1591                            Some(case_data.inner_sequence.clone()),
1592                            case_data.assertion_failure,
1593                            None, // check mode
1594                            &invariant_contract,
1595                            invariant_contract.anchor(),
1596                            &self.cr.mcr.known_contracts,
1597                            identified_contracts.clone(),
1598                            &mut self.result.logs,
1599                            &mut self.result.traces,
1600                            &mut self.result.debug_bytecodes,
1601                            &mut self.result.line_coverage,
1602                            &mut self.result.deprecated_cheatcodes,
1603                            progress.as_ref(),
1604                            &self.tcfg.early_exit,
1605                            Some((1, total_broken)),
1606                        ) {
1607                            Ok(call_sequence) if !call_sequence.is_empty() => {
1608                                record_invariant_failure(
1609                                    failure_dir.as_path(),
1610                                    primary_failure_file.as_path(),
1611                                    &call_sequence,
1612                                    &current_settings,
1613                                    case_data.assertion_failure,
1614                                );
1615                                any_failure_persisted = true;
1616                                Some(CounterExample::Sequence(calls.len(), call_sequence))
1617                            }
1618                            Ok(_) => None,
1619                            Err(err) => {
1620                                error!(%err, "Failed to replay invariant error");
1621                                None
1622                            }
1623                        }
1624                    }
1625                    InvariantFuzzError::MaxAssumeRejects(_) => None,
1626                    // Handler bugs live in `handler_errors`; defensive None here.
1627                    InvariantFuzzError::HandlerAssertion(_) => None,
1628                };
1629                invariant_failures.push(InvariantFailure::Predicate {
1630                    name: invariant_contract.anchor().name.clone(),
1631                    reason: error.revert_reason().unwrap_or_default(),
1632                    counterexample: anchor_counterexample,
1633                    persisted_path: primary_failure_file,
1634                    is_anchor: true,
1635                });
1636            }
1637
1638            // Shrink each broken non-primary invariant in turn so users get a ready-to-debug
1639            // counterexample for every failure in a single run. Loop is serial; on Ctrl+C we
1640            // still record every known secondary failure (without shrinking or persisting), so
1641            // the final report matches what the live progress bar showed.
1642            //
1643            // `next_position` tracks where this invariant sits in the broken queue (primary is
1644            // 1, secondaries follow). Only incremented when a secondary is actually shrunk so
1645            // the bar's `[i/N]` counter matches user-visible progress.
1646            let mut next_position = 2usize;
1647            // Iterate every invariant; skip the anchor (handled in the primary path above).
1648            for (idx, (invariant, _)) in invariant_contract.invariant_fns.iter().enumerate() {
1649                if idx == invariant_contract.anchor_idx {
1650                    continue;
1651                }
1652
1653                // Skip invariants whose counterexample is already persisted from a prior run
1654                // (those were filtered out of the live campaign earlier; `errors` won't contain
1655                // them, but the dir check is a belt-and-braces safety net). Use the same
1656                // settings-aware compatibility check as the filter so a stale persisted cache
1657                // doesn't suppress a freshly-broken secondary.
1658                let persisted_failure = invariant_failure_file(&failure_dir, invariant);
1659                if !persisted_invariants.contains(invariant.name.as_str())
1660                    && let Some(error) = invariant_result.errors.get(&invariant.name)
1661                    && let InvariantFuzzError::BrokenInvariant(case_data)
1662                    | InvariantFuzzError::Revert(case_data) = error
1663                    && let TestError::Fail(_, ref calls) = case_data.test_error
1664                {
1665                    let original_seq_len = calls.len();
1666                    // On Ctrl+C: skip the (potentially long) replay+shrink, but still persist
1667                    // the un-shrunk sequence so the next run targeting this invariant picks it
1668                    // up and shrinks from the saved counterexample. The current run's output
1669                    // still gets a terse `name: reason` line via the no-counterexample path.
1670                    let secondary_counterexample = if self.tcfg.early_exit.should_stop() {
1671                        let unshrunk_sequence = calls
1672                            .iter()
1673                            .map(|tx| {
1674                                BaseCounterExample::from_invariant_call(
1675                                    tx,
1676                                    identified_contracts,
1677                                    None,
1678                                    invariant_config.show_solidity,
1679                                )
1680                            })
1681                            .collect::<Vec<_>>();
1682                        record_invariant_failure(
1683                            failure_dir.as_path(),
1684                            persisted_failure.as_path(),
1685                            &unshrunk_sequence,
1686                            &current_settings,
1687                            case_data.assertion_failure,
1688                        );
1689                        any_failure_persisted = true;
1690                        None
1691                    } else {
1692                        let position = next_position;
1693                        next_position += 1;
1694                        match replay_error(
1695                            invariant_config.clone(),
1696                            self.clone_executor(),
1697                            calls,
1698                            Some(case_data.inner_sequence.clone()),
1699                            case_data.assertion_failure,
1700                            None, // check mode
1701                            &invariant_contract,
1702                            invariant,
1703                            &self.cr.mcr.known_contracts,
1704                            identified_contracts.clone(),
1705                            &mut self.result.logs,
1706                            &mut self.result.traces,
1707                            &mut self.result.debug_bytecodes,
1708                            &mut self.result.line_coverage,
1709                            &mut self.result.deprecated_cheatcodes,
1710                            progress.as_ref(),
1711                            &self.tcfg.early_exit,
1712                            Some((position, total_broken)),
1713                        ) {
1714                            Ok(call_sequence) if !call_sequence.is_empty() => {
1715                                record_invariant_failure(
1716                                    failure_dir.as_path(),
1717                                    persisted_failure.as_path(),
1718                                    &call_sequence,
1719                                    &current_settings,
1720                                    case_data.assertion_failure,
1721                                );
1722                                any_failure_persisted = true;
1723                                Some(CounterExample::Sequence(original_seq_len, call_sequence))
1724                            }
1725                            Ok(_) => None,
1726                            Err(err) => {
1727                                error!(%err, "Failed to replay invariant error");
1728                                None
1729                            }
1730                        }
1731                    };
1732                    invariant_failures.push(InvariantFailure::Predicate {
1733                        name: invariant.name.clone(),
1734                        reason: error.revert_reason().unwrap_or_default(),
1735                        counterexample: secondary_counterexample,
1736                        persisted_path: persisted_failure.clone(),
1737                        is_anchor: false,
1738                    });
1739                }
1740            }
1741        }
1742
1743        let invariant_failure_dir = any_failure_persisted.then(|| failure_dir.clone());
1744        let invariant_predicate_results = if is_campaign {
1745            let failures_by_name = invariant_failures
1746                .iter()
1747                .map(|failure| (failure.name(), failure))
1748                .collect::<BTreeMap<_, _>>();
1749            invariant_contract
1750                .invariant_fns
1751                .iter()
1752                .map(|(invariant, _)| {
1753                    if let Some(failure) = failures_by_name.get(invariant.name.as_str()) {
1754                        InvariantPredicateResult {
1755                            name: invariant.name.clone(),
1756                            status: TestStatus::Failure,
1757                            reason: Some(failure.reason().to_string()),
1758                        }
1759                    } else {
1760                        InvariantPredicateResult {
1761                            name: invariant.name.clone(),
1762                            status: TestStatus::Success,
1763                            reason: None,
1764                        }
1765                    }
1766                })
1767                .chain(skipped_predicate_results)
1768                .sorted_by_key(|predicate| {
1769                    self.cr
1770                        .contract
1771                        .abi
1772                        .functions()
1773                        .position(|func| func.name == predicate.name)
1774                        .unwrap_or(usize::MAX)
1775                })
1776                .collect()
1777        } else {
1778            Vec::new()
1779        };
1780
1781        // Convert handler-side assertion bugs into render-ready entries. The name is a
1782        // best-effort `Contract::function` from `identified_contracts`, falling back to
1783        // `0xreverter::0xselector`. Map is keyed by `(reverter, selector)` site so multiple
1784        // code paths through the same function collapse to one entry, rendered in the
1785        // dedicated handler assertions section.
1786        let identified_contracts_ro = identified_contracts;
1787        let invariant_handler_failures = invariant_result
1788            .handler_errors
1789            .iter()
1790            .sorted_by(|(ka, _), (kb, _)| {
1791                // Stable order across runs: sort by `(reverter, selector)` site directly.
1792                ka.cmp(kb)
1793            })
1794            .filter_map(|(site, err)| err.as_handler_assertion().map(|f| (site, f)))
1795            .map(|(_site, failure)| {
1796                let reverter = failure.reverter;
1797                let selector = failure.selector;
1798                // Resolve `Contract::function` from identified contracts when possible.
1799                let resolved_name = identified_contracts_ro
1800                    .get(&reverter)
1801                    .and_then(|(contract_name, abi)| {
1802                        abi.functions()
1803                            .find(|f| f.selector() == selector)
1804                            .map(|f| format!("{contract_name}::{}", f.name))
1805                    })
1806                    .unwrap_or_else(|| format!("{reverter}::{selector}"));
1807
1808                let counterexample_calls = failure
1809                    .call_sequence
1810                    .iter()
1811                    .map(|tx| {
1812                        BaseCounterExample::from_invariant_call(
1813                            tx,
1814                            identified_contracts_ro,
1815                            None,
1816                            invariant_config.show_solidity,
1817                        )
1818                    })
1819                    .collect::<Vec<_>>();
1820
1821                // Persist for next-run replay (skip if nothing to record).
1822                if !counterexample_calls.is_empty() {
1823                    record_handler_failure(
1824                        failure_dir.as_path(),
1825                        reverter,
1826                        selector,
1827                        &counterexample_calls,
1828                        &current_settings,
1829                    );
1830                }
1831
1832                let counterexample = if counterexample_calls.is_empty() {
1833                    None
1834                } else {
1835                    // Preserve pre-shrink length for `(original: N, shrunk: M)` rendering.
1836                    Some(CounterExample::Sequence(
1837                        failure.original_sequence_len,
1838                        counterexample_calls,
1839                    ))
1840                };
1841
1842                InvariantFailure::Handler {
1843                    name: resolved_name,
1844                    reverter,
1845                    selector,
1846                    reason: failure.revert_reason.clone(),
1847                    counterexample,
1848                }
1849            })
1850            .collect::<Vec<_>>();
1851
1852        self.result.invariant_result(
1853            invariant_result.gas_report_traces,
1854            success,
1855            invariant_failures,
1856            invariant_predicate_results,
1857            invariant_failure_dir,
1858            invariant_count,
1859            invariant_handler_failures,
1860            counterexample,
1861            invariant_result.runs,
1862            invariant_result.calls,
1863            invariant_result.reverts,
1864            invariant_result.metrics,
1865            invariant_result.failed_corpus_replays,
1866            invariant_result.workers,
1867            invariant_result.optimization_best_value,
1868        );
1869        self.result
1870    }
1871
1872    fn invariant_skip_reason(&self, func: &Function) -> Option<SkipReason> {
1873        match self.executor.call(
1874            self.sender,
1875            self.address,
1876            func,
1877            &[],
1878            U256::ZERO,
1879            Some(self.revert_decoder()),
1880        ) {
1881            Err(EvmError::Skip(reason)) => Some(reason),
1882            _ => None,
1883        }
1884    }
1885
1886    /// Runs a fuzzed test.
1887    ///
1888    /// Applies the before test txes (if any), fuzzes the current function and returns the
1889    /// `TestResult`.
1890    ///
1891    /// Before test txes are applied in order and state modifications committed to the EVM database
1892    /// (therefore the fuzz test will use the modified state).
1893    /// State modifications of before test txes and fuzz test are discarded after test ends,
1894    /// similar to `eth_call`.
1895    fn run_fuzz_test(mut self, func: &Function) -> TestResult {
1896        // Prepare fuzz test execution.
1897        if self.prepare_test(func).is_err() {
1898            return self.result;
1899        }
1900
1901        let runner = self.fuzz_runner();
1902        let mut fuzz_config = self.config.fuzz.clone();
1903        let (failure_dir, failure_file) = test_paths(
1904            &mut fuzz_config.corpus,
1905            fuzz_config.failure_persist_dir.clone().unwrap(),
1906            self.cr.name,
1907            &func.name,
1908        );
1909
1910        // Showmap replay mode: replay the persisted corpus and emit coverage
1911        // files instead of running the fuzz campaign.
1912        if let Some(showmap) = self.cr.mcr.tcfg.showmap.clone() {
1913            let corpus_dir =
1914                showmap.corpus_dir.clone().or_else(|| fuzz_config.corpus.corpus_dir.clone());
1915            return self.run_showmap(func, corpus_dir, &showmap, Some(func), None, None);
1916        }
1917
1918        let progress = start_fuzz_progress(
1919            self.cr.progress,
1920            self.cr.name,
1921            &func.name,
1922            fuzz_config.timeout,
1923            if fuzz_config.run.is_some() { 1 } else { fuzz_config.runs },
1924        );
1925
1926        let state = self.build_fuzz_state(false);
1927        let mut executor = self.executor.into_owned();
1928        // Enable edge coverage if running with coverage guided fuzzing or with edge coverage
1929        // metrics (useful for benchmarking the fuzzer).
1930        executor.inspector_mut().collect_edge_coverage_with_config(&fuzz_config.corpus);
1931        executor.inspector_mut().collect_evm_cmp_log(fuzz_config.corpus.collect_evm_cmp_log());
1932        executor.inspector_mut().collect_sancov_edges(fuzz_config.corpus.collect_sancov_edges());
1933        executor
1934            .inspector_mut()
1935            .collect_sancov_trace_cmp(fuzz_config.corpus.collect_sancov_trace_cmp());
1936        // Load persisted counterexample, if any.
1937        let persisted_failure =
1938            foundry_common::fs::read_json_file::<BaseCounterExample>(failure_file.as_path()).ok();
1939        // Run fuzz test.
1940        let mut fuzzed_executor =
1941            FuzzedExecutor::new(executor, runner, self.tcfg.sender, fuzz_config, persisted_failure);
1942        let result = match fuzzed_executor.fuzz(
1943            func,
1944            &self.setup.fuzz_fixtures,
1945            state,
1946            self.address,
1947            &self.cr.mcr.revert_decoder,
1948            progress.as_ref(),
1949            &self.tcfg.early_exit,
1950            &self.cr.tokio_handle,
1951        ) {
1952            Ok(x) => x,
1953            Err(e) => {
1954                self.result.fuzz_setup_fail(e);
1955                return self.result;
1956            }
1957        };
1958
1959        // Record counterexample.
1960        if let Some(CounterExample::Single(counterexample)) = &result.counterexample {
1961            if let Err(err) = foundry_common::fs::create_dir_all(failure_dir) {
1962                error!(%err, "Failed to create fuzz failure dir");
1963            } else if let Err(err) =
1964                foundry_common::fs::write_json_file(failure_file.as_path(), counterexample)
1965            {
1966                error!(%err, "Failed to record call sequence");
1967            }
1968        }
1969
1970        self.result.fuzz_result(result);
1971        self.result
1972    }
1973
1974    /// Prepares single unit test and fuzz test execution:
1975    /// - set up the test result and executor
1976    /// - check if before test txes are configured and apply them in order
1977    ///
1978    /// Before test txes are arrays of arbitrary calldata obtained by calling the `beforeTest`
1979    /// function with test selector as a parameter.
1980    ///
1981    /// Unit tests within same contract (or even current test) are valid options for before test tx
1982    /// configuration. Test execution stops if any of before test txes fails.
1983    fn prepare_test(&mut self, func: &Function) -> Result<(), ()> {
1984        let address = self.setup.address;
1985
1986        // Apply before test configured functions (if any).
1987        if self.cr.contract.abi.functions().any(|func| func.name.is_before_test_setup()) {
1988            for calldata in self.executor.call_sol_default(
1989                address,
1990                &ITest::beforeTestSetupCall { testSelector: func.selector() },
1991            ) {
1992                let spec_id: SpecId = self.executor.spec_id().into();
1993                debug!(?calldata, spec=%spec_id, "applying before_test_setup");
1994                // Apply before test configured calldata.
1995                match self.executor.to_mut().transact_raw(
1996                    self.tcfg.sender,
1997                    address,
1998                    calldata,
1999                    U256::ZERO,
2000                ) {
2001                    Ok(call_result) => {
2002                        let reverted = call_result.reverted;
2003
2004                        // Merge tx result traces in unit test result.
2005                        self.result.extend(call_result);
2006
2007                        // To continue unit test execution the call should not revert.
2008                        if reverted {
2009                            self.result.single_fail(None);
2010                            return Err(());
2011                        }
2012                    }
2013                    Err(_) => {
2014                        self.result.single_fail(None);
2015                        return Err(());
2016                    }
2017                }
2018            }
2019        }
2020        Ok(())
2021    }
2022
2023    fn fuzz_runner(&self) -> TestRunner {
2024        let config = &self.config.fuzz;
2025        fuzzer_with_cases(config.seed, config.runs, config.max_test_rejects)
2026    }
2027
2028    /// Replays the persisted corpus and writes AFL-`afl-showmap`-style files.
2029    fn run_showmap(
2030        mut self,
2031        func: &Function,
2032        corpus_dir: Option<PathBuf>,
2033        showmap: &crate::multi_runner::ShowmapConfig,
2034        fuzzed_function: Option<&Function>,
2035        fuzzed_contracts: Option<&foundry_evm::fuzz::invariant::FuzzRunIdentifiedContracts>,
2036        dynamic: Option<&foundry_evm::executors::DynamicTargetCtx<'_>>,
2037    ) -> TestResult {
2038        let Some(corpus_dir) = corpus_dir else {
2039            self.result.replay_skip("no corpus_dir configured for this test");
2040            return self.result;
2041        };
2042
2043        // Configure executor with the requested coverage collectors. Showmap
2044        // ignores fuzz config defaults: the CLI domain is the source of truth.
2045        // For EVM we enable line coverage rather than edge coverage so the IDs
2046        // (bytecode_hash, pc) are deterministic across forge processes —
2047        // `EdgeCovInspector` uses a per-process random hash and would yield
2048        // non-comparable IDs across approaches.
2049        let mut executor = self.clone_executor();
2050        let domain = showmap.domain;
2051        executor.inspector_mut().collect_line_coverage(domain.includes_evm());
2052        executor.inspector_mut().collect_sancov_edges(domain.includes_sancov());
2053
2054        // Fold test identity into the approach dir so each `<approach>/` contains
2055        // trials of a single test — what `differential-coverage` expects. Invariant
2056        // tests share one corpus per contract, so omit the function name for them
2057        // to avoid emitting duplicate approach dirs that replay the same corpus.
2058        let safe_id = self.cr.name.replace(['/', '\\', ':'], "_");
2059        let approach = if fuzzed_contracts.is_some() {
2060            format!("{}__{safe_id}", showmap.approach)
2061        } else {
2062            let safe_fn = func.name.replace(['/', '\\', ':', '(', ')', ',', ' '], "_");
2063            format!("{}__{safe_id}__{safe_fn}", showmap.approach)
2064        };
2065        let opts = ShowmapOpts {
2066            out_dir: showmap.out_dir.clone(),
2067            approach,
2068            trial: showmap.trial.clone(),
2069            per_input: showmap.per_input,
2070            domain,
2071        };
2072
2073        let start = std::time::Instant::now();
2074        let result = replay_corpus_to_showmap(
2075            &executor,
2076            &corpus_dir,
2077            fuzzed_function,
2078            fuzzed_contracts,
2079            dynamic,
2080            &opts,
2081        );
2082        let duration = start.elapsed();
2083        match result {
2084            Ok(stats) => {
2085                if stats.sancov_requested && !stats.sancov_observed && stats.corpus_entries > 0 {
2086                    let _ = sh_warn!(
2087                        "{}::{}: sancov coverage requested but no hits observed (build is likely not sancov-instrumented)",
2088                        self.cr.name,
2089                        func.name,
2090                    );
2091                }
2092                self.result.replay_result(
2093                    stats.corpus_entries,
2094                    stats.showmap_files,
2095                    stats.skipped_entries,
2096                    duration,
2097                );
2098            }
2099            Err(e) => {
2100                self.result.single_fail(Some(e.to_string()));
2101            }
2102        }
2103        self.result
2104    }
2105
2106    fn invariant_runner(&self) -> TestRunner {
2107        let config = &self.config.invariant;
2108        fuzzer_with_cases(self.config.fuzz.seed, config.runs, config.max_assume_rejects)
2109    }
2110
2111    fn clone_executor(&self) -> Executor<FEN> {
2112        self.executor.clone().into_owned()
2113    }
2114
2115    fn build_fuzz_state(&self, invariant: bool) -> EvmFuzzState {
2116        let config =
2117            if invariant { self.config.invariant.dictionary } else { self.config.fuzz.dictionary };
2118        if let Some(db) = self.executor.backend().active_fork_db() {
2119            EvmFuzzState::new(
2120                &self.setup.deployed_libs,
2121                db,
2122                config,
2123                Some(&self.cr.mcr.fuzz_literals),
2124            )
2125        } else {
2126            let db = self.executor.backend().mem_db();
2127            EvmFuzzState::new(
2128                &self.setup.deployed_libs,
2129                db,
2130                config,
2131                Some(&self.cr.mcr.fuzz_literals),
2132            )
2133        }
2134    }
2135}
2136
2137fn fuzzer_with_cases(seed: Option<U256>, cases: u32, max_global_rejects: u32) -> TestRunner {
2138    let config = proptest::test_runner::Config {
2139        cases,
2140        max_global_rejects,
2141        // Disable proptest shrink: for fuzz tests we provide single counterexample,
2142        // for invariant tests we shrink outside proptest.
2143        max_shrink_iters: 0,
2144        ..Default::default()
2145    };
2146
2147    if let Some(seed) = seed {
2148        trace!(target: "forge::test", %seed, "building deterministic fuzzer");
2149        let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &seed.to_be_bytes::<32>());
2150        TestRunner::new_with_rng(config, rng)
2151    } else {
2152        trace!(target: "forge::test", "building stochastic fuzzer");
2153        TestRunner::new(config)
2154    }
2155}
2156
2157/// Holds data about a persisted invariant failure.
2158#[derive(Serialize, Deserialize)]
2159struct InvariantPersistedFailure {
2160    /// Recorded counterexample.
2161    call_sequence: Vec<BaseCounterExample>,
2162    /// Invariant settings when the counterexample was generated.
2163    /// Used to determine if the counterexample is still valid.
2164    settings: InvariantSettings,
2165    /// Whether the persisted failure came from a handler assertion instead of the invariant body.
2166    #[serde(default)]
2167    assertion_failure: bool,
2168}
2169
2170/// Mirrors `check_sequence`'s return: `(success, replayed_entirely, optional_reason)`.
2171type CheckSequenceResult = eyre::Result<(bool, bool, Option<String>)>;
2172
2173/// Borrowed context shared by primary-invariant and handler-side replay helpers.
2174struct ReplayContext<'a> {
2175    invariant_contract: &'a InvariantContract<'a>,
2176    invariant_config: &'a InvariantConfig,
2177    revert_decoder: &'a RevertDecoder,
2178    show_solidity: bool,
2179}
2180
2181/// Helper function to load failed call sequence from file.
2182/// Ignores failure if generated with different invariant settings than the current ones.
2183fn persisted_call_sequence(
2184    path: &Path,
2185    current_settings: &InvariantSettings,
2186) -> Option<InvariantPersistedFailure> {
2187    foundry_common::fs::read_json_file::<InvariantPersistedFailure>(path).ok().and_then(
2188        |persisted_failure| {
2189            if let Some(diff) = persisted_failure.settings.diff(current_settings) {
2190                let _ = sh_warn!(
2191                    "Failure from {:?} file was ignored because invariant test settings have changed: {}",
2192                    path,
2193                    diff
2194                );
2195                return None;
2196            }
2197            Some(persisted_failure)
2198        },
2199    )
2200}
2201
2202/// Returns the current invariant failure cache path.
2203fn invariant_failure_file(failure_dir: &Path, invariant: &Function) -> PathBuf {
2204    canonicalized(failure_dir.join("invariants").join(&invariant.name))
2205}
2206
2207/// Returns the legacy invariant failure cache path.
2208fn legacy_invariant_failure_file(failure_dir: &Path, invariant: &Function) -> PathBuf {
2209    canonicalized(failure_dir.join(&invariant.name))
2210}
2211
2212/// Loads a persisted invariant failure from the new cache path, falling back to the legacy path.
2213fn persisted_invariant_failure(
2214    failure_dir: &Path,
2215    invariant: &Function,
2216    current_settings: &InvariantSettings,
2217) -> Option<InvariantPersistedFailure> {
2218    persisted_call_sequence(invariant_failure_file(failure_dir, invariant).as_path(), current_settings)
2219        .or_else(|| {
2220            let legacy_path = legacy_invariant_failure_file(failure_dir, invariant);
2221            let persisted = persisted_call_sequence(legacy_path.as_path(), current_settings)?;
2222            let _ = sh_warn!(
2223                "Using legacy invariant failure cache at {}; new failures will be persisted under {}/invariants.",
2224                legacy_path.display(),
2225                failure_dir.display(),
2226            );
2227            Some(persisted)
2228        })
2229}
2230
2231/// Converts a persisted counterexample to `BasicTxDetails`, setting `show_solidity` in place.
2232fn base_counterexamples_to_txes(
2233    ctx: &ReplayContext<'_>,
2234    call_sequence: &mut [BaseCounterExample],
2235) -> Vec<BasicTxDetails> {
2236    call_sequence
2237        .iter_mut()
2238        .map(|seq| {
2239            seq.show_solidity = ctx.show_solidity;
2240            BasicTxDetails {
2241                warp: seq.warp,
2242                roll: seq.roll,
2243                sender: seq.sender.unwrap_or_default(),
2244                call_details: CallDetails {
2245                    target: seq.addr.unwrap_or_default(),
2246                    calldata: seq.calldata.clone(),
2247                    value: seq.value,
2248                },
2249            }
2250        })
2251        .collect()
2252}
2253
2254/// Converts a persisted `BaseCounterExample` sequence into `BasicTxDetails` (applying
2255/// `ctx.show_solidity` in place) and replays it via `check_sequence`.
2256fn replay_persisted_call_sequence<FEN: FoundryEvmNetwork>(
2257    ctx: &ReplayContext<'_>,
2258    executor: Executor<FEN>,
2259    call_sequence: &mut [BaseCounterExample],
2260    expect_assertion_failure: bool,
2261) -> (Vec<BasicTxDetails>, CheckSequenceResult) {
2262    let txes = base_counterexamples_to_txes(ctx, call_sequence);
2263    let result = check_sequence(
2264        executor,
2265        &txes,
2266        (0..min(txes.len(), ctx.invariant_config.depth as usize)).collect(),
2267        ctx.invariant_contract.address,
2268        ctx.invariant_contract.anchor().selector().to_vec().into(),
2269        CheckSequenceOptions {
2270            accumulate_warp_roll: ctx.invariant_config.has_delay(),
2271            fail_on_revert: ctx.invariant_config.fail_on_revert,
2272            expect_assertion_failure,
2273            call_after_invariant: ctx.invariant_contract.call_after_invariant,
2274            rd: Some(ctx.revert_decoder),
2275        },
2276    );
2277    (txes, result)
2278}
2279
2280/// Helper function to set test corpus dir and to compose persisted failure paths.
2281fn test_paths(
2282    corpus_config: &mut FuzzCorpusConfig,
2283    persist_dir: PathBuf,
2284    contract_name: &str,
2285    test_name: &str,
2286) -> (PathBuf, PathBuf) {
2287    let contract = contract_name.split(':').next_back().unwrap();
2288    // Update config with corpus dir for current test.
2289    corpus_config.with_test(contract, test_name);
2290
2291    let failures_dir = canonicalized(persist_dir.join("failures").join(contract));
2292    let failure_file = canonicalized(failures_dir.join(test_name));
2293    (failures_dir, failure_file)
2294}
2295
2296/// Sets the invariant corpus directory and returns the contract-level failure directory.
2297fn invariant_suite_paths(
2298    corpus_config: &mut FuzzCorpusConfig,
2299    persist_dir: PathBuf,
2300    contract_name: &str,
2301    invariant_name: &str,
2302    is_optimization: bool,
2303) -> PathBuf {
2304    let failure_dir = invariant_failure_dir(persist_dir, contract_name);
2305    let contract = invariant_contract_name(contract_name);
2306    if let Some(corpus_dir) = &corpus_config.corpus_dir {
2307        let mut corpus_dir = corpus_dir.join(contract);
2308        if is_optimization {
2309            corpus_dir = corpus_dir.join(invariant_name);
2310        }
2311        corpus_config.corpus_dir = Some(canonicalized(corpus_dir));
2312    }
2313
2314    failure_dir
2315}
2316
2317/// Returns the contract-level invariant failure directory.
2318fn invariant_failure_dir(persist_dir: PathBuf, contract_name: &str) -> PathBuf {
2319    canonicalized(persist_dir.join("failures").join(invariant_contract_name(contract_name)))
2320}
2321
2322/// Returns the invariant test contract name without the file path prefix.
2323fn invariant_contract_name(contract_name: &str) -> &str {
2324    contract_name.split(':').next_back().unwrap()
2325}
2326
2327/// Helper function to persist invariant failure.
2328fn record_invariant_failure(
2329    failure_dir: &Path,
2330    failure_file: &Path,
2331    call_sequence: &[BaseCounterExample],
2332    settings: &InvariantSettings,
2333    assertion_failure: bool,
2334) {
2335    if let Err(err) = foundry_common::fs::create_dir_all(failure_dir) {
2336        error!(%err, "Failed to create invariant failure dir");
2337        return;
2338    }
2339    if let Some(parent) = failure_file.parent()
2340        && let Err(err) = foundry_common::fs::create_dir_all(parent)
2341    {
2342        error!(%err, "Failed to create invariant failure file parent dir");
2343        return;
2344    }
2345
2346    if let Err(err) = foundry_common::fs::write_json_file(
2347        failure_file,
2348        &InvariantPersistedFailure {
2349            call_sequence: call_sequence.to_owned(),
2350            settings: settings.clone(),
2351            assertion_failure,
2352        },
2353    ) {
2354        error!(%err, "Failed to record call sequence");
2355    }
2356}
2357
2358/// Persists a handler-side assertion bug under `<failure_dir>/handlers/<site>.json`,
2359/// where `<site>` is `keccak256(reverter || selector)`.
2360fn record_handler_failure(
2361    failure_dir: &Path,
2362    reverter: Address,
2363    selector: Selector,
2364    call_sequence: &[BaseCounterExample],
2365    settings: &InvariantSettings,
2366) {
2367    let handlers_dir = failure_dir.join("handlers");
2368    if let Err(err) = foundry_common::fs::create_dir_all(&handlers_dir) {
2369        error!(%err, "Failed to create handler failure dir");
2370        return;
2371    }
2372    let mut buf = [0u8; 24];
2373    buf[..20].copy_from_slice(reverter.as_slice());
2374    buf[20..].copy_from_slice(selector.as_slice());
2375    let site_hash = alloy_primitives::keccak256(buf);
2376    let file = handlers_dir.join(format!("{site_hash:x}.json"));
2377    record_invariant_failure(&handlers_dir, &file, call_sequence, settings, true);
2378}
2379
2380/// Replays persisted handler-side assertion bugs. A file is kept only if the anchor still
2381/// asserts at the same `(reverter, selector)` site; stale files (anchor no longer asserts,
2382/// asserts at a different site, or earlier call asserts) are deleted in place.
2383fn replay_persisted_handler_failures<FEN: FoundryEvmNetwork>(
2384    handlers_dir: &Path,
2385    current_settings: &InvariantSettings,
2386    executor: Executor<FEN>,
2387    ctx: &ReplayContext<'_>,
2388) -> std::collections::HashMap<(Address, Selector), InvariantFuzzError> {
2389    let mut replayed: std::collections::HashMap<(Address, Selector), InvariantFuzzError> =
2390        std::collections::HashMap::new();
2391    let entries = match std::fs::read_dir(handlers_dir) {
2392        Ok(e) => e,
2393        Err(err) if err.kind() == std::io::ErrorKind::NotFound => return replayed,
2394        Err(err) => {
2395            error!(%err, "Failed to read handler failure dir");
2396            return replayed;
2397        }
2398    };
2399    for entry in entries.flatten() {
2400        let path = entry.path();
2401        if path.extension().and_then(|s| s.to_str()) != Some("json") {
2402            continue;
2403        }
2404        let Some(persisted) = persisted_call_sequence(&path, current_settings) else {
2405            continue;
2406        };
2407        let mut call_sequence = persisted.call_sequence;
2408        if call_sequence.is_empty() {
2409            let _ = std::fs::remove_file(&path);
2410            continue;
2411        }
2412        let txes = base_counterexamples_to_txes(ctx, &mut call_sequence);
2413        // Expected site = (target, selector) of the persisted reproducer's last call.
2414        let Some(last) = txes.last() else {
2415            let _ = std::fs::remove_file(&path);
2416            continue;
2417        };
2418        let expected_target = last.call_details.target;
2419        let expected_selector_bytes: [u8; 4] =
2420            last.call_details.calldata.get(..4).and_then(|s| s.try_into().ok()).unwrap_or_default();
2421        let expected_site = (expected_target, Selector::from(expected_selector_bytes));
2422        let sequence: Vec<usize> =
2423            (0..min(txes.len(), ctx.invariant_config.depth as usize)).collect();
2424        let outcome = replay_handler_failure_sequence(
2425            executor.clone(),
2426            &txes,
2427            sequence,
2428            ctx.invariant_config.has_delay(),
2429            Some(ctx.revert_decoder),
2430        );
2431        match outcome {
2432            Ok(outcome) if outcome.anchor_asserted => {
2433                let _ = sh_warn!(
2434                    "Replayed handler-side assertion bug from {path:?}. \nRun `forge clean` or remove file to ignore."
2435                );
2436                let failure = HandlerAssertionFailure::from_replayed_sequence(
2437                    txes,
2438                    outcome.anchor_fingerprint,
2439                    outcome.revert_reason.unwrap_or_default(),
2440                );
2441                // On collision keep the shorter reproducer. Inlined: `replayed` uses the legacy
2442                // `(reverter, selector)` key, not the unified `FailureKey`.
2443                let already_shorter = replayed
2444                    .get(&expected_site)
2445                    .and_then(InvariantFuzzError::as_handler_assertion)
2446                    .is_some_and(|existing| {
2447                        existing.call_sequence.len() <= failure.call_sequence.len()
2448                    });
2449                if !already_shorter {
2450                    replayed.insert(expected_site, InvariantFuzzError::HandlerAssertion(failure));
2451                }
2452            }
2453            // Stale: anchor doesn't assert or earlier call asserts.
2454            Ok(_) => {
2455                let _ = std::fs::remove_file(&path);
2456            }
2457            Err(err) => {
2458                error!(%err, "Failed to replay handler-side assertion bug");
2459            }
2460        }
2461    }
2462    replayed
2463}