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