1use super::{fuzz::FuzzRunArgs, watch::WatchArgs};
2use crate::{
3 MultiContractRunner, MultiContractRunnerBuilder, brutalizer,
4 decode::decode_console_logs,
5 gas_report::GasReport,
6 multi_runner::{
7 FuzzFailureReplayConfig, FuzzMinimizeConfig, FuzzMinimizeEdgeIndices, FuzzMinimizeMode,
8 FuzzMinimizeObservation, MultiNetworkConfig, ShowmapConfig, SymbolicArtifactReplayConfig,
9 TestFunctionMatcher, is_generated_symbolic_regression_contract,
10 },
11 mutation::{MutationRunConfig, run_mutation_testing},
12 result::{
13 SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA, SuiteResult, SymbolicCounterexampleArtifact,
14 SymbolicReplayStatus, TestKind, TestKindReport, TestOutcome, TestResult, TestStatus,
15 },
16 runner::{effective_test_function_kind, inline_config_for},
17 symbolic_regression::{
18 SymbolicRegression, SymbolicRegressionConfig, attach_symbolic_regressions_to_suites,
19 collect_symbolic_artifacts_from_suites, emit_symbolic_regressions,
20 },
21 traces::{
22 CallTraceDecoderBuilder, InternalTraceMode, TraceKind,
23 debug::{ContractSources, DebugTraceIdentifier},
24 decode_trace_arena, folded_stack_trace,
25 identifier::SignaturesIdentifier,
26 render_trace_arena_inner, speedscope,
27 },
28 workspace,
29};
30use alloy_json_abi::JsonAbi;
31use alloy_primitives::U256;
32use chrono::Utc;
33use clap::{Parser, ValueEnum, ValueHint};
34use dialoguer::{Select, console::Term};
35use eyre::{Context, OptionExt, Result, bail};
36use foundry_cli::{
37 opts::{BuildOpts, EvmArgs, GlobalArgs, TracingArgs},
38 utils::{self, FoundryPathExt, LoadConfig},
39};
40use foundry_common::{
41 ContractsByArtifact, EmptyTestFilter, TestFilter, TestFunctionExt, TestFunctionKind,
42 compile::{ProjectCompiler, compile_abi_project, compile_abi_project_cached},
43 fs, sh_status, sh_warn, shell,
44};
45use foundry_compilers::{
46 Artifact, ArtifactId, ProjectCompileOutput,
47 artifacts::{
48 BytecodeObject, ConfigurableContractArtifact, Libraries,
49 output_selection::ContractOutputSelection,
50 },
51 compilers::{
52 Language,
53 multi::{MultiCompiler, MultiCompilerLanguage},
54 },
55 utils::source_files_iter,
56};
57use foundry_config::{
58 Config, InlineConfig, InvariantDepthMode, InvariantWorkers, figment,
59 figment::{
60 Metadata, Profile, Provider,
61 value::{Dict, Map, Value},
62 },
63 filter::GlobMatcher,
64 fs_permissions::FsAccessPermission,
65};
66use foundry_debugger::{Debugger, DebuggerLayout};
67use foundry_evm::{
68 core::evm::{
69 BlockEnvFor, EthEvmNetwork, FoundryEvmNetwork, SpecFor, TempoEvmNetwork, TxEnvFor,
70 },
71 executors::{ExecutorBuilder, ShowmapDomain},
72 fork::ResolvedFork,
73 fuzz::{BaseCounterExample, BasicTxDetails, CounterExample},
74 opts::EvmOpts,
75 traces::{
76 backtrace::BacktraceBuilder, identifier::TraceIdentifiers, prune_trace_depth,
77 trace_arena_at_depth,
78 },
79};
80use foundry_evm_networks::NetworkVariant;
81use foundry_tui::tui_mode;
82use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite};
83use rand::Rng;
84use regex::Regex;
85use revm::{bytecode::opcode::OpCode, context::Transaction};
86use std::{
87 collections::{BTreeMap, BTreeSet},
88 fmt::Write,
89 path::{Path, PathBuf},
90 sync::{Arc, Mutex, mpsc::channel},
91 time::{Duration, Instant},
92};
93use tempfile::TempDir;
94use yansi::Paint;
95
96#[cfg(feature = "base")]
97use foundry_evm::core::evm::BaseEvmNetwork;
98
99#[cfg(feature = "monad")]
100use foundry_evm::core::evm::MonadEvmNetwork;
101
102#[cfg(feature = "optimism")]
103use foundry_evm::core::evm::OpEvmNetwork;
104
105mod evm_profile_server;
106mod filter;
107mod summary;
108use filter::RerunFailures;
109use summary::{TestSummaryReport, format_invariant_metrics_table};
110
111pub use filter::{FilterArgs, ProjectPathsAwareFilter, RerunFailure};
112
113const DEBUGGER_MATCHING_TESTS_DISPLAY_LIMIT: usize = 12;
114const AUTO_FUZZ_FAILURE_DIR: &str = "fuzz";
115const AUTO_CORPUS_DIR: &str = "corpus";
116
117foundry_config::merge_impl_figment_convert!(TestArgs, build, evm);
119
120fn validate_showmap_config(showmap: &ShowmapConfig) -> Result<()> {
121 for (kind, name) in [("approach", &showmap.approach), ("trial", &showmap.trial)] {
122 let path = Path::new(name);
123 if name.is_empty()
124 || path.is_absolute()
125 || path.components().count() != 1
126 || name.contains(['/', '\\'])
127 || matches!(name.as_str(), "." | "..")
128 {
129 bail!(
130 "invalid showmap {kind} `{name}`: expected a single file-name component without path separators"
131 );
132 }
133 }
134 Ok(())
135}
136
137pub(crate) struct FuzzMinimizeReplaySession {
139 filter: ProjectPathsAwareFilter,
140 passes: Vec<FuzzMinimizeReplayPass>,
141}
142
143type FuzzMinimizeReplay = Box<dyn Fn(&ProjectPathsAwareFilter, FuzzMinimizeConfig) -> Result<()>>;
144
145struct FuzzMinimizeReplayPass {
146 target_count: usize,
147 replay: FuzzMinimizeReplay,
148}
149
150impl FuzzMinimizeReplaySession {
151 pub(crate) fn replay(
152 &self,
153 sequence: Vec<BasicTxDetails>,
154 evm_edge_indices: FuzzMinimizeEdgeIndices,
155 mode: FuzzMinimizeMode,
156 ) -> Result<Vec<FuzzMinimizeObservation>> {
157 let observations = Arc::new(Mutex::new(Vec::new()));
158 let fuzz_minimize = FuzzMinimizeConfig {
159 input: sequence.into(),
160 mode,
161 evm_edge_indices,
162 observations: observations.clone(),
163 };
164 for pass in self.passes.iter().filter(|pass| pass.target_count > 0) {
165 (pass.replay)(&self.filter, fuzz_minimize.clone())?;
166 }
167 let observations = observations
168 .lock()
169 .map_err(|_| eyre::eyre!("minimize observations lock poisoned"))?
170 .clone();
171 if observations.is_empty() {
172 bail!("fuzz minimization replay produced no observation for the matched test");
173 }
174 Ok(observations)
175 }
176}
177
178fn fuzz_minimize_pass<FEN: FoundryEvmNetwork>(
179 runner: MultiContractRunner<FEN>,
180 filter: &ProjectPathsAwareFilter,
181) -> FuzzMinimizeReplayPass {
182 let target_count = count_fuzz_minimize_targets(&runner, filter);
183 let replay = move |filter: &ProjectPathsAwareFilter, fuzz_minimize| -> Result<()> {
184 let mut runner = runner.clone();
185 runner.tcfg.fuzz_minimize = Some(fuzz_minimize);
186 for (suite, suite_result) in runner.test_collect(filter)? {
187 for (test, test_result) in suite_result.test_results {
188 if test_result.status == TestStatus::Failure {
189 bail!(
190 "fuzz minimization replay failed for {suite}::{test}: {}",
191 test_result.reason.as_deref().unwrap_or("unknown error")
192 );
193 }
194 }
195 }
196 Ok(())
197 };
198 FuzzMinimizeReplayPass { target_count, replay: Box::new(replay) }
199}
200
201fn count_fuzz_minimize_targets<FEN: FoundryEvmNetwork>(
202 runner: &MultiContractRunner<FEN>,
203 filter: &dyn TestFilter,
204) -> usize {
205 let matcher = runner.test_function_matcher();
206 runner
207 .matching_contracts(filter)
208 .map(|(id, contract)| {
209 let (fuzz, invariant) = matcher.count_fuzz_engine_targets(
210 filter,
211 id,
212 &contract.abi,
213 &runner.tcfg.multi_network,
214 );
215 fuzz + invariant
216 })
217 .sum()
218}
219
220macro_rules! dispatch_network {
222 ($evm_opts:expr, | $fen:ident | $body:expr) => {
223 match $evm_opts.networks.execution_network() {
224 #[cfg(feature = "base")]
225 NetworkVariant::Base => {
226 type $fen = BaseEvmNetwork;
227 $body
228 }
229 NetworkVariant::Tempo => {
230 type $fen = TempoEvmNetwork;
231 $body
232 }
233 #[cfg(feature = "monad")]
234 NetworkVariant::Monad => {
235 type $fen = MonadEvmNetwork;
236 $body
237 }
238 #[cfg(feature = "optimism")]
239 NetworkVariant::Optimism => {
240 type $fen = OpEvmNetwork;
241 $body
242 }
243 NetworkVariant::Ethereum => {
244 type $fen = EthEvmNetwork;
245 $body
246 }
247 }
248 };
249}
250
251#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
253pub enum EvmProfileFormat {
254 #[default]
256 Speedscope,
257}
258
259#[derive(Clone, Copy, Debug, PartialEq, Eq)]
260enum TraceOutputKind {
261 Flamegraph,
262 Flamechart,
263 EvmProfile(EvmProfileFormat),
264}
265
266impl TraceOutputKind {
267 const fn label(self) -> &'static str {
268 match self {
269 Self::Flamegraph => "flamegraph",
270 Self::Flamechart => "flamechart",
271 Self::EvmProfile(_) => "EVM profile",
272 }
273 }
274}
275
276#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, ValueEnum)]
278#[clap(rename_all = "lowercase")]
279pub enum ShowmapDomainArg {
280 #[default]
281 Evm,
282 Sancov,
283 Both,
284}
285
286impl From<ShowmapDomainArg> for ShowmapDomain {
287 fn from(d: ShowmapDomainArg) -> Self {
288 match d {
289 ShowmapDomainArg::Evm => Self::Evm,
290 ShowmapDomainArg::Sancov => Self::Sancov,
291 ShowmapDomainArg::Both => Self::Both,
292 }
293 }
294}
295
296#[derive(Clone, Debug)]
297pub(crate) struct TestExecutionOptions {
298 pub(crate) coverage: bool,
299 pub(crate) decode_internal: InternalTraceMode,
300 pub(crate) multi_network: MultiNetworkConfig,
301 pub(crate) fuzz_input: Option<FuzzFailureReplayConfig>,
302 pub(crate) replay_symbolic_artifact: Option<SymbolicArtifactReplayConfig>,
303 pub(crate) inline_config: Arc<InlineConfig>,
304 pub(crate) selected_sources: BTreeSet<PathBuf>,
305}
306
307impl TestExecutionOptions {
308 pub(crate) fn default_run(inline_config: Arc<InlineConfig>) -> Self {
309 Self {
310 coverage: false,
311 decode_internal: InternalTraceMode::None,
312 multi_network: MultiNetworkConfig::default(),
313 fuzz_input: None,
314 replay_symbolic_artifact: None,
315 inline_config,
316 selected_sources: BTreeSet::new(),
317 }
318 }
319
320 pub(crate) fn coverage(inline_config: Arc<InlineConfig>) -> Self {
321 Self { coverage: true, ..Self::default_run(inline_config) }
322 }
323}
324
325struct NetworkPass {
327 config: Config,
328 evm_opts: EvmOpts,
329 multi_network: MultiNetworkConfig,
330}
331
332fn network_passes(
334 config: Config,
335 evm_opts: EvmOpts,
336 override_networks: &[NetworkVariant],
337) -> (NetworkPass, Vec<NetworkPass>) {
338 let multi_network = |pass_network| MultiNetworkConfig {
339 all_override_networks: override_networks.to_vec(),
340 pass_network,
341 };
342 let override_passes = override_networks
343 .iter()
344 .map(|&network| {
345 let mut evm_opts = evm_opts.clone();
346 evm_opts.set_explicit_network(network);
347 let mut config = config.clone();
348 config.networks = evm_opts.networks;
349 NetworkPass { config, evm_opts, multi_network: multi_network(Some(network)) }
350 })
351 .collect();
352 (NetworkPass { config, evm_opts, multi_network: multi_network(None) }, override_passes)
353}
354
355struct CompiledTestProject {
356 project_root: PathBuf,
357 config: Config,
358 evm_opts: EvmOpts,
359 output: ProjectCompileOutput,
360 filter: ProjectPathsAwareFilter,
361 inline_config: Arc<InlineConfig>,
362 replay_symbolic_artifact: Option<SymbolicArtifactReplayConfig>,
363 selected_sources: BTreeSet<PathBuf>,
364 _brutalized_workspace: Option<TempDir>,
366}
367
368#[derive(Clone, Debug, Parser)]
370#[command(next_help_heading = "Campaign options")]
371pub struct CampaignArgs {
372 #[arg(long, value_name = "RUNS")]
374 pub runs: Option<u64>,
375
376 #[arg(long, value_name = "TIMEOUT")]
378 pub timeout: Option<u32>,
379
380 #[arg(long)]
382 pub seed: Option<U256>,
383
384 #[arg(long, value_name = "DEPTH")]
386 pub depth: Option<u32>,
387
388 #[arg(long, value_name = "DEPTH")]
390 pub min_depth: Option<u32>,
391
392 #[arg(long, value_name = "fixed|random")]
394 pub depth_mode: Option<InvariantDepthMode>,
395
396 #[arg(long, value_name = "WORKERS")]
398 pub workers: Option<InvariantWorkers>,
399
400 #[arg(long, value_name = "PATH", value_hint = ValueHint::DirPath)]
402 pub corpus_dir: Option<PathBuf>,
403
404 #[arg(long, value_name = "PERCENT")]
406 pub dictionary_weight: Option<u32>,
407
408 #[arg(long, value_name = "N|max")]
410 pub dictionary_addresses: Option<String>,
411
412 #[arg(long, value_name = "N|max")]
414 pub dictionary_values: Option<String>,
415
416 #[arg(long, value_name = "N|max")]
418 pub dictionary_literals: Option<String>,
419
420 #[arg(long, value_name = "PERCENT")]
423 pub corpus_random_sequence_weight: Option<u32>,
424
425 #[arg(long, value_name = "PERCENT")]
427 pub payable_value_weight: Option<u32>,
428
429 #[arg(long, value_name = "WEIGHT")]
431 pub mutation_weight_splice: Option<u32>,
432
433 #[arg(long, value_name = "WEIGHT")]
435 pub mutation_weight_repeat: Option<u32>,
436
437 #[arg(long, value_name = "WEIGHT")]
439 pub mutation_weight_interleave: Option<u32>,
440
441 #[arg(long, value_name = "WEIGHT")]
443 pub mutation_weight_prefix: Option<u32>,
444
445 #[arg(long, value_name = "WEIGHT")]
447 pub mutation_weight_suffix: Option<u32>,
448
449 #[arg(long, value_name = "WEIGHT")]
451 pub mutation_weight_abi: Option<u32>,
452
453 #[arg(long, value_name = "WEIGHT")]
455 pub mutation_weight_cmp: Option<u32>,
456
457 #[arg(long, value_name = "PATH", value_hint = ValueHint::DirPath)]
459 pub frontier_dir: Option<PathBuf>,
460
461 #[arg(long, value_name = "COUNT")]
463 pub frontier_limit: Option<usize>,
464}
465
466#[derive(Clone, Debug, Default, Parser)]
468#[command(next_help_heading = "Test options")]
469pub struct TestArgs {
470 #[arg(skip)]
472 fuzz_only: bool,
473
474 #[arg(skip)]
476 auto_fuzz_corpus: bool,
477
478 #[arg(skip)]
480 pub(crate) showmap_override: Option<ShowmapConfig>,
481
482 #[arg(skip)]
484 pub(crate) fuzz_failure_replay: bool,
485
486 #[arg(skip)]
488 pub(crate) invariant_runs_override: Option<u64>,
489
490 #[arg(skip)]
492 pub(crate) invariant_timeout_override: Option<u32>,
493
494 #[command(flatten)]
496 pub global: GlobalArgs,
497
498 #[arg(value_hint = ValueHint::FilePath)]
500 pub path: Option<GlobMatcher>,
501
502 #[arg(long, conflicts_with_all = ["flamegraph", "flamechart", "evm_profile", "decode_internal", "rerun"])]
509 debug: bool,
510
511 #[arg(long = "debug-layout", requires = "debug", value_enum)]
513 debug_layout: Option<DebuggerLayout>,
514
515 #[arg(
520 long,
521 group = "trace_output",
522 conflicts_with_all = ["flamechart", "evm_profile", "json", "junit", "list"]
523 )]
524 flamegraph: bool,
525
526 #[arg(
531 long,
532 group = "trace_output",
533 conflicts_with_all = ["flamegraph", "evm_profile", "json", "junit", "list"]
534 )]
535 flamechart: bool,
536
537 #[arg(
543 long,
544 value_name = "FORMAT",
545 num_args = 0..=1,
546 default_missing_value = "speedscope",
547 value_enum,
548 group = "trace_output",
549 conflicts_with_all = ["flamegraph", "flamechart", "json", "junit", "list"]
550 )]
551 evm_profile: Option<EvmProfileFormat>,
552
553 #[arg(long, requires = "trace_output")]
557 no_open: bool,
558
559 #[command(flatten)]
560 tracing: TracingArgs,
561
562 #[arg(
564 long,
565 requires = "debug",
566 value_hint = ValueHint::FilePath,
567 value_name = "PATH"
568 )]
569 dump: Option<PathBuf>,
570
571 #[arg(long, env = "FORGE_GAS_REPORT")]
573 gas_report: bool,
574
575 #[arg(long, env = "FORGE_SNAPSHOT_CHECK")]
577 gas_snapshot_check: Option<bool>,
578
579 #[arg(long, env = "FORGE_SNAPSHOT_EMIT")]
581 gas_snapshot_emit: Option<bool>,
582
583 #[arg(long, env = "FORGE_ALLOW_FAILURE")]
585 allow_failure: bool,
586
587 #[arg(long, short, env = "FORGE_SUPPRESS_SUCCESSFUL_TRACES", help_heading = "Trace options")]
589 suppress_successful_traces: bool,
590
591 #[arg(
593 long,
594 value_name = "PATH",
595 value_hint = ValueHint::FilePath,
596 conflicts_with = "list",
597 help_heading = "Display options"
598 )]
599 json_file: Option<PathBuf>,
600
601 #[arg(long, conflicts_with_all = ["quiet", "json", "gas_report", "summary", "list", "show_progress"], help_heading = "Display options")]
603 pub junit: bool,
604
605 #[arg(long)]
607 pub fail_fast: bool,
608
609 #[arg(long, env = "ETHERSCAN_API_KEY", value_name = "KEY")]
611 etherscan_api_key: Option<String>,
612
613 #[arg(long, short, conflicts_with_all = ["show_progress", "decode_internal", "summary"], help_heading = "Display options")]
615 list: bool,
616
617 #[arg(long)]
619 pub fuzz_seed: Option<U256>,
620
621 #[arg(long, env = "FOUNDRY_FUZZ_RUNS", value_name = "RUNS")]
622 pub fuzz_runs: Option<u64>,
623
624 #[arg(long, env = "FOUNDRY_INVARIANT_WORKERS", value_name = "WORKERS")]
626 pub invariant_workers: Option<InvariantWorkers>,
627
628 #[arg(long, env = "FOUNDRY_FUZZ_RUN", value_name = "RUN")]
630 pub fuzz_run: Option<u32>,
631
632 #[arg(long, env = "FOUNDRY_FUZZ_WORKER", value_name = "WORKER", requires = "fuzz_run")]
634 pub fuzz_worker: Option<u32>,
635
636 #[arg(long, env = "FOUNDRY_FUZZ_TIMEOUT", value_name = "TIMEOUT")]
638 pub fuzz_timeout: Option<u64>,
639
640 #[arg(long, env = "FOUNDRY_FUZZ_DICTIONARY_WEIGHT", value_name = "PERCENT")]
642 pub fuzz_dictionary_weight: Option<u32>,
643
644 #[arg(long, env = "FOUNDRY_FUZZ_MAX_FUZZ_DICTIONARY_ADDRESSES", value_name = "N|max")]
646 pub fuzz_dictionary_addresses: Option<String>,
647
648 #[arg(long, env = "FOUNDRY_FUZZ_MAX_FUZZ_DICTIONARY_VALUES", value_name = "N|max")]
650 pub fuzz_dictionary_values: Option<String>,
651
652 #[arg(long, env = "FOUNDRY_FUZZ_MAX_FUZZ_DICTIONARY_LITERALS", value_name = "N|max")]
654 pub fuzz_dictionary_literals: Option<String>,
655
656 #[arg(long, env = "FOUNDRY_FUZZ_CORPUS_RANDOM_SEQUENCE_WEIGHT", value_name = "PERCENT")]
659 pub fuzz_corpus_random_sequence_weight: Option<u32>,
660
661 #[arg(long, env = "FOUNDRY_FUZZ_CORPUS_DIR", value_name = "PATH", value_hint = ValueHint::DirPath)]
663 pub fuzz_corpus_dir: Option<PathBuf>,
664
665 #[arg(long, env = "FOUNDRY_FUZZ_FRONTIER_DIR", value_name = "PATH", value_hint = ValueHint::DirPath)]
667 pub fuzz_frontier_dir: Option<PathBuf>,
668
669 #[arg(long, env = "FOUNDRY_FUZZ_FRONTIER_LIMIT", value_name = "COUNT")]
671 pub fuzz_frontier_limit: Option<usize>,
672
673 #[arg(long, env = "FOUNDRY_FUZZ_PAYABLE_VALUE_WEIGHT", value_name = "PERCENT")]
675 pub fuzz_payable_value_weight: Option<u32>,
676
677 #[arg(long, env = "FOUNDRY_FUZZ_MUTATION_WEIGHT_SPLICE", value_name = "WEIGHT")]
679 pub fuzz_mutation_weight_splice: Option<u32>,
680
681 #[arg(long, env = "FOUNDRY_FUZZ_MUTATION_WEIGHT_REPEAT", value_name = "WEIGHT")]
683 pub fuzz_mutation_weight_repeat: Option<u32>,
684
685 #[arg(long, env = "FOUNDRY_FUZZ_MUTATION_WEIGHT_INTERLEAVE", value_name = "WEIGHT")]
687 pub fuzz_mutation_weight_interleave: Option<u32>,
688
689 #[arg(long, env = "FOUNDRY_FUZZ_MUTATION_WEIGHT_PREFIX", value_name = "WEIGHT")]
691 pub fuzz_mutation_weight_prefix: Option<u32>,
692
693 #[arg(long, env = "FOUNDRY_FUZZ_MUTATION_WEIGHT_SUFFIX", value_name = "WEIGHT")]
695 pub fuzz_mutation_weight_suffix: Option<u32>,
696
697 #[arg(long, env = "FOUNDRY_FUZZ_MUTATION_WEIGHT_ABI", value_name = "WEIGHT")]
699 pub fuzz_mutation_weight_abi: Option<u32>,
700
701 #[arg(long, env = "FOUNDRY_FUZZ_MUTATION_WEIGHT_CMP", value_name = "WEIGHT")]
703 pub fuzz_mutation_weight_cmp: Option<u32>,
704
705 #[arg(
707 long,
708 value_name = "PATH",
709 value_hint = ValueHint::FilePath,
710 conflicts_with_all = ["fuzz_run", "list"]
711 )]
712 pub fuzz_input_file: Option<PathBuf>,
713
714 #[arg(long, env = "FOUNDRY_INVARIANT_DEPTH", value_name = "DEPTH")]
716 pub invariant_depth: Option<u32>,
717
718 #[arg(long, env = "FOUNDRY_INVARIANT_MIN_DEPTH", value_name = "DEPTH")]
720 pub invariant_min_depth: Option<u32>,
721
722 #[arg(long, env = "FOUNDRY_INVARIANT_DEPTH_MODE", value_name = "fixed|random")]
724 pub invariant_depth_mode: Option<InvariantDepthMode>,
725
726 #[arg(long, env = "FOUNDRY_INVARIANT_DICTIONARY_WEIGHT", value_name = "PERCENT")]
728 pub invariant_dictionary_weight: Option<u32>,
729
730 #[arg(long, env = "FOUNDRY_INVARIANT_MAX_FUZZ_DICTIONARY_ADDRESSES", value_name = "N|max")]
732 pub invariant_dictionary_addresses: Option<String>,
733
734 #[arg(long, env = "FOUNDRY_INVARIANT_MAX_FUZZ_DICTIONARY_VALUES", value_name = "N|max")]
736 pub invariant_dictionary_values: Option<String>,
737
738 #[arg(long, env = "FOUNDRY_INVARIANT_MAX_FUZZ_DICTIONARY_LITERALS", value_name = "N|max")]
740 pub invariant_dictionary_literals: Option<String>,
741
742 #[arg(long, env = "FOUNDRY_INVARIANT_CORPUS_RANDOM_SEQUENCE_WEIGHT", value_name = "PERCENT")]
745 pub invariant_corpus_random_sequence_weight: Option<u32>,
746
747 #[arg(long, env = "FOUNDRY_INVARIANT_CORPUS_DIR", value_name = "PATH", value_hint = ValueHint::DirPath)]
749 pub invariant_corpus_dir: Option<PathBuf>,
750
751 #[arg(long, env = "FOUNDRY_INVARIANT_FRONTIER_DIR", value_name = "PATH", value_hint = ValueHint::DirPath)]
753 pub invariant_frontier_dir: Option<PathBuf>,
754
755 #[arg(long, env = "FOUNDRY_INVARIANT_FRONTIER_LIMIT", value_name = "COUNT")]
757 pub invariant_frontier_limit: Option<usize>,
758
759 #[arg(long, env = "FOUNDRY_INVARIANT_PAYABLE_VALUE_WEIGHT", value_name = "PERCENT")]
761 pub invariant_payable_value_weight: Option<u32>,
762
763 #[arg(long, env = "FOUNDRY_INVARIANT_MUTATION_WEIGHT_SPLICE", value_name = "WEIGHT")]
765 pub invariant_mutation_weight_splice: Option<u32>,
766
767 #[arg(long, env = "FOUNDRY_INVARIANT_MUTATION_WEIGHT_REPEAT", value_name = "WEIGHT")]
769 pub invariant_mutation_weight_repeat: Option<u32>,
770
771 #[arg(long, env = "FOUNDRY_INVARIANT_MUTATION_WEIGHT_INTERLEAVE", value_name = "WEIGHT")]
773 pub invariant_mutation_weight_interleave: Option<u32>,
774
775 #[arg(long, env = "FOUNDRY_INVARIANT_MUTATION_WEIGHT_PREFIX", value_name = "WEIGHT")]
777 pub invariant_mutation_weight_prefix: Option<u32>,
778
779 #[arg(long, env = "FOUNDRY_INVARIANT_MUTATION_WEIGHT_SUFFIX", value_name = "WEIGHT")]
781 pub invariant_mutation_weight_suffix: Option<u32>,
782
783 #[arg(long, env = "FOUNDRY_INVARIANT_MUTATION_WEIGHT_ABI", value_name = "WEIGHT")]
785 pub invariant_mutation_weight_abi: Option<u32>,
786
787 #[arg(long, env = "FOUNDRY_INVARIANT_MUTATION_WEIGHT_CMP", value_name = "WEIGHT")]
789 pub invariant_mutation_weight_cmp: Option<u32>,
790
791 #[arg(long, env = "FOUNDRY_SYMBOLIC")]
793 pub symbolic: bool,
794
795 #[arg(
797 long,
798 value_name = "PATH",
799 value_hint = ValueHint::FilePath,
800 conflicts_with_all = [
801 "debug",
802 "flamegraph",
803 "flamechart",
804 "rerun",
805 "fuzz_input_file",
806 "showmap_out",
807 "path",
808 "test_pattern",
809 "test_pattern_inverse",
810 "contract_pattern",
811 "contract_pattern_inverse",
812 "path_pattern",
813 "no-match-path",
814 ],
815 )]
816 pub replay_symbolic_artifact: Option<PathBuf>,
817
818 #[arg(long, env = "FOUNDRY_SYMBOLIC_EMIT_REGRESSION")]
820 pub emit_regression: bool,
821
822 #[arg(
824 long,
825 env = "FOUNDRY_SYMBOLIC_REGRESSION_OUT",
826 value_name = "PATH",
827 value_hint = ValueHint::AnyPath,
828 requires = "emit_regression"
829 )]
830 pub regression_out: Option<PathBuf>,
831
832 #[arg(long, env = "FOUNDRY_SYMBOLIC_REGRESSION_OVERWRITE", requires = "emit_regression")]
834 pub regression_overwrite: bool,
835
836 #[arg(long, env = "FOUNDRY_SYMBOLIC_SEED_CORPUS")]
838 pub symbolic_seed_corpus: bool,
839
840 #[arg(long, env = "FOUNDRY_SYMBOLIC_USE_FUZZ_CORPUS")]
842 pub symbolic_use_fuzz_corpus: bool,
843
844 #[arg(long, env = "FOUNDRY_SYMBOLIC_CORPUS_SEED_LIMIT", value_name = "COUNT")]
846 pub symbolic_corpus_seed_limit: Option<usize>,
847
848 #[arg(long, env = "FOUNDRY_SYMBOLIC_USE_FUZZ_FRONTIERS")]
850 pub symbolic_use_fuzz_frontiers: bool,
851
852 #[arg(long, env = "FOUNDRY_SYMBOLIC_CHECK_INVARIANT_FRONTIERS")]
854 pub symbolic_check_invariant_frontiers: bool,
855
856 #[arg(long, env = "FOUNDRY_SYMBOLIC_FRONTIER_LIMIT", value_name = "COUNT")]
858 pub symbolic_frontier_limit: Option<usize>,
859
860 #[arg(long, env = "FOUNDRY_SYMBOLIC_FRONTIER_IDS", value_name = "IDS", value_delimiter = ',')]
862 pub symbolic_frontier_ids: Option<Vec<u64>>,
863
864 #[arg(long, env = "FOUNDRY_SYMBOLIC_FRONTIER_PCS", value_name = "PCS", value_delimiter = ',')]
866 pub symbolic_frontier_pcs: Option<Vec<usize>>,
867
868 #[arg(
870 long,
871 env = "FOUNDRY_SYMBOLIC_FRONTIER_SELECTORS",
872 value_name = "SELECTORS",
873 value_delimiter = ','
874 )]
875 pub symbolic_frontier_selectors: Option<Vec<String>>,
876
877 #[arg(long, env = "FOUNDRY_SYMBOLIC_SOLVER", value_name = "PATH_OR_NAME")]
879 pub symbolic_solver: Option<String>,
880
881 #[arg(long, env = "FOUNDRY_SYMBOLIC_SOLVER_COMMAND", value_name = "COMMAND")]
883 pub symbolic_solver_command: Option<String>,
884
885 #[arg(
887 long,
888 env = "FOUNDRY_SYMBOLIC_SOLVER_PORTFOLIO",
889 value_delimiter = ',',
890 value_name = "SOLVER_OR_COMMAND,..."
891 )]
892 pub symbolic_solver_portfolio: Option<Vec<String>>,
893
894 #[arg(long, env = "FOUNDRY_SYMBOLIC_TIMEOUT", value_name = "SECONDS")]
896 pub symbolic_timeout: Option<u32>,
897
898 #[arg(long, env = "FOUNDRY_SYMBOLIC_LOOP", value_name = "N")]
900 pub symbolic_loop: Option<u32>,
901
902 #[arg(long, env = "FOUNDRY_SYMBOLIC_DEPTH", value_name = "N")]
904 pub symbolic_depth: Option<u32>,
905
906 #[arg(long, env = "FOUNDRY_SYMBOLIC_WIDTH", value_name = "N")]
908 pub symbolic_width: Option<u32>,
909
910 #[arg(long, env = "FOUNDRY_SYMBOLIC_MAX_DEPTH", value_name = "N")]
912 pub symbolic_max_depth: Option<u32>,
913
914 #[arg(long, env = "FOUNDRY_SYMBOLIC_MAX_PATHS", value_name = "N")]
916 pub symbolic_max_paths: Option<u32>,
917
918 #[arg(long, env = "FOUNDRY_SYMBOLIC_INVARIANT_DEPTH", value_name = "N")]
920 pub symbolic_invariant_depth: Option<u32>,
921
922 #[arg(long, env = "FOUNDRY_SYMBOLIC_MAX_SOLVER_QUERIES", value_name = "N")]
924 pub symbolic_max_solver_queries: Option<u32>,
925
926 #[arg(long, env = "FOUNDRY_SYMBOLIC_DEFAULT_DYNAMIC_LENGTH", value_name = "N")]
928 pub symbolic_default_dynamic_length: Option<u32>,
929
930 #[arg(long, env = "FOUNDRY_SYMBOLIC_MAX_DYNAMIC_LENGTH", value_name = "N")]
932 pub symbolic_max_dynamic_length: Option<u32>,
933
934 #[arg(
936 long,
937 env = "FOUNDRY_SYMBOLIC_ARRAY_LENGTHS",
938 value_delimiter = ',',
939 value_name = "N,..."
940 )]
941 pub symbolic_array_lengths: Option<Vec<u32>>,
942
943 #[arg(long, env = "FOUNDRY_SYMBOLIC_MAX_CALLDATA_BYTES", value_name = "N")]
945 pub symbolic_max_calldata_bytes: Option<u32>,
946
947 #[arg(long, env = "FOUNDRY_SYMBOLIC_CALL_TARGETS")]
949 pub symbolic_call_targets: bool,
950
951 #[arg(long, env = "FOUNDRY_SYMBOLIC_DUMP_SMT")]
953 pub symbolic_dump_smt: bool,
954
955 #[arg(
957 long,
958 env = "FOUNDRY_SYMBOLIC_STORAGE_LAYOUT",
959 value_name = "solidity|generic",
960 value_parser = ["solidity", "generic"]
961 )]
962 pub symbolic_storage_layout: Option<String>,
963
964 #[arg(long, conflicts_with_all = ["quiet", "json"], help_heading = "Display options")]
966 pub show_progress: bool,
967
968 #[arg(long)]
971 pub rerun: bool,
972
973 #[arg(long, value_parser = parse_opcode, value_delimiter(','), conflicts_with_all = ["json", "junit", "list", "debug"])]
980 pub opcodes: Vec<OpCode>,
981
982 #[arg(long, help_heading = "Display options")]
984 pub summary: bool,
985
986 #[arg(long, help_heading = "Display options", requires = "summary")]
988 pub detailed: bool,
989
990 #[arg(
994 long,
995 value_name = "DIR",
996 value_hint = ValueHint::DirPath,
997 help_heading = "Showmap replay",
998 conflicts_with_all = ["debug", "flamegraph", "flamechart", "evm_profile", "rerun", "fuzz_input_file", "gas_report"],
999 )]
1000 pub showmap_out: Option<PathBuf>,
1001
1002 #[arg(long, help_heading = "Showmap replay", requires = "showmap_out")]
1004 pub showmap_per_input: bool,
1005
1006 #[arg(
1008 long,
1009 value_enum,
1010 default_value_t = ShowmapDomainArg::Evm,
1011 help_heading = "Showmap replay",
1012 requires = "showmap_out",
1013 )]
1014 pub showmap_domain: ShowmapDomainArg,
1015
1016 #[arg(
1018 long,
1019 default_value = "replay",
1020 help_heading = "Showmap replay",
1021 requires = "showmap_out"
1022 )]
1023 pub showmap_approach: String,
1024
1025 #[arg(long, help_heading = "Showmap replay", requires = "showmap_out")]
1028 pub showmap_trial: Option<String>,
1029
1030 #[arg(
1033 long,
1034 value_name = "PATH",
1035 value_hint = ValueHint::DirPath,
1036 help_heading = "Showmap replay",
1037 requires = "showmap_out",
1038 )]
1039 pub showmap_corpus_dir: Option<PathBuf>,
1040
1041 #[arg(long)]
1044 pub decode_external_storage: bool,
1045
1046 #[command(flatten)]
1047 filter: FilterArgs,
1048
1049 #[command(flatten)]
1050 evm: EvmArgs,
1051
1052 #[command(flatten)]
1053 pub build: BuildOpts,
1054
1055 #[command(flatten)]
1056 pub watch: WatchArgs,
1057
1058 #[arg(long, num_args(0..), value_name = "PATH")]
1061 pub mutate: Option<Vec<PathBuf>>,
1062
1063 #[arg(long, value_name = "PATTERN", requires = "mutate", conflicts_with = "mutate_contract")]
1068 pub mutate_path: Option<GlobMatcher>,
1069
1070 #[arg(long, value_name = "REGEX", requires = "mutate")]
1074 pub mutate_contract: Option<regex::Regex>,
1075
1076 #[arg(long, value_name = "JOBS", requires = "mutate")]
1079 pub mutation_jobs: Option<usize>,
1080
1081 #[arg(long, value_name = "TIMEOUT", requires = "mutate")]
1087 pub mutation_timeout: Option<u32>,
1088
1089 #[arg(long, value_name = "RUNS", requires = "mutate")]
1091 pub mutation_optimizer_runs: Option<u32>,
1092
1093 #[arg(long, default_missing_value = "true", num_args = 0..=1, requires = "mutate")]
1095 pub mutation_via_ir: Option<bool>,
1096
1097 #[arg(long, conflicts_with_all = ["mutate", "replay_symbolic_artifact"])]
1114 pub brutalize: bool,
1115}
1116
1117impl TestArgs {
1118 pub async fn run(mut self) -> Result<TestOutcome> {
1119 trace!(target: "forge::test", "executing test command");
1120 self.compile_and_run().await
1121 }
1122
1123 pub(crate) fn ensure_mutation_mode_compatible(&self, coverage: bool) -> Result<()> {
1124 if self.mutate.is_none() {
1125 return Ok(());
1126 }
1127 let conflicts = enabled_flags([
1131 (self.list, "--list"),
1132 (self.debug, "--debug"),
1133 (self.flamegraph, "--flamegraph"),
1134 (self.flamechart, "--flamechart"),
1135 (self.evm_profile.is_some(), "--evm-profile"),
1136 (self.junit, "--junit"),
1137 (self.json_file.is_some(), "--json-file"),
1138 (coverage, "coverage"),
1139 (self.showmap_out.is_some(), "--showmap-out"),
1140 (self.replay_symbolic_artifact.is_some(), "--replay-symbolic-artifact"),
1141 ]);
1142 if !conflicts.is_empty() {
1143 bail!(
1144 "`--mutate` cannot be combined with: {}. Re-run without those flags to use \
1145 mutation testing.",
1146 conflicts.join(", ")
1147 );
1148 }
1149 Ok(())
1150 }
1151
1152 pub(crate) fn ensure_coverage_mode_compatible(&self) -> Result<()> {
1153 self.ensure_mutation_mode_compatible(true)?;
1154 let conflicts = enabled_flags([
1155 (shell::is_json(), "--json"),
1156 (self.junit, "--junit"),
1157 (self.json_file.is_some(), "--json-file"),
1158 (self.list, "--list"),
1159 (self.debug, "--debug"),
1160 (self.flamegraph, "--flamegraph"),
1161 (self.flamechart, "--flamechart"),
1162 (self.evm_profile.is_some(), "--evm-profile"),
1163 (self.showmap_out.is_some(), "--showmap-out"),
1164 (self.brutalize, "--brutalize"),
1165 (self.replay_symbolic_artifact.is_some(), "--replay-symbolic-artifact"),
1166 ]);
1167 if !conflicts.is_empty() {
1168 bail!(
1169 "`forge coverage` cannot be combined with: {}. Use `--report lcov` for an \
1170 interoperable coverage report or `--report attribution` for per-test JSON \
1171 attribution.",
1172 conflicts.join(", ")
1173 );
1174 }
1175 Ok(())
1176 }
1177
1178 fn showmap_config(&self) -> Result<Option<ShowmapConfig>> {
1180 let showmap = match (&self.showmap_override, &self.showmap_out) {
1181 (Some(showmap), _) => showmap.clone(),
1182 (None, Some(out_dir)) => ShowmapConfig {
1183 out_dir: out_dir.clone(),
1184 approach: self.showmap_approach.clone(),
1185 trial: self.showmap_trial.clone().unwrap_or_else(|| {
1188 let ns = std::time::SystemTime::now()
1189 .duration_since(std::time::UNIX_EPOCH)
1190 .map(|d| d.as_nanos())
1191 .unwrap_or(0);
1192 format!("trial-{ns}")
1193 }),
1194 per_input: self.showmap_per_input,
1195 domain: self.showmap_domain.into(),
1196 corpus_dir: self.showmap_corpus_dir.clone(),
1197 emit_files: true,
1198 },
1199 (None, None) => return Ok(None),
1200 };
1201 validate_showmap_config(&showmap)?;
1202 Ok(Some(showmap))
1203 }
1204
1205 pub(crate) const fn enable_fuzz_only(&mut self) {
1207 self.fuzz_only = true;
1208 }
1209
1210 pub(crate) const fn enable_fuzz_only_with_auto_fuzz_corpus(&mut self) {
1213 self.fuzz_only = true;
1214 self.auto_fuzz_corpus = true;
1215 }
1216
1217 fn apply_test_config_overrides(&self, config: &mut Config) {
1218 if self.auto_fuzz_corpus && config.fuzz.corpus.corpus_dir.is_none() {
1219 config.fuzz.corpus.corpus_dir = Some(match &config.fuzz.failure_persist_dir {
1220 Some(root) => root.join(AUTO_CORPUS_DIR),
1221 None => config.cache_path.join(AUTO_FUZZ_FAILURE_DIR).join(AUTO_CORPUS_DIR),
1222 });
1223 }
1224 if self.debug && !config.extra_output.contains(&ContractOutputSelection::StorageLayout) {
1225 config.extra_output.push(ContractOutputSelection::StorageLayout);
1226 }
1227 }
1228
1229 const fn apply_gas_report_overrides(&self, config: &mut Config, evm_opts: &mut EvmOpts) {
1232 if self.gas_report {
1233 evm_opts.isolate = true;
1234 } else {
1235 config.fuzz.gas_report_samples = 0;
1236 config.invariant.gas_report_samples = 0;
1237 }
1238 }
1239
1240 pub(crate) fn set_showmap_override(&mut self, showmap: ShowmapConfig) {
1243 self.showmap_override = Some(showmap);
1244 }
1245
1246 pub(crate) fn set_fuzz_minimize_replay_options(
1248 &mut self,
1249 global: GlobalArgs,
1250 evm: EvmArgs,
1251 build: BuildOpts,
1252 filter: FilterArgs,
1253 ) {
1254 self.global = global;
1255 self.evm = evm;
1256 self.build = build;
1257 self.filter = filter;
1258 }
1259
1260 pub(crate) const fn enable_fuzz_failure_replay(&mut self) {
1262 self.fuzz_failure_replay = true;
1263 }
1264
1265 fn warn_unsupported_engine_flags(
1266 &self,
1267 output: &ProjectCompileOutput,
1268 config: &Config,
1269 inline_config: &InlineConfig,
1270 filter: &ProjectPathsAwareFilter,
1271 multi_network: &MultiNetworkConfig,
1272 ) -> Result<()> {
1273 if !self.fuzz_only {
1274 return Ok(());
1275 }
1276 let matcher = TestFunctionMatcher::new(config, inline_config, None);
1277 let (mut fuzz, mut invariant) = (0, 0);
1278 for (id, _, abi) in matching_test_contracts(output, config, &matcher, filter) {
1279 let (f, i) = matcher.count_fuzz_engine_targets(filter, &id, abi, multi_network);
1280 fuzz += f;
1281 invariant += i;
1282 }
1283 let unused: &[(bool, &str, &str)] = if fuzz == 0 && invariant > 0 {
1284 &[
1285 (
1286 self.fuzz_frontier_dir.is_some() && self.invariant_frontier_dir.is_none(),
1287 "--frontier-dir",
1288 "fuzz",
1289 ),
1290 (
1291 self.fuzz_frontier_limit.is_some() && self.invariant_frontier_limit.is_none(),
1292 "--frontier-limit",
1293 "fuzz",
1294 ),
1295 (self.fuzz_run.is_some(), "--fuzz-run", "fuzz"),
1296 ]
1297 } else if invariant == 0 && fuzz > 0 {
1298 &[
1299 (self.invariant_depth.is_some(), "--depth", "invariant"),
1300 (self.invariant_min_depth.is_some(), "--min-depth", "invariant"),
1301 (self.invariant_depth_mode.is_some(), "--depth-mode", "invariant"),
1302 (self.invariant_workers.is_some(), "--workers", "invariant"),
1303 ]
1304 } else {
1305 &[]
1306 };
1307 for (set, flag, engine) in unused {
1308 if *set {
1309 sh_warn!(
1310 "`{flag}` only applies to {engine} tests; no matched {engine} tests were found."
1311 )?;
1312 }
1313 }
1314 Ok(())
1315 }
1316
1317 pub(crate) fn from_fuzz_run(args: FuzzRunArgs) -> Self {
1319 let campaign = args.campaign;
1320 Self {
1321 fuzz_only: true,
1322 global: args.global,
1323 path: args.path,
1324 gas_report: args.gas_report,
1325 allow_failure: args.allow_failure,
1326 junit: args.junit,
1327 fail_fast: args.fail_fast,
1328 etherscan_api_key: args.etherscan_api_key,
1329 list: args.list,
1330 fuzz_input_file: args.fuzz_input_file,
1331 show_progress: args.show_progress,
1332 rerun: args.rerun,
1333 showmap_out: args.showmap_out,
1334 showmap_per_input: args.showmap_per_input,
1335 showmap_domain: args.showmap_domain,
1336 showmap_approach: args.showmap_approach,
1337 showmap_trial: args.showmap_trial,
1338 showmap_corpus_dir: args.showmap_corpus_dir,
1339 filter: args.filter,
1340 evm: args.evm,
1341 build: args.build,
1342 fuzz_seed: campaign.seed,
1343 fuzz_runs: campaign.runs,
1344 invariant_runs_override: campaign.runs,
1345 fuzz_timeout: campaign.timeout.map(u64::from),
1346 invariant_timeout_override: campaign.timeout,
1347 fuzz_dictionary_weight: campaign.dictionary_weight,
1348 invariant_dictionary_weight: campaign.dictionary_weight,
1349 fuzz_dictionary_addresses: campaign.dictionary_addresses.clone(),
1350 invariant_dictionary_addresses: campaign.dictionary_addresses,
1351 fuzz_dictionary_values: campaign.dictionary_values.clone(),
1352 invariant_dictionary_values: campaign.dictionary_values,
1353 fuzz_dictionary_literals: campaign.dictionary_literals.clone(),
1354 invariant_dictionary_literals: campaign.dictionary_literals,
1355 fuzz_corpus_random_sequence_weight: campaign.corpus_random_sequence_weight,
1356 invariant_corpus_random_sequence_weight: campaign.corpus_random_sequence_weight,
1357 fuzz_corpus_dir: campaign.corpus_dir.clone(),
1358 invariant_corpus_dir: campaign.corpus_dir,
1359 fuzz_payable_value_weight: campaign.payable_value_weight,
1360 invariant_payable_value_weight: campaign.payable_value_weight,
1361 fuzz_mutation_weight_splice: campaign.mutation_weight_splice,
1362 invariant_mutation_weight_splice: campaign.mutation_weight_splice,
1363 fuzz_mutation_weight_repeat: campaign.mutation_weight_repeat,
1364 invariant_mutation_weight_repeat: campaign.mutation_weight_repeat,
1365 fuzz_mutation_weight_interleave: campaign.mutation_weight_interleave,
1366 invariant_mutation_weight_interleave: campaign.mutation_weight_interleave,
1367 fuzz_mutation_weight_prefix: campaign.mutation_weight_prefix,
1368 invariant_mutation_weight_prefix: campaign.mutation_weight_prefix,
1369 fuzz_mutation_weight_suffix: campaign.mutation_weight_suffix,
1370 invariant_mutation_weight_suffix: campaign.mutation_weight_suffix,
1371 fuzz_mutation_weight_abi: campaign.mutation_weight_abi,
1372 invariant_mutation_weight_abi: campaign.mutation_weight_abi,
1373 fuzz_mutation_weight_cmp: campaign.mutation_weight_cmp,
1374 invariant_mutation_weight_cmp: campaign.mutation_weight_cmp,
1375 fuzz_frontier_dir: campaign.frontier_dir.clone(),
1376 invariant_frontier_dir: campaign.frontier_dir,
1377 fuzz_frontier_limit: campaign.frontier_limit,
1378 invariant_frontier_limit: campaign.frontier_limit,
1379 invariant_depth: campaign.depth,
1380 invariant_min_depth: campaign.min_depth,
1381 invariant_depth_mode: campaign.depth_mode,
1382 invariant_workers: campaign.workers,
1383 ..Self::default()
1384 }
1385 }
1386
1387 fn load_symbolic_artifact_replay(&self) -> Result<Option<SymbolicArtifactReplayConfig>> {
1388 let Some(path) = &self.replay_symbolic_artifact else {
1389 return Ok(None);
1390 };
1391 if !self.filter.is_empty() || self.path.is_some() {
1392 bail!(
1393 "symbolic artifact mode cannot be combined with test selection filters; \
1394 the artifact selects its original target"
1395 );
1396 }
1397
1398 let display = path.display();
1399 let value = fs::read_json_file::<serde_json::Value>(path)
1400 .wrap_err(format!("failed to read symbolic counterexample artifact {display}"))?;
1401 let schema_version =
1402 value.get("schema_version").and_then(serde_json::Value::as_u64).ok_or_else(|| {
1403 eyre::eyre!(
1404 "symbolic counterexample artifact {display} is missing numeric schema_version"
1405 )
1406 })?;
1407 if schema_version != 1 {
1408 bail!(
1409 "unsupported symbolic counterexample artifact schema version {schema_version} in {display}"
1410 );
1411 }
1412 let schema = value.get("schema").and_then(serde_json::Value::as_str).ok_or_else(|| {
1413 eyre::eyre!("symbolic counterexample artifact {display} is missing string schema")
1414 })?;
1415 if schema != SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA {
1416 bail!("unsupported symbolic counterexample artifact schema `{schema}` in {display}");
1417 }
1418 let artifact = serde_json::from_value::<SymbolicCounterexampleArtifact>(value)
1419 .wrap_err(format!("failed to parse symbolic counterexample artifact {display}"))?;
1420 if artifact.calls.is_empty() {
1421 bail!("symbolic counterexample artifact {display} has no calls");
1422 }
1423 if artifact.replay.status != SymbolicReplayStatus::Confirmed {
1424 bail!(
1425 "symbolic counterexample artifact {display} replay status must be confirmed, got {:?}",
1426 artifact.replay.status,
1427 );
1428 }
1429 let contract = &artifact.test.contract;
1430 if !matches!(contract.rsplit_once(':'), Some((p, n)) if !p.is_empty() && !n.is_empty()) {
1431 bail!(
1432 "symbolic counterexample artifact {display} test.contract must be `path:Contract`, got `{contract}`"
1433 );
1434 }
1435 Ok(Some(SymbolicArtifactReplayConfig { artifact, path: path.clone() }))
1436 }
1437
1438 fn load_fuzz_input(
1439 &self,
1440 output: &ProjectCompileOutput,
1441 config: &Config,
1442 inline_config: &InlineConfig,
1443 filter: &ProjectPathsAwareFilter,
1444 ) -> Result<Option<FuzzFailureReplayConfig>> {
1445 let Some(path) = &self.fuzz_input_file else {
1446 return Ok(None);
1447 };
1448 let failure = fs::read_json_file::<BaseCounterExample>(path)?;
1449 let Some(selector) = failure.calldata.get(..4) else {
1450 bail!(
1451 "fuzz input file {} contains calldata shorter than a 4-byte selector",
1452 path.display()
1453 );
1454 };
1455 let targets =
1456 matching_fuzz_replay_targets(output, config, inline_config, filter, selector)?;
1457 let [(contract, test)] = targets.as_slice() else {
1458 if targets.is_empty() {
1459 bail!(
1460 "fuzz input file {} does not match any selected stateless fuzz test",
1461 path.display()
1462 );
1463 }
1464 bail!(
1465 "fuzz input file {} matches {} selected stateless fuzz tests; replay requires exactly one target",
1466 path.display(),
1467 targets.len()
1468 );
1469 };
1470 Ok(Some(FuzzFailureReplayConfig {
1471 failure: Arc::new(failure),
1472 contract: contract.clone(),
1473 test: test.clone(),
1474 }))
1475 }
1476
1477 #[instrument(target = "forge::test", skip_all)]
1485 fn get_sources_to_compile(
1486 &self,
1487 config: &Config,
1488 test_filter: &ProjectPathsAwareFilter,
1489 symbolic_artifact_replay: Option<&SymbolicArtifactReplayConfig>,
1490 ) -> Result<(BTreeSet<PathBuf>, Option<Arc<InlineConfig>>)> {
1491 let src_files = || source_files_iter(&config.src, MultiCompilerLanguage::FILE_EXTENSIONS);
1492 let test_files = || source_files_iter(&config.test, MultiCompilerLanguage::FILE_EXTENSIONS);
1493
1494 if test_filter.is_empty() {
1497 return Ok((src_files().chain(test_files()).collect(), None));
1498 }
1499
1500 let mut project = config.create_project(config.cache, true)?;
1501 let sources = src_files()
1502 .chain(
1503 test_files().filter(|path| !path.is_sol_test() || test_filter.matches_path(path)),
1506 )
1507 .collect::<BTreeSet<_>>();
1508 let output = compile_abi_project_cached(
1509 &mut project,
1510 ProjectCompiler::new()
1511 .files(sources.iter().cloned())
1512 .dynamic_test_linking(config.dynamic_test_linking)
1513 .quiet(true),
1514 )?;
1515 if output.has_compiler_errors() {
1516 sh_println!("{output}")?;
1517 bail!("Compilation failed");
1518 }
1519
1520 let inline_config = Arc::new(InlineConfig::new_parsed(&output, config)?);
1521 let test_matcher =
1522 TestFunctionMatcher::new(config, &inline_config, symbolic_artifact_replay);
1523 let paths = config.project_paths::<MultiCompilerLanguage>();
1524 let empty_filter = EmptyTestFilter::default();
1525 let filter_args = test_filter.args();
1526 let has_contract_or_test_filter = filter_args.test_pattern.is_some()
1527 || filter_args.test_pattern_inverse.is_some()
1528 || filter_args.contract_pattern.is_some()
1529 || filter_args.contract_pattern_inverse.is_some();
1530
1531 let files = output
1536 .artifact_ids()
1537 .filter_map(|(id, artifact)| artifact.abi.as_ref().map(|abi| (id, abi)))
1538 .filter(|(id, _)| sources.contains(&id.source))
1542 .filter(|(id, abi)| {
1543 if id.source.starts_with(&paths.sources) {
1544 return true;
1545 }
1546 if paths.is_script(&id.source) && !paths.is_test(&id.source) {
1547 return false;
1548 }
1549 let stripped = id.clone().with_stripped_file_prefixes(&config.root);
1550 if stripped.source.is_sol_test() {
1554 return if has_contract_or_test_filter {
1555 test_matcher.matches_contract(test_filter, &stripped, abi)
1556 } else {
1557 test_filter.matches_path(&stripped.source)
1558 };
1559 }
1560 !test_matcher.matches_contract(&empty_filter, &stripped, abi)
1561 || test_matcher.matches_contract(test_filter, &stripped, abi)
1562 })
1563 .map(|(id, _)| id.source)
1564 .collect();
1565 Ok((files, Some(inline_config)))
1566 }
1567
1568 pub async fn compile_and_run(&mut self) -> Result<TestOutcome> {
1575 self.ensure_mutation_mode_compatible(false)?;
1576 let compiled = self.compile_project().await?;
1577 self.run_tests(
1578 &compiled.project_root,
1579 compiled.config,
1580 compiled.evm_opts,
1581 &compiled.output,
1582 &compiled.filter,
1583 TestExecutionOptions {
1584 replay_symbolic_artifact: compiled.replay_symbolic_artifact,
1585 selected_sources: compiled.selected_sources,
1586 ..TestExecutionOptions::default_run(compiled.inline_config)
1587 },
1588 )
1589 .await
1590 }
1591
1592 fn brutalize_workspace(&self, config: &mut Config) -> Result<TempDir> {
1595 let silent = shell::is_json();
1596 let temp_dir = TempDir::with_prefix("forge_brutalize_")?;
1597 let temp_path = temp_dir.path();
1598
1599 if config.via_ir && !silent {
1600 sh_warn!(
1601 "--brutalize value cast dirty-bits checks are ineffective with via-IR; memory and free-memory-pointer checks still apply"
1602 )?;
1603 }
1604 if !silent {
1605 sh_status!("Brutalizing source files...")?;
1606 }
1607 workspace::copy_project(config, temp_path)?;
1608 let count = brutalizer::brutalize_project(config, temp_path)?;
1609 if !silent {
1610 sh_status!("Brutalized {count} source files, compiling from temp workspace...")?;
1611 }
1612
1613 let test_failures_file = config.test_failures_file.clone();
1614 *config = workspace::rebase_config_paths(config, temp_path).sanitized();
1615 config.test_failures_file = test_failures_file;
1616 Ok(temp_dir)
1617 }
1618
1619 async fn compile_project(&mut self) -> Result<CompiledTestProject> {
1620 let (mut config, evm_opts) = self.load_config_and_evm_opts()?;
1622
1623 self.install_missing_dependencies(&mut config)?;
1624 let brutalized_workspace =
1625 if self.brutalize { Some(self.brutalize_workspace(&mut config)?) } else { None };
1626 let should_mutate = self.mutate.is_some();
1627 if should_mutate {
1628 config.dynamic_test_linking = true;
1630 config.cache = true;
1631 apply_mutation_compiler_overrides(&mut config);
1632 }
1633 self.apply_test_config_overrides(&mut config);
1634
1635 let mut project = config.project()?;
1637 let project_root = project.paths.root.clone();
1638
1639 let replay_symbolic_artifact = self.load_symbolic_artifact_replay()?;
1640 let mut filter = self.filter(&config)?;
1641 if let Some(replay) = &replay_symbolic_artifact {
1642 let filter_args = filter.args_mut();
1643 filter_args.test_pattern_inverse = None;
1644 filter_args.contract_pattern_inverse = None;
1645 filter_args.path_pattern_inverse = None;
1646 let contract = replay.artifact.test.contract.as_str();
1647 let (path, contract) = contract.rsplit_once(':').unwrap_or(("", contract));
1648 filter_args.test_pattern =
1649 Some(Regex::new(&format!("^{}$", regex::escape(&replay.artifact.test.test)))?);
1650 filter_args.contract_pattern =
1651 Some(Regex::new(&format!("^{}$", regex::escape(contract)))?);
1652 if !path.is_empty() {
1653 filter_args.path_pattern = Some(globset::escape(path).parse::<GlobMatcher>()?);
1654 }
1655 }
1656 trace!(target: "forge::test", ?filter, "using filter");
1657
1658 let compiler = ProjectCompiler::new()
1659 .dynamic_test_linking(config.dynamic_test_linking)
1660 .quiet(shell::is_json() || self.junit);
1661 let (output, selected_sources, inline_config) = if self.list {
1662 let compiler = if filter.args().path_pattern.is_some()
1664 && config.extra_output.is_empty()
1665 && config.extra_output_files.is_empty()
1666 && !config.build_info
1667 {
1668 let files = project
1669 .paths
1670 .input_files_iter()
1671 .filter(|path| filter.matches_path(path))
1672 .collect::<Vec<_>>();
1673 if files.is_empty() { compiler } else { compiler.files(files) }
1674 } else {
1675 compiler
1676 };
1677 (compile_abi_project(&mut project, compiler)?, BTreeSet::new(), None)
1678 } else {
1679 let (files, inline_config) =
1680 self.get_sources_to_compile(&config, &filter, replay_symbolic_artifact.as_ref())?;
1681 let output = compiler.files(files.clone()).compile(&project);
1682 let output = if should_mutate {
1683 output.wrap_err(
1684 "Mutation testing compiler profile failed to compile before applying mutations",
1685 )?
1686 } else {
1687 output?
1688 };
1689 (output, files, inline_config)
1690 };
1691 let inline_config = match inline_config {
1692 Some(inline_config) => inline_config,
1693 None => Arc::new(InlineConfig::new_parsed(&output, &config)?),
1694 };
1695
1696 Ok(CompiledTestProject {
1697 project_root,
1698 config,
1699 evm_opts,
1700 output,
1701 filter,
1702 inline_config,
1703 replay_symbolic_artifact,
1704 selected_sources,
1705 _brutalized_workspace: brutalized_workspace,
1706 })
1707 }
1708
1709 pub(crate) async fn prepare_fuzz_minimize_replay(
1710 &mut self,
1711 corpus_dir: &Path,
1712 ) -> Result<FuzzMinimizeReplaySession> {
1713 let CompiledTestProject { mut config, mut evm_opts, output, filter, inline_config, .. } =
1714 self.compile_project().await?;
1715
1716 if config.fuzz.run == Some(0) {
1717 bail!("`fuzz.run` must be greater than 0");
1718 }
1719 self.apply_gas_report_overrides(&mut config, &mut evm_opts);
1720 for corpus in [&mut config.fuzz.corpus, &mut config.invariant.corpus] {
1721 corpus.corpus_dir.get_or_insert_with(|| corpus_dir.to_path_buf());
1722 }
1723 config.fuzz.seed = config.fuzz.seed.or(Some(U256::ZERO));
1724
1725 evm_opts.infer_network_from_fork().await?;
1726 config.networks = evm_opts.networks;
1727
1728 let override_networks = inline_config.referenced_override_networks(&config.profile);
1729 let (default_pass, override_passes) = network_passes(config, evm_opts, &override_networks);
1730 let mut passes = Vec::new();
1731 for NetworkPass { config, evm_opts, multi_network } in
1732 std::iter::once(default_pass).chain(override_passes)
1733 {
1734 let execution = TestExecutionOptions {
1735 multi_network,
1736 ..TestExecutionOptions::default_run(inline_config.clone())
1737 };
1738 let config = Arc::new(config);
1739 passes.push(dispatch_network!(&evm_opts, |Net| {
1740 let runner = self
1741 .build_runner::<Net>(
1742 config,
1743 evm_opts,
1744 &output,
1745 execution,
1746 None,
1747 ExecutorBuilder::<Net>::new(),
1748 )
1749 .await?;
1750 fuzz_minimize_pass(runner, &filter)
1751 }));
1752 }
1753
1754 if passes.iter().all(|pass| pass.target_count == 0) {
1755 bail!("fuzz minimization requires at least one matched fuzz or invariant test");
1756 }
1757 Ok(FuzzMinimizeReplaySession { filter, passes })
1758 }
1759
1760 pub(crate) async fn run_tests(
1764 &mut self,
1765 project_root: &Path,
1766 mut config: Config,
1767 mut evm_opts: EvmOpts,
1768 output: &ProjectCompileOutput,
1769 filter: &ProjectPathsAwareFilter,
1770 mut execution: TestExecutionOptions,
1771 ) -> Result<TestOutcome> {
1772 self.ensure_mutation_mode_compatible(execution.coverage)?;
1773
1774 if config.fuzz.run == Some(0) {
1775 bail!("`fuzz.run` must be greater than 0");
1776 }
1777
1778 if self.list {
1779 return list_from_output(
1780 output,
1781 &config,
1782 &execution.inline_config,
1783 filter,
1784 self.fuzz_only,
1785 execution.replay_symbolic_artifact.as_ref(),
1786 );
1787 }
1788
1789 execution.fuzz_input =
1790 self.load_fuzz_input(output, &config, &execution.inline_config, filter)?;
1791 self.warn_unsupported_engine_flags(
1792 output,
1793 &config,
1794 &execution.inline_config,
1795 filter,
1796 &execution.multi_network,
1797 )?;
1798
1799 let mut filter = filter.clone();
1800 self.apply_gas_report_overrides(&mut config, &mut evm_opts);
1801
1802 config.fuzz.seed = config
1804 .fuzz
1805 .seed
1806 .or_else(|| Some(U256::from_be_bytes(rand::rng().random::<[u8; 32]>())));
1807
1808 let trace_output = if self.flamegraph {
1809 Some(TraceOutputKind::Flamegraph)
1810 } else if self.flamechart {
1811 Some(TraceOutputKind::Flamechart)
1812 } else {
1813 self.evm_profile.map(TraceOutputKind::EvmProfile)
1814 };
1815
1816 if evm_opts.verbosity < 3 && (self.gas_report || trace_output.is_some()) {
1818 evm_opts.verbosity = 3;
1819 }
1820
1821 config.tracing = self.tracing.resolve(&config.tracing, evm_opts.verbosity);
1824 let json_trace_depth = config.tracing.trace_depth;
1825 execution.decode_internal = if config.tracing.decode_internal || trace_output.is_some() {
1826 InternalTraceMode::Simple
1827 } else {
1828 InternalTraceMode::None
1829 };
1830
1831 evm_opts.infer_network_from_fork().await?;
1833 config.networks = evm_opts.networks;
1836 let verbosity = evm_opts.verbosity;
1837
1838 let config_for_mutation = config.clone();
1840 let evm_opts_for_mutation = evm_opts.clone();
1841 let mutation_fork =
1842 if self.mutate.is_some() { evm_opts.resolve_fork().await? } else { None };
1843
1844 let override_networks =
1846 execution.inline_config.referenced_override_networks(&config.profile);
1847 let is_multi_pass = !override_networks.is_empty();
1848 let multi_pass_timer = Instant::now();
1849 let (default_pass, override_passes) = network_passes(config, evm_opts, &override_networks);
1850 let (libraries, mut outcome) = self
1851 .run_network_pass(
1852 default_pass,
1853 output,
1854 &mut filter,
1855 execution.clone(),
1856 mutation_fork.as_ref(),
1857 )
1858 .await?;
1859 for pass in override_passes {
1860 let (_, pass_outcome) =
1861 self.run_network_pass(pass, output, &mut filter, execution.clone(), None).await?;
1862 merge_outcomes(&mut outcome, pass_outcome);
1863 }
1864 if is_multi_pass {
1865 self.print_summary(&outcome, multi_pass_timer.elapsed())?;
1867 }
1868
1869 if let Some(replay) = &execution.replay_symbolic_artifact {
1870 let target = &replay.artifact.test;
1871 match outcome.tests().count() {
1872 0 => bail!(
1873 "symbolic artifact target `{}::{}` was not found",
1874 target.contract,
1875 target.test
1876 ),
1877 1 => {}
1878 replayed => bail!(
1879 "symbolic artifact target `{}::{}` matched {replayed} tests; replay requires exactly one target",
1880 target.contract,
1881 target.test
1882 ),
1883 }
1884 }
1885
1886 if let Some(path) = &self.json_file {
1887 let mut results =
1888 outcome.json_file_results.take().unwrap_or_else(|| outcome.results.clone());
1889 prepare_results_for_json(&mut results, verbosity, json_trace_depth);
1890 fs::write_json_file(path, &results)?;
1891 }
1892
1893 if let Some(trace_output) = trace_output {
1894 self.render_trace_output(trace_output, &mut outcome).await?;
1895 }
1896
1897 if self.debug {
1898 let (_, _, test_result) =
1900 outcome.remove_first().ok_or_eyre("no tests were executed")?;
1901 let sources =
1902 ContractSources::from_project_output(output, project_root, Some(&libraries))?;
1903
1904 let mut traces = test_result
1907 .traces
1908 .iter()
1909 .filter(|(kind, _)| kind.is_execution())
1910 .cloned()
1911 .collect::<Vec<_>>();
1912 if traces.is_empty() {
1913 traces = test_result.traces.clone();
1914 }
1915 if let Some(decoder) = &outcome.last_run_decoder {
1916 for (_, arena) in &mut traces {
1917 decode_trace_arena(arena, decoder).await;
1918 }
1919 }
1920
1921 let mut builder = Debugger::builder()
1922 .traces(traces)
1923 .sources(sources)
1924 .breakpoints(test_result.breakpoints)
1925 .layout(self.debug_layout.unwrap_or_default());
1926 if let Some(decoder) = &outcome.last_run_decoder {
1927 builder = builder.decoder(decoder);
1928 }
1929 if let Some(known_contracts) = &outcome.known_contracts {
1930 builder = builder.known_contracts(known_contracts);
1931 }
1932 let mut debugger = builder.build();
1933 if let Some(dump_path) = &self.dump {
1934 debugger.dump_to_file(dump_path)?;
1935 } else {
1936 debugger.try_run_tui()?;
1937 }
1938 }
1939
1940 if let Some(mutate) = &self.mutate {
1942 if outcome.failed() > 0 {
1943 bail!(
1944 "Mutation testing compiler profile failed its unmutated baseline run; \
1945 adjust `--mutation-via-ir` / `--mutation-optimizer-runs` or fix the tests \
1946 before running mutation testing"
1947 );
1948 }
1949 if outcome.successes().next().is_none() {
1952 bail!(
1953 "Mutation testing requires at least one passing baseline test; the current \
1954 filter/path selection matched zero non-skipped tests. Loosen `--match-test` / \
1955 `--match-contract` / `--match-path` or check the project layout."
1956 );
1957 }
1958 if !mutate.is_empty() && self.mutate_path.is_some() {
1960 bail!(
1961 "`--mutate-path <PATTERN>` cannot be combined with explicit paths passed to `--mutate`; pass either paths or a glob pattern, not both"
1962 );
1963 }
1964 if is_multi_pass {
1968 bail!(
1969 "Mutation testing does not yet support inline per-test network overrides \
1970 (found {} annotated network(s)). Re-run without `--mutate` or remove the \
1971 per-test network annotations.",
1972 override_networks.len()
1973 );
1974 }
1975 ensure_mutation_workspace_safe(&config_for_mutation)?;
1976
1977 let json_output = shell::is_json();
1978 let selected_sources_relative = execution
1979 .selected_sources
1980 .iter()
1981 .filter_map(|path| {
1982 path.strip_prefix(&config_for_mutation.root).ok().map(PathBuf::from)
1983 })
1984 .collect::<Vec<_>>();
1985 let mutation_config = MutationRunConfig {
1986 mutate_paths: mutate.clone(),
1987 mutate_path_pattern: self.mutate_path.clone(),
1988 mutate_contract_pattern: self.mutate_contract.clone(),
1989 num_workers: self.mutation_jobs.unwrap_or(0),
1990 show_progress: self.show_progress,
1991 json_output,
1992 filter_args: filter.args().clone(),
1996 rerun_failures: filter.rerun_failures().map(<[RerunFailure]>::to_vec),
1997 selected_sources_relative,
1998 isolate: evm_opts_for_mutation.isolate,
1999 };
2000 let result = run_mutation_testing(
2001 Arc::new(config_for_mutation),
2002 output,
2003 evm_opts_for_mutation,
2004 mutation_fork,
2005 mutation_config,
2006 )
2007 .await?;
2008 if result.cancelled {
2009 std::process::exit(130);
2010 }
2011 if json_output {
2012 let json_output = result.summary.to_json_output(result.duration_secs);
2013 sh_println!("{}", serde_json::to_string(&json_output)?)?;
2014 }
2015 outcome = TestOutcome::empty(None, true);
2016 }
2017
2018 Ok(outcome)
2019 }
2020
2021 async fn render_trace_output(
2023 &self,
2024 trace_output: TraceOutputKind,
2025 outcome: &mut TestOutcome,
2026 ) -> Result<()> {
2027 let label = trace_output.label();
2028 let no_tests = match trace_output {
2029 TraceOutputKind::EvmProfile(_) => "cannot generate EVM profile: no tests were executed",
2030 TraceOutputKind::Flamegraph | TraceOutputKind::Flamechart => "no tests were executed",
2031 };
2032 if outcome.tests().next().is_none() {
2033 bail!("{no_tests}");
2034 }
2035 let decoder = outcome
2036 .last_run_decoder
2037 .clone()
2038 .ok_or_else(|| eyre::eyre!("cannot generate {label}: missing trace decoder"))?;
2039 let (suite_name, test_name, test_result) =
2040 outcome
2041 .results
2042 .iter_mut()
2043 .find_map(|(suite_name, suite)| {
2044 suite.test_results.iter_mut().next().map(|(test_name, result)| {
2045 (suite_name.as_str(), test_name.as_str(), result)
2046 })
2047 })
2048 .ok_or_else(|| eyre::eyre!("{no_tests}"))?;
2049 let contract = suite_name.split(':').next_back().unwrap();
2050 let test_name = test_name.trim_end_matches("()");
2051 let (_, arena) = test_result
2052 .traces
2053 .iter_mut()
2054 .find(|(kind, _)| *kind == TraceKind::Execution)
2055 .ok_or_else(|| {
2056 eyre::eyre!(
2057 "cannot generate {label} for {contract}::{test_name}: no execution trace \
2058 (test may have failed in setUp/constructor or been skipped)"
2059 )
2060 })?;
2061 decode_trace_arena(arena, &decoder).await;
2062
2063 match trace_output {
2064 TraceOutputKind::Flamegraph | TraceOutputKind::Flamechart => {
2065 let mut folded_stack_trace = folded_stack_trace::build(arena, self.evm.isolate);
2066 let flame_chart = trace_output == TraceOutputKind::Flamechart;
2067 if flame_chart {
2068 folded_stack_trace.reverse();
2069 }
2070 let file_name = format!("cache/{label}_{contract}_{test_name}.svg");
2071 let file = std::fs::File::create(&file_name).wrap_err("failed to create file")?;
2072 let mut options = inferno::flamegraph::Options::default();
2073 options.title = format!("{label} {contract}::{test_name}");
2074 options.count_name = "gas".to_string();
2075 options.flame_chart = flame_chart;
2076 inferno::flamegraph::from_lines(
2077 &mut options,
2078 folded_stack_trace.iter().map(String::as_str),
2079 std::io::BufWriter::new(file),
2080 )
2081 .wrap_err("failed to write svg")?;
2082 sh_println!("Saved to {file_name}")?;
2083 if !self.no_open
2084 && let Err(e) = opener::open(&file_name)
2085 {
2086 sh_err!("Failed to open {file_name}; please open it manually: {e}")?;
2087 }
2088 }
2089 TraceOutputKind::EvmProfile(EvmProfileFormat::Speedscope) => {
2090 let profile =
2091 speedscope::builder::build(arena, test_name, contract, self.evm.isolate);
2092 let profile_json = serde_json::to_vec(&profile)?;
2093 let profile_path = format!("cache/evm_profile_{contract}_{test_name}.json");
2094 fs::write(&profile_path, &profile_json)?;
2095 sh_println!("Profile saved to {profile_path}")?;
2096 if !self.no_open {
2097 evm_profile_server::serve_and_open(profile_json, test_name, contract).await?;
2098 }
2099 }
2100 }
2101 Ok(())
2102 }
2103
2104 async fn build_runner<FEN: FoundryEvmNetwork>(
2106 &self,
2107 config: Arc<Config>,
2108 evm_opts: EvmOpts,
2109 output: &ProjectCompileOutput,
2110 execution: TestExecutionOptions,
2111 resolved_fork: Option<&ResolvedFork>,
2112 executor_builder: ExecutorBuilder<FEN>,
2113 ) -> Result<MultiContractRunner<FEN>> {
2114 let (evm_env, tx_env, fork) = if let Some(fork) = resolved_fork {
2115 let (evm_env, tx_env) = evm_opts
2116 .env_with_resolved_fork::<SpecFor<FEN>, BlockEnvFor<FEN>, TxEnvFor<FEN>>(Some(fork))
2117 .await?;
2118 (evm_env, tx_env, Some(fork.clone()))
2119 } else {
2120 evm_opts.env_resolved::<SpecFor<FEN>, BlockEnvFor<FEN>, TxEnvFor<FEN>>().await?
2121 };
2122 let fork_context = fork.as_ref().map(|fork| fork.context());
2123 let create2_deployer_available =
2124 evm_opts.can_use_create2_deployer_resolved(fork.as_ref()).await?;
2125
2126 MultiContractRunnerBuilder::new(config.clone(), execution.inline_config)
2127 .set_debug(self.debug)
2128 .set_decode_internal(execution.decode_internal)
2129 .set_record_all_steps(self.evm_profile.is_some())
2130 .initial_balance(evm_opts.initial_balance)
2131 .sender(evm_opts.sender)
2132 .with_fork(evm_opts.get_fork_resolved(&config, evm_env.cfg_env.chain_id, fork.as_ref()))
2133 .with_fork_chain_id(fork_context.map(|context| context.source_chain_id))
2134 .with_fork_hardfork(fork_context.and_then(|context| context.hardfork))
2135 .enable_isolation(evm_opts.isolate)
2136 .fail_fast(self.fail_fast)
2137 .set_coverage(execution.coverage)
2138 .with_multi_network(execution.multi_network)
2139 .with_showmap(self.showmap_config()?)
2140 .with_fuzz_only(self.fuzz_only)
2141 .with_fuzz_failure_replay(self.fuzz_failure_replay)
2142 .with_fuzz_input(execution.fuzz_input)
2143 .with_symbolic_artifact_replay(execution.replay_symbolic_artifact)
2144 .with_create2_deployer_available(create2_deployer_available)
2145 .build::<FEN, MultiCompiler>(output, evm_env, tx_env, evm_opts, executor_builder)
2146 }
2147
2148 async fn run_network_pass(
2150 &self,
2151 pass: NetworkPass,
2152 output: &ProjectCompileOutput,
2153 filter: &mut ProjectPathsAwareFilter,
2154 execution: TestExecutionOptions,
2155 resolved_fork: Option<&ResolvedFork>,
2156 ) -> Result<(Libraries, TestOutcome)> {
2157 let NetworkPass { config, evm_opts, multi_network } = pass;
2158 let execution = TestExecutionOptions { multi_network, ..execution };
2159 let verbosity = evm_opts.verbosity;
2160 let config = Arc::new(config);
2161 dispatch_network!(&evm_opts, |Net| {
2162 let runner = self
2163 .build_runner::<Net>(
2164 config.clone(),
2165 evm_opts,
2166 output,
2167 execution,
2168 resolved_fork,
2169 ExecutorBuilder::<Net>::new(),
2170 )
2171 .await?;
2172 let libraries = runner.libraries.clone();
2173 let outcome = self.run_tests_inner(runner, config, verbosity, filter, output).await?;
2174 Ok((libraries, outcome))
2175 })
2176 }
2177
2178 fn emit_symbolic_regressions(
2181 &self,
2182 config: &Config,
2183 known_contracts: &ContractsByArtifact,
2184 results: &mut BTreeMap<String, SuiteResult>,
2185 ) -> Result<Vec<SymbolicRegression>> {
2186 if !self.emit_regression {
2187 return Ok(Vec::new());
2188 }
2189 let regression = SymbolicRegressionConfig {
2190 out: self
2191 .regression_out
2192 .clone()
2193 .map(|path| if path.is_relative() { config.root.join(path) } else { path }),
2194 overwrite: self.regression_overwrite,
2195 };
2196 let artifacts = collect_symbolic_artifacts_from_suites(results.values());
2197 let regressions =
2198 emit_symbolic_regressions(config, ®ression, known_contracts, &artifacts)?;
2199 attach_symbolic_regressions_to_suites(results.values_mut(), ®ressions);
2200 Ok(regressions)
2201 }
2202
2203 fn print_summary(&self, outcome: &TestOutcome, duration: Duration) -> Result<()> {
2205 if !self.summary && !shell::is_json() {
2206 sh_println!("{}", outcome.summary(duration))?;
2207 }
2208 if self.summary && !outcome.results.is_empty() {
2209 sh_println!("{}", TestSummaryReport::new(self.detailed, outcome))?;
2210 }
2211 Ok(())
2212 }
2213
2214 async fn run_tests_inner<FEN: FoundryEvmNetwork>(
2216 &self,
2217 mut runner: MultiContractRunner<FEN>,
2218 config: Arc<Config>,
2219 verbosity: u8,
2220 filter: &mut ProjectPathsAwareFilter,
2221 output: &ProjectCompileOutput,
2222 ) -> Result<TestOutcome> {
2223 let fuzz_seed = config.fuzz.seed;
2224
2225 trace!(target: "forge::test", "running all tests");
2226
2227 let silent = shell::is_json() && (self.gas_report || self.summary || self.mutate.is_some());
2229 let tracing = &config.tracing;
2230 let trace_verbosity = tracing.verbosity;
2231
2232 let mut num_filtered = runner.matching_test_functions(filter).count();
2233
2234 if !self.opcodes.is_empty() && trace_verbosity < 5 {
2235 sh_eprintln!()?;
2236 bail!("Not enough verbosity. Use -vvvvv to show opcodes.");
2237 }
2238
2239 if num_filtered == 0 {
2240 let total_tests = if filter.is_empty() {
2241 num_filtered
2242 } else {
2243 runner.matching_test_functions(&EmptyTestFilter::default()).count()
2244 };
2245 if total_tests == 0 {
2246 sh_warn!(
2247 "No tests found in project! Forge looks for functions that start with `test`"
2248 )?;
2249 } else {
2250 let mut msg = format!("no tests match the provided pattern:\n{filter}");
2251 if let Some(test_pattern) = &filter.args().test_pattern {
2253 let candidates = runner.all_test_functions(filter).map(|f| &f.name);
2255 if let Some(suggestion) =
2256 utils::did_you_mean(test_pattern.as_str(), candidates).pop()
2257 {
2258 write!(msg, "\nDid you mean `{suggestion}`?")?;
2259 }
2260 }
2261 sh_warn!("{msg}")?;
2262 }
2263 return Ok(TestOutcome::empty(Some(runner.known_contracts.clone()), false));
2264 }
2265
2266 let debug_selection_term = Term::stderr();
2267 let interactive_debug_selection = self.debug
2268 && num_filtered != 1
2269 && tui_mode().is_interactive()
2270 && debug_selection_term.is_term();
2271 let mut matching_debug_tests = if interactive_debug_selection {
2272 collect_matching_debug_tests(&runner.list_signatures(filter))
2273 } else if self.debug && num_filtered != 1 {
2274 collect_matching_debug_tests(&runner.list(filter))
2275 } else {
2276 Vec::new()
2277 };
2278 if interactive_debug_selection {
2279 ctrlc::set_handler(|| {
2280 let _ = Term::stderr().show_cursor();
2281 std::process::exit(130);
2282 })?;
2283
2284 let Some(selected) = Select::new()
2285 .with_prompt("Select a test to debug")
2286 .items(
2287 matching_debug_tests
2288 .iter()
2289 .map(|test| format!("{}.{}", test.contract, test.test)),
2290 )
2291 .max_length(DEBUGGER_MATCHING_TESTS_DISPLAY_LIMIT)
2292 .interact_on_opt(&debug_selection_term)?
2293 else {
2294 bail!("Debugger test selection cancelled");
2295 };
2296
2297 filter.set_rerun_failures(vec![matching_debug_tests.swap_remove(selected)]);
2298 num_filtered = 1;
2299 }
2300
2301 if num_filtered != 1
2302 && (self.debug || self.flamegraph || self.flamechart || self.evm_profile.is_some())
2303 {
2304 let action = if self.flamegraph {
2305 "generate a flamegraph"
2306 } else if self.flamechart {
2307 "generate a flamechart"
2308 } else if self.evm_profile.is_some() {
2309 "generate an EVM profile"
2310 } else {
2311 "run the debugger"
2312 };
2313 let filter_hint = if filter.is_empty() {
2314 String::new()
2315 } else {
2316 format!("\n\nFilter used:\n{filter}")
2317 };
2318 let matching_tests_hint = if self.debug {
2319 format_matching_debug_tests(&matching_debug_tests)
2320 } else {
2321 String::new()
2322 };
2323 let narrowing_hint = if self.debug {
2324 "Use --match-test <TEST_NAME>, --match-contract, and --match-path to further limit the search."
2325 } else {
2326 "Use --match-contract and --match-path to further limit the search."
2327 };
2328 bail!(
2329 "{num_filtered} tests matched your criteria, but exactly 1 test must match in order to {action}.{matching_tests_hint}\n\n\
2330 {narrowing_hint}{filter_hint}",
2331 );
2332 }
2333
2334 if num_filtered == 1 && runner.decode_internal != InternalTraceMode::None {
2336 runner.decode_internal = InternalTraceMode::Full;
2337 }
2338
2339 let serialize_json =
2341 self.mutate.is_none() && !self.gas_report && !self.summary && shell::is_json();
2342 if serialize_json || self.junit {
2343 let mut results = runner.test_collect(filter)?;
2344 if serialize_json {
2345 prepare_results_for_json(&mut results, verbosity, tracing.trace_depth);
2346 }
2347 self.emit_symbolic_regressions(&config, &runner.known_contracts, &mut results)?;
2348 let rendered = if serialize_json {
2349 serde_json::to_string(&results)?
2350 } else {
2351 junit_xml_report(&results, verbosity).to_string()?
2352 };
2353 sh_println!("{rendered}")?;
2354 return Ok(TestOutcome::new(
2355 Some(runner.known_contracts),
2356 results,
2357 self.allow_failure,
2358 fuzz_seed,
2359 ));
2360 }
2361
2362 let remote_chain = runner
2363 .fork
2364 .is_some()
2365 .then(|| runner.tcfg.fork_chain_id.or(runner.tx_env.chain_id()))
2366 .flatten()
2367 .map(Into::into);
2368 let known_contracts = runner.known_contracts.clone();
2369 let libraries = runner.libraries.clone();
2370
2371 let is_multi_pass = !runner.tcfg.multi_network.all_override_networks.is_empty();
2375 let resolved_hardfork = runner.tcfg.hardfork;
2376 let networks = runner.tcfg.evm_opts.networks;
2377 let extra_cheatcode_addresses = runner.tcfg.executor_builder.extra_cheatcode_addresses();
2378 let decode_internal = runner.decode_internal != InternalTraceMode::None;
2379
2380 let (tx, rx) = channel::<(String, SuiteResult)>();
2382 let timer = Instant::now();
2383 let show_progress = config.show_progress;
2384 let handle = tokio::task::spawn_blocking({
2385 let filter = filter.clone();
2386 move || runner.test(&filter, tx, show_progress).map(|()| runner)
2387 });
2388
2389 let mut identifier = TraceIdentifiers::new().with_local(&known_contracts);
2391
2392 if !self.gas_report && remote_chain.is_some() {
2396 identifier = identifier.with_external(&config, remote_chain)?;
2397 }
2398
2399 let mut builder = CallTraceDecoderBuilder::new()
2401 .with_tracing_config(tracing)
2402 .with_known_contracts(&known_contracts)
2403 .with_networks(networks)
2404 .with_chain_id(remote_chain.map(|c| c.id()))
2405 .with_hardfork(resolved_hardfork);
2406 if !self.gas_report {
2408 builder =
2409 builder.with_signature_identifier(SignaturesIdentifier::from_config(&config)?);
2410 }
2411 if decode_internal {
2412 let sources =
2413 ContractSources::from_project_output(output, &config.root, Some(&libraries))?;
2414 builder = builder.with_debug_identifier(DebugTraceIdentifier::new(sources));
2415 }
2416 let mut decoder = builder.build();
2417
2418 let mut gas_report = self.gas_report.then(|| {
2419 GasReport::new(
2420 config.gas_reports.clone(),
2421 config.gas_reports_ignore.clone(),
2422 config.gas_reports_include_tests,
2423 extra_cheatcode_addresses.iter().copied(),
2424 )
2425 });
2426
2427 let mut gas_snapshots = BTreeMap::<String, BTreeMap<String, String>>::new();
2428
2429 let mut outcome = TestOutcome::empty(None, self.allow_failure);
2430 outcome.fuzz_seed = fuzz_seed;
2431
2432 let always_identify_traces = self.gas_report
2434 || self.debug
2435 || self.flamegraph
2436 || self.flamechart
2437 || self.evm_profile.is_some();
2438
2439 let mut any_test_failed = false;
2440 let mut backtrace_builder = None;
2441 while let Ok((contract_name, mut suite_result)) = rx.recv() {
2442 let len = suite_result.len();
2443 let tests = &mut suite_result.test_results;
2444 let has_tests = !tests.is_empty();
2445
2446 if is_multi_pass && !has_tests && suite_result.warnings.is_empty() {
2450 continue;
2451 }
2452
2453 decoder.clear_addresses();
2455
2456 if !silent {
2458 sh_println!()?;
2459 for warning in &suite_result.warnings {
2460 sh_warn!("{warning}")?;
2461 }
2462 if has_tests {
2463 let tests = if len > 1 { "tests" } else { "test" };
2464 sh_println!("Ran {len} {tests} for {contract_name}")?;
2465 }
2466 }
2467
2468 for (name, result) in tests {
2470 let test_failed = result.status.is_failure();
2471 let show_traces = !self.suppress_successful_traces || test_failed;
2472 let should_include_trace = |kind: &TraceKind| match kind {
2478 TraceKind::Execution => {
2479 (trace_verbosity == 3 && test_failed) || trace_verbosity >= 4
2480 }
2481 TraceKind::Setup => {
2482 (trace_verbosity == 4 && test_failed) || trace_verbosity >= 5
2483 }
2484 TraceKind::Deployment => false,
2485 };
2486 let renders_trace = !silent
2487 && show_traces
2488 && result.traces.iter().any(|(kind, _)| should_include_trace(kind));
2489 let identify_addresses = always_identify_traces || renders_trace;
2490
2491 if !silent {
2492 sh_println!("{}", result.short_result_with_suite(name, &contract_name))?;
2493 for artifact in &result.counterexample_artifacts {
2494 sh_warn!("Counterexample artifact: {}", artifact.path.display())?;
2495 }
2496
2497 if let TestKind::Invariant { metrics, .. } = &result.kind
2498 && !metrics.is_empty()
2499 {
2500 let _ = sh_println!("\n{}\n", format_invariant_metrics_table(metrics));
2501 }
2502
2503 if verbosity >= 2 && show_traces {
2505 let console_logs = decode_console_logs(&result.logs);
2507 if !console_logs.is_empty() {
2508 sh_println!("Logs:")?;
2509 for log in console_logs {
2510 sh_println!(" {log}")?;
2511 }
2512 sh_println!()?;
2513 }
2514 }
2515 }
2516
2517 any_test_failed |= result.status == TestStatus::Failure;
2520
2521 decoder.clear_addresses();
2523 if identify_addresses {
2524 decoder.labels.extend(result.labels.iter().map(|(k, v)| (*k, v.clone())));
2525 }
2526
2527 let mut decoded_traces = Vec::new();
2529 if identify_addresses {
2530 for (kind, arena) in &mut result.traces {
2531 if self.debug && !result.debug_bytecodes.is_empty() {
2532 let mut local_identifier = TraceIdentifiers::new()
2533 .with_local_and_bytecodes(
2534 &known_contracts,
2535 &result.debug_bytecodes,
2536 );
2537 decoder.identify(arena, &mut local_identifier);
2538 }
2539 decoder.identify(arena, &mut identifier);
2540
2541 if renders_trace && should_include_trace(kind) {
2542 decoder.opcodes = self.opcodes.clone();
2543 decode_trace_arena(arena, &decoder).await;
2544 let rendered = match tracing.trace_depth {
2545 Some(trace_depth) => {
2546 let mut arena = arena.clone();
2547 prune_trace_depth(&mut arena, trace_depth);
2548 render_trace_arena_inner(&arena, false, trace_verbosity > 4)
2549 }
2550 None => render_trace_arena_inner(arena, false, trace_verbosity > 4),
2551 };
2552 decoded_traces.push(rendered);
2553 }
2554 }
2555 }
2556
2557 if !silent && show_traces && !decoded_traces.is_empty() {
2558 sh_println!("Traces:")?;
2559 for trace in &decoded_traces {
2560 sh_println!("{trace}")?;
2561 }
2562 }
2563
2564 if !silent
2568 && test_failed
2569 && trace_verbosity >= 3
2570 && let Some((_, arena)) =
2571 result.traces.iter().find(|(kind, _)| matches!(kind, TraceKind::Execution))
2572 {
2573 let builder = backtrace_builder.get_or_insert_with(|| {
2574 BacktraceBuilder::new(
2575 output,
2576 config.root.clone(),
2577 config.parsed_libraries().ok(),
2578 config.via_ir,
2579 )
2580 });
2581 let backtrace = builder.from_traces(arena);
2582 if !backtrace.is_empty() {
2583 sh_println!("{}", backtrace)?;
2584 }
2585 }
2586
2587 if let Some(gas_report) = &mut gas_report {
2588 gas_report.analyze(result.traces.iter().map(|(_, a)| &a.arena), &decoder).await;
2589
2590 for trace in &result.gas_report_traces {
2591 decoder.clear_addresses();
2592
2593 for (kind, arena) in &result.traces {
2596 if !matches!(kind, TraceKind::Execution) {
2597 decoder.identify_scoped(arena, &mut identifier);
2598 }
2599 }
2600
2601 for arena in trace {
2602 decoder.identify_scoped(arena, &mut identifier);
2603 gas_report.analyze([arena], &decoder).await;
2604 }
2605 }
2606 }
2607
2608 if shell::is_json()
2609 && let Some(trace_depth) = tracing.trace_depth
2610 {
2611 for (_, arena) in &mut result.traces {
2612 *arena = trace_arena_at_depth(arena, trace_depth);
2613 }
2614 }
2615 result.gas_report_traces = Default::default();
2617
2618 for (group, new_snapshots) in &result.gas_snapshots {
2620 gas_snapshots.entry(group.clone()).or_default().extend(new_snapshots.clone());
2621 }
2622 }
2623
2624 if !gas_snapshots.is_empty() {
2625 self.check_and_write_gas_snapshots(&config, &gas_snapshots)?;
2626 }
2627
2628 if !silent && has_tests {
2630 sh_println!("{}", suite_result.summary())?;
2631 }
2632
2633 outcome.results.insert(contract_name, suite_result);
2635
2636 if self.fail_fast && any_test_failed {
2638 break;
2639 }
2640 }
2641 let regressions =
2642 self.emit_symbolic_regressions(&config, &known_contracts, &mut outcome.results)?;
2643 if !silent {
2644 for regression in regressions {
2645 sh_warn!(
2646 "Regression test: {} (from {})",
2647 regression.path.display(),
2648 regression.artifact.display()
2649 )?;
2650 }
2651 }
2652 outcome.last_run_decoder = Some(decoder);
2653 let duration = timer.elapsed();
2654
2655 trace!(target: "forge::test", len=outcome.results.len(), %any_test_failed, "done with results");
2656
2657 if let Some(gas_report) = gas_report {
2658 let finalized = gas_report.finalize();
2659 sh_println!("{finalized}")?;
2660 outcome.gas_report = Some(finalized);
2661 }
2662
2663 if !is_multi_pass {
2664 self.print_summary(&outcome, duration)?;
2665 }
2666
2667 let json_results_rx = self.json_file.is_some().then_some(rx);
2669
2670 match handle.await {
2672 Ok(result) => {
2673 let runner = result?;
2674 outcome.known_contracts = Some(runner.known_contracts);
2675 }
2676 Err(e) => match e.try_into_panic() {
2677 Ok(payload) => std::panic::resume_unwind(payload),
2678 Err(e) => return Err(e.into()),
2679 },
2680 }
2681
2682 if let Some(rx) = json_results_rx {
2684 let mut results = outcome.results.clone();
2685 for (contract_name, suite_result) in rx.try_iter() {
2686 if is_multi_pass
2687 && suite_result.test_results.is_empty()
2688 && suite_result.warnings.is_empty()
2689 {
2690 continue;
2691 }
2692 results.insert(contract_name, suite_result);
2693 }
2694 outcome.json_file_results = Some(results);
2695 }
2696
2697 persist_run_failures(&config, &outcome);
2699
2700 Ok(outcome)
2701 }
2702
2703 fn check_and_write_gas_snapshots(
2709 &self,
2710 config: &Config,
2711 gas_snapshots: &BTreeMap<String, BTreeMap<String, String>>,
2712 ) -> Result<()> {
2713 if self.gas_snapshot_check.unwrap_or(config.gas_snapshot_check) {
2714 let mut differences_found = false;
2715 for (group, snapshots) in gas_snapshots {
2716 let path = config.snapshots.join(format!("{group}.json"));
2717 if !path.exists() {
2719 continue;
2720 }
2721 let previous_snapshots: BTreeMap<String, String> =
2722 fs::read_json_file(&path).expect("Failed to read snapshots from disk");
2723 let diff = snapshots
2724 .iter()
2725 .filter_map(|(k, v)| {
2726 previous_snapshots
2727 .get(k)
2728 .filter(|previous| *previous != v)
2729 .map(|p| (k, p, v))
2730 })
2731 .collect::<Vec<_>>();
2732 if diff.is_empty() {
2733 continue;
2734 }
2735 let _ = sh_eprintln!(
2736 "{}",
2737 format!("\n[{group}] Failed to match snapshots:").red().bold()
2738 );
2739 for (key, previous_snapshot, snapshot) in diff {
2740 let _ = sh_eprintln!(
2741 "{}",
2742 format!("- [{key}] {previous_snapshot} → {snapshot}").red()
2743 );
2744 }
2745 differences_found = true;
2746 }
2747 if differences_found {
2748 sh_eprintln!()?;
2749 bail!("Snapshots differ from previous run");
2750 }
2751 }
2752
2753 if self.gas_snapshot_emit.unwrap_or(config.gas_snapshot_emit) {
2754 fs::create_dir_all(&config.snapshots)?;
2755 for (group, snapshots) in gas_snapshots {
2756 fs::write_pretty_json_file(
2757 &config.snapshots.join(format!("{group}.json")),
2758 &snapshots,
2759 )
2760 .expect("Failed to write gas snapshots to disk");
2761 }
2762 }
2763 Ok(())
2764 }
2765
2766 pub fn filter(&self, config: &Config) -> Result<ProjectPathsAwareFilter> {
2769 let mut filter = self.filter.clone();
2770 let rerun_failures = if self.rerun {
2771 let failures = last_run_failures(config);
2772 filter.test_pattern = failures.test_pattern;
2773 failures.failures
2774 } else {
2775 None
2776 };
2777 if filter.path_pattern.is_some() {
2778 if self.path.is_some() {
2779 bail!("Can not supply both --match-path and |path|");
2780 }
2781 } else {
2782 filter.path_pattern = self.path.clone();
2783 }
2784 let mut filter = filter.merge_with_config(config);
2785 if let Some(failures) = rerun_failures {
2786 filter.set_rerun_failures(failures);
2787 }
2788 Ok(filter)
2789 }
2790
2791 pub const fn is_watch(&self) -> bool {
2793 self.watch.watch.is_some()
2794 }
2795
2796 pub(crate) fn watchexec_config(&self) -> Result<watchexec::Config> {
2798 self.watch.watchexec_config(|| {
2799 let config = self.load_config()?;
2800 Ok([config.src, config.test])
2801 })
2802 }
2803}
2804
2805fn enabled_flags<const N: usize>(flags: [(bool, &'static str); N]) -> Vec<&'static str> {
2807 flags.into_iter().filter_map(|(enabled, name)| enabled.then_some(name)).collect()
2808}
2809
2810fn prepare_results_for_json(
2811 results: &mut BTreeMap<String, SuiteResult>,
2812 verbosity: u8,
2813 trace_depth: Option<usize>,
2814) {
2815 for test_result in results.values_mut().flat_map(|suite| suite.test_results.values_mut()) {
2816 if verbosity >= 2 {
2817 test_result.decoded_logs = decode_console_logs(&test_result.logs);
2818 } else {
2819 test_result.logs = Vec::new();
2820 }
2821 for (_, arena) in &mut test_result.traces {
2822 for node in arena.nodes_mut() {
2824 node.trace.decoded = None;
2825 for log in &mut node.logs {
2826 log.decoded = None;
2827 }
2828 for step in &mut node.trace.steps {
2829 step.decoded = None;
2830 }
2831 }
2832 if let Some(trace_depth) = trace_depth {
2833 *arena = trace_arena_at_depth(arena, trace_depth);
2834 }
2835 }
2836 }
2837}
2838
2839fn ensure_mutation_workspace_safe(config: &Config) -> Result<()> {
2846 if config.ffi {
2847 bail!(
2848 "Mutation testing is unsafe with `ffi = true`: per-mutant workspaces share \
2849 symlinked dependency directories, and arbitrary FFI commands run by tests \
2850 can race or corrupt the real `lib`/`node_modules`/`dependencies` trees. \
2851 Disable ffi in your foundry.toml to run mutation tests."
2852 );
2853 }
2854
2855 let root = &config.root;
2858 let canonicalize_through_existing_ancestor = |path: &Path| -> PathBuf {
2859 let resolved = if path.is_absolute() { path.to_path_buf() } else { root.join(path) };
2860 if let Ok(canon) = dunce::canonicalize(&resolved) {
2861 return canon;
2862 }
2863 let mut missing = Vec::new();
2864 let mut ancestor = resolved.as_path();
2865 while !ancestor.exists() {
2866 let Some(name) = ancestor.file_name() else { break };
2867 missing.push(name.to_owned());
2868 let Some(parent) = ancestor.parent() else { break };
2869 ancestor = parent;
2870 }
2871 let mut canon = dunce::canonicalize(ancestor).unwrap_or_else(|_| ancestor.into());
2872 canon.extend(missing.iter().rev());
2873 canon
2874 };
2875
2876 let shared_dep_dirs = config
2877 .libs
2878 .iter()
2879 .filter(|p| p.exists())
2880 .cloned()
2881 .chain(
2882 ["node_modules", "dependencies"]
2883 .into_iter()
2884 .map(|dep_dir| root.join(dep_dir))
2885 .filter(|dep_path| dep_path.is_dir()),
2886 )
2887 .map(|p| canonicalize_through_existing_ancestor(&p))
2888 .collect::<Vec<_>>();
2889
2890 let permissions = &config.fs_permissions.permissions;
2891 let effective_permission = |path: &Path| -> Option<FsAccessPermission> {
2892 let mut max_path_len = 0;
2893 let mut highest_permission = FsAccessPermission::None;
2894 for perm in permissions {
2895 let permission_path = canonicalize_through_existing_ancestor(&perm.path);
2896 if !path.starts_with(&permission_path) {
2897 continue;
2898 }
2899 let path_len = permission_path.components().count();
2900 if path_len > max_path_len {
2901 max_path_len = path_len;
2902 highest_permission = perm.access;
2903 } else if path_len == max_path_len {
2904 highest_permission = match (highest_permission, perm.access) {
2905 (FsAccessPermission::ReadWrite, _)
2906 | (FsAccessPermission::Read, FsAccessPermission::Write)
2907 | (FsAccessPermission::Write, FsAccessPermission::Read) => {
2908 FsAccessPermission::ReadWrite
2909 }
2910 (FsAccessPermission::None, perm) => perm,
2911 (existing_perm, _) => existing_perm,
2912 };
2913 }
2914 }
2915 (max_path_len > 0).then_some(highest_permission)
2916 };
2917 let grants_write = |path: &Path| {
2918 matches!(
2919 effective_permission(path),
2920 Some(FsAccessPermission::Write | FsAccessPermission::ReadWrite)
2921 )
2922 };
2923
2924 let unsafe_write_paths = permissions
2925 .iter()
2926 .filter(|perm| {
2927 matches!(perm.access, FsAccessPermission::Write | FsAccessPermission::ReadWrite)
2928 })
2929 .filter(|perm| {
2930 let perm_path = canonicalize_through_existing_ancestor(&perm.path);
2931 shared_dep_dirs.iter().any(|dep| {
2932 if perm_path.starts_with(dep) {
2933 grants_write(&perm_path)
2934 } else if dep.starts_with(&perm_path) {
2935 grants_write(dep)
2936 } else {
2937 false
2938 }
2939 })
2940 })
2941 .map(|perm| format!(" - {}", perm.path.display()))
2942 .collect::<Vec<_>>();
2943 if !unsafe_write_paths.is_empty() {
2944 bail!(
2945 "Mutation testing is unsafe with write-capable `fs_permissions` that can \
2946 reach the symlinked dependency trees (`lib`/`node_modules`/`dependencies`); \
2947 per-mutant workspaces share those trees, so `vm.writeFile` calls would race \
2948 against or corrupt your real dependencies. Restrict the following \
2949 `fs_permissions` entries to read-only or scope them away from dependency \
2950 paths:\n{}",
2951 unsafe_write_paths.join("\n")
2952 );
2953 }
2954 Ok(())
2955}
2956
2957macro_rules! dict {
2959 ($($key:literal => $value:expr),* $(,)?) => {{
2960 let mut dict = Dict::default();
2961 $(if let Some(value) = $value {
2962 dict.insert($key.to_string(), Value::from(value));
2963 })*
2964 dict
2965 }};
2966}
2967
2968fn path_string(path: &Option<PathBuf>) -> Option<String> {
2970 path.as_ref().map(|path| path.to_string_lossy().to_string())
2971}
2972
2973impl Provider for TestArgs {
2974 fn metadata(&self) -> Metadata {
2975 Metadata::named("Core Build Args Provider")
2976 }
2977
2978 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
2979 let fuzz = dict! {
2980 "seed" => self.fuzz_seed.map(|seed| seed.to_string()),
2981 "runs" => self.fuzz_runs,
2982 "run" => self.fuzz_run,
2983 "worker" => self.fuzz_worker,
2984 "timeout" => self.fuzz_timeout,
2985 "dictionary_weight" => self.fuzz_dictionary_weight,
2986 "max_fuzz_dictionary_addresses" => self.fuzz_dictionary_addresses.clone(),
2987 "max_fuzz_dictionary_values" => self.fuzz_dictionary_values.clone(),
2988 "max_fuzz_dictionary_literals" => self.fuzz_dictionary_literals.clone(),
2989 "corpus_random_sequence_weight" => self.fuzz_corpus_random_sequence_weight,
2990 "corpus_dir" => path_string(&self.fuzz_corpus_dir),
2991 "frontier_dir" => path_string(&self.fuzz_frontier_dir),
2992 "frontier_limit" => self.fuzz_frontier_limit,
2993 "payable_value_weight" => self.fuzz_payable_value_weight,
2994 "mutation_weight_splice" => self.fuzz_mutation_weight_splice,
2995 "mutation_weight_repeat" => self.fuzz_mutation_weight_repeat,
2996 "mutation_weight_interleave" => self.fuzz_mutation_weight_interleave,
2997 "mutation_weight_prefix" => self.fuzz_mutation_weight_prefix,
2998 "mutation_weight_suffix" => self.fuzz_mutation_weight_suffix,
2999 "mutation_weight_abi" => self.fuzz_mutation_weight_abi,
3000 "mutation_weight_cmp" => self.fuzz_mutation_weight_cmp,
3001 };
3002 let invariant = dict! {
3003 "runs" => self.invariant_runs_override,
3004 "depth" => self.invariant_depth,
3005 "min_depth" => self.invariant_min_depth,
3006 "depth_mode" => self.invariant_depth_mode.map(Value::serialize).transpose()?,
3007 "workers" => self.invariant_workers.map(Value::serialize).transpose()?,
3008 "dictionary_weight" => self.invariant_dictionary_weight,
3009 "max_fuzz_dictionary_addresses" => self.invariant_dictionary_addresses.clone(),
3010 "max_fuzz_dictionary_values" => self.invariant_dictionary_values.clone(),
3011 "max_fuzz_dictionary_literals" => self.invariant_dictionary_literals.clone(),
3012 "corpus_random_sequence_weight" => self.invariant_corpus_random_sequence_weight,
3013 "corpus_random_sequence_weight_configured" =>
3014 self.invariant_corpus_random_sequence_weight.map(|_| true),
3015 "corpus_dir" => path_string(&self.invariant_corpus_dir),
3016 "frontier_dir" => path_string(&self.invariant_frontier_dir),
3017 "frontier_limit" => self.invariant_frontier_limit,
3018 "payable_value_weight" => self.invariant_payable_value_weight,
3019 "timeout" => self.invariant_timeout_override,
3020 "mutation_weight_splice" => self.invariant_mutation_weight_splice,
3021 "mutation_weight_repeat" => self.invariant_mutation_weight_repeat,
3022 "mutation_weight_interleave" => self.invariant_mutation_weight_interleave,
3023 "mutation_weight_prefix" => self.invariant_mutation_weight_prefix,
3024 "mutation_weight_suffix" => self.invariant_mutation_weight_suffix,
3025 "mutation_weight_abi" => self.invariant_mutation_weight_abi,
3026 "mutation_weight_cmp" => self.invariant_mutation_weight_cmp,
3027 };
3028 let symbolic = dict! {
3029 "enabled" => self.symbolic.then_some(true),
3030 "seed_corpus" => self.symbolic_seed_corpus.then_some(true),
3031 "use_fuzz_corpus" => self.symbolic_use_fuzz_corpus.then_some(true),
3032 "corpus_seed_limit" => self.symbolic_corpus_seed_limit,
3033 "use_fuzz_frontiers" => self.symbolic_use_fuzz_frontiers.then_some(true),
3034 "check_invariant_frontiers" =>
3035 self.symbolic_check_invariant_frontiers.then_some(true),
3036 "frontier_limit" => self.symbolic_frontier_limit,
3037 "frontier_ids" => self.symbolic_frontier_ids.clone(),
3038 "frontier_pcs" => self.symbolic_frontier_pcs.clone(),
3039 "frontier_selectors" => self.symbolic_frontier_selectors.clone(),
3040 "solver" => self.symbolic_solver.clone(),
3041 "solver_command" => self.symbolic_solver_command.clone(),
3042 "solver_portfolio" => self.symbolic_solver_portfolio.clone(),
3043 "timeout" => self.symbolic_timeout,
3044 "loop" => self.symbolic_loop,
3045 "depth" => self.symbolic_depth,
3046 "width" => self.symbolic_width,
3047 "max_depth" => self.symbolic_max_depth,
3048 "max_paths" => self.symbolic_max_paths,
3049 "invariant_depth" => self.symbolic_invariant_depth,
3050 "max_solver_queries" => self.symbolic_max_solver_queries,
3051 "default_dynamic_length" => self.symbolic_default_dynamic_length,
3052 "max_dynamic_length" => self.symbolic_max_dynamic_length,
3053 "array_lengths" => self.symbolic_array_lengths.clone(),
3054 "max_calldata_bytes" => self.symbolic_max_calldata_bytes,
3055 "symbolic_call_targets" => self.symbolic_call_targets.then_some(true),
3056 "dump_smt" => self.symbolic_dump_smt.then_some(true),
3057 "storage_layout" => self.symbolic_storage_layout.clone(),
3058 };
3059 let mutation = dict! {
3060 "timeout" => self.mutation_timeout,
3061 "optimizer_runs" => self.mutation_optimizer_runs,
3062 "via_ir" => self.mutation_via_ir,
3063 };
3064
3065 let mut dict = dict! {
3066 "fuzz" => Some(fuzz),
3067 "invariant" => (!invariant.is_empty()).then_some(invariant),
3068 "symbolic" => Some(symbolic),
3069 "etherscan_api_key" =>
3070 self.etherscan_api_key.as_ref().filter(|s| !s.trim().is_empty()).cloned(),
3071 "show_progress" => self.show_progress.then_some(true),
3072 "decode_external_storage" => self.decode_external_storage.then_some(true),
3073 };
3074 if !mutation.is_empty() {
3076 dict.insert("mutation".to_string(), mutation.into());
3077 }
3078 Ok(Map::from([(Config::selected_profile(), dict)]))
3079 }
3080}
3081
3082fn parse_opcode(s: &str) -> Result<OpCode, String> {
3083 OpCode::parse(s).ok_or_else(|| format!("invalid opcode: {s}"))
3084}
3085
3086const fn apply_mutation_compiler_overrides(config: &mut Config) {
3087 if let Some(optimizer_runs) = config.mutation.optimizer_runs {
3088 let default_optimizer_settings =
3089 matches!(config.optimizer, Some(false)) && matches!(config.optimizer_runs, Some(200));
3090 config.optimizer_runs = Some(optimizer_runs as usize);
3091 if default_optimizer_settings {
3092 config.optimizer = None;
3093 }
3094 config.normalize_optimizer_settings();
3095 }
3096 if let Some(via_ir) = config.mutation.via_ir {
3097 config.via_ir = via_ir;
3098 }
3099}
3100
3101fn matching_test_contracts<'a>(
3103 output: &'a ProjectCompileOutput,
3104 config: &'a Config,
3105 matcher: &'a TestFunctionMatcher<'a>,
3106 filter: &'a ProjectPathsAwareFilter,
3107) -> impl Iterator<Item = (ArtifactId, &'a ConfigurableContractArtifact, &'a JsonAbi)> + 'a {
3108 output.artifact_ids().filter_map(move |(id, artifact)| {
3109 let abi = artifact.abi.as_ref()?;
3110 let id = id.with_stripped_file_prefixes(&config.root);
3111 let deployable = abi.constructor.as_ref().is_none_or(|c| c.inputs.is_empty());
3112 (deployable && matcher.matches_contract(filter, &id, abi)).then_some((id, artifact, abi))
3113 })
3114}
3115
3116fn list_from_output(
3118 output: &ProjectCompileOutput,
3119 config: &Config,
3120 inline_config: &InlineConfig,
3121 filter: &ProjectPathsAwareFilter,
3122 fuzz_only: bool,
3123 symbolic_artifact_replay: Option<&SymbolicArtifactReplayConfig>,
3124) -> Result<TestOutcome> {
3125 let matcher = TestFunctionMatcher::new(config, inline_config, symbolic_artifact_replay);
3126 let mut results = BTreeMap::<String, BTreeMap<String, Vec<String>>>::new();
3127 for (id, _, abi) in matching_test_contracts(output, config, &matcher, filter) {
3128 let identifier = id.identifier();
3129 let generated_symbolic_regression = is_generated_symbolic_regression_contract(abi);
3130 let tests = abi
3131 .functions()
3132 .filter(|func| {
3133 let kind =
3134 matcher.test_function_kind(&identifier, func, generated_symbolic_regression);
3135 (!fuzz_only
3136 || matches!(
3137 kind,
3138 TestFunctionKind::FuzzTest { .. } | TestFunctionKind::InvariantTest
3139 ))
3140 && filter.matches_test_function_kind_in_contract(&identifier, func, kind)
3141 })
3142 .map(|func| func.name.clone())
3143 .collect::<Vec<_>>();
3144 if !tests.is_empty() {
3145 results.entry(id.source.display().to_string()).or_default().insert(id.name, tests);
3146 }
3147 }
3148
3149 if shell::is_json() {
3150 sh_println!("{}", serde_json::to_string(&results)?)?;
3151 } else {
3152 for (file, contracts) in &results {
3153 sh_println!("{file}")?;
3154 for (contract, tests) in contracts {
3155 sh_println!(" {contract}")?;
3156 sh_println!(" {}\n", tests.join("\n "))?;
3157 }
3158 }
3159 }
3160 Ok(TestOutcome::empty(None, false))
3161}
3162
3163fn matching_fuzz_replay_targets(
3164 output: &ProjectCompileOutput,
3165 config: &Config,
3166 inline_config: &InlineConfig,
3167 filter: &ProjectPathsAwareFilter,
3168 selector: &[u8],
3169) -> Result<Vec<(String, String)>> {
3170 let matcher = TestFunctionMatcher::new(config, inline_config, None);
3171 let mut targets = Vec::new();
3172 for (id, artifact, abi) in matching_test_contracts(output, config, &matcher, filter) {
3173 let has_creation_code =
3174 artifact.get_bytecode_object().is_some_and(|object| match object.as_ref() {
3175 BytecodeObject::Bytecode(bytecode) => !bytecode.is_empty(),
3176 BytecodeObject::Unlinked(_) => true,
3177 });
3178 if !has_creation_code {
3179 continue;
3180 }
3181 let contract = id.identifier();
3182 let generated_symbolic_regression = is_generated_symbolic_regression_contract(abi);
3183 for func in abi.functions() {
3184 let kind = matcher.test_function_kind(&contract, func, generated_symbolic_regression);
3185 if !matches!(kind, TestFunctionKind::FuzzTest { .. })
3186 || !filter.matches_test_function_kind_in_contract(&contract, func, kind)
3187 {
3188 continue;
3189 }
3190 let function_config = inline_config_for(config, inline_config, &contract, Some(func))?;
3191 if matches!(
3192 effective_test_function_kind(kind, &function_config, func),
3193 TestFunctionKind::FuzzTest { .. }
3194 ) && func.selector() == selector
3195 {
3196 targets.push((contract.clone(), func.signature()));
3197 }
3198 }
3199 }
3200 Ok(targets)
3201}
3202
3203fn merge_outcomes(base: &mut TestOutcome, mut other: TestOutcome) {
3208 if let Some(other_results) = other.json_file_results.take() {
3209 let base_results = base.json_file_results.get_or_insert_with(|| base.results.clone());
3210 merge_suite_results(base_results, other_results);
3211 }
3212 merge_suite_results(&mut base.results, other.results);
3213 if let Some(decoder) = other.last_run_decoder {
3214 base.last_run_decoder = Some(decoder);
3215 }
3216}
3217
3218fn merge_suite_results(
3219 base: &mut BTreeMap<String, SuiteResult>,
3220 other: BTreeMap<String, SuiteResult>,
3221) {
3222 for (suite_id, other_suite) in other {
3223 if let Some(base_suite) = base.get_mut(&suite_id) {
3224 base_suite.test_results.extend(other_suite.test_results);
3225 base_suite.warnings.extend(other_suite.warnings);
3226 base_suite.duration = base_suite.duration.max(other_suite.duration);
3227 } else {
3228 base.insert(suite_id, other_suite);
3229 }
3230 }
3231}
3232
3233fn collect_matching_debug_tests(
3234 matching_tests: &BTreeMap<String, BTreeMap<String, Vec<String>>>,
3235) -> Vec<RerunFailure> {
3236 matching_tests
3237 .iter()
3238 .flat_map(|(source, contracts)| {
3239 contracts.iter().flat_map(move |(contract, tests)| {
3240 tests.iter().map(move |test| RerunFailure {
3241 contract: format!("{source}:{contract}"),
3242 test: test.clone(),
3243 })
3244 })
3245 })
3246 .collect()
3247}
3248
3249fn format_matching_debug_tests(matching_tests: &[RerunFailure]) -> String {
3250 if matching_tests.is_empty() {
3251 return String::new();
3252 }
3253 let mut output = String::from("\n\nMatching tests:");
3254 for test in matching_tests.iter().take(DEBUGGER_MATCHING_TESTS_DISPLAY_LIMIT) {
3255 write!(output, "\n {}.{}", test.contract, test.test).unwrap();
3256 }
3257 if matching_tests.len() > DEBUGGER_MATCHING_TESTS_DISPLAY_LIMIT {
3258 write!(
3259 output,
3260 "\n ... and {} more",
3261 matching_tests.len() - DEBUGGER_MATCHING_TESTS_DISPLAY_LIMIT
3262 )
3263 .unwrap();
3264 }
3265 output
3266}
3267
3268struct LastRunFailures {
3269 test_pattern: Option<Regex>,
3270 failures: Option<Vec<RerunFailure>>,
3271}
3272
3273fn last_run_failures(config: &Config) -> LastRunFailures {
3275 let Ok(filter) = fs::read_to_string(&config.test_failures_file) else {
3276 return LastRunFailures { test_pattern: None, failures: None };
3277 };
3278
3279 if let Ok(failures) = serde_json::from_str::<RerunFailures>(&filter) {
3280 if failures.failures.is_empty() {
3281 return LastRunFailures { test_pattern: None, failures: None };
3282 }
3283 let test_pattern = failures
3284 .failures
3285 .iter()
3286 .map(|failure| regex::escape(&failure.test))
3287 .collect::<Vec<_>>()
3288 .join("|");
3289 let test_pattern = Regex::new(&test_pattern).ok();
3290 return LastRunFailures { test_pattern, failures: Some(failures.failures) };
3291 }
3292
3293 let test_pattern = Regex::new(&filter)
3295 .inspect_err(|e| {
3296 _ = sh_warn!("failed to parse test filter from {:?}: {e}", config.test_failures_file)
3297 })
3298 .ok();
3299 LastRunFailures { test_pattern, failures: None }
3300}
3301
3302fn persist_run_failures(config: &Config, outcome: &TestOutcome) {
3304 if outcome.failed() > 0 && fs::create_file(&config.test_failures_file).is_ok() {
3305 let failures = outcome
3306 .results
3307 .iter()
3308 .flat_map(|(contract, suite)| {
3309 suite.test_results.iter().filter(|(_, result)| result.status.is_failure()).flat_map(
3310 move |(test_name, test_result)| {
3311 rerun_filter_matches(test_name, test_result)
3312 .map(move |test| RerunFailure { contract: contract.clone(), test })
3313 },
3314 )
3315 })
3316 .collect::<Vec<_>>();
3317
3318 if let Ok(output) = serde_json::to_string(&RerunFailures { version: 1, failures }) {
3319 let _ = fs::write(&config.test_failures_file, output);
3320 }
3321 }
3322}
3323
3324fn rerun_filter_matches<'a>(
3327 test_name: &'a str,
3328 test_result: &'a TestResult,
3329) -> impl Iterator<Item = String> + 'a {
3330 let predicate_failures =
3331 test_result.invariant_failures.iter().filter_map(|failure| failure.predicate_name());
3332 let has_predicate_failures = predicate_failures.clone().next().is_some();
3333 let fallback = test_name.is_any_test().then(|| test_name.split('(').next()).flatten();
3334 predicate_failures
3335 .chain(fallback.into_iter().filter(move |_| !has_predicate_failures))
3336 .map(str::to_owned)
3337}
3338
3339fn junit_xml_report(results: &BTreeMap<String, SuiteResult>, verbosity: u8) -> Report {
3341 let mut total_duration = Duration::default();
3342 let mut junit_report = Report::new("Test run");
3343 junit_report.set_timestamp(Utc::now());
3344 for (suite_name, suite_result) in results {
3345 let mut test_suite = TestSuite::new(suite_name);
3346 total_duration += suite_result.duration;
3347 test_suite.set_time(suite_result.duration);
3348 test_suite.set_system_out(suite_result.summary());
3349 for (test_name, test_result) in &suite_result.test_results {
3350 add_junit_test_cases(&mut test_suite, test_name, test_result, verbosity);
3351 }
3352 junit_report.add_test_suite(test_suite);
3353 }
3354 junit_report.set_time(total_duration);
3355 junit_report
3356}
3357
3358fn add_junit_test_cases(
3363 test_suite: &mut TestSuite,
3364 test_name: &str,
3365 test_result: &TestResult,
3366 verbosity: u8,
3367) {
3368 let output = JunitOutput::new(test_result, verbosity);
3369 let expanded_invariant = test_result.kind.is_invariant()
3370 && (!test_result.invariant_predicate_results.is_empty()
3371 || !test_result.invariant_handler_failures.is_empty());
3372
3373 if !expanded_invariant {
3374 add_junit_test_case(
3375 test_suite,
3376 test_name,
3377 test_result.status,
3378 test_result.reason.as_deref(),
3379 test_result.duration,
3380 output.system_out(test_result, test_name),
3381 );
3382 return;
3383 }
3384
3385 let mut add_expanded_case =
3386 |name: &str,
3387 status: TestStatus,
3388 reason: Option<&str>,
3389 counterexample: Option<&CounterExample>| {
3390 add_junit_test_case(
3391 test_suite,
3392 name,
3393 status,
3394 reason,
3395 test_result.duration,
3396 output.case_system_out(status, reason, name, counterexample),
3397 );
3398 };
3399
3400 if test_result.invariant_predicate_results.is_empty() {
3401 let failure = test_result.invariant_failures.first();
3402 let status = if failure.is_some() { TestStatus::Failure } else { TestStatus::Success };
3403 add_expanded_case(
3404 test_name,
3405 status,
3406 failure.map(|failure| failure.reason()),
3407 failure.and_then(|failure| failure.counterexample()),
3408 );
3409 } else {
3410 for predicate in &test_result.invariant_predicate_results {
3411 let failure = test_result
3412 .invariant_failures
3413 .iter()
3414 .find(|failure| failure.name() == predicate.name.as_str());
3415 add_expanded_case(
3416 &format!("{}()", predicate.name),
3417 predicate.status,
3418 predicate.reason.as_deref(),
3419 failure.and_then(|failure| failure.counterexample()),
3420 );
3421 }
3422 }
3423
3424 for failure in &test_result.invariant_handler_failures {
3425 add_expanded_case(
3426 &format!("handler {}", failure.name()),
3427 TestStatus::Failure,
3428 Some(failure.reason()),
3429 failure.counterexample(),
3430 );
3431 }
3432}
3433
3434fn add_junit_test_case(
3436 test_suite: &mut TestSuite,
3437 test_name: &str,
3438 status: TestStatus,
3439 message: Option<&str>,
3440 duration: Duration,
3441 system_out: String,
3442) {
3443 let mut test_status = match status {
3444 TestStatus::Success => TestCaseStatus::success(),
3445 TestStatus::Failure => TestCaseStatus::non_success(NonSuccessKind::Failure),
3446 TestStatus::Skipped => TestCaseStatus::skipped(),
3447 };
3448 if let Some(message) = message {
3449 test_status.set_message(message);
3450 }
3451 let mut test_case = TestCase::new(test_name, test_status);
3452 test_case.set_time(duration);
3453 test_case.set_system_out(system_out);
3454 test_suite.add_test_case(test_case);
3455}
3456
3457struct JunitOutput {
3459 result_report: TestKindReport,
3460 logs: Option<Vec<String>>,
3461}
3462
3463impl JunitOutput {
3464 fn new(test_result: &TestResult, verbosity: u8) -> Self {
3465 Self {
3466 result_report: test_result.kind.report(),
3467 logs: (verbosity >= 2 && !test_result.logs.is_empty())
3468 .then(|| decode_console_logs(&test_result.logs)),
3469 }
3470 }
3471
3472 fn system_out(&self, test_result: &TestResult, test_name: &str) -> String {
3474 let mut sys_out = format!("{test_result} {test_name} {}", self.result_report);
3475 self.append_logs(&mut sys_out);
3476 sys_out
3477 }
3478
3479 fn case_system_out(
3481 &self,
3482 status: TestStatus,
3483 message: Option<&str>,
3484 test_name: &str,
3485 counterexample: Option<&CounterExample>,
3486 ) -> String {
3487 let mut sys_out = match (status, message) {
3488 (TestStatus::Success, _) => "[PASS]".to_string(),
3489 (TestStatus::Failure, message) => format!("[FAIL: {}]", message.unwrap_or_default()),
3490 (TestStatus::Skipped, Some(message)) => format!("[SKIP: {message}]"),
3491 (TestStatus::Skipped, None) => "[SKIP]".to_string(),
3492 };
3493 write!(sys_out, " {test_name} {}", self.result_report).unwrap();
3494 if let Some(CounterExample::Sequence(original, sequence)) = counterexample {
3495 writeln!(sys_out, "\n\t[Sequence] (original: {original}, shrunk: {})", sequence.len())
3496 .unwrap();
3497 for ex in sequence {
3498 writeln!(sys_out, "{ex}").unwrap();
3499 }
3500 }
3501 self.append_logs(&mut sys_out);
3502 sys_out
3503 }
3504
3505 fn append_logs(&self, sys_out: &mut String) {
3507 if let Some(logs) = &self.logs {
3508 write!(sys_out, "\\nLogs:\\n").unwrap();
3509 for log in logs {
3510 write!(sys_out, " {log}\\n").unwrap();
3511 }
3512 }
3513 }
3514}
3515
3516#[cfg(test)]
3517mod tests {
3518 use super::*;
3519
3520 fn parse_with_env(vars: &[(&str, &str)], args: &[&str]) -> TestArgs {
3522 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
3523 let _guard = ENV_LOCK.lock().unwrap();
3524 let previous = vars.iter().map(|(name, _)| std::env::var_os(name)).collect::<Vec<_>>();
3525 for (name, value) in vars {
3526 unsafe { std::env::set_var(name, value) };
3527 }
3528 let parsed =
3529 TestArgs::try_parse_from(["foundry-cli"].into_iter().chain(args.iter().copied()));
3530 for ((name, _), previous) in vars.iter().zip(previous) {
3531 match previous {
3532 Some(previous) => unsafe { std::env::set_var(name, previous) },
3533 None => unsafe { std::env::remove_var(name) },
3534 }
3535 }
3536 parsed.unwrap()
3537 }
3538
3539 #[test]
3540 fn parses_flags_without_cli_coverage() {
3541 assert!(TestArgs::parse_from(["foundry-cli", "-vw"]).watch.watch.is_some());
3542 assert!(TestArgs::parse_from(["foundry-cli", "--compact-labels"]).tracing.compact_labels);
3543 let args =
3545 TestArgs::parse_from(["foundry-cli", "-vvv", "--gas-report", "--fuzz-seed", "0x10"]);
3546 assert!(args.fuzz_seed.is_some());
3547 let args = TestArgs::parse_from(["foundry-cli", "--invariant-workers", "auto"]);
3548 assert_eq!(args.invariant_workers, Some(InvariantWorkers::Auto));
3549 assert_eq!(
3550 figment::Figment::from(&args)
3551 .extract_inner::<InvariantWorkers>("invariant.workers")
3552 .unwrap(),
3553 InvariantWorkers::Auto
3554 );
3555 }
3556
3557 #[test]
3558 fn parses_env_vars() {
3559 let args = parse_with_env(
3560 &[
3561 ("FOUNDRY_INVARIANT_WORKERS", "auto"),
3562 ("FOUNDRY_FUZZ_CORPUS_DIR", "env_fuzz_corpus"),
3563 ("FOUNDRY_INVARIANT_CORPUS_DIR", "env_invariant_corpus"),
3564 ],
3565 &[],
3566 );
3567 assert_eq!(args.invariant_workers, Some(InvariantWorkers::Auto));
3568 assert_eq!(args.fuzz_corpus_dir, Some(PathBuf::from("env_fuzz_corpus")));
3569 assert_eq!(args.invariant_corpus_dir, Some(PathBuf::from("env_invariant_corpus")));
3570 }
3571
3572 #[test]
3573 fn showmap_override_validates_path_component_names() {
3574 let mut args = TestArgs::parse_from(["foundry-cli"]);
3575 args.set_showmap_override(ShowmapConfig {
3576 out_dir: PathBuf::from("showmap"),
3577 approach: "../outside".to_string(),
3578 trial: "trial".to_string(),
3579 per_input: false,
3580 domain: ShowmapDomain::Evm,
3581 corpus_dir: None,
3582 emit_files: false,
3583 });
3584
3585 let err = args.showmap_config().unwrap_err().to_string();
3586 assert!(err.contains("expected a single file-name component"), "{err}");
3587 }
3588
3589 #[test]
3590 fn debugger_test_candidates_preserve_exact_suite_ids() {
3591 let matching = BTreeMap::from([(
3592 "test/Counter.t.sol".to_string(),
3593 BTreeMap::from([(
3594 "CounterTest".to_string(),
3595 vec!["testFuzz_SetNumber(uint256)".to_string(), "test_Increment()".to_string()],
3596 )]),
3597 )]);
3598
3599 let candidates = collect_matching_debug_tests(&matching);
3600
3601 assert_eq!(candidates[0].contract, "test/Counter.t.sol:CounterTest");
3602 assert_eq!(candidates[0].test, "testFuzz_SetNumber(uint256)");
3603 assert_eq!(candidates[1].test, "test_Increment()");
3604 assert_eq!(
3605 format_matching_debug_tests(&candidates),
3606 "\n\nMatching tests:\n test/Counter.t.sol:CounterTest.testFuzz_SetNumber(uint256)\n test/Counter.t.sol:CounterTest.test_Increment()"
3607 );
3608 }
3609
3610 #[test]
3611 fn fuzz_run_adapter_writes_unified_campaign_dials_sparsely() {
3612 let args = FuzzRunArgs::parse_from([
3613 "foundry-cli",
3614 "--runs",
3615 "9",
3616 "--timeout",
3617 "3",
3618 "--seed",
3619 "0x10",
3620 "--depth",
3621 "7",
3622 "--workers",
3623 "2",
3624 "--frontier-dir",
3625 "frontiers",
3626 "--frontier-limit",
3627 "17",
3628 ]);
3629 let args = TestArgs::from_fuzz_run(args);
3630 let figment = figment::Figment::from(&args);
3631
3632 assert_eq!(figment.extract_inner::<u64>("fuzz.runs").unwrap(), 9);
3633 assert_eq!(figment.extract_inner::<u64>("fuzz.timeout").unwrap(), 3);
3634 assert_eq!(figment.extract_inner::<String>("fuzz.seed").unwrap(), "16");
3635 assert_eq!(figment.extract_inner::<u64>("invariant.runs").unwrap(), 9);
3636 assert_eq!(figment.extract_inner::<u32>("invariant.timeout").unwrap(), 3);
3637 assert_eq!(figment.extract_inner::<u32>("invariant.depth").unwrap(), 7);
3638 assert_eq!(
3639 figment.extract_inner::<PathBuf>("fuzz.frontier_dir").unwrap(),
3640 PathBuf::from("frontiers")
3641 );
3642 assert_eq!(figment.extract_inner::<usize>("fuzz.frontier_limit").unwrap(), 17);
3643 assert_eq!(
3644 figment.extract_inner::<PathBuf>("invariant.frontier_dir").unwrap(),
3645 PathBuf::from("frontiers")
3646 );
3647 assert_eq!(figment.extract_inner::<usize>("invariant.frontier_limit").unwrap(), 17);
3648 assert_eq!(
3649 figment.extract_inner::<InvariantWorkers>("invariant.workers").unwrap(),
3650 InvariantWorkers::Fixed(std::num::NonZeroUsize::new(2).unwrap())
3651 );
3652 }
3653
3654 #[test]
3655 fn fuzz_run_adapter_writes_invariant_workers_sparsely() {
3656 let args = TestArgs::from_fuzz_run(FuzzRunArgs::parse_from(["foundry-cli"]));
3657 let figment = figment::Figment::from(&args);
3658
3659 assert_eq!(args.invariant_workers, None);
3660 assert!(figment.extract_inner::<InvariantWorkers>("invariant.workers").is_err());
3661 }
3662
3663 #[test]
3664 fn mutation_compiler_overrides_are_extracted() {
3665 let args = TestArgs::parse_from([
3666 "foundry-cli",
3667 "--mutate",
3668 "--mutation-optimizer-runs",
3669 "1",
3670 "--mutation-via-ir",
3671 "false",
3672 ]);
3673 let figment = figment::Figment::from(&args);
3674 assert_eq!(figment.extract_inner::<u32>("mutation.optimizer_runs").unwrap(), 1);
3675 assert!(!figment.extract_inner::<bool>("mutation.via_ir").unwrap());
3676 }
3677
3678 #[test]
3679 fn mutation_compiler_overrides_update_only_mutation_config_clone() {
3680 let mut config = Config {
3681 optimizer_runs: Some(999),
3682 via_ir: true,
3683 mutation: foundry_config::MutationConfig {
3684 optimizer_runs: Some(1),
3685 via_ir: Some(false),
3686 ..Default::default()
3687 },
3688 ..Default::default()
3689 };
3690
3691 apply_mutation_compiler_overrides(&mut config);
3692
3693 assert_eq!(config.optimizer_runs, Some(1));
3694 assert!(!config.via_ir);
3695 }
3696
3697 #[test]
3698 fn mutation_optimizer_runs_normalize_default_optimizer_settings() {
3699 let mut config = Config {
3700 optimizer: Some(false),
3701 optimizer_runs: Some(200),
3702 mutation: foundry_config::MutationConfig {
3703 optimizer_runs: Some(1),
3704 ..Default::default()
3705 },
3706 ..Default::default()
3707 };
3708
3709 apply_mutation_compiler_overrides(&mut config);
3710
3711 assert_eq!(config.optimizer, Some(true));
3712 assert_eq!(config.optimizer_runs, Some(1));
3713 }
3714
3715 #[test]
3716 fn auto_fuzz_corpus_defaults_to_cache_failure_layout() {
3717 let mut args = TestArgs::parse_from(["foundry-cli"]);
3718 args.enable_fuzz_only_with_auto_fuzz_corpus();
3719 let mut config = Config::default();
3720
3721 args.apply_test_config_overrides(&mut config);
3722
3723 assert_eq!(
3724 config.fuzz.corpus.corpus_dir,
3725 Some(config.cache_path.join(AUTO_FUZZ_FAILURE_DIR).join(AUTO_CORPUS_DIR))
3726 );
3727 assert_eq!(config.invariant.corpus.corpus_dir, None);
3728 }
3729
3730 #[test]
3731 fn auto_fuzz_corpus_uses_configured_failure_persist_dirs() {
3732 let mut args = TestArgs::parse_from(["foundry-cli"]);
3733 args.enable_fuzz_only_with_auto_fuzz_corpus();
3734 let mut config = Config::default();
3735 config.fuzz.failure_persist_dir = Some(PathBuf::from("custom_fuzz_failures"));
3736
3737 args.apply_test_config_overrides(&mut config);
3738
3739 assert_eq!(
3740 config.fuzz.corpus.corpus_dir,
3741 Some(PathBuf::from("custom_fuzz_failures").join(AUTO_CORPUS_DIR))
3742 );
3743 assert_eq!(config.invariant.corpus.corpus_dir, None);
3744 }
3745
3746 #[test]
3747 fn auto_fuzz_corpus_preserves_configured_corpus_dirs() {
3748 let mut args = TestArgs::parse_from(["foundry-cli"]);
3749 args.enable_fuzz_only_with_auto_fuzz_corpus();
3750 let mut config = Config::default();
3751 config.fuzz.corpus.corpus_dir = Some(PathBuf::from("configured_fuzz_corpus"));
3752 config.invariant.corpus.corpus_dir = Some(PathBuf::from("configured_invariant_corpus"));
3753
3754 args.apply_test_config_overrides(&mut config);
3755
3756 assert_eq!(config.fuzz.corpus.corpus_dir, Some(PathBuf::from("configured_fuzz_corpus")));
3757 assert_eq!(
3758 config.invariant.corpus.corpus_dir,
3759 Some(PathBuf::from("configured_invariant_corpus"))
3760 );
3761 }
3762
3763 #[test]
3764 fn fuzz_only_does_not_enable_auto_fuzz_corpus() {
3765 let mut args = TestArgs::parse_from(["foundry-cli"]);
3766 args.enable_fuzz_only();
3767 let mut config = Config::default();
3768
3769 args.apply_test_config_overrides(&mut config);
3770
3771 assert_eq!(config.fuzz.corpus.corpus_dir, None);
3772 assert_eq!(config.invariant.corpus.corpus_dir, None);
3773 }
3774
3775 #[test]
3776 fn debug_brutalize_includes_storage_layout_output() {
3777 let args = TestArgs::parse_from(["foundry-cli", "--debug", "--brutalize"]);
3778 let mut config = Config::default();
3779
3780 args.apply_test_config_overrides(&mut config);
3781
3782 assert_eq!(config.extra_output, vec![ContractOutputSelection::StorageLayout]);
3783 }
3784
3785 #[test]
3786 fn fuzz_and_invariant_config_flags() {
3787 let args = TestArgs::parse_from([
3788 "foundry-cli",
3789 "--fuzz-dictionary-weight",
3790 "35",
3791 "--fuzz-dictionary-addresses",
3792 "max",
3793 "--fuzz-dictionary-values",
3794 "1234",
3795 "--fuzz-dictionary-literals",
3796 "4321",
3797 "--fuzz-corpus-random-sequence-weight",
3798 "55",
3799 "--fuzz-corpus-dir",
3800 "fuzz_corpus",
3801 "--fuzz-frontier-dir",
3802 "fuzz_frontiers",
3803 "--fuzz-frontier-limit",
3804 "7",
3805 "--fuzz-payable-value-weight",
3806 "12",
3807 "--fuzz-mutation-weight-splice",
3808 "4",
3809 "--fuzz-mutation-weight-abi",
3810 "3",
3811 "--fuzz-mutation-weight-cmp",
3812 "5",
3813 "--symbolic-use-fuzz-frontiers",
3814 "--symbolic-check-invariant-frontiers",
3815 "--symbolic-frontier-limit",
3816 "3",
3817 "--symbolic-frontier-ids",
3818 "4,9",
3819 "--symbolic-frontier-pcs",
3820 "123,456",
3821 "--symbolic-frontier-selectors",
3822 "0x12345678,deadbeef",
3823 "--invariant-depth",
3824 "300",
3825 "--invariant-min-depth",
3826 "20",
3827 "--invariant-depth-mode",
3828 "random",
3829 "--invariant-workers",
3830 "4",
3831 "--invariant-dictionary-weight",
3832 "45",
3833 "--invariant-dictionary-addresses",
3834 "8765",
3835 "--invariant-dictionary-values",
3836 "max",
3837 "--invariant-dictionary-literals",
3838 "6789",
3839 "--invariant-corpus-random-sequence-weight",
3840 "25",
3841 "--invariant-corpus-dir",
3842 "invariant_corpus",
3843 "--invariant-payable-value-weight",
3844 "34",
3845 "--invariant-mutation-weight-splice",
3846 "2",
3847 "--invariant-mutation-weight-cmp",
3848 "7",
3849 ]);
3850
3851 let config = Config::default().merge_inline_provider(&args).unwrap();
3852 assert_eq!(config.fuzz.dictionary.dictionary_weight, 35);
3853 assert_eq!(config.fuzz.dictionary.max_fuzz_dictionary_addresses, usize::MAX);
3854 assert_eq!(config.fuzz.dictionary.max_fuzz_dictionary_values, 1234);
3855 assert_eq!(config.fuzz.dictionary.max_fuzz_dictionary_literals, 4321);
3856 assert_eq!(config.fuzz.corpus.corpus_random_sequence_weight, 55);
3857 assert_eq!(config.fuzz.corpus.corpus_dir, Some(PathBuf::from("fuzz_corpus")));
3858 assert_eq!(config.fuzz.corpus.frontier_dir, Some(PathBuf::from("fuzz_frontiers")));
3859 assert_eq!(config.fuzz.corpus.frontier_limit, 7);
3860 assert_eq!(config.fuzz.corpus.payable_value_weight, 12);
3861 assert_eq!(config.fuzz.corpus.mutation_weights.mutation_weight_splice, 4);
3862 assert_eq!(config.fuzz.corpus.mutation_weights.mutation_weight_abi, 3);
3863 assert_eq!(config.fuzz.corpus.mutation_weights.mutation_weight_cmp, 5);
3864 assert!(config.symbolic.use_fuzz_frontiers);
3865 assert!(config.symbolic.check_invariant_frontiers);
3866 assert_eq!(config.symbolic.frontier_limit, 3);
3867 assert_eq!(config.symbolic.frontier_ids, vec![4, 9]);
3868 assert_eq!(config.symbolic.frontier_pcs, vec![123, 456]);
3869 assert_eq!(config.symbolic.frontier_selectors, vec!["0x12345678", "deadbeef"]);
3870 assert_eq!(config.invariant.depth, 300);
3871 assert_eq!(config.invariant.min_depth, 20);
3872 assert_eq!(config.invariant.depth_mode, InvariantDepthMode::Random);
3873 assert_eq!(config.invariant.dictionary.dictionary_weight, 45);
3874 assert_eq!(config.invariant.dictionary.max_fuzz_dictionary_addresses, 8765);
3875 assert_eq!(config.invariant.dictionary.max_fuzz_dictionary_values, usize::MAX);
3876 assert_eq!(config.invariant.dictionary.max_fuzz_dictionary_literals, 6789);
3877 assert_eq!(config.invariant.corpus.corpus_random_sequence_weight, 25);
3878 assert_eq!(config.invariant.corpus.corpus_dir, Some(PathBuf::from("invariant_corpus")));
3879 assert!(config.invariant.corpus_random_sequence_weight_configured);
3880 assert_eq!(
3881 config.invariant.workers,
3882 InvariantWorkers::Fixed(std::num::NonZeroUsize::new(4).unwrap())
3883 );
3884 assert!(config.invariant.workers_configured);
3885 assert_eq!(config.invariant.corpus.payable_value_weight, 34);
3886 assert_eq!(config.invariant.corpus.mutation_weights.mutation_weight_splice, 2);
3887 assert_eq!(config.invariant.corpus.mutation_weights.mutation_weight_cmp, 7);
3888 }
3889}