1use crate::{
4 ContractRunner, TestFilter,
5 progress::TestsProgress,
6 result::{SuiteResult, SymbolicCounterexampleArtifact, SymbolicCounterexampleArtifactKind},
7 runner::{
8 ContractRunnerContext, InvariantCampaignScope, count_runnable_invariant_campaign_anchors,
9 function_matches_network_pass,
10 },
11 symbolic_regression::SYMBOLIC_REGRESSION_MARKER,
12};
13use alloy_json_abi::{Function, JsonAbi};
14use alloy_primitives::{Address, Bytes, ChainId, U256};
15use eyre::Result;
16use foundry_cli::opts::configure_pcx_from_compile_output;
17use foundry_common::{
18 ContractsByArtifact, ContractsByArtifactBuilder, EmptyTestFilter, LIBRARY_DEPLOYER,
19 TestFunctionKind, external_compiler::external_artifact_is_test_eligible, get_contract_name,
20 namespaced_storage,
21};
22use foundry_compilers::{
23 Artifact, ArtifactId, Compiler, ProjectCompileOutput,
24 artifacts::{Contract, Libraries},
25};
26use foundry_config::{Config, FoundryHardfork, InlineConfig};
27use foundry_evm::{
28 backend::Backend,
29 core::evm::{EvmEnvFor, FoundryEvmNetwork, SpecFor, TxEnvFor},
30 decode::RevertDecoder,
31 executors::{EarlyExit, Executor, ExecutorBuilder, ReplayObservation, ShowmapDomain},
32 fuzz::{
33 BaseCounterExample, BasicTxDetails,
34 strategies::{EnumBounds, LiteralsDictionary},
35 },
36 inspectors::{CheatsConfig, EdgeIndexMap},
37 opts::{EvmOpts, ExecutionSpecContext, resolve_execution_spec},
38 traces::{InternalTraceMode, TraceRequirements},
39};
40use foundry_evm_networks::NetworkVariant;
41
42use foundry_linking::{DetailedLinkOutput, LinkOutput, Linker, LinkerError, Resolver};
43use rayon::prelude::*;
44use std::{
45 borrow::Borrow,
46 collections::{BTreeMap, BTreeSet},
47 ops::{Deref, DerefMut},
48 path::PathBuf,
49 sync::{Arc, Mutex, mpsc},
50 time::Instant,
51};
52
53#[derive(Debug, Clone)]
54pub struct TestContract {
55 pub abi: JsonAbi,
56 pub bytecode: Bytes,
57 pub library_addresses: BTreeSet<Address>,
58}
59
60pub type DeployableContracts = BTreeMap<ArtifactId, TestContract>;
61
62#[derive(Clone, Debug)]
65pub struct MultiContractRunner<FEN: FoundryEvmNetwork> {
66 pub contracts: DeployableContracts,
69 pub known_contracts: ContractsByArtifact,
71 pub revert_decoder: RevertDecoder,
73 pub libs_to_deploy: Vec<Bytes>,
75 pub library_addresses: Vec<Address>,
77 pub library_deployment: LibraryDeployment,
79 pub libraries: Libraries,
81 pub analysis: Arc<solar::sema::Compiler>,
83 pub fuzz_literals: LiteralsDictionary,
85 pub invariant_literals: LiteralsDictionary,
87 pub enum_bounds: EnumBounds,
89
90 pub backend: Backend<FEN>,
92
93 pub tcfg: TestRunnerConfig<FEN>,
95}
96
97#[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 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 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 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 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 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 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 let db = self.backend.clone();
232
233 let find_timer = Instant::now();
234 let contracts = self.matching_contracts(filter).collect::<Vec<_>>();
235 debug!(
236 "Found {} test contracts out of {} in {:?}",
237 contracts.len(),
238 self.contracts.len(),
239 find_timer.elapsed(),
240 );
241 let num_invariant_campaign_anchors = contracts
242 .iter()
243 .map(|(id, contract)| {
244 count_runnable_invariant_campaign_anchors(
245 &contract.abi,
246 filter,
247 InvariantCampaignScope {
248 config: &self.tcfg.config,
249 inline_config: &self.tcfg.inline_config,
250 contract_name: &id.identifier(),
251 all_override_networks: &self.tcfg.multi_network.all_override_networks,
252 pass_network: self.tcfg.multi_network.pass_network.as_ref(),
253 },
254 )
255 })
256 .sum();
257
258 let progress = show_progress
259 .then(|| TestsProgress::new(contracts.len(), rayon::current_num_threads()));
260 let run_suite = |&(id, contract): &(&ArtifactId, &TestContract)| {
261 let _guard = tokio_handle.enter();
262 let identifier = id.identifier();
263 if let Some(progress) = &progress {
264 progress.inner.lock().start_suite_progress(&identifier);
265 }
266 let result = self.run_test_suite(
267 id,
268 contract,
269 &db,
270 filter,
271 ContractRunnerContext {
272 progress: progress.as_ref(),
273 tokio_handle: tokio_handle.clone(),
274 num_invariant_campaign_anchors,
275 },
276 );
277 if let Some(progress) = &progress {
278 progress.inner.lock().end_suite_progress(&identifier, result.summary());
279 }
280 (identifier, result)
281 };
282
283 if let Some(progress) = &progress {
284 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#[derive(Clone, Debug, Default)]
342pub struct MultiNetworkConfig {
343 pub all_override_networks: Vec<NetworkVariant>,
346 pub pass_network: Option<NetworkVariant>,
351}
352
353#[derive(Clone, Debug)]
356pub struct ShowmapConfig {
357 pub out_dir: PathBuf,
359 pub approach: String,
361 pub trial: String,
363 pub per_input: bool,
365 pub domain: ShowmapDomain,
367 pub corpus_dir: Option<PathBuf>,
370 pub emit_files: bool,
372}
373
374pub type FuzzMinimizeEdgeIndices = Arc<Mutex<BTreeMap<String, Arc<Mutex<EdgeIndexMap>>>>>;
375
376#[derive(Clone, Copy, Debug, PartialEq, Eq)]
378pub enum FuzzMinimizeMode {
379 Cmin,
381 Tmin,
383}
384
385#[derive(Clone, Debug)]
388pub struct FuzzMinimizeConfig {
389 pub input: Arc<[BasicTxDetails]>,
391 pub mode: FuzzMinimizeMode,
393 pub evm_edge_indices: FuzzMinimizeEdgeIndices,
396 pub observations: Arc<Mutex<Vec<FuzzMinimizeObservation>>>,
398}
399
400#[derive(Clone, Debug)]
402pub struct FuzzMinimizeObservation {
403 pub target: String,
405 pub observation: ReplayObservation,
407}
408
409#[derive(Clone, Debug)]
410pub struct SymbolicArtifactReplayConfig {
411 pub artifact: SymbolicCounterexampleArtifact,
413 pub path: PathBuf,
415}
416
417#[derive(Clone, Debug)]
419pub struct FuzzFailureReplayConfig {
420 pub failure: Arc<BaseCounterExample>,
422 pub contract: String,
424 pub test: String,
426}
427
428#[derive(Clone, Debug)]
432pub struct TestRunnerConfig<FEN: FoundryEvmNetwork> {
433 pub config: Arc<Config>,
435 pub inline_config: Arc<InlineConfig>,
437
438 pub evm_opts: EvmOpts,
440 pub executor_builder: ExecutorBuilder<FEN>,
442 pub evm_env: EvmEnvFor<FEN>,
444 pub tx_env: TxEnvFor<FEN>,
446 pub spec_id: SpecFor<FEN>,
448 pub hardfork: Option<FoundryHardfork>,
450 pub fork_chain_id: Option<ChainId>,
452 pub fork_hardfork: Option<FoundryHardfork>,
454 pub sender: Address,
456
457 pub line_coverage: bool,
459 pub debug: bool,
461 pub decode_internal: InternalTraceMode,
463 pub record_all_steps: bool,
465 pub isolation: bool,
467 pub early_exit: EarlyExit,
469
470 pub multi_network: MultiNetworkConfig,
472
473 pub showmap: Option<ShowmapConfig>,
476 pub fuzz_minimize: Option<FuzzMinimizeConfig>,
478 pub fuzz_only: bool,
480 pub fuzz_failure_replay: bool,
482 pub fuzz_input: Option<FuzzFailureReplayConfig>,
484
485 pub symbolic_artifact_replay: Option<SymbolicArtifactReplayConfig>,
487}
488
489impl<FEN: FoundryEvmNetwork> TestRunnerConfig<FEN> {
490 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 self.evm_opts.always_use_create_2_factory = config.always_use_create_2_factory;
510 self.evm_opts.no_fork_bal = config.no_fork_bal;
511 self.evm_opts.fork_prefetch = config.fork_prefetch;
512 self.evm_opts.fork_state_by_number = config.fork_state_by_number;
513 self.config = config;
514 }
515
516 pub fn configure_executor(&self, executor: &mut Executor<FEN>) {
518 debug_assert!(
519 executor.backend().networks().has_same_execution_profile(&self.evm_opts.networks)
520 );
521 debug_assert!(
522 executor.inspector().networks.has_same_execution_profile(&self.evm_opts.networks)
523 );
524 let inspector = executor.inspector_mut();
525 if let Some(cheatcodes) = inspector.cheatcodes.as_mut() {
526 let mut config = cheatcodes.core.config.clone_with(&self.config, self.evm_opts.clone());
527 config.isolate = self.isolation;
528 cheatcodes.core.config = Arc::new(config);
529 }
530 inspector.tracing_requirements(self.trace_requirements());
531 inspector.collect_line_coverage(self.line_coverage);
532 inspector.enable_isolation(self.isolation);
533 executor.set_spec_id(self.spec_id);
534 executor.set_legacy_assertions(self.config.legacy_assertions);
535 }
536
537 pub fn executor(
539 &self,
540 known_contracts: ContractsByArtifact,
541 analysis: Arc<solar::sema::Compiler>,
542 artifact_id: &ArtifactId,
543 db: Backend<FEN>,
544 ) -> Executor<FEN> {
545 let mut cheats_config = CheatsConfig::new(
546 &self.config,
547 self.evm_opts.clone(),
548 Some(known_contracts),
549 Some(artifact_id.clone()),
550 false,
551 );
552 cheats_config.isolate = self.isolation;
553 let cheats_config = Arc::new(cheats_config);
554 self.executor_builder
555 .clone()
556 .inspectors(|stack| {
557 stack
558 .logs(self.config.live_logs)
559 .cheatcodes(cheats_config)
560 .trace_requirements(self.trace_requirements())
561 .line_coverage(self.line_coverage)
562 .enable_isolation(self.isolation)
563 .create2_deployer(self.evm_opts.create2_deployer)
564 .set_analysis(analysis)
565 })
566 .spec_id(self.spec_id)
567 .gas_limit(self.evm_opts.gas_limit())
568 .legacy_assertions(self.config.legacy_assertions)
569 .build(self.evm_env.clone(), self.tx_env.clone(), db, self.evm_opts.networks)
570 }
571
572 fn trace_requirements(&self) -> TraceRequirements {
573 TraceRequirements::none()
574 .with_debug_recording(self.debug)
575 .with_decode_internal(self.decode_internal)
576 .with_all_steps(self.record_all_steps)
577 .with_verbosity(self.config.tracing.verbosity.max(self.evm_opts.verbosity))
578 }
579}
580
581#[derive(Clone)]
583#[must_use = "builders do nothing unless you call `build` on them"]
584pub struct MultiContractRunnerBuilder {
585 pub sender: Option<Address>,
588 pub initial_balance: U256,
590 pub config: Arc<Config>,
592 pub inline_config: Arc<InlineConfig>,
594 pub line_coverage: bool,
596 pub debug: bool,
598 pub decode_internal: InternalTraceMode,
600 pub record_all_steps: bool,
602 pub isolation: bool,
604 pub fail_fast: bool,
606 pub multi_network: MultiNetworkConfig,
608 pub showmap: Option<ShowmapConfig>,
610 pub fuzz_only: bool,
612 pub fuzz_failure_replay: bool,
614 pub fuzz_input: Option<FuzzFailureReplayConfig>,
616 pub symbolic_artifact_replay: Option<SymbolicArtifactReplayConfig>,
618 pub create2_deployer_available: Option<bool>,
620}
621
622impl MultiContractRunnerBuilder {
623 fn create2_deployer_available<FEN: FoundryEvmNetwork>(
624 &self,
625 evm_opts: &EvmOpts,
626 backend: &Backend<FEN>,
627 ) -> bool {
628 self.create2_deployer_available.unwrap_or_else(|| {
629 !backend.is_in_forking_mode()
630 && evm_opts.fork_url.is_none()
631 && evm_opts.create2_deployer == foundry_evm::constants::DEFAULT_CREATE2_DEPLOYER
632 })
633 }
634
635 pub fn new(config: Arc<Config>, inline_config: Arc<InlineConfig>) -> Self {
636 Self {
637 config,
638 inline_config,
639 sender: None,
640 initial_balance: U256::ZERO,
641 line_coverage: false,
642 debug: false,
643 isolation: false,
644 decode_internal: Default::default(),
645 record_all_steps: false,
646 fail_fast: false,
647 multi_network: Default::default(),
648 showmap: None,
649 fuzz_only: false,
650 fuzz_failure_replay: false,
651 fuzz_input: None,
652 symbolic_artifact_replay: None,
653 create2_deployer_available: None,
654 }
655 }
656
657 pub const fn with_create2_deployer_available(mut self, available: bool) -> Self {
658 self.create2_deployer_available = Some(available);
659 self
660 }
661
662 pub fn with_showmap(mut self, showmap: Option<ShowmapConfig>) -> Self {
663 self.showmap = showmap;
664 self
665 }
666
667 pub const fn with_fuzz_only(mut self, fuzz_only: bool) -> Self {
668 self.fuzz_only = fuzz_only;
669 self
670 }
671
672 pub const fn with_fuzz_failure_replay(mut self, fuzz_failure_replay: bool) -> Self {
673 self.fuzz_failure_replay = fuzz_failure_replay;
674 self
675 }
676
677 pub fn with_fuzz_input(mut self, fuzz_input: Option<FuzzFailureReplayConfig>) -> Self {
678 self.fuzz_input = fuzz_input;
679 self
680 }
681
682 pub fn with_symbolic_artifact_replay(
683 mut self,
684 replay: Option<SymbolicArtifactReplayConfig>,
685 ) -> Self {
686 self.symbolic_artifact_replay = replay;
687 self
688 }
689
690 pub const fn sender(mut self, sender: Address) -> Self {
691 self.sender = Some(sender);
692 self
693 }
694
695 pub const fn initial_balance(mut self, initial_balance: U256) -> Self {
696 self.initial_balance = initial_balance;
697 self
698 }
699
700 pub const fn set_coverage(mut self, enable: bool) -> Self {
701 self.line_coverage = enable;
702 self
703 }
704
705 pub const fn set_debug(mut self, enable: bool) -> Self {
706 self.debug = enable;
707 self
708 }
709
710 pub const fn set_decode_internal(mut self, mode: InternalTraceMode) -> Self {
711 self.decode_internal = mode;
712 self
713 }
714
715 pub const fn set_record_all_steps(mut self, enable: bool) -> Self {
716 self.record_all_steps = enable;
717 self
718 }
719
720 pub fn with_multi_network(mut self, multi_network: MultiNetworkConfig) -> Self {
721 self.multi_network = multi_network;
722 self
723 }
724
725 pub const fn fail_fast(mut self, fail_fast: bool) -> Self {
726 self.fail_fast = fail_fast;
727 self
728 }
729
730 pub const fn enable_isolation(mut self, enable: bool) -> Self {
731 self.isolation = enable;
732 self
733 }
734
735 pub fn build<FEN: FoundryEvmNetwork, C: Compiler<CompilerContract = Contract>>(
738 self,
739 output: &ProjectCompileOutput,
740 mut evm_env: EvmEnvFor<FEN>,
741 tx_env: TxEnvFor<FEN>,
742 evm_opts: EvmOpts,
743 backend: Backend<FEN>,
744 executor_builder: ExecutorBuilder<FEN>,
745 ) -> Result<MultiContractRunner<FEN>> {
746 let root = &self.config.root;
747 let coverage_artifacts = self
748 .line_coverage
749 .then(|| self.config.coverage_cache_path())
750 .flatten()
751 .map(|path| path.join("artifacts"));
752 let artifact_id = |mut id: ArtifactId| {
753 if let Some(coverage_artifacts) = &coverage_artifacts
756 && let Ok(path) = id.path.strip_prefix(coverage_artifacts)
757 {
758 id.path = self.config.out.join(path);
759 }
760 id.with_stripped_file_prefixes(root).with_slashed_paths()
761 };
762 let contracts = output.artifact_ids().map(|(id, v)| (artifact_id(id), v)).collect();
763 let linker = Linker::new(root, contracts);
764
765 let abis = linker
767 .contracts
768 .values()
769 .filter_map(|contract| contract.abi.as_ref().map(|abi| abi.borrow()));
770 let revert_decoder = RevertDecoder::new().with_abis(abis);
771
772 let configured_libraries = self.config.libraries_with_remappings()?;
773 let create2 = if self.create2_deployer_available(&evm_opts, &backend) {
774 match linker.link_with_create2_detailed(
775 configured_libraries.clone(),
776 evm_opts.create2_deployer,
777 self.config.create2_library_salt,
778 linker.contracts.keys(),
779 ) {
780 Ok(output) => Some(output),
781 Err(LinkerError::CyclicDependency) => None,
782 Err(err) => return Err(err.into()),
783 }
784 } else {
785 None
786 };
787 let (
788 DetailedLinkOutput {
789 output: LinkOutput { libraries, library_addresses, libs_to_deploy },
790 artifact_libraries,
791 ..
792 },
793 library_deployment,
794 ) = match create2 {
795 Some(output) => {
796 let deployment = if output.output.libs_to_deploy.is_empty() {
797 LibraryDeployment::Nonce
798 } else {
799 LibraryDeployment::Create2 {
800 deployer: evm_opts.create2_deployer,
801 salt: self.config.create2_library_salt,
802 }
803 };
804 (output, deployment)
805 }
806 None => (
807 linker.link_with_nonce_or_address_detailed(
808 configured_libraries,
809 LIBRARY_DEPLOYER,
810 0,
811 linker.contracts.keys(),
812 )?,
813 LibraryDeployment::Nonce,
814 ),
815 };
816
817 let linked_contracts = linker
818 .get_linked_artifacts_cow_with_artifact_libraries(&libraries, &artifact_libraries)?;
819 let inline_config = self.inline_config;
820
821 let mut deployable_contracts = DeployableContracts::default();
823 let test_matcher = TestFunctionMatcher::new(
824 &self.config,
825 &inline_config,
826 self.symbolic_artifact_replay.as_ref(),
827 );
828 let empty_filter = EmptyTestFilter::default();
829 let resolver = Resolver::new(&linker);
830 for (id, contract) in linked_contracts.iter() {
831 let Some(abi) = contract.abi.as_ref() else { continue };
832 if abi.constructor.as_ref().is_some_and(|c| !c.inputs.is_empty())
833 || !test_matcher.matches_contract(&empty_filter, id, abi)
834 {
835 continue;
836 }
837 linker.ensure_linked(contract, id)?;
838 let Some(bytecode) =
839 contract.get_bytecode_bytes().map(|b| b.into_owned()).filter(|b| !b.is_empty())
840 else {
841 continue;
842 };
843 let artifact_libraries = artifact_libraries.get(id).unwrap_or(&libraries);
844 let library_addresses = resolver.linked_library_addresses(id, artifact_libraries)?;
845 deployable_contracts.insert(
846 id.clone(),
847 TestContract { abi: abi.clone().into_owned(), bytecode, library_addresses },
848 );
849 }
850
851 let mut analysis = solar::sema::Compiler::new(
853 solar::interface::Session::builder().with_stderr_emitter().build(),
854 );
855 let dcx = analysis.dcx_mut();
856 dcx.set_emitter(Box::new(
857 solar::interface::diagnostics::HumanEmitter::stderr(Default::default())
858 .source_map(Some(dcx.source_map().unwrap())),
859 ));
860 dcx.set_flags_mut(|f| f.track_diagnostics = false);
861
862 let files: Vec<_> = output.output().sources.as_ref().keys().cloned().collect();
864 let target_paths = (!self.line_coverage && !files.is_empty()).then_some(&files[..]);
865 analysis.enter_mut(|compiler| -> Result<()> {
866 let mut pcx = compiler.parse();
867 configure_pcx_from_compile_output(&mut pcx, &self.config, output, target_paths)?;
868 pcx.parse();
869 let _ = compiler.lower_asts();
870 Ok(())
871 })?;
872
873 let storage_layouts = output.artifact_ids().filter_map(|(id, artifact)| {
877 artifact.storage_layout.as_ref().map(|layout| (artifact_id(id), layout.clone()))
878 });
879 let storage_layouts = if target_paths.is_none() {
880 namespaced_storage::extend_storage_layouts(&mut analysis, root, storage_layouts)
881 } else {
882 let mut compiler = solar::sema::Compiler::new(
883 solar::interface::Session::builder()
884 .with_buffer_emitter(Default::default())
885 .build(),
886 );
887 compiler.enter_mut(|compiler| -> Result<()> {
888 let mut pcx = compiler.parse();
889 configure_pcx_from_compile_output(&mut pcx, &self.config, output, None)?;
890 pcx.parse();
891 Ok(())
892 })?;
893 namespaced_storage::extend_storage_layouts(&mut compiler, root, storage_layouts)
894 };
895 let known_contracts = ContractsByArtifactBuilder::new(linked_contracts)
896 .with_storage_layouts(storage_layouts)
897 .build();
898 let analysis = Arc::new(analysis);
899
900 let enum_bounds = EnumBounds::collect(&analysis);
902 let literals = |max_literals| {
903 LiteralsDictionary::new(
904 Some(analysis.clone()),
905 Some(self.config.project_paths()),
906 max_literals,
907 )
908 };
909 let fuzz_max_literals = self.config.fuzz.dictionary.max_fuzz_dictionary_literals;
910 let invariant_max_literals = self.config.invariant.dictionary.max_fuzz_dictionary_literals;
911 let fuzz_literals = literals(fuzz_max_literals);
912 let invariant_literals = if invariant_max_literals == fuzz_max_literals {
913 fuzz_literals.clone()
914 } else {
915 literals(invariant_max_literals)
916 };
917
918 let fork_context = backend.fork()?.as_ref().map(|fork| fork.context());
919 let fork_chain_id = fork_context.map(|context| context.source_chain_id);
920 let fork_hardfork = fork_context.and_then(|context| context.hardfork);
921 let hardfork = resolve_execution_spec(
922 self.config.evm_version,
923 self.config.hardfork,
924 &mut evm_env,
925 ExecutionSpecContext::local_or_fork(fork_chain_id, fork_hardfork),
926 None,
927 );
928 let spec_id = evm_env.cfg_env.spec;
929
930 Ok(MultiContractRunner {
931 contracts: deployable_contracts,
932 revert_decoder,
933 known_contracts,
934 libs_to_deploy,
935 library_addresses,
936 library_deployment,
937 libraries,
938 analysis,
939 fuzz_literals,
940 invariant_literals,
941 enum_bounds,
942
943 tcfg: TestRunnerConfig {
944 evm_opts,
945 executor_builder,
946 evm_env,
947 tx_env,
948 spec_id,
949 hardfork,
950 fork_chain_id,
951 fork_hardfork,
952 sender: self.sender.unwrap_or(self.config.sender),
953 line_coverage: self.line_coverage,
954 debug: self.debug,
955 decode_internal: self.decode_internal,
956 record_all_steps: self.record_all_steps,
957 inline_config,
958 isolation: self.isolation,
959 early_exit: EarlyExit::new(self.fail_fast),
960 multi_network: self.multi_network,
961 showmap: self.showmap,
962 fuzz_minimize: None,
963 fuzz_only: self.fuzz_only,
964 fuzz_failure_replay: self.fuzz_failure_replay,
965 fuzz_input: self.fuzz_input,
966 symbolic_artifact_replay: self.symbolic_artifact_replay,
967 config: self.config,
968 },
969
970 backend,
971 })
972 }
973}
974
975#[derive(Clone, Copy)]
976pub(crate) struct TestFunctionMatcher<'a> {
977 config: &'a Config,
978 inline_config: &'a InlineConfig,
979 symbolic_artifact_replay: Option<&'a SymbolicArtifactReplayConfig>,
980}
981
982impl<'a> TestFunctionMatcher<'a> {
983 pub(crate) const fn new(
984 config: &'a Config,
985 inline_config: &'a InlineConfig,
986 symbolic_artifact_replay: Option<&'a SymbolicArtifactReplayConfig>,
987 ) -> Self {
988 Self { config, inline_config, symbolic_artifact_replay }
989 }
990
991 fn symbolic_tests_enabled(&self, contract_id: &str) -> bool {
992 self.symbolic_artifact_replay.is_some_and(|artifact| {
993 artifact.artifact.kind == SymbolicCounterexampleArtifactKind::SingleCall
994 }) || self.inline_config.contract_symbolic_enabled(
995 &self.config.profile,
996 contract_id,
997 self.config.symbolic.enabled,
998 )
999 }
1000
1001 pub(crate) fn test_function_kind(
1002 &self,
1003 contract_id: &str,
1004 func: &Function,
1005 generated_symbolic_regression: bool,
1006 ) -> TestFunctionKind {
1007 if generated_symbolic_regression && !func.name.starts_with("test_regression_") {
1008 return TestFunctionKind::Unknown;
1009 }
1010
1011 TestFunctionKind::classify(
1012 func.name.as_str(),
1013 !func.inputs.is_empty(),
1014 self.symbolic_tests_enabled(contract_id),
1015 )
1016 }
1017
1018 pub(crate) fn test_functions(
1021 self,
1022 contract_id: String,
1023 abi: &JsonAbi,
1024 mut keep: impl FnMut(&str, &Function, TestFunctionKind) -> bool,
1025 ) -> impl Iterator<Item = &Function> {
1026 let generated_symbolic_regression = is_generated_symbolic_regression_contract(abi);
1027 abi.functions().filter(move |func| {
1028 let kind = self.test_function_kind(&contract_id, func, generated_symbolic_regression);
1029 keep(&contract_id, func, kind)
1030 })
1031 }
1032
1033 fn matching_test_functions<'b>(
1035 self,
1036 filter: &dyn TestFilter,
1037 id: &ArtifactId,
1038 abi: &'b JsonAbi,
1039 ) -> impl Iterator<Item = &'b Function> {
1040 self.test_functions(id.identifier(), abi, move |contract_id, func, kind| {
1041 filter.matches_test_function_kind_in_contract(contract_id, func, kind)
1042 })
1043 }
1044
1045 pub(crate) fn count_fuzz_engine_targets(
1048 &self,
1049 filter: &dyn TestFilter,
1050 id: &ArtifactId,
1051 abi: &JsonAbi,
1052 multi_network: &MultiNetworkConfig,
1053 ) -> (usize, usize) {
1054 let contract_name = id.identifier();
1055 let matches_network_pass = |func: &Function| {
1056 function_matches_network_pass(
1057 &multi_network.all_override_networks,
1058 multi_network.pass_network.as_ref(),
1059 self.inline_config.network_for(&self.config.profile, &contract_name, &func.name),
1060 )
1061 };
1062 let fuzz = self
1063 .test_functions(contract_name.clone(), abi, |contract_id, func, kind| {
1064 kind.is_fuzz_test()
1065 && filter.matches_test_function_kind_in_contract(contract_id, func, kind)
1066 && matches_network_pass(func)
1067 })
1068 .count();
1069 let invariant = count_runnable_invariant_campaign_anchors(
1070 abi,
1071 filter,
1072 InvariantCampaignScope {
1073 config: self.config,
1074 inline_config: self.inline_config,
1075 contract_name: &contract_name,
1076 all_override_networks: &multi_network.all_override_networks,
1077 pass_network: multi_network.pass_network.as_ref(),
1078 },
1079 );
1080 (fuzz, invariant)
1081 }
1082
1083 pub(crate) fn matches_contract(
1084 &self,
1085 filter: &dyn TestFilter,
1086 id: &ArtifactId,
1087 abi: &JsonAbi,
1088 ) -> bool {
1089 external_artifact_is_test_eligible(&id.build_id)
1090 && filter.matches_path(&id.source)
1091 && filter.matches_contract(&id.name)
1092 && self.matching_test_functions(filter, id, abi).next().is_some()
1093 }
1094}
1095
1096pub(crate) fn is_generated_symbolic_regression_contract(abi: &JsonAbi) -> bool {
1097 abi.functions().any(|func| func.name == SYMBOLIC_REGRESSION_MARKER && func.inputs.is_empty())
1098}
1099
1100#[cfg(test)]
1101mod tests {
1102 use super::*;
1103 use foundry_evm::core::evm::EthEvmNetwork;
1104
1105 fn abi_with_functions(functions: &[&str]) -> JsonAbi {
1106 let mut abi = JsonAbi::new();
1107 for function in functions {
1108 let function = Function::parse(function).unwrap();
1109 abi.functions.entry(function.name.clone()).or_default().push(function);
1110 }
1111 abi
1112 }
1113
1114 #[test]
1115 fn generated_symbolic_regression_detection_uses_marker() {
1116 let user_suffix_abi = abi_with_functions(&["test_fails()"]);
1117 assert!(!is_generated_symbolic_regression_contract(&user_suffix_abi));
1118
1119 let generated_abi =
1120 abi_with_functions(&[&format!("{SYMBOLIC_REGRESSION_MARKER}()"), "test_fails()"]);
1121 assert!(is_generated_symbolic_regression_contract(&generated_abi));
1122 }
1123
1124 #[test]
1125 fn create2_deployer_availability_default_is_conservative() {
1126 let config = Arc::new(Config::default());
1127 let builder = MultiContractRunnerBuilder::new(config, Arc::new(InlineConfig::new()));
1128 let mut evm_opts = EvmOpts::default();
1129 let backend = Backend::<EthEvmNetwork>::spawn(None).unwrap();
1130 assert!(builder.create2_deployer_available(&evm_opts, &backend));
1131
1132 evm_opts.fork_url = Some("http://localhost:8545".into());
1133 assert!(!builder.create2_deployer_available(&evm_opts, &backend));
1134 evm_opts.fork_url = None;
1135 evm_opts.create2_deployer = Address::ZERO;
1136 assert!(!builder.create2_deployer_available(&evm_opts, &backend));
1137 assert!(
1138 builder
1139 .with_create2_deployer_available(true)
1140 .create2_deployer_available(&evm_opts, &backend)
1141 );
1142 }
1143}