1use crate::{
7 MultiContractRunnerBuilder,
8 cmd::test::{FilterArgs, RerunFailure},
9 mutation::{
10 SurvivedSpans,
11 mutant::{Mutant, MutationResult},
12 progress::MutationProgress,
13 },
14 result::SuiteResult,
15 workspace,
16};
17use eyre::Result;
18use foundry_common::{compile::ProjectCompiler, sh_eprintln, sh_println};
19use foundry_compilers::compilers::multi::MultiCompiler;
20use foundry_config::{Config, InlineConfig};
21use foundry_evm::{
22 core::evm::{
23 BlockEnvFor, EthEvmNetwork, FoundryEvmNetwork, SpecFor, TempoEvmNetwork, TxEnvFor,
24 },
25 executors::ExecutorBuilder,
26 fork::ResolvedFork,
27 opts::EvmOpts,
28};
29use rayon::prelude::*;
30use std::{
31 collections::BTreeMap,
32 fs,
33 panic::{self, AssertUnwindSafe},
34 path::{Path, PathBuf},
35 sync::{
36 Arc, Mutex,
37 atomic::{AtomicBool, AtomicUsize, Ordering},
38 mpsc,
39 },
40 thread::JoinHandle,
41 time::Duration,
42};
43use tempfile::TempDir;
44
45#[cfg(feature = "base")]
46use foundry_evm::core::evm::BaseEvmNetwork;
47
48#[cfg(feature = "monad")]
49use foundry_evm::core::evm::MonadEvmNetwork;
50
51#[cfg(feature = "optimism")]
52use foundry_evm::core::evm::OpEvmNetwork;
53
54const MUTATION_STACK_SIZE: usize = 16 * 1024 * 1024;
55
56#[cfg(test)]
57const MUTATION_STACK_PROBE_ENV: &str = "FOUNDRY_MUTATION_STACK_PROBE";
58#[cfg(test)]
59const MUTATION_STACK_PROBE_MARKER_ENV: &str = "FOUNDRY_MUTATION_STACK_PROBE_MARKER";
60#[cfg(test)]
61static MUTATION_STACK_PROBE_RAN: AtomicBool = AtomicBool::new(false);
62
63#[derive(Debug, Clone)]
65pub struct MutantTestResult {
66 pub mutant: Mutant,
67 pub result: MutationResult,
68}
69
70#[derive(Debug, Clone)]
72pub struct MutationBatchResult {
73 pub results: Vec<MutantTestResult>,
74 pub cancelled: bool,
75}
76
77#[derive(Clone)]
79pub struct MutationEvmConfig {
80 pub opts: EvmOpts,
81 pub resolved_fork: Option<ResolvedFork>,
82 pub create2_deployer_available: bool,
83}
84
85pub struct SharedMutationState {
87 pub survived_spans: Mutex<SurvivedSpans>,
89 pub completed: AtomicUsize,
91 pub total: AtomicUsize,
92 pub cancelled: Arc<AtomicBool>,
94 pub progress: Option<MutationProgress>,
96 pub silent: bool,
98 pub pending_workers: Mutex<Vec<JoinHandle<()>>>,
103 max_pending_workers: AtomicUsize,
106}
107
108impl SharedMutationState {
109 pub fn new(
110 cancelled: Arc<AtomicBool>,
111 silent: bool,
112 progress: Option<MutationProgress>,
113 ) -> Self {
114 Self {
115 survived_spans: Mutex::new(SurvivedSpans::new()),
116 completed: AtomicUsize::new(0),
117 total: AtomicUsize::new(0),
118 cancelled,
119 progress,
120 silent,
121 pending_workers: Mutex::new(Vec::new()),
122 max_pending_workers: AtomicUsize::new(usize::MAX),
123 }
124 }
125
126 pub fn is_cancelled(&self) -> bool {
127 self.cancelled.load(Ordering::SeqCst)
128 }
129
130 pub fn cancel(&self) {
131 self.cancelled.store(true, Ordering::SeqCst);
132 if let Some(ref progress) = self.progress {
133 progress.cancel();
134 }
135 }
136
137 pub fn should_skip_span(&self, span: solar::ast::Span) -> bool {
138 self.survived_spans.lock().map(|guard| guard.should_skip_in_live_run(span)).unwrap_or(false)
140 }
141
142 pub fn mark_span_survived(&self, span: solar::ast::Span) {
143 if let Ok(mut guard) = self.survived_spans.lock() {
145 guard.mark_survived(span);
146 }
147 }
148
149 pub fn increment_completed(&self) -> usize {
150 self.completed.fetch_add(1, Ordering::SeqCst) + 1
151 }
152
153 pub fn set_max_pending_workers(&self, max: usize) {
154 self.max_pending_workers.store(max.max(1), Ordering::SeqCst);
155 }
156
157 fn park_timed_out_worker(&self, handle: JoinHandle<()>) {
158 let mut pending = match self.pending_workers.lock() {
159 Ok(pending) => pending,
160 Err(_) => {
161 let _ = handle.join();
162 return;
163 }
164 };
165
166 let max_pending = self.max_pending_workers.load(Ordering::SeqCst).max(1);
167 while pending.len() >= max_pending {
168 let old_handle = pending.remove(0);
169 drop(pending);
170 let _ = old_handle.join();
171 pending = match self.pending_workers.lock() {
172 Ok(pending) => pending,
173 Err(_) => {
174 let _ = handle.join();
175 return;
176 }
177 };
178 }
179
180 pending.push(handle);
181 }
182}
183
184impl Default for SharedMutationState {
185 fn default() -> Self {
186 Self::new(Arc::new(AtomicBool::new(false)), false, None)
187 }
188}
189
190#[allow(clippy::too_many_arguments)]
192pub fn run_mutations_parallel_with_progress(
193 mutants: Vec<Mutant>,
194 source_path: PathBuf,
195 original_source: Arc<String>,
196 config: Arc<Config>,
197 evm: MutationEvmConfig,
198 num_workers: usize,
199 progress: Option<MutationProgress>,
200 silent: bool,
201 filter_args: FilterArgs,
202 rerun_failures: Option<Vec<RerunFailure>>,
203 selected_sources_relative: Arc<Vec<PathBuf>>,
204 isolate: bool,
205 cancellation_requested: Arc<AtomicBool>,
206) -> Result<MutationBatchResult> {
207 let total = mutants.len();
208 if total == 0 {
209 return Ok(MutationBatchResult { results: vec![], cancelled: false });
210 }
211
212 let num_workers = if num_workers == 0 {
214 std::thread::available_parallelism().map(|p| p.get()).unwrap_or(1)
215 } else {
216 num_workers
217 };
218
219 let shared_state = Arc::new(SharedMutationState::new(cancellation_requested, silent, progress));
220 shared_state.total.store(total, Ordering::SeqCst);
221 shared_state.set_max_pending_workers(num_workers);
222
223 if shared_state.progress.is_none() && !shared_state.silent {
225 let _ = sh_println!("Running {} mutants in parallel with {} workers", total, num_workers);
226 }
227
228 let source_abs =
231 if source_path.is_absolute() { source_path } else { config.root.join(&source_path) };
232
233 let root_abs = config.root.canonicalize().unwrap_or_else(|_| config.root.clone());
234 let source_abs = source_abs.canonicalize().unwrap_or(source_abs);
235
236 let source_relative = source_abs
237 .strip_prefix(&root_abs)
238 .map_err(|_| {
239 eyre::eyre!(
240 "Source path {} is not under project root {}",
241 source_abs.display(),
242 root_abs.display()
243 )
244 })?
245 .to_path_buf();
246
247 workspace::ensure_safe_relative_path(&source_relative, "source", &source_abs)?;
248
249 let temp_root = std::env::temp_dir();
253 let temp_root = dunce::canonicalize(&temp_root).map_err(|err| {
254 eyre::eyre!("failed to canonicalize mutation temp root {}: {err}", temp_root.display())
255 })?;
256
257 let pool = rayon::ThreadPoolBuilder::new()
259 .num_threads(num_workers)
260 .stack_size(MUTATION_STACK_SIZE)
261 .build()
262 .map_err(|e| eyre::eyre!("Failed to create thread pool: {}", e))?;
263
264 let completed_results: Arc<Mutex<Vec<MutantTestResult>>> =
266 Arc::new(Mutex::new(Vec::with_capacity(total)));
267
268 let filter_args = Arc::new(filter_args);
269 let rerun_failures = Arc::new(rerun_failures);
270
271 pool.install(|| {
272 mutants.into_par_iter().for_each(|mutant| {
273 if shared_state.is_cancelled() {
275 return;
276 }
277
278 let mutant_clone = mutant.clone();
280 let result = panic::catch_unwind(AssertUnwindSafe(|| {
281 test_single_mutant_isolated(
282 mutant,
283 &source_relative,
284 &original_source,
285 &config,
286 &evm,
287 &shared_state,
288 &temp_root,
289 &filter_args,
290 &rerun_failures,
291 &selected_sources_relative,
292 isolate,
293 )
294 }));
295
296 let test_result = match result {
297 Ok(r) => r,
298 Err(_) => {
299 if shared_state.progress.is_none() {
300 let _ = sh_eprintln!("Panic while testing mutant: {}", mutant_clone);
301 }
302 MutantTestResult { mutant: mutant_clone, result: MutationResult::Invalid }
303 }
304 };
305
306 if let Ok(mut results) = completed_results.lock() {
308 results.push(test_result);
309 }
310 });
311 });
312
313 let results = Arc::try_unwrap(completed_results)
315 .map(|m| m.into_inner().unwrap_or_default())
316 .unwrap_or_default();
317
318 let pending = shared_state
330 .pending_workers
331 .lock()
332 .map(|mut g| std::mem::take(&mut *g))
333 .unwrap_or_default();
334 let pending_count = pending.len();
335 if pending_count > 0 && !shared_state.silent && shared_state.progress.is_none() {
336 let _ = sh_println!("Waiting for {pending_count} timed-out worker(s) to finish cleanup...");
337 }
338 for handle in pending {
339 let _ = handle.join();
340 }
341
342 let cancelled = shared_state.is_cancelled();
343
344 if let Some(ref progress) = shared_state.progress {
346 progress.clear();
347 }
348 if cancelled && !shared_state.silent {
349 let _ = sh_println!(
350 "\nMutation testing cancelled. Showing results for {} completed mutants.\n",
351 results.len()
352 );
353 }
354
355 Ok(MutationBatchResult { results, cancelled })
356}
357
358#[allow(clippy::too_many_arguments)]
360fn test_single_mutant_isolated(
361 mutant: Mutant,
362 source_relative: &PathBuf,
363 original_source: &Arc<String>,
364 config: &Arc<Config>,
365 evm: &MutationEvmConfig,
366 shared_state: &Arc<SharedMutationState>,
367 temp_root: &Path,
368 filter_args: &Arc<FilterArgs>,
369 rerun_failures: &Arc<Option<Vec<RerunFailure>>>,
370 selected_sources_relative: &Arc<Vec<PathBuf>>,
371 isolate: bool,
372) -> MutantTestResult {
373 if shared_state.should_skip_span(mutant.span) {
375 if let Some(ref progress) = shared_state.progress {
376 progress.complete_mutant(&mutant, &MutationResult::Skipped);
377 } else if !shared_state.silent {
378 let completed = shared_state.increment_completed();
379 let total = shared_state.total.load(Ordering::SeqCst);
380 let _ = sh_println!(
381 "[{}/{}] Skipping mutant (adaptive: span already has surviving mutation)",
382 completed,
383 total
384 );
385 }
386 return MutantTestResult { mutant, result: MutationResult::Skipped };
387 }
388
389 if let Some(ref progress) = shared_state.progress {
391 progress.start_mutant(&mutant);
392 } else if !shared_state.silent {
393 let completed = shared_state.increment_completed();
394 let total = shared_state.total.load(Ordering::SeqCst);
395 let _ = sh_println!("[{}/{}] Testing mutant: {}", completed, total, mutant);
396 }
397
398 let temp_dir = match TempDir::with_prefix_in("forge_mutation_", temp_root) {
400 Ok(dir) => dir,
401 Err(e) => {
402 let _ =
403 sh_eprintln!("Failed to create temp directory in {}: {}", temp_root.display(), e);
404 return MutantTestResult { mutant, result: MutationResult::Invalid };
405 }
406 };
407
408 if let Err(e) = workspace::copy_project(config, temp_dir.path()) {
410 let _ = sh_eprintln!("Failed to copy project: {}", e);
411 return MutantTestResult { mutant, result: MutationResult::Invalid };
412 }
413
414 let mutated_source_path = temp_dir.path().join(source_relative);
416 if let Err(e) = apply_mutation(&mutant, original_source, &mutated_source_path) {
417 let _ = sh_eprintln!("Failed to apply mutation: {}", e);
418 return MutantTestResult { mutant, result: MutationResult::Invalid };
419 }
420
421 let temp_path = temp_dir.path().to_path_buf();
422 let temp_config = temp_config_for_mutation(config, &temp_path);
423 let temp_config = Arc::new(temp_config);
424
425 let timeout = config.mutation.timeout.map(|s| Duration::from_secs(s as u64));
439
440 let result = match timeout {
441 Some(budget) => run_compile_and_test_with_timeout(
442 temp_config,
443 evm,
444 budget,
445 temp_dir,
446 shared_state,
447 filter_args.clone(),
448 rerun_failures.clone(),
449 selected_sources_relative.clone(),
450 isolate,
451 ),
452 None => {
453 let res = match compile_and_test(
454 &temp_config,
455 evm,
456 filter_args,
457 rerun_failures.as_ref().as_deref(),
458 selected_sources_relative,
459 isolate,
460 ) {
461 Ok(true) => MutationResult::Dead,
462 Ok(false) => MutationResult::Alive,
463 Err(_) => MutationResult::Invalid,
464 };
465 drop(temp_dir); res
467 }
468 };
469
470 if matches!(result, MutationResult::Alive) {
473 shared_state.mark_span_survived(mutant.span);
474 }
475
476 if let Some(ref progress) = shared_state.progress {
478 progress.complete_mutant(&mutant, &result);
479 }
480
481 MutantTestResult { mutant, result }
482}
483
484#[allow(clippy::too_many_arguments)]
493fn run_compile_and_test_with_timeout(
494 config: Arc<Config>,
495 evm: &MutationEvmConfig,
496 budget: Duration,
497 temp_dir: TempDir,
498 shared_state: &Arc<SharedMutationState>,
499 filter_args: Arc<FilterArgs>,
500 rerun_failures: Arc<Option<Vec<RerunFailure>>>,
501 selected_sources_relative: Arc<Vec<PathBuf>>,
502 isolate: bool,
503) -> MutationResult {
504 let (tx, rx) = mpsc::channel::<Result<bool>>();
505 let evm = evm.clone();
506 let cfg = Arc::clone(&config);
510 let filter_for_worker = Arc::clone(&filter_args);
511 let rerun_for_worker = Arc::clone(&rerun_failures);
512 let selected_sources_for_worker = Arc::clone(&selected_sources_relative);
513
514 let spawn_result = std::thread::Builder::new()
515 .stack_size(MUTATION_STACK_SIZE)
516 .name("mutation-worker".to_string())
517 .spawn(move || {
518 let res = panic::catch_unwind(AssertUnwindSafe(|| {
522 with_mutation_test_pool(|| {
523 compile_and_test(
524 &cfg,
525 &evm,
526 &filter_for_worker,
527 rerun_for_worker.as_ref().as_deref(),
528 &selected_sources_for_worker,
529 isolate,
530 )
531 })
532 }))
533 .unwrap_or_else(|_| Err(eyre::eyre!("worker panicked")));
534 let _ = tx.send(res);
535 drop(temp_dir);
539 });
540
541 let handle = match spawn_result {
542 Ok(h) => h,
543 Err(_) => return MutationResult::Invalid,
544 };
545
546 match rx.recv_timeout(budget) {
547 Ok(Ok(true)) => {
548 let _ = handle.join();
551 MutationResult::Dead
552 }
553 Ok(Ok(false)) => {
554 let _ = handle.join();
555 MutationResult::Alive
556 }
557 Ok(Err(_)) => {
558 let _ = handle.join();
559 MutationResult::Invalid
560 }
561 Err(_) => {
562 shared_state.park_timed_out_worker(handle);
566 MutationResult::TimedOut
567 }
568 }
569}
570
571fn with_mutation_test_pool<T: Send>(op: impl FnOnce() -> Result<T> + Send) -> Result<T> {
572 rayon::ThreadPoolBuilder::new()
573 .num_threads(1)
574 .stack_size(MUTATION_STACK_SIZE)
575 .thread_name(|_| "mutation-test".to_string())
576 .build()
577 .map_err(|err| eyre::eyre!("failed to create mutation test pool: {err}"))?
578 .install(|| {
579 #[cfg(test)]
580 if std::env::var_os(MUTATION_STACK_PROBE_ENV).is_some() {
581 std::hint::black_box(mutation_stack_probe(1024));
582 MUTATION_STACK_PROBE_RAN.store(true, Ordering::Release);
583 }
584 op()
585 })
586}
587
588#[cfg(test)]
589#[inline(never)]
590fn mutation_stack_probe(depth: usize) -> usize {
591 let frame = [depth as u8; 8 * 1024];
592 std::hint::black_box(&frame);
593 if depth == 0 {
594 frame[0] as usize
595 } else {
596 let result = mutation_stack_probe(depth - 1).wrapping_add(frame[depth % frame.len()] as _);
597 std::hint::black_box(&frame);
598 result
599 }
600}
601
602fn apply_mutation(mutant: &Mutant, original_source: &str, dest_path: &Path) -> Result<()> {
604 let span = mutant.span;
605 let replacement = mutant.mutation.to_string();
606 let start_pos = span.lo().0 as usize;
607 let end_pos = span.hi().0 as usize;
608
609 let before = original_source.get(..start_pos).ok_or_else(|| {
611 eyre::eyre!(
612 "Invalid mutation span: start {} is out of bounds for source length {}",
613 start_pos,
614 original_source.len()
615 )
616 })?;
617
618 let after = original_source.get(end_pos..).ok_or_else(|| {
619 eyre::eyre!(
620 "Invalid mutation span: end {} is out of bounds for source length {}",
621 end_pos,
622 original_source.len()
623 )
624 })?;
625
626 let mut new_content = String::with_capacity(before.len() + replacement.len() + after.len());
627 new_content.push_str(before);
628 new_content.push_str(&replacement);
629 new_content.push_str(after);
630
631 if let Some(parent) = dest_path.parent() {
633 fs::create_dir_all(parent)?;
634 }
635
636 fs::write(dest_path, new_content)?;
637 Ok(())
638}
639
640fn temp_config_for_mutation(config: &Config, temp_path: &Path) -> Config {
646 let mut temp_config = workspace::rebase_config_paths(config, temp_path);
647
648 if let Some(mutation_timeout) = config.mutation.timeout {
654 temp_config.fuzz.timeout = Some(match temp_config.fuzz.timeout {
655 Some(existing) => existing.min(mutation_timeout),
656 None => mutation_timeout,
657 });
658 temp_config.invariant.timeout = Some(match temp_config.invariant.timeout {
659 Some(existing) => existing.min(mutation_timeout),
660 None => mutation_timeout,
661 });
662 }
663
664 temp_config
665}
666
667fn compile_and_test(
671 config: &Arc<Config>,
672 evm: &MutationEvmConfig,
673 filter_args: &FilterArgs,
674 rerun_failures: Option<&[RerunFailure]>,
675 selected_sources_relative: &[PathBuf],
676 isolate: bool,
677) -> Result<bool> {
678 if evm.opts.networks.is_tempo() {
679 compile_and_test_inner::<TempoEvmNetwork>(
680 config,
681 evm,
682 filter_args,
683 rerun_failures,
684 selected_sources_relative,
685 isolate,
686 ExecutorBuilder::<TempoEvmNetwork>::new(),
687 )
688 } else {
689 #[cfg(feature = "base")]
690 if evm.opts.networks.is_base() {
691 return compile_and_test_inner::<BaseEvmNetwork>(
692 config,
693 evm,
694 filter_args,
695 rerun_failures,
696 selected_sources_relative,
697 isolate,
698 ExecutorBuilder::<BaseEvmNetwork>::new(),
699 );
700 }
701 #[cfg(feature = "monad")]
702 if evm.opts.networks.is_monad() {
703 return compile_and_test_inner::<MonadEvmNetwork>(
704 config,
705 evm,
706 filter_args,
707 rerun_failures,
708 selected_sources_relative,
709 isolate,
710 ExecutorBuilder::<MonadEvmNetwork>::new(),
711 );
712 }
713 #[cfg(feature = "optimism")]
714 if evm.opts.networks.is_optimism() {
715 return compile_and_test_inner::<OpEvmNetwork>(
716 config,
717 evm,
718 filter_args,
719 rerun_failures,
720 selected_sources_relative,
721 isolate,
722 ExecutorBuilder::<OpEvmNetwork>::new(),
723 );
724 }
725 compile_and_test_inner::<EthEvmNetwork>(
726 config,
727 evm,
728 filter_args,
729 rerun_failures,
730 selected_sources_relative,
731 isolate,
732 ExecutorBuilder::<EthEvmNetwork>::new(),
733 )
734 }
735}
736
737fn compile_and_test_inner<FEN: FoundryEvmNetwork>(
738 config: &Arc<Config>,
739 evm: &MutationEvmConfig,
740 filter_args: &FilterArgs,
741 rerun_failures: Option<&[RerunFailure]>,
742 selected_sources_relative: &[PathBuf],
743 isolate: bool,
744 executor_builder: ExecutorBuilder<FEN>,
745) -> Result<bool> {
746 let evm_opts = &evm.opts;
747 let resolved_fork = evm.resolved_fork.as_ref();
748 let files = selected_sources_relative
750 .iter()
751 .map(|path| config.root.join(path))
752 .filter(|path| path.exists())
753 .collect::<Vec<_>>();
754 let compiler = ProjectCompiler::new()
755 .dynamic_test_linking(config.dynamic_test_linking)
756 .quiet(true)
757 .files(files);
758
759 let compile_output = compiler.compile(&config.project()?)?;
760 let inline_config = Arc::new(InlineConfig::new_parsed(&compile_output, config)?);
761
762 let mut filter = filter_args.clone().merge_with_config(config);
767 if let Some(rerun_failures) = rerun_failures {
768 filter.set_rerun_failures(rerun_failures.to_vec());
769 }
770
771 let rt = tokio::runtime::Builder::new_multi_thread()
774 .worker_threads(1) .enable_all()
776 .build()
777 .map_err(|e| eyre::eyre!("Failed to create tokio runtime: {}", e))?;
778
779 let results: BTreeMap<String, SuiteResult> = rt.block_on(async {
781 let (evm_env, tx_env) = evm_opts
782 .env_with_resolved_fork::<SpecFor<FEN>, BlockEnvFor<FEN>, TxEnvFor<FEN>>(resolved_fork)
783 .await?;
784 let fork_context = resolved_fork.map(ResolvedFork::context);
785 let fork_chain_id = fork_context.map(|context| context.source_chain_id);
786 let fork_hardfork = fork_context.and_then(|context| context.hardfork);
787
788 let mut runner = MultiContractRunnerBuilder::new(config.clone(), inline_config)
793 .set_debug(false)
794 .initial_balance(evm_opts.initial_balance)
795 .sender(evm_opts.sender)
796 .with_fork(evm_opts.get_fork_resolved(config, evm_env.cfg_env.chain_id, resolved_fork))
797 .with_fork_chain_id(fork_chain_id)
798 .with_fork_hardfork(fork_hardfork)
799 .enable_isolation(isolate)
800 .fail_fast(true)
801 .with_create2_deployer_available(evm.create2_deployer_available)
802 .build::<FEN, MultiCompiler>(
803 &compile_output,
804 evm_env,
805 tx_env,
806 evm_opts.clone(),
807 executor_builder,
808 )?;
809
810 runner.test_collect(&filter)
811 })?;
812
813 let killed = results.values().any(|suite| suite.failed() > 0);
815
816 Ok(killed)
817}
818
819#[cfg(test)]
820mod tests {
821 use super::*;
822 use alloy_primitives::U256;
823 use std::process::Command;
824
825 #[test]
826 fn timeout_path_uses_enlarged_test_worker_stack() {
827 if std::env::var_os(MUTATION_STACK_PROBE_ENV).is_some() {
828 let workspace = TempDir::new().unwrap();
829 let source = PathBuf::from("src/StackProbe.t.sol");
830 fs::create_dir_all(workspace.path().join("src")).unwrap();
831 fs::write(
832 workspace.path().join(&source),
833 "contract StackProbeTest { function test_stackProbe() public {} }",
834 )
835 .unwrap();
836 let mut config = Config::with_root(workspace.path());
837 config.out = workspace.path().join("out");
838 config.cache_path = workspace.path().join("cache/solidity-files-cache.json");
839 let config = Arc::new(config);
840 let evm = MutationEvmConfig {
841 opts: EvmOpts::default(),
842 resolved_fork: None,
843 create2_deployer_available: false,
844 };
845 let shared_state = Arc::new(SharedMutationState::default());
846
847 let result = run_compile_and_test_with_timeout(
848 config,
849 &evm,
850 Duration::from_secs(60),
851 workspace,
852 &shared_state,
853 Arc::new(FilterArgs::default()),
854 Arc::new(None),
855 Arc::new(vec![source]),
856 false,
857 );
858
859 assert!(!matches!(result, MutationResult::TimedOut), "unexpected result: {result:?}");
860 assert!(MUTATION_STACK_PROBE_RAN.load(Ordering::Acquire));
861 fs::write(std::env::var_os(MUTATION_STACK_PROBE_MARKER_ENV).unwrap(), b"ok").unwrap();
862 return;
863 }
864
865 let marker_dir = TempDir::new().unwrap();
866 let marker = marker_dir.path().join("completed");
867 let output = Command::new(std::env::current_exe().unwrap())
868 .args([
869 "--exact",
870 "mutation::runner::tests::timeout_path_uses_enlarged_test_worker_stack",
871 ])
872 .env(MUTATION_STACK_PROBE_ENV, "1")
873 .env(MUTATION_STACK_PROBE_MARKER_ENV, &marker)
874 .output()
875 .unwrap();
876 let stdout = String::from_utf8_lossy(&output.stdout);
877 let stderr = String::from_utf8_lossy(&output.stderr);
878
879 assert!(
880 output.status.success(),
881 "stack-pressure child failed:\nstdout:\n{stdout}\nstderr:\n{stderr}"
882 );
883 assert!(marker.exists(), "stack-pressure child did not complete the production path");
884 }
885
886 #[test]
887 fn park_timed_out_worker_bounds_pending_handles() {
888 let state = SharedMutationState::default();
889 state.set_max_pending_workers(1);
890
891 state.park_timed_out_worker(std::thread::spawn(|| {}));
892 assert_eq!(state.pending_workers.lock().unwrap().len(), 1);
893
894 state.park_timed_out_worker(std::thread::spawn(|| {}));
895 assert_eq!(state.pending_workers.lock().unwrap().len(), 1);
896
897 let pending = std::mem::take(&mut *state.pending_workers.lock().unwrap());
898 for handle in pending {
899 handle.join().unwrap();
900 }
901 }
902
903 #[test]
904 fn temp_config_preserves_materialized_overrides_and_rebases_paths() {
905 let project = TempDir::new().unwrap();
906 let temp = TempDir::new().unwrap();
907 let root = project.path();
908
909 let mut config = Config {
910 root: root.to_path_buf(),
911 src: root.join("contracts"),
912 test: root.join("checks"),
913 script: root.join("deploy"),
914 out: root.join("custom-out"),
915 cache_path: root.join("custom-cache"),
916 snapshots: root.join("custom-snapshots"),
917 broadcast: root.join("custom-broadcast"),
918 mutation_dir: root.join("custom-cache/mutation"),
919 libs: vec![root.join("vendor")],
920 include_paths: vec![root.join("shared")],
921 allow_paths: vec![root.join("fixtures")],
922 dynamic_test_linking: true,
923 cache: true,
924 ..Default::default()
925 };
926 config.fuzz.seed = Some(U256::from(42));
927 config.fuzz.timeout = Some(90);
928 config.invariant.timeout = Some(80);
929 config.fuzz.failure_persist_dir = Some(root.join("custom-cache/fuzz"));
930 config.invariant.failure_persist_dir = Some(root.join("custom-cache/invariant"));
931 config.mutation.timeout = Some(5);
932
933 let temp_config = temp_config_for_mutation(&config, temp.path());
934 let expected_root = dunce::canonicalize(temp.path()).unwrap();
935
936 assert_eq!(temp_config.root, expected_root);
937 assert_eq!(temp_config.src, expected_root.join("contracts"));
938 assert_eq!(temp_config.test, expected_root.join("checks"));
939 assert_eq!(temp_config.script, expected_root.join("deploy"));
940 assert_eq!(temp_config.out, expected_root.join("custom-out"));
941 assert_eq!(temp_config.cache_path, expected_root.join("custom-cache"));
942 assert_eq!(temp_config.snapshots, expected_root.join("custom-snapshots"));
943 assert_eq!(temp_config.broadcast, expected_root.join("custom-broadcast"));
944 assert_eq!(temp_config.mutation_dir, expected_root.join("custom-cache/mutation"));
945 assert_eq!(temp_config.libs, vec![expected_root.join("vendor")]);
946 assert_eq!(temp_config.include_paths, vec![expected_root.join("shared")]);
947 assert_eq!(temp_config.allow_paths, vec![expected_root.join("fixtures")]);
948 assert_eq!(
949 temp_config.fuzz.failure_persist_dir,
950 Some(expected_root.join("custom-cache/fuzz"))
951 );
952 assert_eq!(
953 temp_config.invariant.failure_persist_dir,
954 Some(expected_root.join("custom-cache/invariant"))
955 );
956 assert!(temp_config.dynamic_test_linking);
957 assert!(temp_config.cache);
958 assert_eq!(temp_config.fuzz.seed, Some(U256::from(42)));
959 assert_eq!(temp_config.fuzz.timeout, Some(5));
960 assert_eq!(temp_config.invariant.timeout, Some(5));
961 }
962}