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, 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    fork::CreateFork,
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    /// The fork to use at launch
91    pub fork: Option<CreateFork>,
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 = Backend::spawn(self.fork.take())?;
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.config = config;
511    }
512
513    /// Configures the given executor with this configuration.
514    pub fn configure_executor(&self, executor: &mut Executor<FEN>) {
515        debug_assert!(
516            executor.backend().networks().has_same_execution_profile(&self.evm_opts.networks)
517        );
518        debug_assert!(
519            executor.inspector().networks.has_same_execution_profile(&self.evm_opts.networks)
520        );
521        let inspector = executor.inspector_mut();
522        if let Some(cheatcodes) = inspector.cheatcodes.as_mut() {
523            let mut config = cheatcodes.config.clone_with(&self.config, self.evm_opts.clone());
524            config.isolate = self.isolation;
525            cheatcodes.config = Arc::new(config);
526        }
527        inspector.tracing_requirements(self.trace_requirements());
528        inspector.collect_line_coverage(self.line_coverage);
529        inspector.enable_isolation(self.isolation);
530        executor.set_spec_id(self.spec_id);
531        executor.set_legacy_assertions(self.config.legacy_assertions);
532    }
533
534    /// Creates a new executor with this configuration.
535    pub fn executor(
536        &self,
537        known_contracts: ContractsByArtifact,
538        analysis: Arc<solar::sema::Compiler>,
539        artifact_id: &ArtifactId,
540        db: Backend<FEN>,
541    ) -> Executor<FEN> {
542        let mut cheats_config = CheatsConfig::new(
543            &self.config,
544            self.evm_opts.clone(),
545            Some(known_contracts),
546            Some(artifact_id.clone()),
547            false,
548        );
549        cheats_config.isolate = self.isolation;
550        let cheats_config = Arc::new(cheats_config);
551        self.executor_builder
552            .clone()
553            .inspectors(|stack| {
554                stack
555                    .logs(self.config.live_logs)
556                    .cheatcodes(cheats_config)
557                    .trace_requirements(self.trace_requirements())
558                    .line_coverage(self.line_coverage)
559                    .enable_isolation(self.isolation)
560                    .create2_deployer(self.evm_opts.create2_deployer)
561                    .set_analysis(analysis)
562            })
563            .spec_id(self.spec_id)
564            .gas_limit(self.evm_opts.gas_limit())
565            .legacy_assertions(self.config.legacy_assertions)
566            .build(self.evm_env.clone(), self.tx_env.clone(), db, self.evm_opts.networks)
567    }
568
569    fn trace_requirements(&self) -> TraceRequirements {
570        TraceRequirements::none()
571            .with_debug(self.debug)
572            .with_decode_internal(self.decode_internal)
573            .with_all_steps(self.record_all_steps)
574            .with_verbosity(self.config.tracing.verbosity.max(self.evm_opts.verbosity))
575    }
576}
577
578/// Builder used for instantiating the multi-contract runner
579#[derive(Clone)]
580#[must_use = "builders do nothing unless you call `build` on them"]
581pub struct MultiContractRunnerBuilder {
582    /// The address which will be used to deploy the initial contracts and send all
583    /// transactions
584    pub sender: Option<Address>,
585    /// The initial balance for each one of the deployed smart contracts
586    pub initial_balance: U256,
587    /// The fork to use at launch
588    pub fork: Option<CreateFork>,
589    /// Source chain ID used to resolve the fork's hardfork schedule.
590    pub fork_chain_id: Option<ChainId>,
591    /// Exact hardfork reported by the fork endpoint.
592    pub fork_hardfork: Option<FoundryHardfork>,
593    /// Project config.
594    pub config: Arc<Config>,
595    /// Parsed inline configuration.
596    pub inline_config: Arc<InlineConfig>,
597    /// Whether or not to collect line coverage info
598    pub line_coverage: bool,
599    /// Whether or not to collect debug info
600    pub debug: bool,
601    /// Whether to enable steps tracking in the tracer.
602    pub decode_internal: InternalTraceMode,
603    /// Whether to record every opcode step without debugger snapshots.
604    pub record_all_steps: bool,
605    /// Whether to enable call isolation
606    pub isolation: bool,
607    /// Whether to exit early on test failure.
608    pub fail_fast: bool,
609    /// Multi-network pass configuration.
610    pub multi_network: MultiNetworkConfig,
611    /// Showmap replay mode (CLI-only, off by default).
612    pub showmap: Option<ShowmapConfig>,
613    /// Run only fuzz and invariant tests.
614    pub fuzz_only: bool,
615    /// Replay persisted fuzz failures without running a new fuzz campaign.
616    pub fuzz_failure_replay: bool,
617    /// Validated explicit stateless fuzz failure to replay.
618    pub fuzz_input: Option<FuzzFailureReplayConfig>,
619    /// Symbolic artifact replay mode (CLI-only, off by default).
620    pub symbolic_artifact_replay: Option<SymbolicArtifactReplayConfig>,
621    /// Whether the configured CREATE2 deployer is available in the execution environment.
622    pub create2_deployer_available: Option<bool>,
623}
624
625impl MultiContractRunnerBuilder {
626    fn create2_deployer_available(&self, evm_opts: &EvmOpts) -> bool {
627        self.create2_deployer_available.unwrap_or_else(|| {
628            self.fork.is_none()
629                && evm_opts.fork_url.is_none()
630                && evm_opts.create2_deployer == foundry_evm::constants::DEFAULT_CREATE2_DEPLOYER
631        })
632    }
633
634    pub fn new(config: Arc<Config>, inline_config: Arc<InlineConfig>) -> Self {
635        Self {
636            config,
637            inline_config,
638            sender: None,
639            initial_balance: U256::ZERO,
640            fork: None,
641            fork_chain_id: None,
642            fork_hardfork: None,
643            line_coverage: false,
644            debug: false,
645            isolation: false,
646            decode_internal: Default::default(),
647            record_all_steps: false,
648            fail_fast: false,
649            multi_network: Default::default(),
650            showmap: None,
651            fuzz_only: false,
652            fuzz_failure_replay: false,
653            fuzz_input: None,
654            symbolic_artifact_replay: None,
655            create2_deployer_available: None,
656        }
657    }
658
659    pub const fn with_create2_deployer_available(mut self, available: bool) -> Self {
660        self.create2_deployer_available = Some(available);
661        self
662    }
663
664    pub fn with_showmap(mut self, showmap: Option<ShowmapConfig>) -> Self {
665        self.showmap = showmap;
666        self
667    }
668
669    pub const fn with_fuzz_only(mut self, fuzz_only: bool) -> Self {
670        self.fuzz_only = fuzz_only;
671        self
672    }
673
674    pub const fn with_fuzz_failure_replay(mut self, fuzz_failure_replay: bool) -> Self {
675        self.fuzz_failure_replay = fuzz_failure_replay;
676        self
677    }
678
679    pub fn with_fuzz_input(mut self, fuzz_input: Option<FuzzFailureReplayConfig>) -> Self {
680        self.fuzz_input = fuzz_input;
681        self
682    }
683
684    pub fn with_symbolic_artifact_replay(
685        mut self,
686        replay: Option<SymbolicArtifactReplayConfig>,
687    ) -> Self {
688        self.symbolic_artifact_replay = replay;
689        self
690    }
691
692    pub const fn sender(mut self, sender: Address) -> Self {
693        self.sender = Some(sender);
694        self
695    }
696
697    pub const fn initial_balance(mut self, initial_balance: U256) -> Self {
698        self.initial_balance = initial_balance;
699        self
700    }
701
702    pub fn with_fork(mut self, fork: Option<CreateFork>) -> Self {
703        self.fork = fork;
704        self
705    }
706
707    pub const fn with_fork_chain_id(mut self, chain_id: Option<ChainId>) -> Self {
708        self.fork_chain_id = chain_id;
709        self
710    }
711
712    pub const fn with_fork_hardfork(mut self, hardfork: Option<FoundryHardfork>) -> Self {
713        self.fork_hardfork = hardfork;
714        self
715    }
716
717    pub const fn set_coverage(mut self, enable: bool) -> Self {
718        self.line_coverage = enable;
719        self
720    }
721
722    pub const fn set_debug(mut self, enable: bool) -> Self {
723        self.debug = enable;
724        self
725    }
726
727    pub const fn set_decode_internal(mut self, mode: InternalTraceMode) -> Self {
728        self.decode_internal = mode;
729        self
730    }
731
732    pub const fn set_record_all_steps(mut self, enable: bool) -> Self {
733        self.record_all_steps = enable;
734        self
735    }
736
737    pub fn with_multi_network(mut self, multi_network: MultiNetworkConfig) -> Self {
738        self.multi_network = multi_network;
739        self
740    }
741
742    pub const fn fail_fast(mut self, fail_fast: bool) -> Self {
743        self.fail_fast = fail_fast;
744        self
745    }
746
747    pub const fn enable_isolation(mut self, enable: bool) -> Self {
748        self.isolation = enable;
749        self
750    }
751
752    /// Given an EVM, proceeds to return a runner which is able to execute all tests
753    /// against that evm
754    pub fn build<FEN: FoundryEvmNetwork, C: Compiler<CompilerContract = Contract>>(
755        self,
756        output: &ProjectCompileOutput,
757        mut evm_env: EvmEnvFor<FEN>,
758        tx_env: TxEnvFor<FEN>,
759        evm_opts: EvmOpts,
760        executor_builder: ExecutorBuilder<FEN>,
761    ) -> Result<MultiContractRunner<FEN>> {
762        let root = &self.config.root;
763        let contracts = output
764            .artifact_ids()
765            .map(|(id, v)| (id.with_stripped_file_prefixes(root), v))
766            .collect();
767        let linker = Linker::new(root, contracts);
768
769        // Build revert decoder from ABIs of all artifacts.
770        let abis = linker
771            .contracts
772            .values()
773            .filter_map(|contract| contract.abi.as_ref().map(|abi| abi.borrow()));
774        let revert_decoder = RevertDecoder::new().with_abis(abis);
775
776        let configured_libraries = self.config.libraries_with_remappings()?;
777        let create2 = if self.create2_deployer_available(&evm_opts) {
778            match linker.link_with_create2_detailed(
779                configured_libraries.clone(),
780                evm_opts.create2_deployer,
781                self.config.create2_library_salt,
782                linker.contracts.keys(),
783            ) {
784                Ok(output) => Some(output),
785                Err(LinkerError::CyclicDependency) => None,
786                Err(err) => return Err(err.into()),
787            }
788        } else {
789            None
790        };
791        let (
792            DetailedLinkOutput {
793                output: LinkOutput { libraries, library_addresses, libs_to_deploy },
794                artifact_libraries,
795                ..
796            },
797            library_deployment,
798        ) = match create2 {
799            Some(output) => {
800                let deployment = if output.output.libs_to_deploy.is_empty() {
801                    LibraryDeployment::Nonce
802                } else {
803                    LibraryDeployment::Create2 {
804                        deployer: evm_opts.create2_deployer,
805                        salt: self.config.create2_library_salt,
806                    }
807                };
808                (output, deployment)
809            }
810            None => (
811                linker.link_with_nonce_or_address_detailed(
812                    configured_libraries,
813                    LIBRARY_DEPLOYER,
814                    0,
815                    linker.contracts.keys(),
816                )?,
817                LibraryDeployment::Nonce,
818            ),
819        };
820
821        let linked_contracts = linker
822            .get_linked_artifacts_cow_with_artifact_libraries(&libraries, &artifact_libraries)?;
823        let inline_config = self.inline_config;
824
825        // Collect every deployable test contract: a test contract with a default constructor.
826        let mut deployable_contracts = DeployableContracts::default();
827        let test_matcher = TestFunctionMatcher::new(
828            &self.config,
829            &inline_config,
830            self.symbolic_artifact_replay.as_ref(),
831        );
832        let empty_filter = EmptyTestFilter::default();
833        let resolver = Resolver::new(&linker);
834        for (id, contract) in linked_contracts.iter() {
835            let Some(abi) = contract.abi.as_ref() else { continue };
836            if abi.constructor.as_ref().is_some_and(|c| !c.inputs.is_empty())
837                || !test_matcher.matches_contract(&empty_filter, id, abi)
838            {
839                continue;
840            }
841            linker.ensure_linked(contract, id)?;
842            let Some(bytecode) =
843                contract.get_bytecode_bytes().map(|b| b.into_owned()).filter(|b| !b.is_empty())
844            else {
845                continue;
846            };
847            let artifact_libraries = artifact_libraries.get(id).unwrap_or(&libraries);
848            let library_addresses = resolver.linked_library_addresses(id, artifact_libraries)?;
849            deployable_contracts.insert(
850                id.clone(),
851                TestContract { abi: abi.clone().into_owned(), bytecode, library_addresses },
852            );
853        }
854
855        // Create known contracts from linked contracts and storage layout information (if any).
856        let known_contracts =
857            ContractsByArtifactBuilder::new(linked_contracts).with_output(output, root).build();
858
859        // Initialize and configure the solar compiler.
860        let mut analysis = solar::sema::Compiler::new(
861            solar::interface::Session::builder().with_stderr_emitter().build(),
862        );
863        let dcx = analysis.dcx_mut();
864        dcx.set_emitter(Box::new(
865            solar::interface::diagnostics::HumanEmitter::stderr(Default::default())
866                .source_map(Some(dcx.source_map().unwrap())),
867        ));
868        dcx.set_flags_mut(|f| f.track_diagnostics = false);
869
870        // Populate solar's global context by parsing and lowering the sources.
871        let files: Vec<_> = output.output().sources.as_ref().keys().cloned().collect();
872        analysis.enter_mut(|compiler| -> Result<()> {
873            let mut pcx = compiler.parse();
874            configure_pcx_from_compile_output(
875                &mut pcx,
876                &self.config,
877                output,
878                (!files.is_empty()).then_some(&files),
879            )?;
880            pcx.parse();
881            let _ = compiler.lower_asts();
882            Ok(())
883        })?;
884        let analysis = Arc::new(analysis);
885
886        // Enum variant counts used to constrain fuzzed enum inputs to valid values.
887        let enum_bounds = EnumBounds::collect(&analysis);
888        let literals = |max_literals| {
889            LiteralsDictionary::new(
890                Some(analysis.clone()),
891                Some(self.config.project_paths()),
892                max_literals,
893            )
894        };
895        let fuzz_max_literals = self.config.fuzz.dictionary.max_fuzz_dictionary_literals;
896        let invariant_max_literals = self.config.invariant.dictionary.max_fuzz_dictionary_literals;
897        let fuzz_literals = literals(fuzz_max_literals);
898        let invariant_literals = if invariant_max_literals == fuzz_max_literals {
899            fuzz_literals.clone()
900        } else {
901            literals(invariant_max_literals)
902        };
903
904        let fork_chain_id = self.fork_chain_id.or_else(|| {
905            (self.fork.is_some() || evm_opts.fork_url.is_some()).then_some(evm_env.cfg_env.chain_id)
906        });
907        let hardfork = resolve_execution_spec(
908            self.config.evm_version,
909            self.config.hardfork,
910            &mut evm_env,
911            ExecutionSpecContext::local_or_fork(fork_chain_id, self.fork_hardfork),
912            None,
913        );
914        let spec_id = evm_env.cfg_env.spec;
915
916        Ok(MultiContractRunner {
917            contracts: deployable_contracts,
918            revert_decoder,
919            known_contracts,
920            libs_to_deploy,
921            library_addresses,
922            library_deployment,
923            libraries,
924            analysis,
925            fuzz_literals,
926            invariant_literals,
927            enum_bounds,
928
929            tcfg: TestRunnerConfig {
930                evm_opts,
931                executor_builder,
932                evm_env,
933                tx_env,
934                spec_id,
935                hardfork,
936                fork_chain_id,
937                fork_hardfork: self.fork_hardfork,
938                sender: self.sender.unwrap_or(self.config.sender),
939                line_coverage: self.line_coverage,
940                debug: self.debug,
941                decode_internal: self.decode_internal,
942                record_all_steps: self.record_all_steps,
943                inline_config,
944                isolation: self.isolation,
945                early_exit: EarlyExit::new(self.fail_fast),
946                multi_network: self.multi_network,
947                showmap: self.showmap,
948                fuzz_minimize: None,
949                fuzz_only: self.fuzz_only,
950                fuzz_failure_replay: self.fuzz_failure_replay,
951                fuzz_input: self.fuzz_input,
952                symbolic_artifact_replay: self.symbolic_artifact_replay,
953                config: self.config,
954            },
955
956            fork: self.fork,
957        })
958    }
959}
960
961#[derive(Clone, Copy)]
962pub(crate) struct TestFunctionMatcher<'a> {
963    config: &'a Config,
964    inline_config: &'a InlineConfig,
965    symbolic_artifact_replay: Option<&'a SymbolicArtifactReplayConfig>,
966}
967
968impl<'a> TestFunctionMatcher<'a> {
969    pub(crate) const fn new(
970        config: &'a Config,
971        inline_config: &'a InlineConfig,
972        symbolic_artifact_replay: Option<&'a SymbolicArtifactReplayConfig>,
973    ) -> Self {
974        Self { config, inline_config, symbolic_artifact_replay }
975    }
976
977    fn symbolic_tests_enabled(&self, contract_id: &str) -> bool {
978        self.symbolic_artifact_replay.is_some_and(|artifact| {
979            artifact.artifact.kind == SymbolicCounterexampleArtifactKind::SingleCall
980        }) || self.inline_config.contract_symbolic_enabled(
981            &self.config.profile,
982            contract_id,
983            self.config.symbolic.enabled,
984        )
985    }
986
987    pub(crate) fn test_function_kind(
988        &self,
989        contract_id: &str,
990        func: &Function,
991        generated_symbolic_regression: bool,
992    ) -> TestFunctionKind {
993        if generated_symbolic_regression && !func.name.starts_with("test_regression_") {
994            return TestFunctionKind::Unknown;
995        }
996
997        TestFunctionKind::classify(
998            func.name.as_str(),
999            !func.inputs.is_empty(),
1000            self.symbolic_tests_enabled(contract_id),
1001        )
1002    }
1003
1004    /// Returns the functions of `abi` accepted by `keep`, which is given the contract identifier,
1005    /// the function and its classification.
1006    pub(crate) fn test_functions(
1007        self,
1008        contract_id: String,
1009        abi: &JsonAbi,
1010        mut keep: impl FnMut(&str, &Function, TestFunctionKind) -> bool,
1011    ) -> impl Iterator<Item = &Function> {
1012        let generated_symbolic_regression = is_generated_symbolic_regression_contract(abi);
1013        abi.functions().filter(move |func| {
1014            let kind = self.test_function_kind(&contract_id, func, generated_symbolic_regression);
1015            keep(&contract_id, func, kind)
1016        })
1017    }
1018
1019    /// Returns the test functions of `abi` that match `filter`.
1020    fn matching_test_functions<'b>(
1021        self,
1022        filter: &dyn TestFilter,
1023        id: &ArtifactId,
1024        abi: &'b JsonAbi,
1025    ) -> impl Iterator<Item = &'b Function> {
1026        self.test_functions(id.identifier(), abi, move |contract_id, func, kind| {
1027            filter.matches_test_function_kind_in_contract(contract_id, func, kind)
1028        })
1029    }
1030
1031    /// Counts the fuzz test functions and runnable invariant campaign anchors of `abi` that
1032    /// match `filter` in the current network pass.
1033    pub(crate) fn count_fuzz_engine_targets(
1034        &self,
1035        filter: &dyn TestFilter,
1036        id: &ArtifactId,
1037        abi: &JsonAbi,
1038        multi_network: &MultiNetworkConfig,
1039    ) -> (usize, usize) {
1040        let contract_name = id.identifier();
1041        let matches_network_pass = |func: &Function| {
1042            function_matches_network_pass(
1043                &multi_network.all_override_networks,
1044                multi_network.pass_network.as_ref(),
1045                self.inline_config.network_for(&self.config.profile, &contract_name, &func.name),
1046            )
1047        };
1048        let fuzz = self
1049            .test_functions(contract_name.clone(), abi, |contract_id, func, kind| {
1050                matches!(kind, TestFunctionKind::FuzzTest { .. })
1051                    && filter.matches_test_function_kind_in_contract(contract_id, func, kind)
1052                    && matches_network_pass(func)
1053            })
1054            .count();
1055        let invariant = count_runnable_invariant_campaign_anchors(
1056            abi,
1057            filter,
1058            InvariantCampaignScope {
1059                config: self.config,
1060                inline_config: self.inline_config,
1061                contract_name: &contract_name,
1062                all_override_networks: &multi_network.all_override_networks,
1063                pass_network: multi_network.pass_network.as_ref(),
1064            },
1065        );
1066        (fuzz, invariant)
1067    }
1068
1069    pub(crate) fn matches_contract(
1070        &self,
1071        filter: &dyn TestFilter,
1072        id: &ArtifactId,
1073        abi: &JsonAbi,
1074    ) -> bool {
1075        filter.matches_path(&id.source)
1076            && filter.matches_contract(&id.name)
1077            && self.matching_test_functions(filter, id, abi).next().is_some()
1078    }
1079}
1080
1081pub(crate) fn is_generated_symbolic_regression_contract(abi: &JsonAbi) -> bool {
1082    abi.functions().any(|func| func.name == SYMBOLIC_REGRESSION_MARKER && func.inputs.is_empty())
1083}
1084
1085#[cfg(test)]
1086mod tests {
1087    use super::*;
1088
1089    fn abi_with_functions(functions: &[&str]) -> JsonAbi {
1090        let mut abi = JsonAbi::new();
1091        for function in functions {
1092            let function = Function::parse(function).unwrap();
1093            abi.functions.entry(function.name.clone()).or_default().push(function);
1094        }
1095        abi
1096    }
1097
1098    #[test]
1099    fn generated_symbolic_regression_detection_uses_marker() {
1100        let user_suffix_abi = abi_with_functions(&["test_fails()"]);
1101        assert!(!is_generated_symbolic_regression_contract(&user_suffix_abi));
1102
1103        let generated_abi =
1104            abi_with_functions(&[&format!("{SYMBOLIC_REGRESSION_MARKER}()"), "test_fails()"]);
1105        assert!(is_generated_symbolic_regression_contract(&generated_abi));
1106    }
1107
1108    #[test]
1109    fn create2_deployer_availability_default_is_conservative() {
1110        let config = Arc::new(Config::default());
1111        let mut builder = MultiContractRunnerBuilder::new(config, Arc::new(InlineConfig::new()));
1112        let mut evm_opts = EvmOpts::default();
1113        assert!(builder.create2_deployer_available(&evm_opts));
1114
1115        builder.fork = Some(CreateFork {
1116            enable_caching: false,
1117            url: "http://localhost:8545".into(),
1118            evm_opts: evm_opts.clone(),
1119            resolved: None,
1120        });
1121        assert!(!builder.create2_deployer_available(&evm_opts));
1122        builder.fork = None;
1123
1124        evm_opts.fork_url = Some("http://localhost:8545".into());
1125        assert!(!builder.create2_deployer_available(&evm_opts));
1126        evm_opts.fork_url = None;
1127        evm_opts.create2_deployer = Address::ZERO;
1128        assert!(!builder.create2_deployer_available(&evm_opts));
1129        assert!(
1130            builder.with_create2_deployer_available(true).create2_deployer_available(&evm_opts)
1131        );
1132    }
1133}