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,
7    runner::{
8        ContractRunnerContext, InvariantCampaignScope, LIBRARY_DEPLOYER,
9        count_runnable_invariant_campaign_anchors, is_symbolic_entrypoint,
10    },
11};
12use alloy_json_abi::{Function, JsonAbi};
13use alloy_primitives::{Address, Bytes, U256};
14use eyre::Result;
15use foundry_cli::opts::configure_pcx_from_compile_output;
16use foundry_common::{
17    ContractsByArtifact, ContractsByArtifactBuilder, TestFunctionExt, get_contract_name,
18};
19use foundry_compilers::{
20    Artifact, ArtifactId, Compiler, ProjectCompileOutput,
21    artifacts::{Contract, Libraries},
22};
23use foundry_config::{Config, InlineConfig};
24use foundry_evm::{
25    backend::Backend,
26    core::evm::{EvmEnvFor, FoundryEvmNetwork, SpecFor, TxEnvFor},
27    decode::RevertDecoder,
28    executors::{EarlyExit, Executor, ExecutorBuilder, ShowmapDomain},
29    fork::CreateFork,
30    fuzz::strategies::LiteralsDictionary,
31    inspectors::CheatsConfig,
32    opts::EvmOpts,
33    traces::{InternalTraceMode, TraceMode},
34};
35use foundry_evm_networks::NetworkVariant;
36
37use foundry_linking::{LinkOutput, Linker};
38use rayon::prelude::*;
39use std::{
40    borrow::Borrow,
41    collections::BTreeMap,
42    ops::{Deref, DerefMut},
43    path::{Path, PathBuf},
44    sync::{Arc, mpsc},
45    time::Instant,
46};
47
48#[derive(Debug, Clone)]
49pub struct TestContract {
50    pub abi: JsonAbi,
51    pub bytecode: Bytes,
52}
53
54pub type DeployableContracts = BTreeMap<ArtifactId, TestContract>;
55
56/// A multi contract runner receives a set of contracts deployed in an EVM instance and proceeds
57/// to run all test functions in these contracts.
58#[derive(Clone, Debug)]
59pub struct MultiContractRunner<FEN: FoundryEvmNetwork> {
60    /// Mapping of contract name to JsonAbi, creation bytecode and library bytecode which
61    /// needs to be deployed & linked against
62    pub contracts: DeployableContracts,
63    /// Known contracts linked with computed library addresses.
64    pub known_contracts: ContractsByArtifact,
65    /// Revert decoder. Contains all known errors and their selectors.
66    pub revert_decoder: RevertDecoder,
67    /// Libraries to deploy.
68    pub libs_to_deploy: Vec<Bytes>,
69    /// Library addresses used to link contracts.
70    pub libraries: Libraries,
71    /// Solar compiler instance, to grant syntactic and semantic analysis capabilities
72    pub analysis: Arc<solar::sema::Compiler>,
73    /// Literals dictionary for fuzzing.
74    pub fuzz_literals: LiteralsDictionary,
75
76    /// The fork to use at launch
77    pub fork: Option<CreateFork>,
78
79    /// The base configuration for the test runner.
80    pub tcfg: TestRunnerConfig<FEN>,
81}
82
83impl<FEN: FoundryEvmNetwork> Deref for MultiContractRunner<FEN> {
84    type Target = TestRunnerConfig<FEN>;
85
86    fn deref(&self) -> &Self::Target {
87        &self.tcfg
88    }
89}
90
91impl<FEN: FoundryEvmNetwork> DerefMut for MultiContractRunner<FEN> {
92    fn deref_mut(&mut self) -> &mut Self::Target {
93        &mut self.tcfg
94    }
95}
96
97impl<FEN: FoundryEvmNetwork> MultiContractRunner<FEN> {
98    fn matches_test_function(
99        &self,
100        filter: &dyn TestFilter,
101        contract_id: &str,
102        func: &Function,
103    ) -> bool {
104        matches_test_function(filter, contract_id, func, self.config.symbolic.enabled)
105    }
106
107    fn matches_artifact(&self, filter: &dyn TestFilter, id: &ArtifactId, abi: &JsonAbi) -> bool {
108        matches_artifact(filter, id, abi, self.config.symbolic.enabled)
109    }
110
111    /// Returns an iterator over all contracts that match the filter.
112    pub fn matching_contracts<'a: 'b, 'b>(
113        &'a self,
114        filter: &'b dyn TestFilter,
115    ) -> impl Iterator<Item = (&'a ArtifactId, &'a TestContract)> + 'b {
116        self.contracts.iter().filter(|&(id, c)| self.matches_artifact(filter, id, &c.abi))
117    }
118
119    /// Returns an iterator over all test functions that match the filter.
120    pub fn matching_test_functions<'a: 'b, 'b>(
121        &'a self,
122        filter: &'b dyn TestFilter,
123    ) -> impl Iterator<Item = &'a Function> + 'b {
124        self.matching_contracts(filter).flat_map(move |(id, c)| {
125            let identifier = id.identifier();
126            c.abi
127                .functions()
128                .filter(move |func| self.matches_test_function(filter, &identifier, func))
129        })
130    }
131
132    /// Returns an iterator over all test functions in contracts that match the filter.
133    pub fn all_test_functions<'a: 'b, 'b>(
134        &'a self,
135        filter: &'b dyn TestFilter,
136    ) -> impl Iterator<Item = &'a Function> + 'b {
137        self.contracts
138            .iter()
139            .filter(|(id, _)| filter.matches_path(&id.source) && filter.matches_contract(&id.name))
140            .flat_map(|(_, c)| c.abi.functions())
141            .filter(|func| {
142                func.is_any_test() || (self.config.symbolic.enabled && is_symbolic_entrypoint(func))
143            })
144    }
145
146    /// Returns all matching tests grouped by contract grouped by file (file -> (contract -> tests))
147    pub fn list(&self, filter: &dyn TestFilter) -> BTreeMap<String, BTreeMap<String, Vec<String>>> {
148        self.matching_contracts(filter)
149            .map(|(id, c)| {
150                let source = id.source.as_path().display().to_string();
151                let name = id.name.clone();
152                let identifier = id.identifier();
153                let tests = c
154                    .abi
155                    .functions()
156                    .filter(|func| self.matches_test_function(filter, &identifier, func))
157                    .map(|func| func.name.clone())
158                    .collect::<Vec<_>>();
159                (source, name, tests)
160            })
161            .fold(BTreeMap::new(), |mut acc, (source, name, tests)| {
162                acc.entry(source).or_default().insert(name, tests);
163                acc
164            })
165    }
166
167    /// Executes _all_ tests that match the given `filter`.
168    ///
169    /// The same as [`test`](Self::test), but returns the results instead of streaming them.
170    ///
171    /// Note that this method returns only when all tests have been executed.
172    pub fn test_collect(
173        &mut self,
174        filter: &dyn TestFilter,
175    ) -> Result<BTreeMap<String, SuiteResult>> {
176        Ok(self.test_iter(filter)?.collect())
177    }
178
179    /// Executes _all_ tests that match the given `filter`.
180    ///
181    /// The same as [`test`](Self::test), but returns the results instead of streaming them.
182    ///
183    /// Note that this method returns only when all tests have been executed.
184    pub fn test_iter(
185        &mut self,
186        filter: &dyn TestFilter,
187    ) -> Result<impl Iterator<Item = (String, SuiteResult)>> {
188        let (tx, rx) = mpsc::channel();
189        self.test(filter, tx, false)?;
190        Ok(rx.into_iter())
191    }
192
193    /// Executes _all_ tests that match the given `filter`.
194    ///
195    /// This will create the runtime based on the configured `evm` ops and create the `Backend`
196    /// before executing all contracts and their tests in _parallel_.
197    ///
198    /// Each Executor gets its own instance of the `Backend`.
199    pub fn test(
200        &mut self,
201        filter: &dyn TestFilter,
202        tx: mpsc::Sender<(String, SuiteResult)>,
203        show_progress: bool,
204    ) -> Result<()> {
205        let tokio_handle = tokio::runtime::Handle::current();
206        trace!("running all tests");
207
208        // The DB backend that serves all the data.
209        let db = Backend::spawn(self.fork.take())?;
210
211        let find_timer = Instant::now();
212        let contracts = self.matching_contracts(filter).collect::<Vec<_>>();
213        let find_time = find_timer.elapsed();
214        debug!(
215            "Found {} test contracts out of {} in {:?}",
216            contracts.len(),
217            self.contracts.len(),
218            find_time,
219        );
220        let num_invariant_campaign_anchors = contracts
221            .iter()
222            .map(|(id, contract)| {
223                count_runnable_invariant_campaign_anchors(
224                    &contract.abi,
225                    filter,
226                    InvariantCampaignScope {
227                        config: &self.tcfg.config,
228                        inline_config: &self.tcfg.inline_config,
229                        contract_name: &id.identifier(),
230                        all_override_networks: &self.tcfg.multi_network.all_override_networks,
231                        pass_network: self.tcfg.multi_network.pass_network.as_ref(),
232                    },
233                )
234            })
235            .sum();
236
237        if show_progress {
238            let tests_progress = TestsProgress::new(contracts.len(), rayon::current_num_threads());
239            // Collect test suite results to stream at the end of test run.
240            let results: Vec<(String, SuiteResult)> = contracts
241                .par_iter()
242                .map(|&(id, contract)| {
243                    let _guard = tokio_handle.enter();
244                    tests_progress.inner.lock().start_suite_progress(&id.identifier());
245
246                    let result = self.run_test_suite(
247                        id,
248                        contract,
249                        &db,
250                        filter,
251                        ContractRunnerContext {
252                            progress: Some(&tests_progress),
253                            tokio_handle: tokio_handle.clone(),
254                            num_invariant_campaign_anchors,
255                        },
256                    );
257
258                    tests_progress
259                        .inner
260                        .lock()
261                        .end_suite_progress(&id.identifier(), result.summary());
262
263                    (id.identifier(), result)
264                })
265                .collect();
266
267            tests_progress.inner.lock().clear();
268
269            for result in &results {
270                let _ = tx.send(result.to_owned());
271            }
272        } else {
273            contracts.par_iter().for_each(|&(id, contract)| {
274                let _guard = tokio_handle.enter();
275                let result = self.run_test_suite(
276                    id,
277                    contract,
278                    &db,
279                    filter,
280                    ContractRunnerContext {
281                        progress: None,
282                        tokio_handle: tokio_handle.clone(),
283                        num_invariant_campaign_anchors,
284                    },
285                );
286                let _ = tx.send((id.identifier(), result));
287            })
288        }
289
290        Ok(())
291    }
292
293    fn run_test_suite(
294        &self,
295        artifact_id: &ArtifactId,
296        contract: &TestContract,
297        db: &Backend<FEN>,
298        filter: &dyn TestFilter,
299        context: ContractRunnerContext<'_>,
300    ) -> SuiteResult {
301        let identifier = artifact_id.identifier();
302        let span_name = if enabled!(tracing::Level::TRACE) {
303            identifier.as_str()
304        } else {
305            get_contract_name(&identifier)
306        };
307        let span = debug_span!("suite", name = %span_name);
308        let span_local = span.clone();
309        let _guard = span_local.enter();
310
311        debug!("start executing all tests in contract");
312
313        let executor = self.tcfg.executor(
314            self.known_contracts.clone(),
315            self.analysis.clone(),
316            artifact_id,
317            db.clone(),
318        );
319        let runner = ContractRunner::new(&identifier, contract, executor, span, self, context);
320        let r = runner.run_tests(filter);
321
322        debug!(duration=?r.duration, "executed all tests in contract");
323
324        r
325    }
326}
327
328/// Tracks network assignment across a multi-network test run.
329///
330/// When inline config specifies different networks for different tests, the runner performs one
331/// pass per distinct network. This struct encodes which pass we're in so each `ContractRunner`
332/// can skip tests that belong to a different pass.
333///
334/// Default (empty `all_override_networks`, `None` pass) = single-pass mode, every test runs.
335#[derive(Clone, Debug, Default)]
336pub struct MultiNetworkConfig {
337    /// All networks explicitly referenced in inline config annotations across the whole suite.
338    /// Empty means single-pass mode (no per-test network overrides present).
339    pub all_override_networks: Vec<NetworkVariant>,
340    /// The network this pass is responsible for.
341    /// `None` = default pass: runs tests *without* an explicit network annotation (or annotated
342    /// with a network not in `all_override_networks`).
343    /// `Some(v)` = override pass: runs only tests annotated with exactly `v`.
344    pub pass_network: Option<NetworkVariant>,
345}
346
347/// CLI-only options that switch fuzz/invariant tests into corpus replay
348/// mode that emits AFL-`afl-showmap`-style coverage files.
349#[derive(Clone, Debug)]
350pub struct ShowmapConfig {
351    /// Output root directory for showmap files.
352    pub out_dir: PathBuf,
353    /// Approach name; used as a subdirectory under `out_dir`.
354    pub approach: String,
355    /// Trial identifier embedded in each emitted filename to keep reruns separate.
356    pub trial: String,
357    /// One file per corpus entry instead of one aggregated file per test.
358    pub per_input: bool,
359    /// Which bitmap(s) to dump.
360    pub domain: ShowmapDomain,
361    /// Optional override for the corpus directory to replay from.
362    /// When unset, the per-test corpus dir derived from config is used.
363    pub corpus_dir: Option<PathBuf>,
364}
365
366/// Configuration for the test runner.
367///
368/// This is modified after instantiation through inline config.
369#[derive(Clone, Debug)]
370pub struct TestRunnerConfig<FEN: FoundryEvmNetwork> {
371    /// Project config.
372    pub config: Arc<Config>,
373    /// Inline configuration.
374    pub inline_config: Arc<InlineConfig>,
375
376    /// EVM configuration.
377    pub evm_opts: EvmOpts,
378    /// EVM environment.
379    pub evm_env: EvmEnvFor<FEN>,
380    /// Transaction environment.
381    pub tx_env: TxEnvFor<FEN>,
382    /// EVM version.
383    pub spec_id: SpecFor<FEN>,
384    /// The address which will be used to deploy the initial contracts and send all transactions.
385    pub sender: Address,
386
387    /// Whether to collect line coverage info
388    pub line_coverage: bool,
389    /// Whether to collect debug info
390    pub debug: bool,
391    /// Whether to enable steps tracking in the tracer.
392    pub decode_internal: InternalTraceMode,
393    /// Whether to enable call isolation.
394    pub isolation: bool,
395    /// Whether to exit early on test failure or if test run interrupted.
396    pub early_exit: EarlyExit,
397
398    /// Multi-network pass configuration. Default = single-pass mode.
399    pub multi_network: MultiNetworkConfig,
400
401    /// When set, fuzz/invariant tests run in corpus replay mode and emit
402    /// AFL-`afl-showmap`-style files instead of running a campaign.
403    pub showmap: Option<ShowmapConfig>,
404}
405
406impl<FEN: FoundryEvmNetwork> TestRunnerConfig<FEN> {
407    /// Reconfigures all fields using the given `config`.
408    /// This is for example used to override the configuration with inline config.
409    pub fn reconfigure_with(&mut self, config: Arc<Config>) {
410        debug_assert!(!Arc::ptr_eq(&self.config, &config));
411
412        self.spec_id = config.evm_spec_id();
413        self.sender = config.sender;
414        self.evm_opts.networks = config.networks;
415        self.isolation = config.isolate;
416
417        // Specific to Forge, not present in config.
418        // self.line_coverage = N/A;
419        // self.debug = N/A;
420        // self.decode_internal = N/A;
421
422        // TODO: self.evm_opts
423        self.evm_opts.always_use_create_2_factory = config.always_use_create_2_factory;
424
425        // TODO: self.env
426
427        self.config = config;
428    }
429
430    /// Configures the given executor with this configuration.
431    pub fn configure_executor(&self, executor: &mut Executor<FEN>) {
432        // TODO: See above
433
434        let inspector = executor.inspector_mut();
435        // inspector.set_env(&self.env);
436        if let Some(cheatcodes) = inspector.cheatcodes.as_mut() {
437            cheatcodes.config =
438                Arc::new(cheatcodes.config.clone_with(&self.config, self.evm_opts.clone()));
439        }
440        inspector.tracing(self.trace_mode());
441        inspector.collect_line_coverage(self.line_coverage);
442        inspector.enable_isolation(self.isolation);
443        inspector.networks(self.evm_opts.networks);
444        // inspector.set_create2_deployer(self.evm_opts.create2_deployer);
445
446        // executor.env_mut().clone_from(&self.env);
447        executor.set_spec_id(self.spec_id);
448        // executor.set_gas_limit(self.evm_opts.gas_limit());
449        executor.set_legacy_assertions(self.config.legacy_assertions);
450    }
451
452    /// Creates a new executor with this configuration.
453    pub fn executor(
454        &self,
455        known_contracts: ContractsByArtifact,
456        analysis: Arc<solar::sema::Compiler>,
457        artifact_id: &ArtifactId,
458        db: Backend<FEN>,
459    ) -> Executor<FEN> {
460        let cheats_config = Arc::new(CheatsConfig::new(
461            &self.config,
462            self.evm_opts.clone(),
463            Some(known_contracts),
464            Some(artifact_id.clone()),
465            None,
466            false,
467        ));
468        ExecutorBuilder::default()
469            .inspectors(|stack| {
470                stack
471                    .logs(self.config.live_logs)
472                    .cheatcodes(cheats_config)
473                    .trace_mode(self.trace_mode())
474                    .line_coverage(self.line_coverage)
475                    .enable_isolation(self.isolation)
476                    .networks(self.evm_opts.networks)
477                    .create2_deployer(self.evm_opts.create2_deployer)
478                    .set_analysis(analysis)
479            })
480            .spec_id(self.spec_id)
481            .gas_limit(self.evm_opts.gas_limit())
482            .legacy_assertions(self.config.legacy_assertions)
483            .build(self.evm_env.clone(), self.tx_env.clone(), db)
484    }
485
486    fn trace_mode(&self) -> TraceMode {
487        TraceMode::default()
488            .with_debug(self.debug)
489            .with_decode_internal(self.decode_internal)
490            .with_verbosity(self.evm_opts.verbosity)
491    }
492}
493
494/// Builder used for instantiating the multi-contract runner
495#[derive(Clone)]
496#[must_use = "builders do nothing unless you call `build` on them"]
497pub struct MultiContractRunnerBuilder {
498    /// The address which will be used to deploy the initial contracts and send all
499    /// transactions
500    pub sender: Option<Address>,
501    /// The initial balance for each one of the deployed smart contracts
502    pub initial_balance: U256,
503    /// The fork to use at launch
504    pub fork: Option<CreateFork>,
505    /// Project config.
506    pub config: Arc<Config>,
507    /// Whether or not to collect line coverage info
508    pub line_coverage: bool,
509    /// Whether or not to collect debug info
510    pub debug: bool,
511    /// Whether to enable steps tracking in the tracer.
512    pub decode_internal: InternalTraceMode,
513    /// Whether to enable call isolation
514    pub isolation: bool,
515    /// Whether to exit early on test failure.
516    pub fail_fast: bool,
517    /// Multi-network pass configuration.
518    pub multi_network: MultiNetworkConfig,
519    /// Showmap replay mode (CLI-only, off by default).
520    pub showmap: Option<ShowmapConfig>,
521}
522
523impl MultiContractRunnerBuilder {
524    pub fn new(config: Arc<Config>) -> Self {
525        Self {
526            config,
527            sender: Default::default(),
528            initial_balance: Default::default(),
529            fork: Default::default(),
530            line_coverage: Default::default(),
531            debug: Default::default(),
532            isolation: Default::default(),
533            decode_internal: Default::default(),
534            fail_fast: false,
535            multi_network: Default::default(),
536            showmap: None,
537        }
538    }
539
540    pub fn with_showmap(mut self, showmap: Option<ShowmapConfig>) -> Self {
541        self.showmap = showmap;
542        self
543    }
544
545    pub const fn sender(mut self, sender: Address) -> Self {
546        self.sender = Some(sender);
547        self
548    }
549
550    pub const fn initial_balance(mut self, initial_balance: U256) -> Self {
551        self.initial_balance = initial_balance;
552        self
553    }
554
555    pub fn with_fork(mut self, fork: Option<CreateFork>) -> Self {
556        self.fork = fork;
557        self
558    }
559
560    pub const fn set_coverage(mut self, enable: bool) -> Self {
561        self.line_coverage = enable;
562        self
563    }
564
565    pub const fn set_debug(mut self, enable: bool) -> Self {
566        self.debug = enable;
567        self
568    }
569
570    pub const fn set_decode_internal(mut self, mode: InternalTraceMode) -> Self {
571        self.decode_internal = mode;
572        self
573    }
574
575    pub fn with_multi_network(mut self, multi_network: MultiNetworkConfig) -> Self {
576        self.multi_network = multi_network;
577        self
578    }
579
580    pub const fn fail_fast(mut self, fail_fast: bool) -> Self {
581        self.fail_fast = fail_fast;
582        self
583    }
584
585    pub const fn enable_isolation(mut self, enable: bool) -> Self {
586        self.isolation = enable;
587        self
588    }
589
590    /// Given an EVM, proceeds to return a runner which is able to execute all tests
591    /// against that evm
592    pub fn build<FEN: FoundryEvmNetwork, C: Compiler<CompilerContract = Contract>>(
593        self,
594        output: &ProjectCompileOutput,
595        evm_env: EvmEnvFor<FEN>,
596        tx_env: TxEnvFor<FEN>,
597        evm_opts: EvmOpts,
598    ) -> Result<MultiContractRunner<FEN>> {
599        let root = &self.config.root;
600        let contracts = output
601            .artifact_ids()
602            .map(|(id, v)| (id.with_stripped_file_prefixes(root), v))
603            .collect();
604        let linker = Linker::new(root, contracts);
605
606        // Build revert decoder from ABIs of all artifacts.
607        let abis = linker
608            .contracts
609            .values()
610            .filter_map(|contract| contract.abi.as_ref().map(|abi| abi.borrow()));
611        let revert_decoder = RevertDecoder::new().with_abis(abis);
612
613        let LinkOutput { libraries, libs_to_deploy } = linker.link_with_nonce_or_address(
614            Default::default(),
615            LIBRARY_DEPLOYER,
616            0,
617            linker.contracts.keys(),
618        )?;
619
620        let linked_contracts = linker.get_linked_artifacts_cow(&libraries)?;
621
622        // Create a mapping of name => (abi, deployment code, Vec<library deployment code>)
623        let mut deployable_contracts = DeployableContracts::default();
624
625        for (id, contract) in linked_contracts.iter() {
626            let Some(abi) = contract.abi.as_ref() else { continue };
627
628            // if it's a test, link it and add to deployable contracts
629            if abi.constructor.as_ref().map(|c| c.inputs.is_empty()).unwrap_or(true)
630                && abi.functions().any(|func| {
631                    func.name.is_any_test()
632                        || self.config.symbolic.enabled && is_symbolic_entrypoint(func)
633                })
634            {
635                linker.ensure_linked(contract, id)?;
636
637                let Some(bytecode) =
638                    contract.get_bytecode_bytes().map(|b| b.into_owned()).filter(|b| !b.is_empty())
639                else {
640                    continue;
641                };
642
643                deployable_contracts
644                    .insert(id.clone(), TestContract { abi: abi.clone().into_owned(), bytecode });
645            }
646        }
647
648        // Create known contracts from linked contracts and storage layout information (if any).
649        let known_contracts =
650            ContractsByArtifactBuilder::new(linked_contracts).with_output(output, root).build();
651
652        // Initialize and configure the solar compiler.
653        let mut analysis = solar::sema::Compiler::new(
654            solar::interface::Session::builder().with_stderr_emitter().build(),
655        );
656        let dcx = analysis.dcx_mut();
657        dcx.set_emitter(Box::new(
658            solar::interface::diagnostics::HumanEmitter::stderr(Default::default())
659                .source_map(Some(dcx.source_map().unwrap())),
660        ));
661        dcx.set_flags_mut(|f| f.track_diagnostics = false);
662
663        // Populate solar's global context by parsing and lowering the sources.
664        let files: Vec<_> = output.output().sources.as_ref().keys().cloned().collect();
665
666        analysis.enter_mut(|compiler| -> Result<()> {
667            let mut pcx = compiler.parse();
668            configure_pcx_from_compile_output(
669                &mut pcx,
670                &self.config,
671                output,
672                if files.is_empty() { None } else { Some(&files) },
673            )?;
674            pcx.parse();
675            let _ = compiler.lower_asts();
676            Ok(())
677        })?;
678
679        let analysis = Arc::new(analysis);
680        let fuzz_literals = LiteralsDictionary::new(
681            Some(analysis.clone()),
682            Some(self.config.project_paths()),
683            self.config.fuzz.dictionary.max_fuzz_dictionary_literals,
684        );
685
686        Ok(MultiContractRunner {
687            contracts: deployable_contracts,
688            revert_decoder,
689            known_contracts,
690            libs_to_deploy,
691            libraries,
692            analysis,
693            fuzz_literals,
694
695            tcfg: TestRunnerConfig {
696                evm_opts,
697                evm_env,
698                tx_env,
699                spec_id: self.config.evm_spec_id(),
700                sender: self.sender.unwrap_or(self.config.sender),
701                line_coverage: self.line_coverage,
702                debug: self.debug,
703                decode_internal: self.decode_internal,
704                inline_config: Arc::new(InlineConfig::new_parsed(output, &self.config)?),
705                isolation: self.isolation,
706                early_exit: EarlyExit::new(self.fail_fast),
707                multi_network: self.multi_network,
708                showmap: self.showmap,
709                config: self.config,
710            },
711
712            fork: self.fork,
713        })
714    }
715}
716
717pub fn matches_artifact(
718    filter: &dyn TestFilter,
719    id: &ArtifactId,
720    abi: &JsonAbi,
721    symbolic_enabled: bool,
722) -> bool {
723    matches_contract(
724        filter,
725        &id.source,
726        &id.name,
727        &id.identifier(),
728        abi.functions(),
729        symbolic_enabled,
730    )
731}
732
733pub(crate) fn matches_contract(
734    filter: &dyn TestFilter,
735    path: &Path,
736    contract_name: &str,
737    contract_id: &str,
738    functions: impl IntoIterator<Item = impl std::borrow::Borrow<Function>>,
739    symbolic_enabled: bool,
740) -> bool {
741    (filter.matches_path(path) && filter.matches_contract(contract_name))
742        && functions
743            .into_iter()
744            .any(|func| matches_test_function(filter, contract_id, func.borrow(), symbolic_enabled))
745}
746
747fn matches_test_function(
748    filter: &dyn TestFilter,
749    contract_id: &str,
750    func: &Function,
751    symbolic_enabled: bool,
752) -> bool {
753    if symbolic_enabled && is_symbolic_entrypoint(func) {
754        filter.matches_test(&func.signature())
755    } else {
756        filter.matches_test_function_in_contract(contract_id, func)
757    }
758}
759
760#[cfg(test)]
761mod tests {
762    use super::*;
763    use foundry_common::EmptyTestFilter;
764
765    #[test]
766    fn matches_contract_includes_symbolic_entrypoints_when_enabled() {
767        let filter = EmptyTestFilter::default();
768        let path = Path::new("test/Symbolic.t.sol");
769        let func = Function::parse("checkFilteredCompile(uint256)").unwrap();
770
771        assert!(matches_contract(&filter, path, "Symbolic", "Symbolic", [func.clone()], true));
772        assert!(!matches_contract(&filter, path, "Symbolic", "Symbolic", [func], false));
773    }
774}