Skip to main content

forge/
multi_runner.rs

1//! Forge test runner for multiple contracts.
2
3use crate::{
4    ContractRunner, TestFilter,
5    progress::TestsProgress,
6    result::{SuiteResult, SymbolicCounterexampleArtifact, SymbolicCounterexampleArtifactKind},
7    runner::{
8        ContractRunnerContext, InvariantCampaignScope, count_runnable_invariant_campaign_anchors,
9    },
10    symbolic_regression::SYMBOLIC_REGRESSION_MARKER,
11};
12use alloy_json_abi::{Function, JsonAbi};
13use alloy_primitives::{Address, Bytes, U256};
14use eyre::Result;
15use foundry_cli::opts::configure_pcx_from_compile_output;
16use foundry_common::{
17    ContractsByArtifact, ContractsByArtifactBuilder, EmptyTestFilter, LIBRARY_DEPLOYER,
18    TestFunctionKind, get_contract_name,
19};
20use foundry_compilers::{
21    Artifact, ArtifactId, Compiler, ProjectCompileOutput,
22    artifacts::{Contract, Libraries},
23};
24use foundry_config::{Config, InlineConfig};
25use foundry_evm::{
26    backend::Backend,
27    core::evm::{EvmEnvFor, FoundryEvmNetwork, SpecFor, TxEnvFor},
28    decode::RevertDecoder,
29    executors::{EarlyExit, Executor, ExecutorBuilder, ReplayObservation, ShowmapDomain},
30    fork::CreateFork,
31    fuzz::{
32        BasicTxDetails,
33        strategies::{EnumBounds, LiteralsDictionary},
34    },
35    inspectors::{CheatsConfig, EdgeIndexMap},
36    opts::EvmOpts,
37    traces::{InternalTraceMode, TraceRequirements},
38};
39use foundry_evm_networks::NetworkVariant;
40
41use foundry_linking::{DetailedLinkOutput, LinkOutput, Linker, LinkerError, Resolver};
42use rayon::prelude::*;
43use std::{
44    borrow::Borrow,
45    collections::{BTreeMap, BTreeSet},
46    ops::{Deref, DerefMut},
47    path::{Path, PathBuf},
48    sync::{Arc, Mutex, mpsc},
49    time::Instant,
50};
51
52#[derive(Debug, Clone)]
53pub struct TestContract {
54    pub abi: JsonAbi,
55    pub bytecode: Bytes,
56    pub library_addresses: BTreeSet<Address>,
57}
58
59pub type DeployableContracts = BTreeMap<ArtifactId, TestContract>;
60
61/// A multi contract runner receives a set of contracts deployed in an EVM instance and proceeds
62/// to run all test functions in these contracts.
63#[derive(Clone, Debug)]
64pub struct MultiContractRunner<FEN: FoundryEvmNetwork> {
65    /// Mapping of contract name to JsonAbi, creation bytecode and library bytecode which
66    /// needs to be deployed & linked against
67    pub contracts: DeployableContracts,
68    /// Known contracts linked with computed library addresses.
69    pub known_contracts: ContractsByArtifact,
70    /// Revert decoder. Contains all known errors and their selectors.
71    pub revert_decoder: RevertDecoder,
72    /// Libraries to deploy.
73    pub libs_to_deploy: Vec<Bytes>,
74    /// Addresses of libraries required by linked test artifacts.
75    pub library_addresses: Vec<Address>,
76    /// How libraries should be deployed.
77    pub library_deployment: LibraryDeployment,
78    /// Library addresses used to link contracts.
79    pub libraries: Libraries,
80    /// Solar compiler instance, to grant syntactic and semantic analysis capabilities
81    pub analysis: Arc<solar::sema::Compiler>,
82    /// Literals dictionary for fuzzing.
83    pub fuzz_literals: LiteralsDictionary,
84    /// Literals dictionary for invariant fuzzing.
85    pub invariant_literals: LiteralsDictionary,
86    /// Variant counts for project enums, used to constrain fuzzed enum inputs.
87    pub enum_bounds: EnumBounds,
88
89    /// The fork to use at launch
90    pub fork: Option<CreateFork>,
91
92    /// The base configuration for the test runner.
93    pub tcfg: TestRunnerConfig<FEN>,
94}
95
96/// Forge-local library deployment strategy.
97#[derive(Clone, Copy, Debug)]
98pub enum LibraryDeployment {
99    Nonce,
100    Create2 { deployer: Address, salt: alloy_primitives::B256 },
101}
102
103impl<FEN: FoundryEvmNetwork> Deref for MultiContractRunner<FEN> {
104    type Target = TestRunnerConfig<FEN>;
105
106    fn deref(&self) -> &Self::Target {
107        &self.tcfg
108    }
109}
110
111impl<FEN: FoundryEvmNetwork> DerefMut for MultiContractRunner<FEN> {
112    fn deref_mut(&mut self) -> &mut Self::Target {
113        &mut self.tcfg
114    }
115}
116
117impl<FEN: FoundryEvmNetwork> MultiContractRunner<FEN> {
118    fn test_function_matcher(&self) -> TestFunctionMatcher<'_> {
119        TestFunctionMatcher::new(
120            &self.config,
121            &self.inline_config,
122            self.tcfg.symbolic_artifact_replay.as_ref(),
123        )
124    }
125
126    /// Returns an iterator over all contracts that match the filter.
127    pub fn matching_contracts<'a: 'b, 'b>(
128        &'a self,
129        filter: &'b dyn TestFilter,
130    ) -> impl Iterator<Item = (&'a ArtifactId, &'a TestContract)> + 'b {
131        let matcher = self.test_function_matcher();
132        self.contracts.iter().filter(move |&(id, c)| matcher.matches_contract(filter, id, &c.abi))
133    }
134
135    /// Returns an iterator over all test functions that match the filter.
136    pub fn matching_test_functions<'a: 'b, 'b>(
137        &'a self,
138        filter: &'b dyn TestFilter,
139    ) -> impl Iterator<Item = &'a Function> + 'b {
140        let matcher = self.test_function_matcher();
141        self.matching_contracts(filter).flat_map(move |(id, c)| {
142            let identifier = id.identifier();
143            let generated_symbolic_regression = is_generated_symbolic_regression_contract(&c.abi);
144            c.abi.functions().filter(move |func| {
145                matcher.matches_test_function(
146                    filter,
147                    &identifier,
148                    func,
149                    generated_symbolic_regression,
150                )
151            })
152        })
153    }
154
155    /// Returns an iterator over all test functions in contracts that match the filter.
156    pub fn all_test_functions<'a: 'b, 'b>(
157        &'a self,
158        filter: &'b dyn TestFilter,
159    ) -> impl Iterator<Item = &'a Function> + 'b {
160        let matcher = self.test_function_matcher();
161        self.contracts
162            .iter()
163            .filter(|(id, _)| filter.matches_path(&id.source) && filter.matches_contract(&id.name))
164            .flat_map(move |(id, c)| {
165                let identifier = id.identifier();
166                let generated_symbolic_regression =
167                    is_generated_symbolic_regression_contract(&c.abi);
168                c.abi.functions().filter(move |func| {
169                    matcher
170                        .test_function_kind(&identifier, func, generated_symbolic_regression)
171                        .is_any_test()
172                })
173            })
174    }
175
176    /// Returns all matching tests grouped by contract grouped by file (file -> (contract -> tests))
177    pub fn list(&self, filter: &dyn TestFilter) -> BTreeMap<String, BTreeMap<String, Vec<String>>> {
178        self.list_with(filter, |func| func.name.clone())
179    }
180
181    pub(crate) fn list_signatures(
182        &self,
183        filter: &dyn TestFilter,
184    ) -> BTreeMap<String, BTreeMap<String, Vec<String>>> {
185        self.list_with(filter, |func| func.signature())
186    }
187
188    fn list_with(
189        &self,
190        filter: &dyn TestFilter,
191        format_test: impl Fn(&Function) -> String + Copy,
192    ) -> BTreeMap<String, BTreeMap<String, Vec<String>>> {
193        let matcher = self.test_function_matcher();
194        self.matching_contracts(filter)
195            .map(move |(id, c)| {
196                let source = id.source.as_path().display().to_string();
197                let name = id.name.clone();
198                let identifier = id.identifier();
199                let generated_symbolic_regression =
200                    is_generated_symbolic_regression_contract(&c.abi);
201                let tests = c
202                    .abi
203                    .functions()
204                    .filter(|func| {
205                        let kind = matcher.test_function_kind(
206                            &identifier,
207                            func,
208                            generated_symbolic_regression,
209                        );
210                        (!self.tcfg.fuzz_only
211                            || matches!(
212                                kind,
213                                TestFunctionKind::FuzzTest { .. } | TestFunctionKind::InvariantTest
214                            ))
215                            && filter.matches_test_function_kind_in_contract(
216                                &identifier,
217                                func,
218                                kind,
219                            )
220                    })
221                    .map(format_test)
222                    .collect::<Vec<_>>();
223                (source, name, tests)
224            })
225            .filter(|(_, _, tests)| !tests.is_empty())
226            .fold(BTreeMap::new(), |mut acc, (source, name, tests)| {
227                acc.entry(source).or_default().insert(name, tests);
228                acc
229            })
230    }
231
232    /// Executes _all_ tests that match the given `filter`.
233    ///
234    /// The same as [`test`](Self::test), but returns the results instead of streaming them.
235    ///
236    /// Note that this method returns only when all tests have been executed.
237    pub fn test_collect(
238        &mut self,
239        filter: &dyn TestFilter,
240    ) -> Result<BTreeMap<String, SuiteResult>> {
241        Ok(self.test_iter(filter)?.collect())
242    }
243
244    /// Executes _all_ tests that match the given `filter`.
245    ///
246    /// The same as [`test`](Self::test), but returns the results instead of streaming them.
247    ///
248    /// Note that this method returns only when all tests have been executed.
249    pub fn test_iter(
250        &mut self,
251        filter: &dyn TestFilter,
252    ) -> Result<impl Iterator<Item = (String, SuiteResult)>> {
253        let (tx, rx) = mpsc::channel();
254        self.test(filter, tx, false)?;
255        Ok(rx.into_iter())
256    }
257
258    /// Executes _all_ tests that match the given `filter`.
259    ///
260    /// This will create the runtime based on the configured `evm` ops and create the `Backend`
261    /// before executing all contracts and their tests in _parallel_.
262    ///
263    /// Each Executor gets its own instance of the `Backend`.
264    pub fn test(
265        &mut self,
266        filter: &dyn TestFilter,
267        tx: mpsc::Sender<(String, SuiteResult)>,
268        show_progress: bool,
269    ) -> Result<()> {
270        let tokio_handle = tokio::runtime::Handle::current();
271        trace!("running all tests");
272
273        // The DB backend that serves all the data.
274        let db = Backend::spawn(self.fork.take())?;
275
276        let find_timer = Instant::now();
277        let contracts = self.matching_contracts(filter).collect::<Vec<_>>();
278        let find_time = find_timer.elapsed();
279        debug!(
280            "Found {} test contracts out of {} in {:?}",
281            contracts.len(),
282            self.contracts.len(),
283            find_time,
284        );
285        let num_invariant_campaign_anchors = contracts
286            .iter()
287            .map(|(id, contract)| {
288                count_runnable_invariant_campaign_anchors(
289                    &contract.abi,
290                    filter,
291                    InvariantCampaignScope {
292                        config: &self.tcfg.config,
293                        inline_config: &self.tcfg.inline_config,
294                        contract_name: &id.identifier(),
295                        all_override_networks: &self.tcfg.multi_network.all_override_networks,
296                        pass_network: self.tcfg.multi_network.pass_network.as_ref(),
297                    },
298                )
299            })
300            .sum();
301
302        if show_progress {
303            let tests_progress = TestsProgress::new(contracts.len(), rayon::current_num_threads());
304            // Collect test suite results to stream at the end of test run.
305            let results: Vec<(String, SuiteResult)> = contracts
306                .par_iter()
307                .map(|&(id, contract)| {
308                    let _guard = tokio_handle.enter();
309                    tests_progress.inner.lock().start_suite_progress(&id.identifier());
310
311                    let result = self.run_test_suite(
312                        id,
313                        contract,
314                        &db,
315                        filter,
316                        ContractRunnerContext {
317                            progress: Some(&tests_progress),
318                            tokio_handle: tokio_handle.clone(),
319                            num_invariant_campaign_anchors,
320                        },
321                    );
322
323                    tests_progress
324                        .inner
325                        .lock()
326                        .end_suite_progress(&id.identifier(), result.summary());
327
328                    (id.identifier(), result)
329                })
330                .collect();
331
332            tests_progress.inner.lock().clear();
333
334            for result in &results {
335                let _ = tx.send(result.to_owned());
336            }
337        } else {
338            contracts.par_iter().for_each(|&(id, contract)| {
339                let _guard = tokio_handle.enter();
340                let result = self.run_test_suite(
341                    id,
342                    contract,
343                    &db,
344                    filter,
345                    ContractRunnerContext {
346                        progress: None,
347                        tokio_handle: tokio_handle.clone(),
348                        num_invariant_campaign_anchors,
349                    },
350                );
351                let _ = tx.send((id.identifier(), result));
352            })
353        }
354
355        Ok(())
356    }
357
358    fn run_test_suite(
359        &self,
360        artifact_id: &ArtifactId,
361        contract: &TestContract,
362        db: &Backend<FEN>,
363        filter: &dyn TestFilter,
364        context: ContractRunnerContext<'_>,
365    ) -> SuiteResult {
366        let identifier = artifact_id.identifier();
367        let span_name = if enabled!(tracing::Level::TRACE) {
368            identifier.as_str()
369        } else {
370            get_contract_name(&identifier)
371        };
372        let span = debug_span!("suite", name = %span_name);
373        let span_local = span.clone();
374        let _guard = span_local.enter();
375
376        debug!("start executing all tests in contract");
377
378        let executor = self.tcfg.executor(
379            self.known_contracts.clone(),
380            self.analysis.clone(),
381            artifact_id,
382            db.clone(),
383        );
384        let runner = ContractRunner::new(&identifier, contract, executor, span, self, context);
385        let r = runner.run_tests(filter);
386
387        debug!(duration=?r.duration, "executed all tests in contract");
388
389        r
390    }
391}
392
393/// Tracks network assignment across a multi-network test run.
394///
395/// When inline config specifies different networks for different tests, the runner performs one
396/// pass per distinct network. This struct encodes which pass we're in so each `ContractRunner`
397/// can skip tests that belong to a different pass.
398///
399/// Default (empty `all_override_networks`, `None` pass) = single-pass mode, every test runs.
400#[derive(Clone, Debug, Default)]
401pub struct MultiNetworkConfig {
402    /// All networks explicitly referenced in inline config annotations across the whole suite.
403    /// Empty means single-pass mode (no per-test network overrides present).
404    pub all_override_networks: Vec<NetworkVariant>,
405    /// The network this pass is responsible for.
406    /// `None` = default pass: runs tests *without* an explicit network annotation (or annotated
407    /// with a network not in `all_override_networks`).
408    /// `Some(v)` = override pass: runs only tests annotated with exactly `v`.
409    pub pass_network: Option<NetworkVariant>,
410}
411
412/// CLI-only options that switch fuzz/invariant tests into corpus replay
413/// mode that emits AFL-`afl-showmap`-style coverage files.
414#[derive(Clone, Debug)]
415pub struct ShowmapConfig {
416    /// Output root directory for showmap files.
417    pub out_dir: PathBuf,
418    /// Approach name; used as a subdirectory under `out_dir`.
419    pub approach: String,
420    /// Trial identifier embedded in each emitted filename to keep reruns separate.
421    pub trial: String,
422    /// One file per corpus entry instead of one aggregated file per test.
423    pub per_input: bool,
424    /// Which bitmap(s) to dump.
425    pub domain: ShowmapDomain,
426    /// Optional override for the corpus directory to replay from.
427    /// When unset, the per-test corpus dir derived from config is used.
428    pub corpus_dir: Option<PathBuf>,
429    /// Whether replay should emit showmap files.
430    pub emit_files: bool,
431}
432
433pub type FuzzMinimizeEdgeIndices = Arc<Mutex<BTreeMap<String, Arc<Mutex<EdgeIndexMap>>>>>;
434
435/// Replay behavior required by a fuzz minimization command.
436#[derive(Clone, Copy, Debug, PartialEq, Eq)]
437pub enum FuzzMinimizeMode {
438    /// Replay complete entries so corpus minimization observes all coverage and failures.
439    Cmin,
440    /// Stop at the campaign boundary so transaction minimization ignores unreachable suffixes.
441    Tmin,
442}
443
444/// CLI-only options that switch fuzz/invariant tests into single-entry replay
445/// mode for corpus minimization.
446#[derive(Clone, Debug)]
447pub struct FuzzMinimizeConfig {
448    /// Entry to replay.
449    pub input: Arc<[BasicTxDetails]>,
450    /// Whether replay serves corpus or transaction minimization.
451    pub mode: FuzzMinimizeMode,
452    /// Shared edge-index assignments for all candidate replays in this minimization invocation,
453    /// namespaced by matched target.
454    pub evm_edge_indices: FuzzMinimizeEdgeIndices,
455    /// Shared replay observations collected from matched fuzz/invariant tests.
456    pub observations: Arc<Mutex<Vec<FuzzMinimizeObservation>>>,
457}
458
459/// Replay observation for one matched minimization target.
460#[derive(Clone, Debug)]
461pub struct FuzzMinimizeObservation {
462    /// Stable target identity for this minimization run.
463    pub target: String,
464    /// Replay result for this target.
465    pub observation: ReplayObservation,
466}
467
468#[derive(Clone, Debug)]
469pub struct SymbolicArtifactReplayConfig {
470    /// Artifact payload to replay.
471    pub artifact: SymbolicCounterexampleArtifact,
472    /// Path the artifact was loaded from, used in diagnostics.
473    pub path: PathBuf,
474}
475
476/// Configuration for the test runner.
477///
478/// This is modified after instantiation through inline config.
479#[derive(Clone, Debug)]
480pub struct TestRunnerConfig<FEN: FoundryEvmNetwork> {
481    /// Project config.
482    pub config: Arc<Config>,
483    /// Inline configuration.
484    pub inline_config: Arc<InlineConfig>,
485
486    /// EVM configuration.
487    pub evm_opts: EvmOpts,
488    /// EVM environment.
489    pub evm_env: EvmEnvFor<FEN>,
490    /// Transaction environment.
491    pub tx_env: TxEnvFor<FEN>,
492    /// EVM version.
493    pub spec_id: SpecFor<FEN>,
494    /// The address which will be used to deploy the initial contracts and send all transactions.
495    pub sender: Address,
496
497    /// Whether to collect line coverage info
498    pub line_coverage: bool,
499    /// Whether to collect debug info
500    pub debug: bool,
501    /// Whether to enable steps tracking in the tracer.
502    pub decode_internal: InternalTraceMode,
503    /// Whether to record every opcode step without debugger snapshots.
504    pub record_all_steps: bool,
505    /// Whether to enable call isolation.
506    pub isolation: bool,
507    /// Whether to exit early on test failure or if test run interrupted.
508    pub early_exit: EarlyExit,
509
510    /// Multi-network pass configuration. Default = single-pass mode.
511    pub multi_network: MultiNetworkConfig,
512
513    /// When set, fuzz/invariant tests run in corpus replay mode and emit
514    /// AFL-`afl-showmap`-style files instead of running a campaign.
515    pub showmap: Option<ShowmapConfig>,
516    /// When set, fuzz/invariant tests replay one candidate input and record minimization facts.
517    pub fuzz_minimize: Option<FuzzMinimizeConfig>,
518    /// Run only fuzz and invariant tests.
519    pub fuzz_only: bool,
520    /// Replay persisted fuzz failures without running a new fuzz campaign.
521    pub fuzz_failure_replay: bool,
522
523    /// When set, run only the matching test and replay this artifact's concrete payload.
524    pub symbolic_artifact_replay: Option<SymbolicArtifactReplayConfig>,
525}
526
527impl<FEN: FoundryEvmNetwork> TestRunnerConfig<FEN> {
528    /// Reconfigures all fields using the given `config`.
529    /// This is for example used to override the configuration with inline config.
530    pub fn reconfigure_with(&mut self, config: Arc<Config>) {
531        debug_assert!(!Arc::ptr_eq(&self.config, &config));
532
533        self.spec_id = config.evm_spec_id();
534        self.sender = config.sender;
535        self.evm_opts.networks = config.networks;
536        self.isolation = config.isolate;
537
538        // Specific to Forge, not present in config.
539        // self.line_coverage = N/A;
540        // self.debug = N/A;
541        // self.decode_internal = N/A;
542        // self.record_all_steps = N/A;
543
544        // TODO: self.evm_opts
545        self.evm_opts.always_use_create_2_factory = config.always_use_create_2_factory;
546
547        // TODO: self.env
548
549        self.config = config;
550    }
551
552    /// Configures the given executor with this configuration.
553    pub fn configure_executor(&self, executor: &mut Executor<FEN>) {
554        // TODO: See above
555
556        let inspector = executor.inspector_mut();
557        // inspector.set_env(&self.env);
558        if let Some(cheatcodes) = inspector.cheatcodes.as_mut() {
559            let mut config = cheatcodes.config.clone_with(&self.config, self.evm_opts.clone());
560            config.isolate = self.isolation;
561            cheatcodes.config = Arc::new(config);
562        }
563        inspector.tracing_requirements(self.trace_requirements());
564        inspector.collect_line_coverage(self.line_coverage);
565        inspector.enable_isolation(self.isolation);
566        inspector.networks(self.evm_opts.networks);
567        // inspector.set_create2_deployer(self.evm_opts.create2_deployer);
568
569        // executor.env_mut().clone_from(&self.env);
570        executor.set_spec_id(self.spec_id);
571        // executor.set_gas_limit(self.evm_opts.gas_limit());
572        executor.set_legacy_assertions(self.config.legacy_assertions);
573    }
574
575    /// Creates a new executor with this configuration.
576    pub fn executor(
577        &self,
578        known_contracts: ContractsByArtifact,
579        analysis: Arc<solar::sema::Compiler>,
580        artifact_id: &ArtifactId,
581        db: Backend<FEN>,
582    ) -> Executor<FEN> {
583        let mut cheats_config = CheatsConfig::new(
584            &self.config,
585            self.evm_opts.clone(),
586            Some(known_contracts),
587            Some(artifact_id.clone()),
588            None,
589            false,
590        );
591        cheats_config.isolate = self.isolation;
592        let cheats_config = Arc::new(cheats_config);
593        ExecutorBuilder::default()
594            .inspectors(|stack| {
595                stack
596                    .logs(self.config.live_logs)
597                    .cheatcodes(cheats_config)
598                    .trace_requirements(self.trace_requirements())
599                    .line_coverage(self.line_coverage)
600                    .enable_isolation(self.isolation)
601                    .networks(self.evm_opts.networks)
602                    .create2_deployer(self.evm_opts.create2_deployer)
603                    .set_analysis(analysis)
604            })
605            .spec_id(self.spec_id)
606            .gas_limit(self.evm_opts.gas_limit())
607            .legacy_assertions(self.config.legacy_assertions)
608            .build(self.evm_env.clone(), self.tx_env.clone(), db)
609    }
610
611    fn trace_requirements(&self) -> TraceRequirements {
612        TraceRequirements::none()
613            .with_debug(self.debug)
614            .with_decode_internal(self.decode_internal)
615            .with_all_steps(self.record_all_steps)
616            .with_verbosity(self.config.tracing.verbosity.max(self.evm_opts.verbosity))
617    }
618}
619
620/// Builder used for instantiating the multi-contract runner
621#[derive(Clone)]
622#[must_use = "builders do nothing unless you call `build` on them"]
623pub struct MultiContractRunnerBuilder {
624    /// The address which will be used to deploy the initial contracts and send all
625    /// transactions
626    pub sender: Option<Address>,
627    /// The initial balance for each one of the deployed smart contracts
628    pub initial_balance: U256,
629    /// The fork to use at launch
630    pub fork: Option<CreateFork>,
631    /// Project config.
632    pub config: Arc<Config>,
633    /// Parsed inline configuration.
634    pub inline_config: Arc<InlineConfig>,
635    /// Whether or not to collect line coverage info
636    pub line_coverage: bool,
637    /// Whether or not to collect debug info
638    pub debug: bool,
639    /// Whether to enable steps tracking in the tracer.
640    pub decode_internal: InternalTraceMode,
641    /// Whether to record every opcode step without debugger snapshots.
642    pub record_all_steps: bool,
643    /// Whether to enable call isolation
644    pub isolation: bool,
645    /// Whether to exit early on test failure.
646    pub fail_fast: bool,
647    /// Multi-network pass configuration.
648    pub multi_network: MultiNetworkConfig,
649    /// Showmap replay mode (CLI-only, off by default).
650    pub showmap: Option<ShowmapConfig>,
651    /// Fuzz minimization replay mode (CLI-only, off by default).
652    pub fuzz_minimize: Option<FuzzMinimizeConfig>,
653    /// Run only fuzz and invariant tests.
654    pub fuzz_only: bool,
655    /// Replay persisted fuzz failures without running a new fuzz campaign.
656    pub fuzz_failure_replay: bool,
657    /// Symbolic artifact replay mode (CLI-only, off by default).
658    pub symbolic_artifact_replay: Option<SymbolicArtifactReplayConfig>,
659    /// Whether the configured CREATE2 deployer is available in the execution environment.
660    pub create2_deployer_available: Option<bool>,
661}
662
663impl MultiContractRunnerBuilder {
664    fn create2_deployer_available(&self, evm_opts: &EvmOpts) -> bool {
665        self.create2_deployer_available.unwrap_or_else(|| {
666            self.fork.is_none()
667                && evm_opts.fork_url.is_none()
668                && evm_opts.create2_deployer == foundry_evm::constants::DEFAULT_CREATE2_DEPLOYER
669        })
670    }
671
672    pub fn new(config: Arc<Config>, inline_config: Arc<InlineConfig>) -> Self {
673        Self {
674            config,
675            inline_config,
676            sender: Default::default(),
677            initial_balance: Default::default(),
678            fork: Default::default(),
679            line_coverage: Default::default(),
680            debug: Default::default(),
681            isolation: Default::default(),
682            decode_internal: Default::default(),
683            record_all_steps: Default::default(),
684            fail_fast: false,
685            multi_network: Default::default(),
686            showmap: None,
687            fuzz_minimize: None,
688            fuzz_only: false,
689            fuzz_failure_replay: false,
690            symbolic_artifact_replay: None,
691            create2_deployer_available: None,
692        }
693    }
694
695    pub const fn with_create2_deployer_available(mut self, available: bool) -> Self {
696        self.create2_deployer_available = Some(available);
697        self
698    }
699
700    pub fn with_showmap(mut self, showmap: Option<ShowmapConfig>) -> Self {
701        self.showmap = showmap;
702        self
703    }
704
705    pub fn with_fuzz_minimize(mut self, fuzz_minimize: Option<FuzzMinimizeConfig>) -> Self {
706        self.fuzz_minimize = fuzz_minimize;
707        self
708    }
709
710    pub const fn with_fuzz_only(mut self, fuzz_only: bool) -> Self {
711        self.fuzz_only = fuzz_only;
712        self
713    }
714
715    pub const fn with_fuzz_failure_replay(mut self, fuzz_failure_replay: bool) -> Self {
716        self.fuzz_failure_replay = fuzz_failure_replay;
717        self
718    }
719
720    pub fn with_symbolic_artifact_replay(
721        mut self,
722        replay: Option<SymbolicArtifactReplayConfig>,
723    ) -> Self {
724        self.symbolic_artifact_replay = replay;
725        self
726    }
727
728    pub const fn sender(mut self, sender: Address) -> Self {
729        self.sender = Some(sender);
730        self
731    }
732
733    pub const fn initial_balance(mut self, initial_balance: U256) -> Self {
734        self.initial_balance = initial_balance;
735        self
736    }
737
738    pub fn with_fork(mut self, fork: Option<CreateFork>) -> Self {
739        self.fork = fork;
740        self
741    }
742
743    pub const fn set_coverage(mut self, enable: bool) -> Self {
744        self.line_coverage = enable;
745        self
746    }
747
748    pub const fn set_debug(mut self, enable: bool) -> Self {
749        self.debug = enable;
750        self
751    }
752
753    pub const fn set_decode_internal(mut self, mode: InternalTraceMode) -> Self {
754        self.decode_internal = mode;
755        self
756    }
757
758    pub const fn set_record_all_steps(mut self, enable: bool) -> Self {
759        self.record_all_steps = enable;
760        self
761    }
762
763    pub fn with_multi_network(mut self, multi_network: MultiNetworkConfig) -> Self {
764        self.multi_network = multi_network;
765        self
766    }
767
768    pub const fn fail_fast(mut self, fail_fast: bool) -> Self {
769        self.fail_fast = fail_fast;
770        self
771    }
772
773    pub const fn enable_isolation(mut self, enable: bool) -> Self {
774        self.isolation = enable;
775        self
776    }
777
778    /// Given an EVM, proceeds to return a runner which is able to execute all tests
779    /// against that evm
780    pub fn build<FEN: FoundryEvmNetwork, C: Compiler<CompilerContract = Contract>>(
781        self,
782        output: &ProjectCompileOutput,
783        evm_env: EvmEnvFor<FEN>,
784        tx_env: TxEnvFor<FEN>,
785        evm_opts: EvmOpts,
786    ) -> Result<MultiContractRunner<FEN>> {
787        let root = &self.config.root;
788        let contracts = output
789            .artifact_ids()
790            .map(|(id, v)| (id.with_stripped_file_prefixes(root), v))
791            .collect();
792        let linker = Linker::new(root, contracts);
793
794        // Build revert decoder from ABIs of all artifacts.
795        let abis = linker
796            .contracts
797            .values()
798            .filter_map(|contract| contract.abi.as_ref().map(|abi| abi.borrow()));
799        let revert_decoder = RevertDecoder::new().with_abis(abis);
800
801        let configured_libraries = self.config.libraries_with_remappings()?;
802        let create2_deployer_available = self.create2_deployer_available(&evm_opts);
803        let create2 = if create2_deployer_available {
804            match linker.link_with_create2_detailed(
805                configured_libraries.clone(),
806                evm_opts.create2_deployer,
807                self.config.create2_library_salt,
808                linker.contracts.keys(),
809            ) {
810                Ok(output) => Some(output),
811                Err(LinkerError::CyclicDependency) => None,
812                Err(err) => return Err(err.into()),
813            }
814        } else {
815            None
816        };
817        let (
818            DetailedLinkOutput {
819                output: LinkOutput { libraries, library_addresses, libs_to_deploy },
820                artifact_libraries,
821                ..
822            },
823            library_deployment,
824        ) = if let Some(output) = create2 {
825            let deployment = if output.output.libs_to_deploy.is_empty() {
826                LibraryDeployment::Nonce
827            } else {
828                LibraryDeployment::Create2 {
829                    deployer: evm_opts.create2_deployer,
830                    salt: self.config.create2_library_salt,
831                }
832            };
833            (output, deployment)
834        } else {
835            (
836                linker.link_with_nonce_or_address_detailed(
837                    configured_libraries,
838                    LIBRARY_DEPLOYER,
839                    0,
840                    linker.contracts.keys(),
841                )?,
842                LibraryDeployment::Nonce,
843            )
844        };
845
846        let linked_contracts = linker
847            .get_linked_artifacts_cow_with_artifact_libraries(&libraries, &artifact_libraries)?;
848        let inline_config = self.inline_config;
849
850        // Create a mapping of name => (abi, deployment code, Vec<library deployment code>)
851        let mut deployable_contracts = DeployableContracts::default();
852        let test_matcher = TestFunctionMatcher::new(
853            &self.config,
854            &inline_config,
855            self.symbolic_artifact_replay.as_ref(),
856        );
857        let empty_filter = EmptyTestFilter::default();
858        let resolver = Resolver::new(&linker);
859
860        for (id, contract) in linked_contracts.iter() {
861            let Some(abi) = contract.abi.as_ref() else { continue };
862
863            // if it's a test, link it and add to deployable contracts
864            if abi.constructor.as_ref().map(|c| c.inputs.is_empty()).unwrap_or(true)
865                && test_matcher.matches_contract(&empty_filter, id, abi.borrow())
866            {
867                linker.ensure_linked(contract, id)?;
868
869                let Some(bytecode) =
870                    contract.get_bytecode_bytes().map(|b| b.into_owned()).filter(|b| !b.is_empty())
871                else {
872                    continue;
873                };
874
875                let artifact_libraries = artifact_libraries.get(id).unwrap_or(&libraries);
876                let library_addresses =
877                    resolver.linked_library_addresses(id, artifact_libraries)?;
878
879                deployable_contracts.insert(
880                    id.clone(),
881                    TestContract { abi: abi.clone().into_owned(), bytecode, library_addresses },
882                );
883            }
884        }
885
886        // Create known contracts from linked contracts and storage layout information (if any).
887        let known_contracts =
888            ContractsByArtifactBuilder::new(linked_contracts).with_output(output, root).build();
889
890        // Initialize and configure the solar compiler.
891        let mut analysis = solar::sema::Compiler::new(
892            solar::interface::Session::builder().with_stderr_emitter().build(),
893        );
894        let dcx = analysis.dcx_mut();
895        dcx.set_emitter(Box::new(
896            solar::interface::diagnostics::HumanEmitter::stderr(Default::default())
897                .source_map(Some(dcx.source_map().unwrap())),
898        ));
899        dcx.set_flags_mut(|f| f.track_diagnostics = false);
900
901        // Populate solar's global context by parsing and lowering the sources.
902        let files: Vec<_> = output.output().sources.as_ref().keys().cloned().collect();
903
904        analysis.enter_mut(|compiler| -> Result<()> {
905            let mut pcx = compiler.parse();
906            configure_pcx_from_compile_output(
907                &mut pcx,
908                &self.config,
909                output,
910                if files.is_empty() { None } else { Some(&files) },
911            )?;
912            pcx.parse();
913            let _ = compiler.lower_asts();
914            Ok(())
915        })?;
916
917        let analysis = Arc::new(analysis);
918        // Enum variant counts used to constrain fuzzed enum inputs to valid values.
919        let enum_bounds = EnumBounds::collect(&analysis);
920        let fuzz_max_literals = self.config.fuzz.dictionary.max_fuzz_dictionary_literals;
921        let invariant_max_literals = self.config.invariant.dictionary.max_fuzz_dictionary_literals;
922        let fuzz_literals = LiteralsDictionary::new(
923            Some(analysis.clone()),
924            Some(self.config.project_paths()),
925            fuzz_max_literals,
926        );
927        let invariant_literals = if invariant_max_literals == fuzz_max_literals {
928            fuzz_literals.clone()
929        } else {
930            LiteralsDictionary::new(
931                Some(analysis.clone()),
932                Some(self.config.project_paths()),
933                invariant_max_literals,
934            )
935        };
936
937        Ok(MultiContractRunner {
938            contracts: deployable_contracts,
939            revert_decoder,
940            known_contracts,
941            libs_to_deploy,
942            library_addresses,
943            library_deployment,
944            libraries,
945            analysis,
946            fuzz_literals,
947            invariant_literals,
948            enum_bounds,
949
950            tcfg: TestRunnerConfig {
951                evm_opts,
952                evm_env,
953                tx_env,
954                spec_id: self.config.evm_spec_id(),
955                sender: self.sender.unwrap_or(self.config.sender),
956                line_coverage: self.line_coverage,
957                debug: self.debug,
958                decode_internal: self.decode_internal,
959                record_all_steps: self.record_all_steps,
960                inline_config,
961                isolation: self.isolation,
962                early_exit: EarlyExit::new(self.fail_fast),
963                multi_network: self.multi_network,
964                showmap: self.showmap,
965                fuzz_minimize: self.fuzz_minimize,
966                fuzz_only: self.fuzz_only,
967                fuzz_failure_replay: self.fuzz_failure_replay,
968                symbolic_artifact_replay: self.symbolic_artifact_replay,
969                config: self.config,
970            },
971
972            fork: self.fork,
973        })
974    }
975}
976
977#[derive(Clone, Copy)]
978pub(crate) struct TestFunctionMatcher<'a> {
979    config: &'a Config,
980    inline_config: &'a InlineConfig,
981    symbolic_artifact_replay: Option<&'a SymbolicArtifactReplayConfig>,
982}
983
984impl<'a> TestFunctionMatcher<'a> {
985    pub(crate) const fn new(
986        config: &'a Config,
987        inline_config: &'a InlineConfig,
988        symbolic_artifact_replay: Option<&'a SymbolicArtifactReplayConfig>,
989    ) -> Self {
990        Self { config, inline_config, symbolic_artifact_replay }
991    }
992
993    fn symbolic_tests_enabled(&self, contract_id: &str) -> bool {
994        self.symbolic_artifact_replay.is_some_and(|artifact| {
995            artifact.artifact.kind == SymbolicCounterexampleArtifactKind::SingleCall
996        }) || self.inline_config.contract_symbolic_enabled(
997            &self.config.profile,
998            contract_id,
999            self.config.symbolic.enabled,
1000        )
1001    }
1002
1003    pub(crate) fn test_function_kind(
1004        &self,
1005        contract_id: &str,
1006        func: &Function,
1007        generated_symbolic_regression: bool,
1008    ) -> TestFunctionKind {
1009        if generated_symbolic_regression && !func.name.starts_with("test_regression_") {
1010            return TestFunctionKind::Unknown;
1011        }
1012
1013        TestFunctionKind::classify(
1014            func.name.as_str(),
1015            !func.inputs.is_empty(),
1016            self.symbolic_tests_enabled(contract_id),
1017        )
1018    }
1019
1020    pub(crate) fn matches_test_function(
1021        &self,
1022        filter: &dyn TestFilter,
1023        contract_id: &str,
1024        func: &Function,
1025        generated_symbolic_regression: bool,
1026    ) -> bool {
1027        filter.matches_test_function_kind_in_contract(
1028            contract_id,
1029            func,
1030            self.test_function_kind(contract_id, func, generated_symbolic_regression),
1031        )
1032    }
1033
1034    pub(crate) fn matches_contract(
1035        &self,
1036        filter: &dyn TestFilter,
1037        id: &ArtifactId,
1038        abi: &JsonAbi,
1039    ) -> bool {
1040        let identifier = id.identifier();
1041        let generated_symbolic_regression = is_generated_symbolic_regression_contract(abi);
1042        matches_contract(filter, &id.source, &id.name, &identifier, abi.functions(), |func| {
1043            self.test_function_kind(&identifier, func, generated_symbolic_regression)
1044        })
1045    }
1046}
1047
1048pub(crate) fn is_generated_symbolic_regression_contract(abi: &JsonAbi) -> bool {
1049    abi.functions().any(|func| func.name == SYMBOLIC_REGRESSION_MARKER && func.inputs.is_empty())
1050}
1051
1052pub(crate) fn matches_contract(
1053    filter: &dyn TestFilter,
1054    path: &Path,
1055    contract_name: &str,
1056    contract_id: &str,
1057    functions: impl IntoIterator<Item = impl std::borrow::Borrow<Function>>,
1058    test_function_kind: impl Fn(&Function) -> TestFunctionKind,
1059) -> bool {
1060    (filter.matches_path(path) && filter.matches_contract(contract_name))
1061        && functions.into_iter().any(|func| {
1062            let func = func.borrow();
1063            filter.matches_test_function_kind_in_contract(
1064                contract_id,
1065                func,
1066                test_function_kind(func),
1067            )
1068        })
1069}
1070
1071#[cfg(test)]
1072mod tests {
1073    use super::*;
1074    use foundry_common::TestFunctionExt;
1075
1076    fn abi_with_functions(functions: &[&str]) -> JsonAbi {
1077        let mut abi = JsonAbi::new();
1078        for function in functions {
1079            let function = Function::parse(function).unwrap();
1080            abi.functions.entry(function.name.clone()).or_default().push(function);
1081        }
1082        abi
1083    }
1084
1085    #[test]
1086    fn matches_contract_uses_provided_function_kind() {
1087        let filter = EmptyTestFilter::default();
1088        let path = Path::new("test/Symbolic.t.sol");
1089        let func = Function::parse("checkFilteredCompile(uint256)").unwrap();
1090
1091        assert!(matches_contract(&filter, path, "Symbolic", "Symbolic", [func.clone()], |_| {
1092            TestFunctionKind::SymbolicTest
1093        },));
1094        assert!(!matches_contract(&filter, path, "Symbolic", "Symbolic", [func], |func| {
1095            func.test_function_kind()
1096        }));
1097    }
1098
1099    #[test]
1100    fn generated_symbolic_regression_detection_uses_marker() {
1101        let user_suffix_abi = abi_with_functions(&["test_fails()"]);
1102        assert!(!is_generated_symbolic_regression_contract(&user_suffix_abi));
1103
1104        let generated_abi =
1105            abi_with_functions(&[&format!("{SYMBOLIC_REGRESSION_MARKER}()"), "test_fails()"]);
1106        assert!(is_generated_symbolic_regression_contract(&generated_abi));
1107    }
1108
1109    #[test]
1110    fn create2_deployer_availability_default_is_conservative() {
1111        let config = Arc::new(Config::default());
1112        let mut builder = MultiContractRunnerBuilder::new(config, Arc::new(InlineConfig::new()));
1113        let mut evm_opts = EvmOpts::default();
1114        assert!(builder.create2_deployer_available(&evm_opts));
1115
1116        builder.fork = Some(CreateFork {
1117            enable_caching: false,
1118            url: "http://localhost:8545".into(),
1119            evm_opts: evm_opts.clone(),
1120        });
1121        assert!(!builder.create2_deployer_available(&evm_opts));
1122        builder.fork = None;
1123
1124        evm_opts.fork_url = Some("http://localhost:8545".into());
1125        assert!(!builder.create2_deployer_available(&evm_opts));
1126        evm_opts.fork_url = None;
1127        evm_opts.create2_deployer = Address::ZERO;
1128        assert!(!builder.create2_deployer_available(&evm_opts));
1129        assert!(
1130            builder.with_create2_deployer_available(true).create2_deployer_available(&evm_opts)
1131        );
1132    }
1133}