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