1use 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#[derive(Clone, Debug)]
64pub struct MultiContractRunner<FEN: FoundryEvmNetwork> {
65 pub contracts: DeployableContracts,
68 pub known_contracts: ContractsByArtifact,
70 pub revert_decoder: RevertDecoder,
72 pub libs_to_deploy: Vec<Bytes>,
74 pub libraries: Libraries,
76 pub analysis: Arc<solar::sema::Compiler>,
78 pub fuzz_literals: LiteralsDictionary,
80 pub invariant_literals: LiteralsDictionary,
82 pub enum_bounds: EnumBounds,
84
85 pub fork: Option<CreateFork>,
87
88 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 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 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 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 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 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 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 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 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 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#[derive(Clone, Debug, Default)]
390pub struct MultiNetworkConfig {
391 pub all_override_networks: Vec<NetworkVariant>,
394 pub pass_network: Option<NetworkVariant>,
399}
400
401#[derive(Clone, Debug)]
404pub struct ShowmapConfig {
405 pub out_dir: PathBuf,
407 pub approach: String,
409 pub trial: String,
411 pub per_input: bool,
413 pub domain: ShowmapDomain,
415 pub corpus_dir: Option<PathBuf>,
418 pub emit_files: bool,
420}
421
422pub type FuzzMinimizeEdgeIndices = Arc<Mutex<BTreeMap<String, Arc<Mutex<EdgeIndexMap>>>>>;
423
424#[derive(Clone, Debug)]
427pub struct FuzzMinimizeConfig {
428 pub input: Arc<[BasicTxDetails]>,
430 pub evm_edge_indices: FuzzMinimizeEdgeIndices,
433 pub observations: Arc<Mutex<Vec<FuzzMinimizeObservation>>>,
435}
436
437#[derive(Clone, Debug)]
439pub struct FuzzMinimizeObservation {
440 pub target: String,
442 pub observation: ReplayObservation,
444}
445
446#[derive(Clone, Debug)]
447pub struct SymbolicArtifactReplayConfig {
448 pub artifact: SymbolicCounterexampleArtifact,
450 pub path: PathBuf,
452}
453
454#[derive(Clone, Debug)]
458pub struct TestRunnerConfig<FEN: FoundryEvmNetwork> {
459 pub config: Arc<Config>,
461 pub inline_config: Arc<InlineConfig>,
463
464 pub evm_opts: EvmOpts,
466 pub evm_env: EvmEnvFor<FEN>,
468 pub tx_env: TxEnvFor<FEN>,
470 pub spec_id: SpecFor<FEN>,
472 pub sender: Address,
474
475 pub line_coverage: bool,
477 pub debug: bool,
479 pub decode_internal: InternalTraceMode,
481 pub record_all_steps: bool,
483 pub isolation: bool,
485 pub early_exit: EarlyExit,
487
488 pub multi_network: MultiNetworkConfig,
490
491 pub showmap: Option<ShowmapConfig>,
494 pub fuzz_minimize: Option<FuzzMinimizeConfig>,
496 pub fuzz_only: bool,
498 pub fuzz_failure_replay: bool,
500
501 pub symbolic_artifact_replay: Option<SymbolicArtifactReplayConfig>,
503}
504
505impl<FEN: FoundryEvmNetwork> TestRunnerConfig<FEN> {
506 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 self.evm_opts.always_use_create_2_factory = config.always_use_create_2_factory;
524
525 self.config = config;
528 }
529
530 pub fn configure_executor(&self, executor: &mut Executor<FEN>) {
532 let inspector = executor.inspector_mut();
535 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 executor.set_spec_id(self.spec_id);
549 executor.set_legacy_assertions(self.config.legacy_assertions);
551 }
552
553 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#[derive(Clone)]
600#[must_use = "builders do nothing unless you call `build` on them"]
601pub struct MultiContractRunnerBuilder {
602 pub sender: Option<Address>,
605 pub initial_balance: U256,
607 pub fork: Option<CreateFork>,
609 pub config: Arc<Config>,
611 pub inline_config: Arc<InlineConfig>,
613 pub line_coverage: bool,
615 pub debug: bool,
617 pub decode_internal: InternalTraceMode,
619 pub record_all_steps: bool,
621 pub isolation: bool,
623 pub fail_fast: bool,
625 pub multi_network: MultiNetworkConfig,
627 pub showmap: Option<ShowmapConfig>,
629 pub fuzz_minimize: Option<FuzzMinimizeConfig>,
631 pub fuzz_only: bool,
633 pub fuzz_failure_replay: bool,
635 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 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 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 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 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 let known_contracts =
804 ContractsByArtifactBuilder::new(linked_contracts).with_output(output, root).build();
805
806 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 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 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}