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