1use super::{install, watch::WatchArgs};
2use crate::{
3 MultiContractRunner, MultiContractRunnerBuilder,
4 decode::decode_console_logs,
5 diagnostic::build::SOLC_ERROR,
6 gas_report::GasReport,
7 multi_runner::{MultiNetworkConfig, ShowmapConfig, matches_artifact},
8 mutation::{MutationRunConfig, run_mutation_testing},
9 result::{SuiteResult, TestKindReport, TestOutcome, TestResult, TestStatus},
10 traces::{
11 CallTraceDecoderBuilder, InternalTraceMode, TraceKind,
12 debug::{ContractSources, DebugTraceIdentifier},
13 decode_trace_arena, folded_stack_trace,
14 identifier::SignaturesIdentifier,
15 },
16};
17use alloy_primitives::U256;
18use chrono::Utc;
19use clap::{Parser, ValueEnum, ValueHint};
20use eyre::{Context, OptionExt, Result, bail};
21use foundry_cli::{
22 ExitCode,
23 json::{JsonEnvelope, JsonMessage, print_json},
24 opts::{BuildOpts, EvmArgs, GlobalArgs},
25 utils::{self, LoadConfig},
26};
27use foundry_common::{
28 EmptyTestFilter, TestFilter, TestFunctionExt, compile::ProjectCompiler, fs, shell,
29};
30use foundry_compilers::{
31 CompilationError, ProjectCompileOutput,
32 artifacts::{Libraries, output_selection::OutputSelection},
33 compilers::{
34 Language,
35 multi::{MultiCompiler, MultiCompilerLanguage},
36 },
37 utils::source_files_iter,
38};
39use foundry_config::{
40 Config, InlineConfig, InvariantWorkers, figment,
41 figment::{
42 Metadata, Profile, Provider,
43 value::{Dict, Map, Value},
44 },
45 filter::GlobMatcher,
46};
47use foundry_debugger::Debugger;
48#[cfg(feature = "optimism")]
49use foundry_evm::core::evm::OpEvmNetwork;
50use foundry_evm::{
51 core::evm::{
52 BlockEnvFor, EthEvmNetwork, FoundryEvmNetwork, SpecFor, TempoEvmNetwork, TxEnvFor,
53 },
54 executors::ShowmapDomain,
55 fuzz::CounterExample,
56 hardforks::TempoHardfork,
57 opts::EvmOpts,
58 traces::{backtrace::BacktraceBuilder, identifier::TraceIdentifiers, prune_trace_depth},
59};
60use rand::Rng;
61use regex::Regex;
62use revm::context::Transaction;
63use std::{
64 collections::{BTreeMap, BTreeSet},
65 fmt::Write,
66 path::{Path, PathBuf},
67 sync::{Arc, mpsc::channel},
68 time::{Duration, Instant},
69};
70use yansi::Paint;
71
72mod filter;
73mod summary;
74use crate::{result::TestKind, traces::render_trace_arena_inner};
75pub use filter::{FilterArgs, ProjectPathsAwareFilter};
76use filter::{RerunFailure, RerunFailures};
77use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite};
78use summary::{TestSummaryReport, format_invariant_metrics_table};
79
80foundry_config::merge_impl_figment_convert!(TestArgs, build, evm);
82
83#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, ValueEnum)]
85#[clap(rename_all = "lowercase")]
86pub enum ShowmapDomainArg {
87 #[default]
88 Evm,
89 Sancov,
90 Both,
91}
92
93impl From<ShowmapDomainArg> for ShowmapDomain {
94 fn from(d: ShowmapDomainArg) -> Self {
95 match d {
96 ShowmapDomainArg::Evm => Self::Evm,
97 ShowmapDomainArg::Sancov => Self::Sancov,
98 ShowmapDomainArg::Both => Self::Both,
99 }
100 }
101}
102
103#[derive(Clone, Debug, Parser)]
105#[command(next_help_heading = "Test options")]
106pub struct TestArgs {
107 #[command(flatten)]
109 pub global: GlobalArgs,
110
111 #[arg(value_hint = ValueHint::FilePath)]
113 pub path: Option<GlobMatcher>,
114
115 #[arg(long, conflicts_with_all = ["flamegraph", "flamechart", "decode_internal", "rerun"])]
122 debug: bool,
123
124 #[arg(long)]
129 flamegraph: bool,
130
131 #[arg(long, conflicts_with = "flamegraph")]
136 flamechart: bool,
137
138 #[arg(long)]
145 decode_internal: bool,
146
147 #[arg(
149 long,
150 requires = "debug",
151 value_hint = ValueHint::FilePath,
152 value_name = "PATH"
153 )]
154 dump: Option<PathBuf>,
155
156 #[arg(long, env = "FORGE_GAS_REPORT")]
158 gas_report: bool,
159
160 #[arg(long, env = "FORGE_SNAPSHOT_CHECK")]
162 gas_snapshot_check: Option<bool>,
163
164 #[arg(long, env = "FORGE_SNAPSHOT_EMIT")]
166 gas_snapshot_emit: Option<bool>,
167
168 #[arg(long, env = "FORGE_ALLOW_FAILURE")]
170 allow_failure: bool,
171
172 #[arg(long, short, env = "FORGE_SUPPRESS_SUCCESSFUL_TRACES", help_heading = "Display options")]
174 suppress_successful_traces: bool,
175
176 #[arg(long)]
178 trace_depth: Option<usize>,
179
180 #[arg(long, conflicts_with_all = ["quiet", "json", "gas_report", "summary", "list", "show_progress"], help_heading = "Display options")]
182 pub junit: bool,
183
184 #[arg(long)]
186 pub fail_fast: bool,
187
188 #[arg(long, env = "ETHERSCAN_API_KEY", value_name = "KEY")]
190 etherscan_api_key: Option<String>,
191
192 #[arg(long, short, conflicts_with_all = ["show_progress", "decode_internal", "summary"], help_heading = "Display options")]
194 list: bool,
195
196 #[arg(long)]
198 pub fuzz_seed: Option<U256>,
199
200 #[arg(long, env = "FOUNDRY_FUZZ_RUNS", value_name = "RUNS")]
201 pub fuzz_runs: Option<u64>,
202
203 #[arg(long, env = "FOUNDRY_INVARIANT_WORKERS", value_name = "WORKERS")]
205 pub invariant_workers: Option<InvariantWorkers>,
206
207 #[arg(long, env = "FOUNDRY_FUZZ_RUN", value_name = "RUN")]
209 pub fuzz_run: Option<u32>,
210
211 #[arg(long, env = "FOUNDRY_FUZZ_WORKER", value_name = "WORKER", requires = "fuzz_run")]
213 pub fuzz_worker: Option<u32>,
214
215 #[arg(long, env = "FOUNDRY_FUZZ_TIMEOUT", value_name = "TIMEOUT")]
217 pub fuzz_timeout: Option<u64>,
218
219 #[arg(long)]
221 pub fuzz_input_file: Option<String>,
222
223 #[arg(long, env = "FOUNDRY_SYMBOLIC")]
225 pub symbolic: bool,
226
227 #[arg(long, env = "FOUNDRY_SYMBOLIC_SOLVER", value_name = "PATH_OR_NAME")]
229 pub symbolic_solver: Option<String>,
230
231 #[arg(long, env = "FOUNDRY_SYMBOLIC_SOLVER_COMMAND", value_name = "COMMAND")]
233 pub symbolic_solver_command: Option<String>,
234
235 #[arg(
237 long,
238 env = "FOUNDRY_SYMBOLIC_SOLVER_PORTFOLIO",
239 value_delimiter = ',',
240 value_name = "SOLVER_OR_COMMAND,..."
241 )]
242 pub symbolic_solver_portfolio: Option<Vec<String>>,
243
244 #[arg(long, env = "FOUNDRY_SYMBOLIC_TIMEOUT", value_name = "SECONDS")]
246 pub symbolic_timeout: Option<u32>,
247
248 #[arg(long, env = "FOUNDRY_SYMBOLIC_LOOP", value_name = "N")]
250 pub symbolic_loop: Option<u32>,
251
252 #[arg(long, env = "FOUNDRY_SYMBOLIC_DEPTH", value_name = "N")]
254 pub symbolic_depth: Option<u32>,
255
256 #[arg(long, env = "FOUNDRY_SYMBOLIC_WIDTH", value_name = "N")]
258 pub symbolic_width: Option<u32>,
259
260 #[arg(long, env = "FOUNDRY_SYMBOLIC_MAX_DEPTH", value_name = "N")]
262 pub symbolic_max_depth: Option<u32>,
263
264 #[arg(long, env = "FOUNDRY_SYMBOLIC_MAX_PATHS", value_name = "N")]
266 pub symbolic_max_paths: Option<u32>,
267
268 #[arg(long, env = "FOUNDRY_SYMBOLIC_INVARIANT_DEPTH", value_name = "N")]
270 pub symbolic_invariant_depth: Option<u32>,
271
272 #[arg(long, env = "FOUNDRY_SYMBOLIC_MAX_SOLVER_QUERIES", value_name = "N")]
274 pub symbolic_max_solver_queries: Option<u32>,
275
276 #[arg(long, env = "FOUNDRY_SYMBOLIC_DEFAULT_DYNAMIC_LENGTH", value_name = "N")]
278 pub symbolic_default_dynamic_length: Option<u32>,
279
280 #[arg(long, env = "FOUNDRY_SYMBOLIC_MAX_DYNAMIC_LENGTH", value_name = "N")]
282 pub symbolic_max_dynamic_length: Option<u32>,
283
284 #[arg(
286 long,
287 env = "FOUNDRY_SYMBOLIC_ARRAY_LENGTHS",
288 value_delimiter = ',',
289 value_name = "N,..."
290 )]
291 pub symbolic_array_lengths: Option<Vec<u32>>,
292
293 #[arg(long, env = "FOUNDRY_SYMBOLIC_MAX_CALLDATA_BYTES", value_name = "N")]
295 pub symbolic_max_calldata_bytes: Option<u32>,
296
297 #[arg(long, env = "FOUNDRY_SYMBOLIC_CALL_TARGETS")]
299 pub symbolic_call_targets: bool,
300
301 #[arg(long, env = "FOUNDRY_SYMBOLIC_DUMP_SMT")]
303 pub symbolic_dump_smt: bool,
304
305 #[arg(
307 long,
308 env = "FOUNDRY_SYMBOLIC_STORAGE_LAYOUT",
309 value_name = "solidity|generic",
310 value_parser = ["solidity", "generic"]
311 )]
312 pub symbolic_storage_layout: Option<String>,
313
314 #[arg(long, conflicts_with_all = ["quiet", "json"], help_heading = "Display options")]
316 pub show_progress: bool,
317
318 #[arg(long)]
321 pub rerun: bool,
322
323 #[arg(long, help_heading = "Display options")]
325 pub summary: bool,
326
327 #[arg(long, help_heading = "Display options", requires = "summary")]
329 pub detailed: bool,
330
331 #[arg(long, help_heading = "Display options")]
333 pub disable_labels: bool,
334
335 #[arg(
339 long,
340 value_name = "DIR",
341 value_hint = ValueHint::DirPath,
342 help_heading = "Showmap replay",
343 conflicts_with_all = ["debug", "flamegraph", "flamechart", "rerun", "fuzz_input_file", "gas_report"],
344 )]
345 pub showmap_out: Option<PathBuf>,
346
347 #[arg(long, help_heading = "Showmap replay", requires = "showmap_out")]
349 pub showmap_per_input: bool,
350
351 #[arg(
353 long,
354 value_enum,
355 default_value_t = ShowmapDomainArg::Evm,
356 help_heading = "Showmap replay",
357 requires = "showmap_out",
358 )]
359 pub showmap_domain: ShowmapDomainArg,
360
361 #[arg(
363 long,
364 default_value = "replay",
365 help_heading = "Showmap replay",
366 requires = "showmap_out"
367 )]
368 pub showmap_approach: String,
369
370 #[arg(long, help_heading = "Showmap replay", requires = "showmap_out")]
373 pub showmap_trial: Option<String>,
374
375 #[arg(
378 long,
379 value_name = "PATH",
380 value_hint = ValueHint::DirPath,
381 help_heading = "Showmap replay",
382 requires = "showmap_out",
383 )]
384 pub showmap_corpus_dir: Option<PathBuf>,
385
386 #[command(flatten)]
387 filter: FilterArgs,
388
389 #[command(flatten)]
390 evm: EvmArgs,
391
392 #[command(flatten)]
393 pub build: BuildOpts,
394
395 #[command(flatten)]
396 pub watch: WatchArgs,
397
398 #[arg(long, num_args(0..), value_name = "PATH")]
401 pub mutate: Option<Vec<PathBuf>>,
402
403 #[arg(long, value_name = "PATTERN", requires = "mutate", conflicts_with = "mutate_contract")]
408 pub mutate_path: Option<GlobMatcher>,
409
410 #[arg(long, value_name = "REGEX", requires = "mutate")]
414 pub mutate_contract: Option<regex::Regex>,
415
416 #[arg(long, value_name = "JOBS", requires = "mutate")]
419 pub mutation_jobs: Option<usize>,
420
421 #[arg(long, value_name = "TIMEOUT", requires = "mutate")]
427 pub mutation_timeout: Option<u32>,
428}
429
430impl TestArgs {
431 pub async fn run(mut self) -> Result<TestOutcome> {
432 trace!(target: "forge::test", "executing test command");
433 self.compile_and_run().await
434 }
435
436 fn showmap_config(&self) -> Option<ShowmapConfig> {
438 let trial = self.showmap_trial.clone().unwrap_or_else(|| {
441 let ns = std::time::SystemTime::now()
442 .duration_since(std::time::UNIX_EPOCH)
443 .map(|d| d.as_nanos())
444 .unwrap_or(0);
445 format!("trial-{ns}")
446 });
447 Some(ShowmapConfig {
448 out_dir: self.showmap_out.clone()?,
449 approach: self.showmap_approach.clone(),
450 trial,
451 per_input: self.showmap_per_input,
452 domain: self.showmap_domain.into(),
453 corpus_dir: self.showmap_corpus_dir.clone(),
454 })
455 }
456
457 pub(crate) fn reject_machine_unsupported_flags(&self) -> Result<()> {
461 if !foundry_cli::is_machine() {
462 return Ok(());
463 }
464 let unsupported = [
465 ("--watch", self.is_watch()),
466 ("--debug", self.debug),
467 ("--flamegraph", self.flamegraph),
468 ("--flamechart", self.flamechart),
469 ("--gas-report", self.gas_report),
470 ("--summary", self.summary),
471 ("--list", self.list),
472 ("--junit", self.junit),
473 ("--show-progress", self.show_progress),
474 ("--mutate", self.mutate.is_some()),
475 ("--live-logs", self.evm.live_logs),
479 ("--gas-snapshot-check", self.gas_snapshot_check.unwrap_or(false)),
481 ("--gas-snapshot-emit", self.gas_snapshot_emit == Some(true)),
484 ]
485 .into_iter()
486 .filter_map(|(name, on)| on.then_some(name))
487 .collect::<Vec<_>>();
488 if !unsupported.is_empty() {
489 foundry_cli::machine::bail_machine_usage_with_details(
490 format!(
491 "`forge test` under `--machine` does not yet support {}; \
492 run without `--machine` or omit those flags.",
493 unsupported.join(", ")
494 ),
495 serde_json::json!({ "unsupported_flags": unsupported }),
496 );
497 }
498 Ok(())
499 }
500
501 #[instrument(target = "forge::test", skip_all)]
508 pub fn get_sources_to_compile(
509 &self,
510 config: &Config,
511 test_filter: &ProjectPathsAwareFilter,
512 ) -> Result<BTreeSet<PathBuf>> {
513 if test_filter.is_empty() {
516 return Ok(source_files_iter(&config.src, MultiCompilerLanguage::FILE_EXTENSIONS)
517 .chain(source_files_iter(&config.test, MultiCompilerLanguage::FILE_EXTENSIONS))
518 .collect());
519 }
520
521 let filter_args = test_filter.args();
522 let has_contract_or_test_filter = filter_args.test_pattern.is_some()
523 || filter_args.test_pattern_inverse.is_some()
524 || filter_args.contract_pattern.is_some()
525 || filter_args.contract_pattern_inverse.is_some();
526 if !has_contract_or_test_filter {
527 return Ok(source_files_iter(&config.src, MultiCompilerLanguage::FILE_EXTENSIONS)
528 .chain(
529 source_files_iter(&config.test, MultiCompilerLanguage::FILE_EXTENSIONS)
530 .filter(|path| test_filter.matches_path(path)),
531 )
532 .collect());
533 }
534
535 let mut project = config.create_project(true, true)?;
536 project.update_output_selection(|selection| {
537 *selection = OutputSelection::common_output_selection(["abi".to_string()]);
538 });
539 let output = project.compile()?;
540 if output.has_compiler_errors() {
541 if foundry_cli::is_machine() {
544 emit_machine_compile_error(&output);
545 }
546 sh_println!("{output}")?;
547 eyre::bail!("Compilation failed");
548 }
549
550 Ok(output
555 .artifact_ids()
556 .filter_map(|(id, artifact)| artifact.abi.as_ref().map(|abi| (id, abi)))
557 .filter(|(id, abi)| {
558 if id.source.starts_with(&config.src) {
559 return true;
560 }
561 let stripped = id.clone().with_stripped_file_prefixes(&config.root);
562 matches_artifact(test_filter, &stripped, abi, config.symbolic.enabled)
563 })
564 .map(|(id, _)| id.source)
565 .collect())
566 }
567
568 pub async fn compile_and_run(&mut self) -> Result<TestOutcome> {
575 let machine_mode = foundry_cli::is_machine();
576
577 let (mut config, evm_opts) = self.load_config_and_evm_opts()?;
579
580 let should_mutate = self.mutate.is_some();
581
582 if should_mutate {
584 config.dynamic_test_linking = true;
585 config.cache = true;
586 }
587
588 if machine_mode {
592 config.show_progress = false;
593 config.live_logs = false;
594 config.gas_snapshot_check = false;
595 config.gas_snapshot_emit = false;
596 }
597
598 if !machine_mode
601 && install::install_missing_dependencies(&mut config).await
602 && config.auto_detect_remappings
603 {
604 config = self.load_config()?;
606 }
607
608 let project = config.project()?;
610
611 let filter = self.filter(&config)?;
612 trace!(target: "forge::test", ?filter, "using filter");
613
614 let mut compiler = ProjectCompiler::new()
615 .dynamic_test_linking(config.dynamic_test_linking)
616 .quiet(shell::is_json() || self.junit || machine_mode)
617 .files(self.get_sources_to_compile(&config, &filter)?);
618 if machine_mode {
621 compiler = compiler.bail(false);
622 }
623 let output = compiler.compile(&project)?;
624
625 if machine_mode && output.has_compiler_errors() {
626 emit_machine_compile_error(&output);
627 }
628
629 self.run_tests(&project.paths.root, config, evm_opts, &output, &filter, false).await
630 }
631
632 pub async fn run_tests(
636 &mut self,
637 project_root: &Path,
638 mut config: Config,
639 mut evm_opts: EvmOpts,
640 output: &ProjectCompileOutput,
641 filter: &ProjectPathsAwareFilter,
642 coverage: bool,
643 ) -> Result<TestOutcome> {
644 if config.fuzz.run == Some(0) {
645 bail!("`fuzz.run` must be greater than 0");
646 }
647
648 if self.mutate.is_some() {
656 let mut conflicts = Vec::new();
657 if self.list {
658 conflicts.push("--list");
659 }
660 if self.debug {
661 conflicts.push("--debug");
662 }
663 if self.flamegraph {
664 conflicts.push("--flamegraph");
665 }
666 if self.flamechart {
667 conflicts.push("--flamechart");
668 }
669 if self.junit {
670 conflicts.push("--junit");
671 }
672 if coverage {
673 conflicts.push("coverage");
674 }
675 if self.showmap_out.is_some() {
676 conflicts.push("--showmap-out");
677 }
678 if !conflicts.is_empty() {
679 bail!(
680 "`--mutate` cannot be combined with: {}. Re-run without those flags to use \
681 mutation testing.",
682 conflicts.join(", ")
683 );
684 }
685 }
686
687 if self.gas_report {
689 evm_opts.isolate = true;
690 } else {
691 config.fuzz.gas_report_samples = 0;
693 config.invariant.gas_report_samples = 0;
694 }
695
696 config.fuzz.seed = config
698 .fuzz
699 .seed
700 .or_else(|| Some(U256::from_be_bytes(rand::rng().random::<[u8; 32]>())));
701
702 let should_debug = self.debug;
704 let should_draw = self.flamegraph || self.flamechart;
705
706 if (self.gas_report && evm_opts.verbosity < 3) || self.flamegraph || self.flamechart {
708 evm_opts.verbosity = 3;
709 }
710
711 if should_draw && !self.decode_internal {
713 self.decode_internal = true;
714 }
715
716 let decode_internal = if self.decode_internal {
718 InternalTraceMode::Simple
721 } else {
722 InternalTraceMode::None
723 };
724
725 evm_opts.infer_network_from_fork().await;
727
728 let config_for_mutation = config.clone();
730 let evm_opts_for_mutation = evm_opts.clone();
731
732 let inline_config = InlineConfig::new_parsed(output, &config)?;
734 let override_networks = inline_config.referenced_override_networks(&config.profile);
735
736 if foundry_cli::is_machine() && !override_networks.is_empty() {
739 let networks: Vec<String> = override_networks.iter().map(|n| n.to_string()).collect();
740 foundry_cli::machine::bail_machine_usage_with_details(
741 "`forge test` under `--machine` does not yet support inline network \
742 overrides; run without `--machine` or remove the inline `network` \
743 annotations.",
744 serde_json::json!({
745 "unsupported_features": ["inline_network_overrides"],
746 "networks": networks,
747 }),
748 );
749 }
750
751 let (libraries, mut outcome) = if override_networks.is_empty() {
752 self.dispatch_network(
754 &evm_opts,
755 config,
756 evm_opts.clone(),
757 output,
758 filter,
759 coverage,
760 should_debug,
761 decode_internal,
762 MultiNetworkConfig::default(),
763 )
764 .await?
765 } else {
766 let all_override_networks = override_networks.clone();
768 let multi_pass_timer = Instant::now();
769
770 let (libraries, mut outcome) = self
772 .dispatch_network(
773 &evm_opts,
774 config.clone(),
775 evm_opts.clone(),
776 output,
777 filter,
778 coverage,
779 should_debug,
780 decode_internal,
781 MultiNetworkConfig {
782 all_override_networks: all_override_networks.clone(),
783 pass_network: None,
784 },
785 )
786 .await?;
787
788 for &network in &override_networks {
790 let mut pass_evm_opts = evm_opts.clone();
791 pass_evm_opts.networks = network.into();
792 let (_, pass_outcome) = self
793 .dispatch_network(
794 &pass_evm_opts,
795 config.clone(),
796 pass_evm_opts.clone(),
797 output,
798 filter,
799 coverage,
800 should_debug,
801 decode_internal,
802 MultiNetworkConfig {
803 all_override_networks: all_override_networks.clone(),
804 pass_network: Some(network),
805 },
806 )
807 .await?;
808 merge_outcomes(&mut outcome, pass_outcome);
809 }
810
811 if !self.summary && !shell::is_json() && !foundry_cli::is_machine() {
814 sh_println!("{}", outcome.summary(multi_pass_timer.elapsed()))?;
815 }
816 if self.summary && !outcome.results.is_empty() && !foundry_cli::is_machine() {
817 let summary_report = TestSummaryReport::new(self.detailed, outcome.clone());
818 sh_println!("{}", &summary_report)?;
819 }
820
821 (libraries, outcome)
822 };
823
824 if should_draw {
825 let (suite_name, test_name, mut test_result) =
826 outcome.remove_first().ok_or_eyre("no tests were executed")?;
827
828 let (_, arena) = test_result
829 .traces
830 .iter_mut()
831 .find(|(kind, _)| *kind == TraceKind::Execution)
832 .unwrap();
833
834 let decoder = outcome.last_run_decoder.as_ref().unwrap();
836 decode_trace_arena(arena, decoder).await;
837 let mut fst = folded_stack_trace::build(arena, self.evm.isolate);
838
839 let label = if self.flamegraph { "flamegraph" } else { "flamechart" };
840 let contract = suite_name.split(':').next_back().unwrap();
841 let test_name = test_name.trim_end_matches("()");
842 let file_name = format!("cache/{label}_{contract}_{test_name}.svg");
843 let file = std::fs::File::create(&file_name).wrap_err("failed to create file")?;
844 let file = std::io::BufWriter::new(file);
845
846 let mut options = inferno::flamegraph::Options::default();
847 options.title = format!("{label} {contract}::{test_name}");
848 options.count_name = "gas".to_string();
849 if self.flamechart {
850 options.flame_chart = true;
851 fst.reverse();
852 }
853
854 inferno::flamegraph::from_lines(&mut options, fst.iter().map(String::as_str), file)
856 .wrap_err("failed to write svg")?;
857 sh_println!("Saved to {file_name}")?;
858
859 if let Err(e) = opener::open(&file_name) {
861 sh_err!("Failed to open {file_name}; please open it manually: {e}")?;
862 }
863 }
864
865 if should_debug {
866 let (_, _, test_result) =
868 outcome.remove_first().ok_or_eyre("no tests were executed")?;
869
870 let sources =
871 ContractSources::from_project_output(output, project_root, Some(&libraries))?;
872
873 let traces = {
876 let execution = test_result
877 .traces
878 .iter()
879 .filter(|(kind, _)| kind.is_execution())
880 .cloned()
881 .collect::<Vec<_>>();
882 if execution.is_empty() { test_result.traces.clone() } else { execution }
883 };
884
885 let mut builder = Debugger::builder()
887 .traces(traces)
888 .sources(sources)
889 .breakpoints(test_result.breakpoints);
890
891 if let Some(decoder) = &outcome.last_run_decoder {
892 builder = builder.decoder(decoder);
893 }
894
895 let mut debugger = builder.build();
896 if let Some(dump_path) = &self.dump {
897 debugger.dump_to_file(dump_path)?;
898 } else {
899 debugger.try_run_tui()?;
900 }
901 }
902
903 if let Some(mutate) = &self.mutate {
905 if outcome.failed() > 0 {
907 eyre::bail!("Cannot run mutation testing with failed tests");
908 }
909
910 if outcome.successes().next().is_none() {
916 eyre::bail!(
917 "Mutation testing requires at least one passing baseline test; the current \
918 filter/path selection matched zero non-skipped tests. Loosen `--match-test` / \
919 `--match-contract` / `--match-path` or check the project layout."
920 );
921 }
922
923 if !mutate.is_empty() && self.mutate_path.is_some() {
927 eyre::bail!(
928 "`--mutate-path <PATTERN>` cannot be combined with explicit paths passed to `--mutate`; pass either paths or a glob pattern, not both"
929 );
930 }
931
932 if !override_networks.is_empty() {
939 eyre::bail!(
940 "Mutation testing does not yet support inline per-test network overrides \
941 (found {} annotated network(s)). Re-run without `--mutate` or remove the \
942 per-test network annotations.",
943 override_networks.len()
944 );
945 }
946
947 use foundry_config::fs_permissions::FsAccessPermission;
955 if config_for_mutation.ffi {
956 eyre::bail!(
957 "Mutation testing is unsafe with `ffi = true`: per-mutant workspaces share \
958 symlinked dependency directories, and arbitrary FFI commands run by tests \
959 can race or corrupt the real `lib`/`node_modules`/`dependencies` trees. \
960 Disable ffi in your foundry.toml to run mutation tests."
961 );
962 }
963
964 let root = &config_for_mutation.root;
970 let canonicalize_through_existing_ancestor = |path: &Path| -> PathBuf {
971 let resolved =
972 if path.is_absolute() { path.to_path_buf() } else { root.join(path) };
973 if let Ok(canon) = dunce::canonicalize(&resolved) {
974 return canon;
975 }
976
977 let mut missing = Vec::new();
978 let mut ancestor = resolved.as_path();
979 while !ancestor.exists() {
980 let Some(name) = ancestor.file_name() else { break };
981 missing.push(name.to_owned());
982 let Some(parent) = ancestor.parent() else { break };
983 ancestor = parent;
984 }
985
986 let mut canon = dunce::canonicalize(ancestor).unwrap_or_else(|_| ancestor.into());
987 for component in missing.iter().rev() {
988 canon.push(component);
989 }
990 canon
991 };
992
993 let mut shared_dep_dirs: Vec<PathBuf> = config_for_mutation
994 .libs
995 .iter()
996 .filter(|p| p.exists())
997 .map(|p| canonicalize_through_existing_ancestor(p))
998 .collect();
999 for dep_dir in ["node_modules", "dependencies"] {
1000 let dep_path = root.join(dep_dir);
1001 if dep_path.exists() && dep_path.is_dir() {
1002 shared_dep_dirs.push(canonicalize_through_existing_ancestor(&dep_path));
1003 }
1004 }
1005
1006 let effective_permission = |path: &Path| -> Option<FsAccessPermission> {
1007 let mut max_path_len = 0;
1008 let mut highest_permission = FsAccessPermission::None;
1009
1010 for perm in &config_for_mutation.fs_permissions.permissions {
1011 let permission_path = canonicalize_through_existing_ancestor(&perm.path);
1012 if path.starts_with(&permission_path) {
1013 let path_len = permission_path.components().count();
1014 if path_len > max_path_len {
1015 max_path_len = path_len;
1016 highest_permission = perm.access;
1017 } else if path_len == max_path_len {
1018 highest_permission = match (highest_permission, perm.access) {
1019 (FsAccessPermission::ReadWrite, _)
1020 | (FsAccessPermission::Read, FsAccessPermission::Write)
1021 | (FsAccessPermission::Write, FsAccessPermission::Read) => {
1022 FsAccessPermission::ReadWrite
1023 }
1024 (FsAccessPermission::None, perm) => perm,
1025 (existing_perm, _) => existing_perm,
1026 };
1027 }
1028 }
1029 }
1030
1031 (max_path_len > 0).then_some(highest_permission)
1032 };
1033
1034 let grants_write = |path: &Path| {
1035 matches!(
1036 effective_permission(path),
1037 Some(FsAccessPermission::Write | FsAccessPermission::ReadWrite)
1038 )
1039 };
1040
1041 let unsafe_write_paths: Vec<&Path> = config_for_mutation
1042 .fs_permissions
1043 .permissions
1044 .iter()
1045 .filter(|perm| {
1046 matches!(perm.access, FsAccessPermission::Write | FsAccessPermission::ReadWrite)
1047 })
1048 .filter(|perm| {
1049 let perm_path = canonicalize_through_existing_ancestor(&perm.path);
1050 shared_dep_dirs.iter().any(|dep| {
1051 if perm_path.starts_with(dep) {
1052 grants_write(&perm_path)
1053 } else if dep.starts_with(&perm_path) {
1054 grants_write(dep)
1055 } else {
1056 false
1057 }
1058 })
1059 })
1060 .map(|p| p.path.as_path())
1061 .collect();
1062
1063 if !unsafe_write_paths.is_empty() {
1064 let paths = unsafe_write_paths
1065 .iter()
1066 .map(|p| format!(" - {}", p.display()))
1067 .collect::<Vec<_>>()
1068 .join("\n");
1069 eyre::bail!(
1070 "Mutation testing is unsafe with write-capable `fs_permissions` that can \
1071 reach the symlinked dependency trees (`lib`/`node_modules`/`dependencies`); \
1072 per-mutant workspaces share those trees, so `vm.writeFile` calls would race \
1073 against or corrupt your real dependencies. Restrict the following \
1074 `fs_permissions` entries to read-only or scope them away from dependency \
1075 paths:\n{paths}"
1076 );
1077 }
1078
1079 let json_output = shell::is_json();
1080 let selected_sources_relative = self
1081 .get_sources_to_compile(&config_for_mutation, filter)?
1082 .into_iter()
1083 .filter_map(|path| {
1084 path.strip_prefix(&config_for_mutation.root).ok().map(PathBuf::from)
1085 })
1086 .collect::<Vec<_>>();
1087
1088 let mutation_config = MutationRunConfig {
1089 mutate_paths: mutate.clone(),
1090 mutate_path_pattern: self.mutate_path.clone(),
1091 mutate_contract_pattern: self.mutate_contract.clone(),
1092 num_workers: self.mutation_jobs.unwrap_or(0),
1093 show_progress: self.show_progress,
1094 json_output,
1095 filter_args: filter.args().clone(),
1106 selected_sources_relative,
1107 isolate: evm_opts_for_mutation.isolate,
1108 };
1109
1110 let result = run_mutation_testing(
1111 Arc::new(config_for_mutation.clone()),
1112 output,
1113 evm_opts_for_mutation.clone(),
1114 mutation_config,
1115 )
1116 .await?;
1117
1118 if result.cancelled {
1119 std::process::exit(130);
1120 }
1121
1122 if json_output {
1124 let json_output = result.summary.to_json_output(result.duration_secs);
1125 sh_println!("{}", serde_json::to_string(&json_output)?)?;
1126 }
1127
1128 outcome = TestOutcome::empty(None, true);
1129 }
1130
1131 Ok(outcome)
1132 }
1133
1134 #[allow(clippy::too_many_arguments)]
1136 async fn build_and_run_tests<FEN: FoundryEvmNetwork>(
1137 &self,
1138 config: Config,
1139 evm_opts: EvmOpts,
1140 output: &ProjectCompileOutput,
1141 filter: &ProjectPathsAwareFilter,
1142 coverage: bool,
1143 should_debug: bool,
1144 decode_internal: InternalTraceMode,
1145 multi_network: MultiNetworkConfig,
1146 ) -> eyre::Result<(Libraries, TestOutcome)> {
1147 let verbosity = evm_opts.verbosity;
1148 let (evm_env, tx_env, fork_block) =
1149 evm_opts.env::<SpecFor<FEN>, BlockEnvFor<FEN>, TxEnvFor<FEN>>().await?;
1150
1151 let config = Arc::new(config);
1152 let showmap = self.showmap_config();
1153 let runner = MultiContractRunnerBuilder::new(config.clone())
1154 .set_debug(should_debug)
1155 .set_decode_internal(decode_internal)
1156 .initial_balance(evm_opts.initial_balance)
1157 .sender(evm_opts.sender)
1158 .with_fork(evm_opts.get_fork(&config, evm_env.cfg_env.chain_id, fork_block))
1159 .enable_isolation(evm_opts.isolate)
1160 .fail_fast(self.fail_fast)
1161 .set_coverage(coverage)
1162 .with_multi_network(multi_network)
1163 .with_showmap(showmap)
1164 .build::<FEN, MultiCompiler>(output, evm_env, tx_env, evm_opts)?;
1165
1166 let libraries = runner.libraries.clone();
1167 let outcome = self.run_tests_inner(runner, config, verbosity, filter, output).await?;
1168 Ok((libraries, outcome))
1169 }
1170
1171 #[allow(clippy::too_many_arguments)]
1173 async fn dispatch_network(
1174 &self,
1175 dispatch_opts: &EvmOpts,
1176 config: Config,
1177 evm_opts: EvmOpts,
1178 output: &ProjectCompileOutput,
1179 filter: &ProjectPathsAwareFilter,
1180 coverage: bool,
1181 should_debug: bool,
1182 decode_internal: InternalTraceMode,
1183 multi_network: MultiNetworkConfig,
1184 ) -> eyre::Result<(Libraries, TestOutcome)> {
1185 if dispatch_opts.networks.is_tempo() {
1186 self.build_and_run_tests::<TempoEvmNetwork>(
1187 config,
1188 evm_opts,
1189 output,
1190 filter,
1191 coverage,
1192 should_debug,
1193 decode_internal,
1194 multi_network,
1195 )
1196 .await
1197 } else {
1198 #[cfg(feature = "optimism")]
1199 if dispatch_opts.networks.is_optimism() {
1200 return self
1201 .build_and_run_tests::<OpEvmNetwork>(
1202 config,
1203 evm_opts,
1204 output,
1205 filter,
1206 coverage,
1207 should_debug,
1208 decode_internal,
1209 multi_network,
1210 )
1211 .await;
1212 }
1213 self.build_and_run_tests::<EthEvmNetwork>(
1214 config,
1215 evm_opts,
1216 output,
1217 filter,
1218 coverage,
1219 should_debug,
1220 decode_internal,
1221 multi_network,
1222 )
1223 .await
1224 }
1225 }
1226
1227 async fn run_tests_inner<FEN: FoundryEvmNetwork>(
1229 &self,
1230 mut runner: MultiContractRunner<FEN>,
1231 config: Arc<Config>,
1232 verbosity: u8,
1233 filter: &ProjectPathsAwareFilter,
1234 output: &ProjectCompileOutput,
1235 ) -> eyre::Result<TestOutcome> {
1236 let fuzz_seed = config.fuzz.seed;
1237 if self.list {
1238 return list(runner, filter);
1239 }
1240
1241 trace!(target: "forge::test", "running all tests");
1242
1243 let machine_mode = foundry_cli::is_machine();
1244
1245 let silent = machine_mode
1248 || self.gas_report && shell::is_json()
1249 || self.summary && shell::is_json()
1250 || self.mutate.is_some() && shell::is_json();
1251
1252 let num_filtered = runner.matching_test_functions(filter).count();
1253
1254 if num_filtered == 0 {
1255 let total_tests = if filter.is_empty() {
1256 num_filtered
1257 } else {
1258 runner.matching_test_functions(&EmptyTestFilter::default()).count()
1259 };
1260 if !machine_mode {
1261 if total_tests == 0 {
1262 sh_println!(
1263 "No tests found in project! Forge looks for functions that start with `test`"
1264 )?;
1265 } else {
1266 let mut msg = format!("no tests match the provided pattern:\n{filter}");
1267 if let Some(test_pattern) = &filter.args().test_pattern {
1269 let test_name = test_pattern.as_str();
1270 let candidates = runner.all_test_functions(filter).map(|f| &f.name);
1272 if let Some(suggestion) = utils::did_you_mean(test_name, candidates).pop() {
1273 write!(msg, "\nDid you mean `{suggestion}`?")?;
1274 }
1275 }
1276 sh_warn!("{msg}")?;
1277 }
1278 }
1279 return Ok(TestOutcome::empty(Some(runner.known_contracts.clone()), false));
1280 }
1281
1282 if num_filtered != 1 && (self.debug || self.flamegraph || self.flamechart) {
1283 let action = if self.flamegraph {
1284 "generate a flamegraph"
1285 } else if self.flamechart {
1286 "generate a flamechart"
1287 } else {
1288 "run the debugger"
1289 };
1290 let filter = if filter.is_empty() {
1291 String::new()
1292 } else {
1293 format!("\n\nFilter used:\n{filter}")
1294 };
1295 eyre::bail!(
1296 "{num_filtered} tests matched your criteria, but exactly 1 test must match in order to {action}.\n\n\
1297 Use --match-contract and --match-path to further limit the search.{filter}",
1298 );
1299 }
1300
1301 if num_filtered == 1 && self.decode_internal {
1303 runner.decode_internal = InternalTraceMode::Full;
1304 }
1305
1306 if self.mutate.is_none()
1309 && !machine_mode
1310 && !self.gas_report
1311 && !self.summary
1312 && shell::is_json()
1313 {
1314 let mut results = runner.test_collect(filter)?;
1315 for suite_result in results.values_mut() {
1316 for test_result in suite_result.test_results.values_mut() {
1317 if verbosity >= 2 {
1318 test_result.decoded_logs = decode_console_logs(&test_result.logs);
1320 } else {
1321 test_result.logs = vec![];
1323 }
1324 }
1325 }
1326 sh_println!("{}", serde_json::to_string(&results)?)?;
1327 let kc = runner.known_contracts.clone();
1328 return Ok(TestOutcome::new(Some(kc), results, self.allow_failure, fuzz_seed));
1329 }
1330
1331 if self.junit {
1332 let results = runner.test_collect(filter)?;
1333 sh_println!("{}", junit_xml_report(&results, verbosity).to_string()?)?;
1334 let kc = runner.known_contracts.clone();
1335 return Ok(TestOutcome::new(Some(kc), results, self.allow_failure, fuzz_seed));
1336 }
1337
1338 let remote_chain =
1339 if runner.fork.is_some() { runner.tx_env.chain_id().map(Into::into) } else { None };
1340 let known_contracts = runner.known_contracts.clone();
1341
1342 let libraries = runner.libraries.clone();
1343
1344 let is_multi_pass = !runner.tcfg.multi_network.all_override_networks.is_empty();
1348 let is_tempo_network = runner.tcfg.evm_opts.networks.is_tempo();
1349
1350 let (tx, rx) = channel::<(String, SuiteResult)>();
1352 let timer = Instant::now();
1353 let show_progress = config.show_progress;
1354 let handle = tokio::task::spawn_blocking({
1355 let filter = filter.clone();
1356 move || runner.test(&filter, tx, show_progress).map(|()| runner)
1357 });
1358
1359 let mut identifier = TraceIdentifiers::new().with_local(&known_contracts);
1361
1362 if !self.gas_report && remote_chain.is_some() {
1366 identifier = identifier.with_external(&config, remote_chain)?;
1367 }
1368
1369 let mut builder = CallTraceDecoderBuilder::new()
1371 .with_known_contracts(&known_contracts)
1372 .with_label_disabled(self.disable_labels)
1373 .with_verbosity(verbosity)
1374 .with_chain_id(remote_chain.map(|c| c.id()))
1375 .with_tempo_hardfork(
1376 (is_tempo_network || remote_chain.is_some_and(|chain| chain.is_tempo()))
1377 .then(|| config.evm_spec_id::<TempoHardfork>()),
1378 );
1379 if !self.gas_report {
1381 builder =
1382 builder.with_signature_identifier(SignaturesIdentifier::from_config(&config)?);
1383 }
1384
1385 if self.decode_internal {
1386 let sources =
1387 ContractSources::from_project_output(output, &config.root, Some(&libraries))?;
1388 builder = builder.with_debug_identifier(DebugTraceIdentifier::new(sources));
1389 }
1390 let mut decoder = builder.build();
1391
1392 let mut gas_report = self.gas_report.then(|| {
1393 GasReport::new(
1394 config.gas_reports.clone(),
1395 config.gas_reports_ignore.clone(),
1396 config.gas_reports_include_tests,
1397 )
1398 });
1399
1400 let mut gas_snapshots = BTreeMap::<String, BTreeMap<String, String>>::new();
1401
1402 let mut outcome = TestOutcome::empty(None, self.allow_failure);
1403 outcome.fuzz_seed = fuzz_seed;
1404
1405 let mut any_test_failed = false;
1406 let mut backtrace_builder = None;
1407 for (contract_name, mut suite_result) in rx {
1408 let len = suite_result.len();
1409 let tests = &mut suite_result.test_results;
1410 let has_tests = !tests.is_empty();
1411
1412 if is_multi_pass && !has_tests && suite_result.warnings.is_empty() {
1416 continue;
1417 }
1418
1419 decoder.clear_addresses();
1421
1422 let identify_addresses = verbosity >= 3
1424 || self.gas_report
1425 || self.debug
1426 || self.flamegraph
1427 || self.flamechart;
1428
1429 if !silent {
1431 sh_println!()?;
1432 for warning in &suite_result.warnings {
1433 sh_warn!("{warning}")?;
1434 }
1435 if has_tests {
1436 let tests = if len > 1 { "tests" } else { "test" };
1437 sh_println!("Ran {len} {tests} for {contract_name}")?;
1438 }
1439 }
1440
1441 for (name, result) in tests {
1443 let show_traces =
1444 !self.suppress_successful_traces || result.status == TestStatus::Failure;
1445 if !silent {
1446 sh_println!("{}", result.short_result_with_suite(name, &contract_name))?;
1447
1448 if let TestKind::Invariant { metrics, .. } = &result.kind
1450 && !metrics.is_empty()
1451 {
1452 let _ = sh_println!("\n{}\n", format_invariant_metrics_table(metrics));
1453 }
1454
1455 if verbosity >= 2 && show_traces {
1457 let console_logs = decode_console_logs(&result.logs);
1459 if !console_logs.is_empty() {
1460 sh_println!("Logs:")?;
1461 for log in console_logs {
1462 sh_println!(" {log}")?;
1463 }
1464 sh_println!()?;
1465 }
1466 }
1467 }
1468
1469 if machine_mode {
1470 emit_test_result_event(&contract_name, name, result)?;
1471 }
1472
1473 any_test_failed |= result.status == TestStatus::Failure;
1476
1477 decoder.clear_addresses();
1479 decoder.labels.extend(result.labels.iter().map(|(k, v)| (*k, v.clone())));
1480
1481 let mut decoded_traces = Vec::with_capacity(result.traces.len());
1483 for (kind, arena) in &mut result.traces {
1484 if identify_addresses {
1485 if self.debug && !result.debug_bytecodes.is_empty() {
1486 let mut local_identifier = TraceIdentifiers::new()
1487 .with_local_and_bytecodes(
1488 &known_contracts,
1489 &result.debug_bytecodes,
1490 );
1491 decoder.identify(arena, &mut local_identifier);
1492 }
1493 decoder.identify(arena, &mut identifier);
1494 }
1495
1496 let should_include = match kind {
1502 TraceKind::Execution => {
1503 (verbosity == 3 && result.status.is_failure()) || verbosity >= 4
1504 }
1505 TraceKind::Setup => {
1506 (verbosity == 4 && result.status.is_failure()) || verbosity >= 5
1507 }
1508 TraceKind::Deployment => false,
1509 };
1510
1511 if should_include {
1512 decode_trace_arena(arena, &decoder).await;
1513
1514 if let Some(trace_depth) = self.trace_depth {
1515 prune_trace_depth(arena, trace_depth);
1516 }
1517
1518 decoded_traces.push(render_trace_arena_inner(arena, false, verbosity > 4));
1519 }
1520 }
1521
1522 if !silent && show_traces && !decoded_traces.is_empty() {
1523 sh_println!("Traces:")?;
1524 for trace in &decoded_traces {
1525 sh_println!("{trace}")?;
1526 }
1527 }
1528
1529 if !silent
1533 && result.status.is_failure()
1534 && verbosity >= 3
1535 && !result.traces.is_empty()
1536 && let Some((_, arena)) =
1537 result.traces.iter().find(|(kind, _)| matches!(kind, TraceKind::Execution))
1538 {
1539 let builder = backtrace_builder.get_or_insert_with(|| {
1541 BacktraceBuilder::new(
1542 output,
1543 config.root.clone(),
1544 config.parsed_libraries().ok(),
1545 config.via_ir,
1546 )
1547 });
1548
1549 let backtrace = builder.from_traces(arena);
1550
1551 if !backtrace.is_empty() {
1552 sh_println!("{}", backtrace)?;
1553 }
1554 }
1555
1556 if let Some(gas_report) = &mut gas_report {
1557 gas_report.analyze(result.traces.iter().map(|(_, a)| &a.arena), &decoder).await;
1558
1559 for trace in &result.gas_report_traces {
1560 decoder.clear_addresses();
1561
1562 for (kind, arena) in &result.traces {
1565 if !matches!(kind, TraceKind::Execution) {
1566 decoder.identify(arena, &mut identifier);
1567 }
1568 }
1569
1570 for arena in trace {
1571 decoder.identify(arena, &mut identifier);
1572 gas_report.analyze([arena], &decoder).await;
1573 }
1574 }
1575 }
1576 result.gas_report_traces = Default::default();
1578
1579 for (group, new_snapshots) in &result.gas_snapshots {
1581 gas_snapshots.entry(group.clone()).or_default().extend(new_snapshots.clone());
1582 }
1583 }
1584
1585 if !gas_snapshots.is_empty() {
1587 if self.gas_snapshot_check.unwrap_or(config.gas_snapshot_check) {
1599 let differences_found =
1600 gas_snapshots.iter().fold(false, |mut found, (group, snapshots)| {
1601 if !&config.snapshots.join(format!("{group}.json")).exists() {
1603 return found;
1604 }
1605
1606 let previous_snapshots: BTreeMap<String, String> =
1607 fs::read_json_file(&config.snapshots.join(format!("{group}.json")))
1608 .expect("Failed to read snapshots from disk");
1609
1610 let diff: BTreeMap<_, _> = snapshots
1611 .iter()
1612 .filter_map(|(k, v)| {
1613 previous_snapshots.get(k).and_then(|previous_snapshot| {
1614 (previous_snapshot != v).then(|| {
1615 (k.clone(), (previous_snapshot.clone(), v.clone()))
1616 })
1617 })
1618 })
1619 .collect();
1620
1621 if !diff.is_empty() {
1622 let _ = sh_eprintln!(
1623 "{}",
1624 format!("\n[{group}] Failed to match snapshots:").red().bold()
1625 );
1626
1627 for (key, (previous_snapshot, snapshot)) in &diff {
1628 let _ = sh_eprintln!(
1629 "{}",
1630 format!("- [{key}] {previous_snapshot} → {snapshot}").red()
1631 );
1632 }
1633
1634 found = true;
1635 }
1636
1637 found
1638 });
1639
1640 if differences_found {
1641 sh_eprintln!()?;
1642 eyre::bail!("Snapshots differ from previous run");
1643 }
1644 }
1645
1646 if self.gas_snapshot_emit.unwrap_or(config.gas_snapshot_emit) {
1656 fs::create_dir_all(&config.snapshots)?;
1658
1659 for (group, snapshots) in &gas_snapshots {
1661 fs::write_pretty_json_file(
1662 &config.snapshots.join(format!("{group}.json")),
1663 &snapshots,
1664 )
1665 .expect("Failed to write gas snapshots to disk");
1666 }
1667 }
1668 }
1669
1670 if !silent && has_tests {
1672 sh_println!("{}", suite_result.summary())?;
1673 }
1674
1675 if machine_mode {
1676 for warning in &suite_result.warnings {
1677 emit_warning_event(&contract_name, warning)?;
1678 }
1679 if has_tests || !suite_result.warnings.is_empty() {
1682 emit_suite_finished_event(&contract_name, &suite_result)?;
1683 }
1684 }
1685
1686 outcome.results.insert(contract_name, suite_result);
1688
1689 if self.fail_fast && any_test_failed {
1691 break;
1692 }
1693 }
1694 outcome.last_run_decoder = Some(decoder);
1695 let duration = timer.elapsed();
1696
1697 trace!(target: "forge::test", len=outcome.results.len(), %any_test_failed, "done with results");
1698
1699 if let Some(gas_report) = gas_report {
1700 let finalized = gas_report.finalize();
1701 sh_println!("{finalized}")?;
1702 outcome.gas_report = Some(finalized);
1703 }
1704
1705 if !is_multi_pass && !self.summary && !shell::is_json() && !machine_mode {
1706 sh_println!("{}", outcome.summary(duration))?;
1707 }
1708
1709 if !is_multi_pass && self.summary && !outcome.results.is_empty() && !machine_mode {
1710 let summary_report = TestSummaryReport::new(self.detailed, outcome.clone());
1711 sh_println!("{summary_report}")?;
1712 }
1713
1714 match handle.await {
1716 Ok(result) => {
1717 let runner = result?;
1718 outcome.known_contracts = Some(runner.known_contracts);
1719 }
1720 Err(e) => match e.try_into_panic() {
1721 Ok(payload) => std::panic::resume_unwind(payload),
1722 Err(e) => return Err(e.into()),
1723 },
1724 }
1725
1726 persist_run_failures(&config, &outcome);
1728
1729 Ok(outcome)
1730 }
1731
1732 pub fn filter(&self, config: &Config) -> Result<ProjectPathsAwareFilter> {
1735 let mut filter = self.filter.clone();
1736 let rerun_failures = if self.rerun {
1737 let failures = last_run_failures(config);
1738 filter.test_pattern = failures.test_pattern;
1739 failures.failures
1740 } else {
1741 None
1742 };
1743 if filter.path_pattern.is_some() {
1744 if self.path.is_some() {
1745 bail!("Can not supply both --match-path and |path|");
1746 }
1747 } else {
1748 filter.path_pattern = self.path.clone();
1749 }
1750 let mut filter = filter.merge_with_config(config);
1751 if let Some(failures) = rerun_failures {
1752 filter.set_rerun_failures(failures);
1753 }
1754 Ok(filter)
1755 }
1756
1757 pub const fn is_watch(&self) -> bool {
1759 self.watch.watch.is_some()
1760 }
1761
1762 pub(crate) fn watchexec_config(&self) -> Result<watchexec::Config> {
1764 self.watch.watchexec_config(|| {
1765 let config = self.load_config()?;
1766 Ok([config.src, config.test])
1767 })
1768 }
1769}
1770
1771#[derive(Clone, Debug, serde::Serialize)]
1774pub struct TestSummaryData {
1775 pub suites: usize,
1776 pub passed: usize,
1777 pub failed: usize,
1778 pub skipped: usize,
1779 pub duration_ms: u128,
1780}
1781
1782impl TestSummaryData {
1783 pub fn from_outcome(outcome: &TestOutcome, wall_clock: Duration) -> Self {
1784 Self {
1785 suites: outcome.results.len(),
1786 passed: outcome.passed(),
1787 failed: outcome.failed(),
1788 skipped: outcome.skipped(),
1789 duration_ms: wall_clock.as_millis(),
1790 }
1791 }
1792}
1793
1794#[derive(serde::Serialize)]
1795struct TestResultEvent<'a> {
1796 suite: &'a str,
1797 name: &'a str,
1798 status: &'static str,
1799 #[serde(skip_serializing_if = "Option::is_none")]
1800 reason: Option<&'a str>,
1801 duration_ms: u128,
1802}
1803
1804#[derive(serde::Serialize)]
1805struct SuiteFinishedEvent<'a> {
1806 suite: &'a str,
1807 passed: usize,
1808 failed: usize,
1809 skipped: usize,
1810 duration_ms: u128,
1811}
1812
1813#[derive(serde::Serialize)]
1814struct WarningEvent<'a> {
1815 suite: &'a str,
1816 code: &'static str,
1817 message: &'a str,
1818}
1819
1820const fn status_str(status: TestStatus) -> &'static str {
1821 match status {
1822 TestStatus::Success => "passed",
1823 TestStatus::Failure => "failed",
1824 TestStatus::Skipped => "skipped",
1825 }
1826}
1827
1828fn emit_test_result_event(
1829 suite: &str,
1830 name: &str,
1831 result: &crate::result::TestResult,
1832) -> Result<()> {
1833 foundry_cli::json::print_stream_record(
1834 crate::introspect::TEST_EVENT_SCHEMA,
1835 "forge.test",
1836 "test_result",
1837 TestResultEvent {
1838 suite,
1839 name,
1840 status: status_str(result.status),
1841 reason: result.reason.as_deref(),
1842 duration_ms: result.duration.as_millis(),
1843 },
1844 )?;
1845 Ok(())
1846}
1847
1848fn emit_suite_finished_event(suite: &str, result: &SuiteResult) -> Result<()> {
1849 foundry_cli::json::print_stream_record(
1850 crate::introspect::TEST_EVENT_SCHEMA,
1851 "forge.test",
1852 "suite_finished",
1853 SuiteFinishedEvent {
1854 suite,
1855 passed: result.passed(),
1856 failed: result.failed(),
1857 skipped: result.skipped(),
1858 duration_ms: result.duration.as_millis(),
1859 },
1860 )?;
1861 Ok(())
1862}
1863
1864fn emit_warning_event(suite: &str, message: &str) -> Result<()> {
1865 foundry_cli::json::print_stream_record(
1866 crate::introspect::TEST_EVENT_SCHEMA,
1867 "forge.test",
1868 "warning",
1869 WarningEvent { suite, code: foundry_cli::diagnostic::test::WARNING, message },
1870 )?;
1871 Ok(())
1872}
1873
1874fn emit_machine_compile_error(output: &ProjectCompileOutput) -> ! {
1877 let errors: Vec<JsonMessage> = output
1878 .output()
1879 .errors
1880 .iter()
1881 .filter(|e| e.is_error())
1882 .map(|e| JsonMessage::error(SOLC_ERROR, e.to_string()))
1883 .collect();
1884 let _ = print_json(&JsonEnvelope::<()>::failure(errors));
1886 std::process::exit(ExitCode::Build.to_i32());
1887}
1888
1889impl Provider for TestArgs {
1890 fn metadata(&self) -> Metadata {
1891 Metadata::named("Core Build Args Provider")
1892 }
1893
1894 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
1895 let mut dict = Dict::default();
1896
1897 let mut fuzz_dict = Dict::default();
1898 if let Some(fuzz_seed) = self.fuzz_seed {
1899 fuzz_dict.insert("seed".to_string(), fuzz_seed.to_string().into());
1900 }
1901 if let Some(fuzz_runs) = self.fuzz_runs {
1902 fuzz_dict.insert("runs".to_string(), fuzz_runs.into());
1903 }
1904 if let Some(fuzz_run) = self.fuzz_run {
1905 fuzz_dict.insert("run".to_string(), fuzz_run.into());
1906 }
1907 if let Some(fuzz_worker) = self.fuzz_worker {
1908 fuzz_dict.insert("worker".to_string(), fuzz_worker.into());
1909 }
1910 if let Some(fuzz_timeout) = self.fuzz_timeout {
1911 fuzz_dict.insert("timeout".to_string(), fuzz_timeout.into());
1912 }
1913 if let Some(fuzz_input_file) = self.fuzz_input_file.clone() {
1914 fuzz_dict.insert("failure_persist_file".to_string(), fuzz_input_file.into());
1915 }
1916 dict.insert("fuzz".to_string(), fuzz_dict.into());
1917
1918 if let Some(invariant_workers) = self.invariant_workers {
1919 dict.insert(
1920 "invariant".to_string(),
1921 Dict::from([("workers".to_string(), Value::serialize(invariant_workers)?)]).into(),
1922 );
1923 }
1924
1925 let mut symbolic_dict = Dict::default();
1926 if self.symbolic {
1927 symbolic_dict.insert("enabled".to_string(), true.into());
1928 }
1929 if let Some(solver) = self.symbolic_solver.clone() {
1930 symbolic_dict.insert("solver".to_string(), solver.into());
1931 }
1932 if let Some(solver_command) = self.symbolic_solver_command.clone() {
1933 symbolic_dict.insert("solver_command".to_string(), solver_command.into());
1934 }
1935 if let Some(solver_portfolio) = self.symbolic_solver_portfolio.clone() {
1936 symbolic_dict.insert("solver_portfolio".to_string(), solver_portfolio.into());
1937 }
1938 if let Some(timeout) = self.symbolic_timeout {
1939 symbolic_dict.insert("timeout".to_string(), timeout.into());
1940 }
1941 if let Some(loop_bound) = self.symbolic_loop {
1942 symbolic_dict.insert("loop".to_string(), loop_bound.into());
1943 }
1944 if let Some(depth) = self.symbolic_depth {
1945 symbolic_dict.insert("depth".to_string(), depth.into());
1946 }
1947 if let Some(width) = self.symbolic_width {
1948 symbolic_dict.insert("width".to_string(), width.into());
1949 }
1950 if let Some(max_depth) = self.symbolic_max_depth {
1951 symbolic_dict.insert("max_depth".to_string(), max_depth.into());
1952 }
1953 if let Some(max_paths) = self.symbolic_max_paths {
1954 symbolic_dict.insert("max_paths".to_string(), max_paths.into());
1955 }
1956 if let Some(invariant_depth) = self.symbolic_invariant_depth {
1957 symbolic_dict.insert("invariant_depth".to_string(), invariant_depth.into());
1958 }
1959 if let Some(max_solver_queries) = self.symbolic_max_solver_queries {
1960 symbolic_dict.insert("max_solver_queries".to_string(), max_solver_queries.into());
1961 }
1962 if let Some(default_dynamic_length) = self.symbolic_default_dynamic_length {
1963 symbolic_dict
1964 .insert("default_dynamic_length".to_string(), default_dynamic_length.into());
1965 }
1966 if let Some(max_dynamic_length) = self.symbolic_max_dynamic_length {
1967 symbolic_dict.insert("max_dynamic_length".to_string(), max_dynamic_length.into());
1968 }
1969 if let Some(array_lengths) = self.symbolic_array_lengths.clone() {
1970 symbolic_dict.insert("array_lengths".to_string(), array_lengths.into());
1971 }
1972 if let Some(max_calldata_bytes) = self.symbolic_max_calldata_bytes {
1973 symbolic_dict.insert("max_calldata_bytes".to_string(), max_calldata_bytes.into());
1974 }
1975 if self.symbolic_call_targets {
1976 symbolic_dict.insert("symbolic_call_targets".to_string(), true.into());
1977 }
1978 if self.symbolic_dump_smt {
1979 symbolic_dict.insert("dump_smt".to_string(), true.into());
1980 }
1981 if let Some(storage_layout) = self.symbolic_storage_layout.clone() {
1982 symbolic_dict.insert("storage_layout".to_string(), storage_layout.into());
1983 }
1984 dict.insert("symbolic".to_string(), symbolic_dict.into());
1985
1986 if let Some(etherscan_api_key) =
1987 self.etherscan_api_key.as_ref().filter(|s| !s.trim().is_empty())
1988 {
1989 dict.insert("etherscan_api_key".to_string(), etherscan_api_key.clone().into());
1990 }
1991
1992 if self.show_progress {
1993 dict.insert("show_progress".to_string(), true.into());
1994 }
1995
1996 if let Some(timeout) = self.mutation_timeout {
1998 let mut mutation_dict = Dict::default();
1999 mutation_dict.insert("timeout".to_string(), timeout.into());
2000 dict.insert("mutation".to_string(), mutation_dict.into());
2001 }
2002
2003 Ok(Map::from([(Config::selected_profile(), dict)]))
2004 }
2005}
2006
2007fn list<FEN: FoundryEvmNetwork>(
2009 runner: MultiContractRunner<FEN>,
2010 filter: &ProjectPathsAwareFilter,
2011) -> Result<TestOutcome> {
2012 let results = runner.list(filter);
2013
2014 if shell::is_json() {
2015 sh_println!("{}", serde_json::to_string(&results)?)?;
2016 } else {
2017 for (file, contracts) in &results {
2018 sh_println!("{file}")?;
2019 for (contract, tests) in contracts {
2020 sh_println!(" {contract}")?;
2021 sh_println!(" {}\n", tests.join("\n "))?;
2022 }
2023 }
2024 }
2025 Ok(TestOutcome::empty(Some(runner.known_contracts), false))
2026}
2027
2028fn merge_outcomes(base: &mut TestOutcome, other: TestOutcome) {
2033 for (suite_id, other_suite) in other.results {
2034 match base.results.entry(suite_id) {
2035 std::collections::btree_map::Entry::Vacant(e) => {
2036 e.insert(other_suite);
2037 }
2038 std::collections::btree_map::Entry::Occupied(mut e) => {
2039 let base_suite = e.get_mut();
2040 base_suite.test_results.extend(other_suite.test_results);
2041 base_suite.warnings.extend(other_suite.warnings);
2042 base_suite.duration = base_suite.duration.max(other_suite.duration);
2043 }
2044 }
2045 }
2046 if let Some(decoder) = other.last_run_decoder {
2047 base.last_run_decoder = Some(decoder);
2048 }
2049}
2050
2051struct LastRunFailures {
2052 test_pattern: Option<regex::Regex>,
2053 failures: Option<Vec<RerunFailure>>,
2054}
2055
2056fn last_run_failures(config: &Config) -> LastRunFailures {
2058 let Ok(filter) = fs::read_to_string(&config.test_failures_file) else {
2059 return LastRunFailures { test_pattern: None, failures: None };
2060 };
2061
2062 if let Ok(failures) = serde_json::from_str::<RerunFailures>(&filter) {
2063 if failures.failures.is_empty() {
2064 return LastRunFailures { test_pattern: None, failures: None };
2065 }
2066 let test_pattern = failures
2067 .failures
2068 .iter()
2069 .map(|failure| regex::escape(&failure.test))
2070 .collect::<Vec<_>>()
2071 .join("|");
2072 let test_pattern = Regex::new(&test_pattern).ok();
2073 return LastRunFailures { test_pattern, failures: Some(failures.failures) };
2074 }
2075
2076 let test_pattern = Regex::new(&filter)
2077 .inspect_err(|e| {
2078 _ = sh_warn!("failed to parse test filter from {:?}: {e}", config.test_failures_file)
2079 })
2080 .ok();
2081 LastRunFailures { test_pattern, failures: None }
2082}
2083
2084fn persist_run_failures(config: &Config, outcome: &TestOutcome) {
2086 if outcome.failed() > 0 && fs::create_file(&config.test_failures_file).is_ok() {
2087 let failures = outcome
2088 .results
2089 .iter()
2090 .flat_map(|(contract, suite)| {
2091 suite.test_results.iter().filter(|(_, result)| result.status.is_failure()).flat_map(
2092 move |(test_name, test_result)| {
2093 rerun_filter_matches(test_name, test_result)
2094 .map(move |test| RerunFailure { contract: contract.clone(), test })
2095 },
2096 )
2097 })
2098 .collect::<Vec<_>>();
2099
2100 let output = serde_json::to_string(&RerunFailures { version: 1, failures });
2101 if let Ok(output) = output {
2102 let _ = fs::write(&config.test_failures_file, output);
2103 }
2104 }
2105}
2106
2107fn rerun_filter_matches<'a>(
2108 test_name: &'a str,
2109 test_result: &'a TestResult,
2110) -> impl Iterator<Item = String> + 'a {
2111 let has_predicate_failures =
2112 test_result.invariant_failures.iter().any(|failure| failure.predicate_name().is_some());
2113 let predicate_failures =
2114 test_result.invariant_failures.iter().filter_map(|failure| failure.predicate_name());
2115
2116 let fallback = test_name.is_any_test().then(|| test_name.split('(').next()).flatten();
2117
2118 predicate_failures
2119 .chain(fallback.into_iter().filter(move |_| !has_predicate_failures))
2120 .map(str::to_owned)
2121}
2122
2123fn junit_xml_report(results: &BTreeMap<String, SuiteResult>, verbosity: u8) -> Report {
2125 let mut total_duration = Duration::default();
2126 let mut junit_report = Report::new("Test run");
2127 junit_report.set_timestamp(Utc::now());
2128 for (suite_name, suite_result) in results {
2129 let mut test_suite = TestSuite::new(suite_name);
2130 total_duration += suite_result.duration;
2131 test_suite.set_time(suite_result.duration);
2132 test_suite.set_system_out(suite_result.summary());
2133 for (test_name, test_result) in &suite_result.test_results {
2134 add_junit_test_cases(&mut test_suite, test_name, test_result, verbosity);
2135 }
2136 junit_report.add_test_suite(test_suite);
2137 }
2138 junit_report.set_time(total_duration);
2139 junit_report
2140}
2141
2142fn add_junit_test_cases(
2147 test_suite: &mut TestSuite,
2148 test_name: &str,
2149 test_result: &TestResult,
2150 verbosity: u8,
2151) {
2152 let output = JunitOutput::new(test_result, verbosity);
2153 let expanded_invariant = test_result.kind.is_invariant()
2154 && (!test_result.invariant_predicate_results.is_empty()
2155 || !test_result.invariant_handler_failures.is_empty());
2156
2157 if !expanded_invariant {
2158 add_junit_test_case(
2159 test_suite,
2160 test_name,
2161 test_result.status,
2162 test_result.reason.as_deref(),
2163 test_result,
2164 output.system_out(test_result, test_name),
2165 );
2166 return;
2167 }
2168
2169 let mut add_expanded_case =
2170 |name: &str,
2171 status: TestStatus,
2172 reason: Option<&str>,
2173 counterexample: Option<&CounterExample>| {
2174 add_junit_test_case(
2175 test_suite,
2176 name,
2177 status,
2178 reason,
2179 test_result,
2180 output.case_system_out(status, reason, name, counterexample),
2181 );
2182 };
2183
2184 if test_result.invariant_predicate_results.is_empty() {
2185 let failure = test_result.invariant_failures.first();
2186 let status = if failure.is_some() { TestStatus::Failure } else { TestStatus::Success };
2187 add_expanded_case(
2188 test_name,
2189 status,
2190 failure.map(|failure| failure.reason()),
2191 failure.and_then(|failure| failure.counterexample()),
2192 );
2193 } else {
2194 for predicate in &test_result.invariant_predicate_results {
2195 let failure = test_result
2196 .invariant_failures
2197 .iter()
2198 .find(|failure| failure.name() == predicate.name.as_str());
2199 let name = format!("{}()", predicate.name);
2200 add_expanded_case(
2201 &name,
2202 predicate.status,
2203 predicate.reason.as_deref(),
2204 failure.and_then(|failure| failure.counterexample()),
2205 );
2206 }
2207 }
2208
2209 for failure in &test_result.invariant_handler_failures {
2210 let name = format!("handler {}", failure.name());
2211 add_expanded_case(
2212 &name,
2213 TestStatus::Failure,
2214 Some(failure.reason()),
2215 failure.counterexample(),
2216 );
2217 }
2218}
2219
2220fn add_junit_test_case(
2222 test_suite: &mut TestSuite,
2223 test_name: &str,
2224 status: TestStatus,
2225 message: Option<&str>,
2226 test_result: &TestResult,
2227 system_out: String,
2228) {
2229 let mut test_status = match status {
2230 TestStatus::Success => TestCaseStatus::success(),
2231 TestStatus::Failure => TestCaseStatus::non_success(NonSuccessKind::Failure),
2232 TestStatus::Skipped => TestCaseStatus::skipped(),
2233 };
2234 if let Some(message) = message {
2235 test_status.set_message(message);
2236 }
2237
2238 let mut test_case = TestCase::new(test_name, test_status);
2239 test_case.set_time(test_result.duration);
2240 test_case.set_system_out(system_out);
2241 test_suite.add_test_case(test_case);
2242}
2243
2244struct JunitOutput {
2246 result_report: TestKindReport,
2247 logs: Option<Vec<String>>,
2248}
2249
2250impl JunitOutput {
2251 fn new(test_result: &TestResult, verbosity: u8) -> Self {
2253 Self {
2254 result_report: test_result.kind.report(),
2255 logs: (verbosity >= 2 && !test_result.logs.is_empty())
2256 .then(|| decode_console_logs(&test_result.logs)),
2257 }
2258 }
2259
2260 fn system_out(&self, test_result: &TestResult, test_name: &str) -> String {
2262 let mut sys_out = String::new();
2263 write!(sys_out, "{test_result} {test_name} {}", self.result_report).unwrap();
2264 self.append_logs(&mut sys_out);
2265 sys_out
2266 }
2267
2268 fn case_system_out(
2270 &self,
2271 status: TestStatus,
2272 message: Option<&str>,
2273 test_name: &str,
2274 counterexample: Option<&CounterExample>,
2275 ) -> String {
2276 let mut sys_out = String::new();
2277 match status {
2278 TestStatus::Success => write!(sys_out, "[PASS]").unwrap(),
2279 TestStatus::Failure => {
2280 let message = message.unwrap_or_default();
2281 write!(sys_out, "[FAIL: {message}]").unwrap();
2282 }
2283 TestStatus::Skipped => {
2284 if let Some(message) = message {
2285 write!(sys_out, "[SKIP: {message}]").unwrap();
2286 } else {
2287 write!(sys_out, "[SKIP]").unwrap();
2288 }
2289 }
2290 }
2291 write!(sys_out, " {test_name} {}", self.result_report).unwrap();
2292 if let Some(CounterExample::Sequence(original, sequence)) = counterexample {
2293 writeln!(sys_out, "\n\t[Sequence] (original: {original}, shrunk: {})", sequence.len())
2294 .unwrap();
2295 for ex in sequence {
2296 writeln!(sys_out, "{ex}").unwrap();
2297 }
2298 }
2299 self.append_logs(&mut sys_out);
2300 sys_out
2301 }
2302
2303 fn append_logs(&self, sys_out: &mut String) {
2305 if let Some(logs) = &self.logs {
2306 write!(sys_out, "\\nLogs:\\n").unwrap();
2307 for log in logs {
2308 write!(sys_out, " {log}\\n").unwrap();
2309 }
2310 }
2311 }
2312}
2313
2314#[cfg(test)]
2315mod tests {
2316 use super::*;
2317 use foundry_config::Chain;
2318
2319 #[test]
2320 fn watch_parse() {
2321 let args: TestArgs = TestArgs::parse_from(["foundry-cli", "-vw"]);
2322 assert!(args.watch.watch.is_some());
2323 }
2324
2325 #[test]
2326 fn fuzz_seed() {
2327 let args: TestArgs = TestArgs::parse_from(["foundry-cli", "--fuzz-seed", "0x10"]);
2328 assert!(args.fuzz_seed.is_some());
2329 }
2330
2331 #[test]
2332 fn depth_trace() {
2333 let args: TestArgs = TestArgs::parse_from(["foundry-cli", "--trace-depth", "2"]);
2334 assert!(args.trace_depth.is_some());
2335 }
2336
2337 #[test]
2339 fn fuzz_seed_exists() {
2340 let args: TestArgs =
2341 TestArgs::parse_from(["foundry-cli", "-vvv", "--gas-report", "--fuzz-seed", "0x10"]);
2342 assert!(args.fuzz_seed.is_some());
2343 }
2344
2345 #[test]
2346 fn fuzz_run() {
2347 let args: TestArgs =
2348 TestArgs::parse_from(["foundry-cli", "--fuzz-run", "10", "--fuzz-worker", "2"]);
2349 assert_eq!(args.fuzz_run, Some(10));
2350 assert_eq!(args.fuzz_worker, Some(2));
2351 }
2352
2353 #[test]
2354 fn invariant_workers() {
2355 let args = TestArgs::parse_from(["foundry-cli", "--invariant-workers", "4"]);
2356 assert_eq!(
2357 args.invariant_workers,
2358 Some(InvariantWorkers::Fixed(std::num::NonZeroUsize::new(4).unwrap()))
2359 );
2360
2361 let figment = figment::Figment::from(&args);
2362 assert_eq!(
2363 figment.extract_inner::<InvariantWorkers>("invariant.workers").unwrap(),
2364 InvariantWorkers::Fixed(std::num::NonZeroUsize::new(4).unwrap())
2365 );
2366 }
2367
2368 #[test]
2369 fn invariant_workers_accepts_auto() {
2370 let args = TestArgs::parse_from(["foundry-cli", "--invariant-workers", "auto"]);
2371 assert_eq!(args.invariant_workers, Some(InvariantWorkers::Auto));
2372
2373 let figment = figment::Figment::from(&args);
2374 assert_eq!(
2375 figment.extract_inner::<InvariantWorkers>("invariant.workers").unwrap(),
2376 InvariantWorkers::Auto
2377 );
2378 }
2379
2380 #[test]
2381 fn invariant_workers_env_accepts_auto() {
2382 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
2383
2384 let _guard = ENV_LOCK.lock().unwrap();
2385 let previous = std::env::var_os("FOUNDRY_INVARIANT_WORKERS");
2386 unsafe {
2387 std::env::set_var("FOUNDRY_INVARIANT_WORKERS", "auto");
2388 }
2389
2390 let args = TestArgs::try_parse_from(["foundry-cli"]);
2391
2392 unsafe {
2393 if let Some(previous) = previous {
2394 std::env::set_var("FOUNDRY_INVARIANT_WORKERS", previous);
2395 } else {
2396 std::env::remove_var("FOUNDRY_INVARIANT_WORKERS");
2397 }
2398 }
2399
2400 assert_eq!(args.unwrap().invariant_workers, Some(InvariantWorkers::Auto));
2401 }
2402
2403 #[test]
2404 fn extract_chain() {
2405 let test = |arg: &str, expected: Chain| {
2406 let args = TestArgs::parse_from(["foundry-cli", arg]);
2407 assert_eq!(args.evm.env.chain, Some(expected));
2408 let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
2409 assert_eq!(config.chain, Some(expected));
2410 assert_eq!(evm_opts.env.chain_id, Some(expected.id()));
2411 };
2412 test("--chain-id=1", Chain::mainnet());
2413 test("--chain-id=42", Chain::from_id(42));
2414 }
2415}