1use 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#[derive(Clone, Debug)]
59pub struct MultiContractRunner<FEN: FoundryEvmNetwork> {
60 pub contracts: DeployableContracts,
63 pub known_contracts: ContractsByArtifact,
65 pub revert_decoder: RevertDecoder,
67 pub libs_to_deploy: Vec<Bytes>,
69 pub libraries: Libraries,
71 pub analysis: Arc<solar::sema::Compiler>,
73 pub fuzz_literals: LiteralsDictionary,
75
76 pub fork: Option<CreateFork>,
78
79 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 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 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 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 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 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 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 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 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 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#[derive(Clone, Debug, Default)]
336pub struct MultiNetworkConfig {
337 pub all_override_networks: Vec<NetworkVariant>,
340 pub pass_network: Option<NetworkVariant>,
345}
346
347#[derive(Clone, Debug)]
350pub struct ShowmapConfig {
351 pub out_dir: PathBuf,
353 pub approach: String,
355 pub trial: String,
357 pub per_input: bool,
359 pub domain: ShowmapDomain,
361 pub corpus_dir: Option<PathBuf>,
364}
365
366#[derive(Clone, Debug)]
370pub struct TestRunnerConfig<FEN: FoundryEvmNetwork> {
371 pub config: Arc<Config>,
373 pub inline_config: Arc<InlineConfig>,
375
376 pub evm_opts: EvmOpts,
378 pub evm_env: EvmEnvFor<FEN>,
380 pub tx_env: TxEnvFor<FEN>,
382 pub spec_id: SpecFor<FEN>,
384 pub sender: Address,
386
387 pub line_coverage: bool,
389 pub debug: bool,
391 pub decode_internal: InternalTraceMode,
393 pub isolation: bool,
395 pub early_exit: EarlyExit,
397
398 pub multi_network: MultiNetworkConfig,
400
401 pub showmap: Option<ShowmapConfig>,
404}
405
406impl<FEN: FoundryEvmNetwork> TestRunnerConfig<FEN> {
407 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 self.evm_opts.always_use_create_2_factory = config.always_use_create_2_factory;
424
425 self.config = config;
428 }
429
430 pub fn configure_executor(&self, executor: &mut Executor<FEN>) {
432 let inspector = executor.inspector_mut();
435 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 executor.set_spec_id(self.spec_id);
448 executor.set_legacy_assertions(self.config.legacy_assertions);
450 }
451
452 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#[derive(Clone)]
496#[must_use = "builders do nothing unless you call `build` on them"]
497pub struct MultiContractRunnerBuilder {
498 pub sender: Option<Address>,
501 pub initial_balance: U256,
503 pub fork: Option<CreateFork>,
505 pub config: Arc<Config>,
507 pub line_coverage: bool,
509 pub debug: bool,
511 pub decode_internal: InternalTraceMode,
513 pub isolation: bool,
515 pub fail_fast: bool,
517 pub multi_network: MultiNetworkConfig,
519 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 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 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 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 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 let known_contracts =
650 ContractsByArtifactBuilder::new(linked_contracts).with_output(output, root).build();
651
652 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 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}