1use crate::{
7 MultiContractRunnerBuilder,
8 cmd::test::{FilterArgs, RerunFailure},
9 mutation::{
10 mutant::{Mutant, MutationResult},
11 progress::MutationProgress,
12 },
13 result::SuiteResult,
14 workspace,
15};
16use eyre::Result;
17use foundry_common::{compile::ProjectCompiler, fs::canonicalize_path, sh_eprintln, sh_println};
18use foundry_compilers::compilers::multi::MultiCompiler;
19use foundry_config::{Config, InlineConfig};
20use foundry_evm::{
21 backend::Backend,
22 core::evm::{EthEvmNetwork, FoundryEvmNetwork},
23 executors::ExecutorBuilder,
24 opts::EvmOpts,
25};
26use std::{
27 collections::BTreeMap,
28 fs,
29 panic::{self, AssertUnwindSafe},
30 path::{Path, PathBuf},
31 sync::{
32 Arc, Mutex,
33 atomic::{AtomicBool, AtomicUsize, Ordering},
34 mpsc,
35 },
36 thread::JoinHandle,
37 time::{Duration, Instant},
38};
39use tempfile::TempDir;
40
41const MUTATION_STACK_SIZE: usize = 16 * 1024 * 1024;
42
43#[cfg(test)]
44const MUTATION_STACK_PROBE_ENV: &str = "FOUNDRY_MUTATION_STACK_PROBE";
45#[cfg(test)]
46const MUTATION_STACK_PROBE_MARKER_ENV: &str = "FOUNDRY_MUTATION_STACK_PROBE_MARKER";
47#[cfg(test)]
48static MUTATION_STACK_PROBE_RAN: AtomicBool = AtomicBool::new(false);
49
50#[derive(Debug, Clone)]
52pub struct MutantTestResult {
53 pub mutant: Mutant,
54 pub result: MutationResult,
55}
56
57#[derive(Debug, Clone)]
59pub struct MutationBatchResult {
60 pub results: Vec<MutantTestResult>,
61 pub cancelled: bool,
62}
63
64#[derive(Clone)]
66pub struct MutationEvmConfig<FEN: FoundryEvmNetwork = EthEvmNetwork> {
67 pub opts: EvmOpts,
68 pub backend: Backend<FEN>,
69 pub executor_builder: ExecutorBuilder<FEN>,
70 pub create2_deployer_available: bool,
71}
72
73pub struct SharedMutationState {
75 pub completed: AtomicUsize,
77 pub total: AtomicUsize,
78 pub cancelled: Arc<AtomicBool>,
80 pub progress: Option<MutationProgress>,
82 pub silent: bool,
84 pub pending_workers: Mutex<Vec<JoinHandle<()>>>,
89 max_pending_workers: AtomicUsize,
92}
93
94impl SharedMutationState {
95 pub const fn new(
96 cancelled: Arc<AtomicBool>,
97 silent: bool,
98 progress: Option<MutationProgress>,
99 ) -> Self {
100 Self {
101 completed: AtomicUsize::new(0),
102 total: AtomicUsize::new(0),
103 cancelled,
104 progress,
105 silent,
106 pending_workers: Mutex::new(Vec::new()),
107 max_pending_workers: AtomicUsize::new(usize::MAX),
108 }
109 }
110
111 pub fn is_cancelled(&self) -> bool {
112 self.cancelled.load(Ordering::SeqCst)
113 }
114
115 pub fn cancel(&self) {
116 self.cancelled.store(true, Ordering::SeqCst);
117 if let Some(ref progress) = self.progress {
118 progress.cancel();
119 }
120 }
121
122 pub fn increment_completed(&self) -> usize {
123 self.completed.fetch_add(1, Ordering::SeqCst) + 1
124 }
125
126 pub fn set_max_pending_workers(&self, max: usize) {
127 self.max_pending_workers.store(max.max(1), Ordering::SeqCst);
128 }
129
130 fn park_timed_out_worker(&self, handle: JoinHandle<()>) {
131 let mut pending = match self.pending_workers.lock() {
132 Ok(pending) => pending,
133 Err(_) => {
134 let _ = handle.join();
135 return;
136 }
137 };
138
139 let max_pending = self.max_pending_workers.load(Ordering::SeqCst).max(1);
140 while pending.len() >= max_pending {
141 let old_handle = pending.remove(0);
142 drop(pending);
143 let _ = old_handle.join();
144 pending = match self.pending_workers.lock() {
145 Ok(pending) => pending,
146 Err(_) => {
147 let _ = handle.join();
148 return;
149 }
150 };
151 }
152
153 pending.push(handle);
154 }
155}
156
157impl Default for SharedMutationState {
158 fn default() -> Self {
159 Self::new(Arc::new(AtomicBool::new(false)), false, None)
160 }
161}
162
163#[allow(clippy::too_many_arguments)]
165pub fn run_mutations_parallel_with_progress<FEN: FoundryEvmNetwork>(
166 mutants: Vec<Mutant>,
167 source_path: PathBuf,
168 original_source: Arc<String>,
169 config: Arc<Config>,
170 evm: MutationEvmConfig<FEN>,
171 num_workers: usize,
172 progress: Option<MutationProgress>,
173 silent: bool,
174 filter_args: FilterArgs,
175 rerun_failures: Option<Vec<RerunFailure>>,
176 selected_sources_relative: Arc<Vec<PathBuf>>,
177 isolate: bool,
178 cancellation_requested: Arc<AtomicBool>,
179) -> Result<MutationBatchResult> {
180 let total = mutants.len();
181 if total == 0 {
182 return Ok(MutationBatchResult { results: vec![], cancelled: false });
183 }
184
185 let num_workers = if num_workers == 0 {
187 std::thread::available_parallelism().map(|p| p.get()).unwrap_or(1)
188 } else {
189 num_workers
190 };
191
192 let shared_state = Arc::new(SharedMutationState::new(cancellation_requested, silent, progress));
193 shared_state.total.store(total, Ordering::SeqCst);
194 shared_state.set_max_pending_workers(num_workers);
195
196 if shared_state.progress.is_none() && !shared_state.silent {
198 let _ = sh_println!("Running {} mutants in parallel with {} workers", total, num_workers);
199 }
200
201 let source_abs =
204 if source_path.is_absolute() { source_path } else { config.root.join(&source_path) };
205
206 let root_abs = config.root.canonicalize().unwrap_or_else(|_| config.root.clone());
207 let source_abs = source_abs.canonicalize().unwrap_or(source_abs);
208
209 let source_relative = source_abs
210 .strip_prefix(&root_abs)
211 .map_err(|_| {
212 eyre::eyre!(
213 "Source path {} is not under project root {}",
214 source_abs.display(),
215 root_abs.display()
216 )
217 })?
218 .to_path_buf();
219
220 workspace::ensure_safe_relative_path(&source_relative, "source", &source_abs)?;
221
222 let temp_root = std::env::temp_dir();
226 let temp_root = canonicalize_path(&temp_root).map_err(|err| {
227 eyre::eyre!("failed to canonicalize mutation temp root {}: {err}", temp_root.display())
228 })?;
229
230 let pool = rayon::ThreadPoolBuilder::new()
232 .num_threads(num_workers)
233 .stack_size(MUTATION_STACK_SIZE)
234 .build()
235 .map_err(|e| eyre::eyre!("Failed to create thread pool: {}", e))?;
236
237 let completed_results: Arc<Mutex<Vec<MutantTestResult>>> =
239 Arc::new(Mutex::new(Vec::with_capacity(total)));
240
241 let filter_args = Arc::new(filter_args);
242 let rerun_failures = Arc::new(rerun_failures);
243
244 let queue = Mutex::new(mutants.into_iter());
249 pool.broadcast(|_| {
250 loop {
251 if shared_state.is_cancelled() {
252 break;
253 }
254 let Some(mutant) = queue.lock().ok().and_then(|mut queue| queue.next()) else {
255 break;
256 };
257
258 let mutant_clone = mutant.clone();
260 let result = panic::catch_unwind(AssertUnwindSafe(|| {
261 test_single_mutant_isolated(
262 mutant,
263 &source_relative,
264 &original_source,
265 &config,
266 &evm,
267 &shared_state,
268 &temp_root,
269 &filter_args,
270 &rerun_failures,
271 &selected_sources_relative,
272 isolate,
273 )
274 }));
275
276 let test_result = match result {
277 Ok(r) => r,
278 Err(_) => {
279 if shared_state.progress.is_none() {
280 let _ = sh_eprintln!("Panic while testing mutant: {}", mutant_clone);
281 }
282 MutantTestResult { mutant: mutant_clone, result: MutationResult::Invalid }
283 }
284 };
285
286 if let Ok(mut results) = completed_results.lock() {
288 results.push(test_result);
289 }
290 }
291 });
292
293 let results = Arc::try_unwrap(completed_results)
295 .map(|m| m.into_inner().unwrap_or_default())
296 .unwrap_or_default();
297
298 let pending = shared_state
310 .pending_workers
311 .lock()
312 .map(|mut g| std::mem::take(&mut *g))
313 .unwrap_or_default();
314 let pending_count = pending.len();
315 if pending_count > 0 && !shared_state.silent && shared_state.progress.is_none() {
316 let _ = sh_println!("Waiting for {pending_count} timed-out worker(s) to finish cleanup...");
317 }
318 for handle in pending {
319 let _ = handle.join();
320 }
321
322 let cancelled = shared_state.is_cancelled();
323
324 if let Some(ref progress) = shared_state.progress {
326 progress.clear();
327 }
328 if cancelled && !shared_state.silent {
329 let _ = sh_println!(
330 "\nMutation testing cancelled. Showing results for {} completed mutants.\n",
331 results.len()
332 );
333 }
334
335 Ok(MutationBatchResult { results, cancelled })
336}
337
338#[allow(clippy::too_many_arguments)]
340fn test_single_mutant_isolated<FEN: FoundryEvmNetwork>(
341 mutant: Mutant,
342 source_relative: &PathBuf,
343 original_source: &Arc<String>,
344 config: &Arc<Config>,
345 evm: &MutationEvmConfig<FEN>,
346 shared_state: &Arc<SharedMutationState>,
347 temp_root: &Path,
348 filter_args: &Arc<FilterArgs>,
349 rerun_failures: &Arc<Option<Vec<RerunFailure>>>,
350 selected_sources_relative: &Arc<Vec<PathBuf>>,
351 isolate: bool,
352) -> MutantTestResult {
353 if let Some(ref progress) = shared_state.progress {
355 progress.start_mutant(&mutant);
356 } else if !shared_state.silent {
357 let completed = shared_state.increment_completed();
358 let total = shared_state.total.load(Ordering::SeqCst);
359 let _ = sh_println!("[{}/{}] Testing mutant: {}", completed, total, mutant);
360 }
361
362 let temp_dir = match TempDir::with_prefix_in("forge_mutation_", temp_root) {
364 Ok(dir) => dir,
365 Err(e) => {
366 let _ =
367 sh_eprintln!("Failed to create temp directory in {}: {}", temp_root.display(), e);
368 return MutantTestResult { mutant, result: MutationResult::Invalid };
369 }
370 };
371
372 if let Err(e) = workspace::copy_project(config, temp_dir.path()) {
374 let _ = sh_eprintln!("Failed to copy project: {}", e);
375 return MutantTestResult { mutant, result: MutationResult::Invalid };
376 }
377
378 if let Err(e) = workspace::unshare_path(&config.root, temp_dir.path(), source_relative) {
381 let _ = sh_eprintln!("Failed to isolate mutated source: {}", e);
382 return MutantTestResult { mutant, result: MutationResult::Invalid };
383 }
384
385 let mutated_source_path = temp_dir.path().join(source_relative);
387 if let Err(e) = apply_mutation(&mutant, original_source, &mutated_source_path) {
388 let _ = sh_eprintln!("Failed to apply mutation: {}", e);
389 return MutantTestResult { mutant, result: MutationResult::Invalid };
390 }
391
392 let temp_config = Arc::new(workspace::rebase_config_paths(config, temp_dir.path()));
396
397 let timeout = config.mutation.timeout.map(|s| Duration::from_secs(s as u64));
411
412 let result = match timeout {
413 Some(budget) => run_compile_and_test_with_timeout(
414 temp_config,
415 evm,
416 budget,
417 temp_dir,
418 shared_state,
419 filter_args.clone(),
420 rerun_failures.clone(),
421 selected_sources_relative.clone(),
422 isolate,
423 ),
424 None => {
425 let res = match compile_and_test(
426 &temp_config,
427 evm,
428 filter_args,
429 rerun_failures.as_ref().as_deref(),
430 selected_sources_relative,
431 isolate,
432 None,
433 ) {
434 Ok(true) => MutationResult::Dead,
435 Ok(false) => MutationResult::Alive,
436 Err(_) => MutationResult::Invalid,
437 };
438 drop(temp_dir); res
440 }
441 };
442
443 if let Some(ref progress) = shared_state.progress {
445 progress.complete_mutant(&mutant, &result);
446 }
447
448 MutantTestResult { mutant, result }
449}
450
451#[allow(clippy::too_many_arguments)]
463fn run_compile_and_test_with_timeout<FEN: FoundryEvmNetwork>(
464 config: Arc<Config>,
465 evm: &MutationEvmConfig<FEN>,
466 budget: Duration,
467 temp_dir: TempDir,
468 shared_state: &Arc<SharedMutationState>,
469 filter_args: Arc<FilterArgs>,
470 rerun_failures: Arc<Option<Vec<RerunFailure>>>,
471 selected_sources_relative: Arc<Vec<PathBuf>>,
472 isolate: bool,
473) -> MutationResult {
474 let deadline = Instant::now() + budget;
475 let (tx, rx) = mpsc::channel::<Result<bool>>();
476 let evm = evm.clone();
477 let cfg = Arc::clone(&config);
481 let filter_for_worker = Arc::clone(&filter_args);
482 let rerun_for_worker = Arc::clone(&rerun_failures);
483 let selected_sources_for_worker = Arc::clone(&selected_sources_relative);
484
485 let spawn_result = std::thread::Builder::new()
486 .stack_size(MUTATION_STACK_SIZE)
487 .name("mutation-worker".to_string())
488 .spawn(move || {
489 let res = panic::catch_unwind(AssertUnwindSafe(|| {
493 with_mutation_test_pool(|| {
494 compile_and_test(
495 &cfg,
496 &evm,
497 &filter_for_worker,
498 rerun_for_worker.as_ref().as_deref(),
499 &selected_sources_for_worker,
500 isolate,
501 Some(deadline),
502 )
503 })
504 }))
505 .unwrap_or_else(|_| Err(eyre::eyre!("worker panicked")));
506 let _ = tx.send(res);
507 drop(temp_dir);
511 });
512
513 let handle = match spawn_result {
514 Ok(h) => h,
515 Err(_) => return MutationResult::Invalid,
516 };
517
518 match rx.recv_timeout(deadline.saturating_duration_since(Instant::now())) {
519 Ok(_) if Instant::now() >= deadline => {
522 let _ = handle.join();
523 MutationResult::TimedOut
524 }
525 Ok(Ok(true)) => {
526 let _ = handle.join();
529 MutationResult::Dead
530 }
531 Ok(Ok(false)) => {
532 let _ = handle.join();
533 MutationResult::Alive
534 }
535 Ok(Err(_)) => {
536 let _ = handle.join();
537 MutationResult::Invalid
538 }
539 Err(_) => {
540 shared_state.park_timed_out_worker(handle);
544 MutationResult::TimedOut
545 }
546 }
547}
548
549fn with_mutation_test_pool<T: Send>(op: impl FnOnce() -> Result<T> + Send) -> Result<T> {
550 rayon::ThreadPoolBuilder::new()
551 .num_threads(1)
552 .stack_size(MUTATION_STACK_SIZE)
553 .thread_name(|_| "mutation-test".to_string())
554 .build()
555 .map_err(|err| eyre::eyre!("failed to create mutation test pool: {err}"))?
556 .install(|| {
557 #[cfg(test)]
558 if std::env::var_os(MUTATION_STACK_PROBE_ENV).is_some() {
559 std::hint::black_box(mutation_stack_probe(1024));
560 MUTATION_STACK_PROBE_RAN.store(true, Ordering::Release);
561 }
562 op()
563 })
564}
565
566#[cfg(test)]
567#[inline(never)]
568fn mutation_stack_probe(depth: usize) -> usize {
569 let frame = [depth as u8; 8 * 1024];
570 std::hint::black_box(&frame);
571 if depth == 0 {
572 frame[0] as usize
573 } else {
574 let result = mutation_stack_probe(depth - 1).wrapping_add(frame[depth % frame.len()] as _);
575 std::hint::black_box(&frame);
576 result
577 }
578}
579
580fn apply_mutation(mutant: &Mutant, original_source: &str, dest_path: &Path) -> Result<()> {
582 let span = mutant.span;
583 let replacement = mutant.mutation.to_string();
584 let start_pos = span.lo().0 as usize;
585 let end_pos = span.hi().0 as usize;
586
587 let before = original_source.get(..start_pos).ok_or_else(|| {
589 eyre::eyre!(
590 "Invalid mutation span: start {} is out of bounds for source length {}",
591 start_pos,
592 original_source.len()
593 )
594 })?;
595
596 let after = original_source.get(end_pos..).ok_or_else(|| {
597 eyre::eyre!(
598 "Invalid mutation span: end {} is out of bounds for source length {}",
599 end_pos,
600 original_source.len()
601 )
602 })?;
603
604 let mut new_content = String::with_capacity(before.len() + replacement.len() + after.len());
605 new_content.push_str(before);
606 new_content.push_str(&replacement);
607 new_content.push_str(after);
608
609 if let Some(parent) = dest_path.parent() {
611 fs::create_dir_all(parent)?;
612 }
613
614 fs::write(dest_path, new_content)?;
615 Ok(())
616}
617
618fn compile_and_test<FEN: FoundryEvmNetwork>(
624 config: &Arc<Config>,
625 evm: &MutationEvmConfig<FEN>,
626 filter_args: &FilterArgs,
627 rerun_failures: Option<&[RerunFailure]>,
628 selected_sources_relative: &[PathBuf],
629 isolate: bool,
630 deadline: Option<Instant>,
631) -> Result<bool> {
632 let evm_opts = &evm.opts;
633 let files = selected_sources_relative
635 .iter()
636 .map(|path| config.root.join(path))
637 .filter(|path| path.exists())
638 .collect::<Vec<_>>();
639 let compiler = ProjectCompiler::new()
640 .dynamic_test_linking(config.dynamic_test_linking)
641 .quiet(true)
642 .files(files);
643
644 let compile_output = compiler.compile(&config.project()?)?;
645 let inline_config = Arc::new(InlineConfig::new_parsed(&compile_output, config)?);
646
647 let mut filter = filter_args.clone().merge_with_config(config);
652 if let Some(rerun_failures) = rerun_failures {
653 filter.set_rerun_failures(rerun_failures.to_vec());
654 }
655
656 let rt = tokio::runtime::Builder::new_multi_thread()
659 .worker_threads(1) .enable_all()
661 .build()
662 .map_err(|e| eyre::eyre!("Failed to create tokio runtime: {}", e))?;
663
664 let results: BTreeMap<String, SuiteResult> = rt.block_on(async {
666 let (evm_env, tx_env) = evm.backend.env(evm_opts).await?;
667
668 let mut runner = MultiContractRunnerBuilder::new(config.clone(), inline_config)
673 .set_debug(false)
674 .initial_balance(evm_opts.initial_balance)
675 .sender(evm_opts.sender)
676 .enable_isolation(isolate)
677 .fail_fast(true)
678 .with_create2_deployer_available(evm.create2_deployer_available)
679 .build::<FEN, MultiCompiler>(
680 &compile_output,
681 evm_env,
682 tx_env,
683 evm_opts.clone(),
684 evm.backend.clone_with_fork_scope()?,
685 evm.executor_builder.clone(),
686 )?;
687
688 if let Some(deadline) = deadline {
689 let early_exit = runner.tcfg.early_exit.clone();
690 tokio::spawn(async move {
691 tokio::time::sleep_until(deadline.into()).await;
692 early_exit.record_ctrl_c();
693 });
694 }
695
696 runner.test_collect(&filter)
697 })?;
698
699 let killed = results.values().any(|suite| suite.failed() > 0);
701
702 Ok(killed)
703}
704
705#[cfg(test)]
706mod tests {
707 use super::*;
708 use alloy_primitives::U256;
709 use std::process::Command;
710
711 #[test]
712 fn timeout_path_uses_enlarged_test_worker_stack() {
713 if std::env::var_os(MUTATION_STACK_PROBE_ENV).is_some() {
714 let workspace = TempDir::new().unwrap();
715 let source = PathBuf::from("src/StackProbe.t.sol");
716 fs::create_dir_all(workspace.path().join("src")).unwrap();
717 fs::write(
718 workspace.path().join(&source),
719 "contract StackProbeTest { function test_stackProbe() public {} }",
720 )
721 .unwrap();
722 let mut config = Config::with_root(workspace.path());
723 config.out = workspace.path().join("out");
724 config.cache_path = workspace.path().join("cache/solidity-files-cache.json");
725 let config = Arc::new(config);
726 let evm = MutationEvmConfig {
727 opts: EvmOpts::default(),
728 backend: Backend::<EthEvmNetwork>::spawn(None).unwrap(),
729 executor_builder: ExecutorBuilder::<EthEvmNetwork>::new(),
730 create2_deployer_available: false,
731 };
732 let shared_state = Arc::new(SharedMutationState::default());
733
734 let result = run_compile_and_test_with_timeout(
735 config,
736 &evm,
737 Duration::from_secs(60),
738 workspace,
739 &shared_state,
740 Arc::new(FilterArgs::default()),
741 Arc::new(None),
742 Arc::new(vec![source]),
743 false,
744 );
745
746 assert!(!matches!(result, MutationResult::TimedOut), "unexpected result: {result:?}");
747 assert!(MUTATION_STACK_PROBE_RAN.load(Ordering::Acquire));
748 fs::write(std::env::var_os(MUTATION_STACK_PROBE_MARKER_ENV).unwrap(), b"ok").unwrap();
749 return;
750 }
751
752 let marker_dir = TempDir::new().unwrap();
753 let marker = marker_dir.path().join("completed");
754 let output = Command::new(std::env::current_exe().unwrap())
755 .args([
756 "--exact",
757 "mutation::runner::tests::timeout_path_uses_enlarged_test_worker_stack",
758 ])
759 .env(MUTATION_STACK_PROBE_ENV, "1")
760 .env(MUTATION_STACK_PROBE_MARKER_ENV, &marker)
761 .output()
762 .unwrap();
763 let stdout = String::from_utf8_lossy(&output.stdout);
764 let stderr = String::from_utf8_lossy(&output.stderr);
765
766 assert!(
767 output.status.success(),
768 "stack-pressure child failed:\nstdout:\n{stdout}\nstderr:\n{stderr}"
769 );
770 assert!(marker.exists(), "stack-pressure child did not complete the production path");
771 }
772
773 #[test]
774 fn park_timed_out_worker_bounds_pending_handles() {
775 let state = SharedMutationState::default();
776 state.set_max_pending_workers(1);
777
778 state.park_timed_out_worker(std::thread::spawn(|| {}));
779 assert_eq!(state.pending_workers.lock().unwrap().len(), 1);
780
781 state.park_timed_out_worker(std::thread::spawn(|| {}));
782 assert_eq!(state.pending_workers.lock().unwrap().len(), 1);
783
784 let pending = std::mem::take(&mut *state.pending_workers.lock().unwrap());
785 for handle in pending {
786 handle.join().unwrap();
787 }
788 }
789
790 #[test]
791 fn temp_config_preserves_materialized_overrides_and_rebases_paths() {
792 let project = TempDir::new().unwrap();
793 let temp = TempDir::new().unwrap();
794 let root = project.path();
795
796 let mut config = Config {
797 root: root.to_path_buf(),
798 src: root.join("contracts"),
799 test: root.join("checks"),
800 script: root.join("deploy"),
801 out: root.join("custom-out"),
802 cache_path: root.join("custom-cache"),
803 snapshots: root.join("custom-snapshots"),
804 broadcast: root.join("custom-broadcast"),
805 mutation_dir: root.join("custom-cache/mutation"),
806 libs: vec![root.join("vendor")],
807 include_paths: vec![root.join("shared")],
808 allow_paths: vec![root.join("fixtures")],
809 dynamic_test_linking: true,
810 cache: true,
811 ..Default::default()
812 };
813 config.fuzz.seed = Some(U256::from(42));
814 config.fuzz.timeout = Some(90);
815 config.invariant.timeout = Some(80);
816 config.fuzz.failure_persist_dir = Some(root.join("custom-cache/fuzz"));
817 config.invariant.failure_persist_dir = Some(root.join("custom-cache/invariant"));
818 config.mutation.timeout = Some(5);
819
820 let temp_config = workspace::rebase_config_paths(&config, temp.path());
821 let expected_root = canonicalize_path(temp.path()).unwrap();
822
823 assert_eq!(temp_config.root, expected_root);
824 assert_eq!(temp_config.src, expected_root.join("contracts"));
825 assert_eq!(temp_config.test, expected_root.join("checks"));
826 assert_eq!(temp_config.script, expected_root.join("deploy"));
827 assert_eq!(temp_config.out, expected_root.join("custom-out"));
828 assert_eq!(temp_config.cache_path, expected_root.join("custom-cache"));
829 assert_eq!(temp_config.snapshots, expected_root.join("custom-snapshots"));
830 assert_eq!(temp_config.broadcast, expected_root.join("custom-broadcast"));
831 assert_eq!(temp_config.mutation_dir, expected_root.join("custom-cache/mutation"));
832 assert_eq!(temp_config.libs, vec![expected_root.join("vendor")]);
833 assert_eq!(temp_config.include_paths, vec![expected_root.join("shared")]);
834 assert_eq!(temp_config.allow_paths, vec![expected_root.join("fixtures")]);
835 assert_eq!(
836 temp_config.fuzz.failure_persist_dir,
837 Some(expected_root.join("custom-cache/fuzz"))
838 );
839 assert_eq!(
840 temp_config.invariant.failure_persist_dir,
841 Some(expected_root.join("custom-cache/invariant"))
842 );
843 assert!(temp_config.dynamic_test_linking);
844 assert!(temp_config.cache);
845 assert_eq!(temp_config.fuzz.seed, Some(U256::from(42)));
846 assert_eq!(temp_config.fuzz.timeout, Some(90));
847 assert_eq!(temp_config.invariant.timeout, Some(80));
848 }
849}