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