1use std::{
10 collections::{BTreeMap, BTreeSet, HashSet},
11 path::{Path, PathBuf},
12 sync::{
13 Arc,
14 atomic::{AtomicBool, Ordering},
15 },
16 time::Instant,
17};
18
19use alloy_primitives::keccak256;
20use eyre::{Result, WrapErr};
21use foundry_cli::utils::FoundryPathExt;
22use foundry_common::{compile::ProjectCompiler, sh_println};
23use foundry_compilers::{
24 Language, ProjectCompileOutput,
25 compilers::multi::{MultiCompiler, MultiCompilerLanguage},
26 utils::source_files_iter,
27};
28use foundry_config::{Config, filter::GlobMatcher};
29use foundry_evm::{fork::ResolvedFork, opts::EvmOpts};
30
31use crate::{
32 cmd::test::{FilterArgs, RerunFailure},
33 mutation::{
34 MutationHandler, MutationProgress, MutationReporter, MutationsSummary,
35 mutant::{Mutant, MutationResult},
36 runner::{MutationEvmConfig, run_mutations_parallel_with_progress},
37 type_analysis::{collect_mutation_exclusions, normalize_path},
38 },
39};
40
41#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, serde::Serialize)]
42struct ArtifactCacheFingerprint {
43 source: String,
44 name: String,
45 version: String,
46 build_id: String,
47 profile: String,
48}
49
50#[derive(serde::Serialize)]
51struct ExecutionCacheFingerprint<'a> {
52 schema: &'static str,
53 config: &'a Config,
54 evm_opts: &'a EvmOpts,
55 resolved_fork: Option<alloy_primitives::B256>,
56 filter_args: FilterArgsFingerprint<'a>,
57 rerun_failures: Option<&'a [RerunFailure]>,
58 num_workers: usize,
59 artifacts: &'a [ArtifactCacheFingerprint],
60}
61
62#[derive(serde::Serialize)]
63struct FilterArgsFingerprint<'a> {
64 test_pattern: Option<&'a str>,
65 test_pattern_inverse: Option<&'a str>,
66 contract_pattern: Option<&'a str>,
67 contract_pattern_inverse: Option<&'a str>,
68 path_pattern: Option<&'a str>,
69 path_pattern_inverse: Option<&'a str>,
70}
71
72pub struct MutationRunConfig {
74 pub mutate_paths: Vec<PathBuf>,
76 pub mutate_path_pattern: Option<GlobMatcher>,
78 pub mutate_contract_pattern: Option<regex::Regex>,
80 pub num_workers: usize,
82 pub show_progress: bool,
84 pub json_output: bool,
86 pub filter_args: FilterArgs,
90 pub rerun_failures: Option<Vec<RerunFailure>>,
94 pub selected_sources_relative: Vec<PathBuf>,
98 pub isolate: bool,
101}
102
103impl MutationRunConfig {
104 pub fn effective_workers(&self) -> usize {
106 if self.num_workers == 0 {
107 std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1)
108 } else {
109 self.num_workers
110 }
111 }
112}
113
114pub struct MutationRunResult {
116 pub summary: MutationsSummary,
118 pub cancelled: bool,
120 pub duration_secs: f64,
122}
123
124pub async fn run_mutation_testing(
133 config: Arc<Config>,
134 output: &ProjectCompileOutput<MultiCompiler>,
135 evm_opts: EvmOpts,
136 resolved_fork: Option<ResolvedFork>,
137 mutation_config: MutationRunConfig,
138) -> Result<MutationRunResult> {
139 let create2_deployer_available =
140 evm_opts.can_use_create2_deployer_resolved(resolved_fork.as_ref()).await?;
141 let mutation_evm = MutationEvmConfig {
142 opts: evm_opts.clone(),
143 resolved_fork: resolved_fork.clone(),
144 create2_deployer_available,
145 };
146 let num_workers = mutation_config.effective_workers();
147 let json_output = mutation_config.json_output;
148 let artifact_link_references = output.artifact_ids().filter_map(|(id, artifact)| {
149 let source = project_relative_path(&config.root, &id.source)?;
150 let links = artifact
151 .all_link_references()
152 .into_keys()
153 .filter_map(|file| project_relative_path(&config.root, Path::new(&file)))
154 .collect::<BTreeSet<_>>();
155 Some((source, links))
156 });
157 let selected_sources_relative = mutation_compile_sources(
158 mutation_config.selected_sources_relative.iter().cloned(),
159 artifact_link_references,
160 );
161
162 let mutate_paths = resolve_mutate_paths(&config, output, &mutation_config)?;
164 let execution_cache_output = ProjectCompiler::new()
165 .dynamic_test_linking(config.dynamic_test_linking)
166 .quiet(json_output)
167 .files(
168 selected_sources_relative
169 .iter()
170 .map(|path| config.root.join(path))
171 .filter(|path| path.exists())
172 .collect::<Vec<_>>(),
173 )
174 .compile(&config.project()?)?;
175 let execution_cache_key = mutation_execution_cache_key(
176 &config,
177 &execution_cache_output,
178 &evm_opts,
179 resolved_fork.as_ref(),
180 &mutation_config.filter_args,
181 mutation_config.rerun_failures.as_deref(),
182 num_workers,
183 )?;
184 let mut mutation_exclusions = collect_mutation_exclusions(&config, output).unwrap_or_default();
185
186 if !mutation_config.show_progress && !json_output {
187 sh_println!("Running mutation tests with {} parallel workers...", num_workers)?;
188 }
189
190 let mut mutation_summary = MutationsSummary::new();
191 let mut cancelled = false;
192 let start_time = Instant::now();
193 let cancellation_requested = Arc::new(AtomicBool::new(false));
194 let ctrlc_handle = {
195 let cancellation_requested = Arc::clone(&cancellation_requested);
196 tokio::spawn(async move {
197 if tokio::signal::ctrl_c().await.is_ok() {
198 cancellation_requested.store(true, Ordering::SeqCst);
199 }
200 })
201 };
202
203 for path in mutate_paths {
204 if cancellation_requested.load(Ordering::SeqCst) {
205 cancelled = true;
206 break;
207 }
208
209 if !mutation_config.show_progress && !json_output {
210 sh_println!("Running mutation tests for {}", path.display())?;
211 }
212
213 let mut handler = MutationHandler::new(path.clone(), config.clone());
216 if let Some(mutations) = mutation_exclusions.remove(&normalize_path(&path)) {
217 handler = handler.with_mutation_exclusions(mutations);
218 }
219 if let Some(filter) = &mutation_config.mutate_contract_pattern {
220 handler = handler.with_contract_filter(filter.clone());
221 }
222 handler.read_source_contract()?;
223
224 let build_id = output
226 .artifact_ids()
227 .find_map(|(id, _)| (id.source == path).then_some(id.build_id))
228 .unwrap_or_default();
229
230 handler.retrieve_survived_spans(&build_id, &execution_cache_key);
234
235 let mut mutants = if let Some(ms) = handler.retrieve_cached_mutants(&build_id) {
239 ms
240 } else {
241 handler.generate_ast().await?;
242 handler.mutations.clone()
243 };
244
245 if mutants.is_empty() {
246 if !mutation_config.show_progress && !json_output {
247 sh_println!(" No mutants generated for {}", path.display())?;
248 }
249 continue;
250 }
251
252 if let Some(prior) =
256 handler.retrieve_cached_mutant_results(&build_id, &execution_cache_key, &mutants)
257 {
258 if !mutation_config.show_progress && !json_output {
259 sh_println!(" Using cached results for {} mutants", prior.len())?;
260 }
261 for (mutant, status) in prior {
262 match status {
263 MutationResult::Dead => handler.add_dead_mutant(mutant),
264 MutationResult::Alive => handler.add_survived_mutant(mutant),
265 MutationResult::Invalid => handler.add_invalid_mutant(mutant),
266 MutationResult::Skipped => handler.add_skipped_mutant(mutant),
267 MutationResult::TimedOut => handler.add_timed_out_mutant(mutant),
268 }
269 }
270 mutation_summary.merge(handler.get_report());
271 continue;
272 }
273
274 mutants.sort_by(|a, b| {
276 a.span.lo().0.cmp(&b.span.lo().0).then_with(|| b.span.hi().0.cmp(&a.span.hi().0))
277 });
278
279 let (mutants_to_test, skipped_results) =
280 partition_adaptively_skipped_mutants(&mut handler, &mutants);
281
282 let progress = if mutation_config.show_progress && !json_output {
284 let p = MutationProgress::with_timeout(
285 mutants_to_test.len(),
286 num_workers,
287 config.mutation.timeout,
288 );
289 let display_path =
291 path.strip_prefix(&config.root).unwrap_or(&path).display().to_string();
292 p.set_current_file(&display_path);
293 Some(p)
294 } else if !json_output {
295 sh_println!(
296 " Generated {} mutants; testing {}, adaptively skipped {}",
297 mutants.len(),
298 mutants_to_test.len(),
299 skipped_results.len()
300 )?;
301 None
302 } else {
303 None
304 };
305
306 let batch = run_mutations_parallel_with_progress(
308 mutants_to_test,
309 path.clone(),
310 handler.src.clone(),
311 config.clone(),
312 mutation_evm.clone(),
313 num_workers,
314 progress,
315 json_output,
316 mutation_config.filter_args.clone(),
317 mutation_config.rerun_failures.clone(),
318 Arc::new(selected_sources_relative.clone()),
319 mutation_config.isolate,
320 Arc::clone(&cancellation_requested),
321 )?;
322 let file_cancelled = batch.cancelled;
323
324 let mut results_vec = Vec::with_capacity(skipped_results.len() + batch.results.len());
326 results_vec.extend(skipped_results);
327 for result in batch.results {
328 results_vec.push((result.mutant.clone(), result.result.clone()));
329 match result.result {
330 MutationResult::Dead => handler.add_dead_mutant(result.mutant),
331 MutationResult::Alive => {
332 handler.mark_span_survived(result.mutant.span);
333 handler.add_survived_mutant(result.mutant);
334 }
335 MutationResult::Invalid => handler.add_invalid_mutant(result.mutant),
336 MutationResult::Skipped => handler.add_skipped_mutant(result.mutant),
337 MutationResult::TimedOut => handler.add_timed_out_mutant(result.mutant),
338 }
339 }
340
341 let complete_run = !file_cancelled && results_vec.len() == mutants.len();
346
347 results_vec.sort_by(|(a, _), (b, _)| {
359 a.span.lo().0.cmp(&b.span.lo().0).then_with(|| a.span.hi().0.cmp(&b.span.hi().0))
360 });
361 if !mutants.is_empty() && !build_id.is_empty() {
362 let _ = handler.persist_cached_mutants(&build_id, &mutants);
363 if complete_run {
364 let _ = handler.persist_cached_results(
365 &build_id,
366 &execution_cache_key,
367 &mutants,
368 &results_vec,
369 );
370 }
371 let _ = handler.persist_survived_spans(&build_id, &execution_cache_key);
372 }
373
374 mutation_summary.merge(handler.get_report());
375
376 if file_cancelled {
378 cancelled = true;
379 break;
380 }
381 }
382 cancelled |= cancellation_requested.load(Ordering::SeqCst);
383
384 let duration = start_time.elapsed();
386 let duration_secs = duration.as_secs_f64();
387
388 if !json_output {
390 MutationReporter::new().report(&mutation_summary, duration);
391 }
392
393 ctrlc_handle.abort();
394
395 Ok(MutationRunResult { summary: mutation_summary, cancelled, duration_secs })
396}
397
398fn mutation_execution_cache_key(
409 config: &Config,
410 output: &ProjectCompileOutput<MultiCompiler>,
411 evm_opts: &EvmOpts,
412 resolved_fork: Option<&ResolvedFork>,
413 filter_args: &FilterArgs,
414 rerun_failures: Option<&[RerunFailure]>,
415 num_workers: usize,
416) -> Result<String> {
417 let artifacts = output
418 .artifact_ids()
419 .map(|(id, _)| ArtifactCacheFingerprint {
420 source: id.source.display().to_string(),
421 name: id.name,
422 version: id.version.to_string(),
423 build_id: id.build_id,
424 profile: id.profile,
425 })
426 .collect::<Vec<_>>();
427 mutation_execution_cache_key_from_parts_with_rerun_failures(
428 config,
429 evm_opts,
430 resolved_fork.map(ResolvedFork::fingerprint),
431 filter_args,
432 rerun_failures,
433 num_workers,
434 artifacts,
435 )
436}
437
438#[cfg(test)]
439fn mutation_execution_cache_key_from_parts(
440 config: &Config,
441 evm_opts: &EvmOpts,
442 filter_args: &FilterArgs,
443 num_workers: usize,
444 artifacts: Vec<ArtifactCacheFingerprint>,
445) -> Result<String> {
446 mutation_execution_cache_key_from_parts_with_rerun_failures(
447 config,
448 evm_opts,
449 None,
450 filter_args,
451 None,
452 num_workers,
453 artifacts,
454 )
455}
456
457fn mutation_execution_cache_key_from_parts_with_rerun_failures(
458 config: &Config,
459 evm_opts: &EvmOpts,
460 resolved_fork: Option<alloy_primitives::B256>,
461 filter_args: &FilterArgs,
462 rerun_failures: Option<&[RerunFailure]>,
463 num_workers: usize,
464 mut artifacts: Vec<ArtifactCacheFingerprint>,
465) -> Result<String> {
466 artifacts.sort();
467 let fingerprint = ExecutionCacheFingerprint {
468 schema: "mutation-results-v2",
469 config,
470 evm_opts,
471 resolved_fork,
472 filter_args: filter_args_fingerprint(filter_args),
473 rerun_failures,
474 num_workers,
475 artifacts: &artifacts,
476 };
477 let encoded = serde_json::to_vec(&fingerprint)
478 .wrap_err("failed to encode mutation execution cache key")?;
479
480 Ok(keccak256(encoded).to_string())
481}
482
483fn filter_args_fingerprint(filter_args: &FilterArgs) -> FilterArgsFingerprint<'_> {
484 FilterArgsFingerprint {
485 test_pattern: filter_args.test_pattern.as_ref().map(|re| re.as_str()),
486 test_pattern_inverse: filter_args.test_pattern_inverse.as_ref().map(|re| re.as_str()),
487 contract_pattern: filter_args.contract_pattern.as_ref().map(|re| re.as_str()),
488 contract_pattern_inverse: filter_args
489 .contract_pattern_inverse
490 .as_ref()
491 .map(|re| re.as_str()),
492 path_pattern: filter_args.path_pattern.as_ref().map(|glob| glob.as_str()),
493 path_pattern_inverse: filter_args.path_pattern_inverse.as_ref().map(|glob| glob.as_str()),
494 }
495}
496
497fn project_relative_path(root: &Path, path: &Path) -> Option<PathBuf> {
498 if path.is_relative() {
499 return Some(path.to_path_buf());
500 }
501
502 if let Ok(stripped) = path.strip_prefix(root) {
503 return Some(stripped.to_path_buf());
504 }
505
506 path.canonicalize().ok()?.strip_prefix(root.canonicalize().ok()?).ok().map(PathBuf::from)
507}
508
509fn mutation_compile_sources(
510 selected_sources: impl IntoIterator<Item = PathBuf>,
511 artifact_link_references: impl IntoIterator<Item = (PathBuf, BTreeSet<PathBuf>)>,
512) -> Vec<PathBuf> {
513 let link_edges = artifact_link_references.into_iter().collect::<BTreeMap<_, _>>();
514 let mut selected_sources_relative = selected_sources.into_iter().collect::<BTreeSet<_>>();
515 let mut queue = selected_sources_relative.iter().cloned().collect::<Vec<_>>();
516
517 while let Some(source) = queue.pop() {
518 if let Some(links) = link_edges.get(&source) {
519 for link in links {
520 if selected_sources_relative.insert(link.clone()) {
521 queue.push(link.clone());
522 }
523 }
524 }
525 }
526
527 selected_sources_relative.into_iter().collect()
528}
529
530fn partition_adaptively_skipped_mutants(
531 handler: &mut MutationHandler,
532 mutants: &[Mutant],
533) -> (Vec<Mutant>, Vec<(Mutant, MutationResult)>) {
534 let mut skipped_results = Vec::new();
535 let mutants_to_test = mutants
536 .iter()
537 .filter_map(|mutant| {
538 if handler.should_skip_span(mutant.span) {
539 handler.add_skipped_mutant(mutant.clone());
540 skipped_results.push((mutant.clone(), MutationResult::Skipped));
541 None
542 } else {
543 Some(mutant.clone())
544 }
545 })
546 .collect();
547
548 (mutants_to_test, skipped_results)
549}
550
551fn resolve_mutate_paths(
562 config: &Config,
563 output: &ProjectCompileOutput<MultiCompiler>,
564 mutation_config: &MutationRunConfig,
565) -> Result<Vec<PathBuf>> {
566 let base: Vec<PathBuf> = if let Some(pattern) = &mutation_config.mutate_path_pattern {
568 let paths: Vec<_> = source_files_iter(&config.src, MultiCompilerLanguage::FILE_EXTENSIONS)
569 .filter(|entry| entry.is_sol() && !entry.is_sol_test() && pattern.is_match(entry))
570 .collect();
571 if paths.is_empty() {
572 eyre::bail!("no source matched --mutate-path pattern `{pattern}`");
573 }
574 paths
575 } else if !mutation_config.mutate_paths.is_empty() {
576 let root_canon =
577 config.root.canonicalize().wrap_err("failed to canonicalize project root")?;
578 let mut validated = Vec::with_capacity(mutation_config.mutate_paths.len());
579 for path in &mutation_config.mutate_paths {
580 let resolved = if path.is_relative() { config.root.join(path) } else { path.clone() };
581 if !resolved.exists() {
582 eyre::bail!("mutate path does not exist: {}", resolved.display());
583 }
584 if !resolved.is_file() {
585 eyre::bail!("mutate path is not a file: {}", resolved.display());
586 }
587 let canon = resolved
588 .canonicalize()
589 .wrap_err_with(|| format!("failed to canonicalize: {}", resolved.display()))?;
590 if !canon.starts_with(&root_canon) {
591 eyre::bail!("mutate path is outside the project root: {}", resolved.display());
592 }
593 if !canon.is_sol() {
594 eyre::bail!("mutate path is not a Solidity file: {}", resolved.display());
595 }
596 if canon.is_sol_test() {
597 eyre::bail!(
598 "mutate path is a test file, not a source file: {}",
599 resolved.display()
600 );
601 }
602 validated.push(canon);
603 }
604 validated
605 } else {
606 source_files_iter(&config.src, MultiCompilerLanguage::FILE_EXTENSIONS)
607 .filter(|entry| entry.is_sol() && !entry.is_sol_test())
608 .collect()
609 };
610
611 let paths = if let Some(contract_pattern) = &mutation_config.mutate_contract_pattern {
615 let matching_sources: HashSet<PathBuf> = output
616 .artifact_ids()
617 .filter_map(|(id, _)| contract_pattern.is_match(&id.name).then_some(id.source.clone()))
618 .collect();
619 let paths: Vec<_> =
620 base.into_iter().filter(|entry| matching_sources.contains(entry)).collect();
621 if paths.is_empty() {
622 if mutation_config.mutate_paths.is_empty()
623 && mutation_config.mutate_path_pattern.is_none()
624 {
625 eyre::bail!("no source matched --mutate-contract pattern `{contract_pattern}`");
626 }
627 eyre::bail!("no source matched --mutate-contract within the selected mutation paths");
628 }
629 paths
630 } else {
631 base
632 };
633
634 Ok(paths)
635}
636
637#[cfg(test)]
638mod tests {
639 use super::*;
640 use std::str::FromStr;
641
642 use crate::mutation::mutant::MutationType;
643 use solar::{ast::Span, interface::BytePos};
644
645 fn artifact(build_id: &str) -> ArtifactCacheFingerprint {
646 ArtifactCacheFingerprint {
647 source: "src/Counter.sol".to_string(),
648 name: "Counter".to_string(),
649 version: "0.8.30".to_string(),
650 build_id: build_id.to_string(),
651 profile: "default".to_string(),
652 }
653 }
654
655 fn filter_args() -> FilterArgs {
656 FilterArgs {
657 test_pattern: None,
658 test_pattern_inverse: None,
659 contract_pattern: None,
660 contract_pattern_inverse: None,
661 path_pattern: None,
662 path_pattern_inverse: None,
663 coverage_pattern_inverse: None,
664 }
665 }
666
667 fn mutant(lo: u32, hi: u32) -> Mutant {
668 Mutant {
669 path: PathBuf::from("src/Counter.sol"),
670 span: Span::new(BytePos(lo), BytePos(hi)),
671 mutation: MutationType::DeleteExpression,
672 original: "number++".to_string(),
673 source_line: "number++;".to_string(),
674 line_number: 1,
675 column_number: 1,
676 }
677 }
678
679 #[test]
680 fn execution_cache_key_changes_when_fuzz_config_changes() {
681 let first = Config::default();
682 let mut second = first.clone();
683 second.fuzz.runs += 1;
684
685 let evm_opts = EvmOpts::default();
686 let filter_args = filter_args();
687 let artifacts = vec![artifact("build-a")];
688
689 let first_key = mutation_execution_cache_key_from_parts(
690 &first,
691 &evm_opts,
692 &filter_args,
693 1,
694 artifacts.clone(),
695 )
696 .unwrap();
697 let second_key =
698 mutation_execution_cache_key_from_parts(&second, &evm_opts, &filter_args, 1, artifacts)
699 .unwrap();
700
701 assert_ne!(first_key, second_key);
702 }
703
704 #[test]
705 fn execution_cache_key_changes_when_evm_options_change() {
706 let config = Config::default();
707 let first = EvmOpts::default();
708 let mut second = first.clone();
709 second.memory_limit = first.memory_limit + 1;
710
711 let filter_args = filter_args();
712 let artifacts = vec![artifact("build-a")];
713
714 let first_key = mutation_execution_cache_key_from_parts(
715 &config,
716 &first,
717 &filter_args,
718 1,
719 artifacts.clone(),
720 )
721 .unwrap();
722 let second_key =
723 mutation_execution_cache_key_from_parts(&config, &second, &filter_args, 1, artifacts)
724 .unwrap();
725
726 assert_ne!(first_key, second_key);
727 }
728
729 #[test]
730 fn execution_cache_key_changes_when_resolved_fork_changes() {
731 let config = Config::default();
732 let evm_opts = EvmOpts::default();
733 let filter_args = filter_args();
734 let artifacts = vec![artifact("build-a")];
735
736 let first_key = mutation_execution_cache_key_from_parts_with_rerun_failures(
737 &config,
738 &evm_opts,
739 Some(alloy_primitives::B256::with_last_byte(1)),
740 &filter_args,
741 None,
742 1,
743 artifacts.clone(),
744 )
745 .unwrap();
746 let second_key = mutation_execution_cache_key_from_parts_with_rerun_failures(
747 &config,
748 &evm_opts,
749 Some(alloy_primitives::B256::with_last_byte(2)),
750 &filter_args,
751 None,
752 1,
753 artifacts,
754 )
755 .unwrap();
756
757 assert_ne!(first_key, second_key);
758 }
759
760 #[test]
761 fn execution_cache_key_changes_when_compiled_artifacts_change() {
762 let config = Config::default();
763 let evm_opts = EvmOpts::default();
764 let filter_args = filter_args();
765
766 let first_key = mutation_execution_cache_key_from_parts(
767 &config,
768 &evm_opts,
769 &filter_args,
770 1,
771 vec![artifact("build-a")],
772 )
773 .unwrap();
774 let second_key = mutation_execution_cache_key_from_parts(
775 &config,
776 &evm_opts,
777 &filter_args,
778 1,
779 vec![artifact("build-b")],
780 )
781 .unwrap();
782
783 assert_ne!(first_key, second_key);
784 }
785
786 #[test]
787 fn execution_cache_key_sorts_artifacts_before_hashing() {
788 let config = Config::default();
789 let evm_opts = EvmOpts::default();
790 let filter_args = filter_args();
791
792 let first = vec![artifact("build-a"), artifact("build-b")];
793 let second = vec![artifact("build-b"), artifact("build-a")];
794
795 let first_key =
796 mutation_execution_cache_key_from_parts(&config, &evm_opts, &filter_args, 1, first)
797 .unwrap();
798 let second_key =
799 mutation_execution_cache_key_from_parts(&config, &evm_opts, &filter_args, 1, second)
800 .unwrap();
801
802 assert_eq!(first_key, second_key);
803 }
804
805 #[test]
806 fn execution_cache_key_changes_when_worker_count_changes() {
807 let config = Config::default();
808 let evm_opts = EvmOpts::default();
809 let filter_args = filter_args();
810 let artifacts = vec![artifact("build-a")];
811
812 let first_key = mutation_execution_cache_key_from_parts(
813 &config,
814 &evm_opts,
815 &filter_args,
816 1,
817 artifacts.clone(),
818 )
819 .unwrap();
820 let second_key =
821 mutation_execution_cache_key_from_parts(&config, &evm_opts, &filter_args, 4, artifacts)
822 .unwrap();
823
824 assert_ne!(first_key, second_key);
825 }
826
827 #[test]
828 fn execution_cache_key_changes_when_match_test_filter_changes() {
829 let config = Config::default();
830 let evm_opts = EvmOpts::default();
831 let mut first_filter = filter_args();
832 let mut second_filter = filter_args();
833 first_filter.test_pattern = Some(regex::Regex::new("testA|testAlpha").unwrap());
834 second_filter.test_pattern = Some(regex::Regex::new("testB|testBeta").unwrap());
835 let artifacts = vec![artifact("build-a")];
836
837 let first_key = mutation_execution_cache_key_from_parts(
838 &config,
839 &evm_opts,
840 &first_filter,
841 1,
842 artifacts.clone(),
843 )
844 .unwrap();
845 let second_key = mutation_execution_cache_key_from_parts(
846 &config,
847 &evm_opts,
848 &second_filter,
849 1,
850 artifacts,
851 )
852 .unwrap();
853
854 assert_ne!(first_key, second_key);
855 }
856
857 #[test]
858 fn execution_cache_key_changes_when_match_path_filter_changes() {
859 let config = Config::default();
860 let evm_opts = EvmOpts::default();
861 let mut first_filter = filter_args();
862 let mut second_filter = filter_args();
863 first_filter.path_pattern = Some(GlobMatcher::from_str("test/A.t.sol").unwrap());
864 second_filter.path_pattern = Some(GlobMatcher::from_str("test/B.t.sol").unwrap());
865 let artifacts = vec![artifact("build-a")];
866
867 let first_key = mutation_execution_cache_key_from_parts(
868 &config,
869 &evm_opts,
870 &first_filter,
871 1,
872 artifacts.clone(),
873 )
874 .unwrap();
875 let second_key = mutation_execution_cache_key_from_parts(
876 &config,
877 &evm_opts,
878 &second_filter,
879 1,
880 artifacts,
881 )
882 .unwrap();
883
884 assert_ne!(first_key, second_key);
885 }
886
887 #[test]
888 fn execution_cache_key_changes_when_rerun_failures_change() {
889 let config = Config::default();
890 let evm_opts = EvmOpts::default();
891 let filter_args = filter_args();
892 let first_failures = vec![RerunFailure {
893 contract: "test/Counter.t.sol:WeakTest".to_string(),
894 test: "test_increment()".to_string(),
895 }];
896 let second_failures = vec![RerunFailure {
897 contract: "test/Counter.t.sol:StrongTest".to_string(),
898 test: "test_increment()".to_string(),
899 }];
900 let artifacts = vec![artifact("build-a")];
901
902 let first_key = mutation_execution_cache_key_from_parts_with_rerun_failures(
903 &config,
904 &evm_opts,
905 None,
906 &filter_args,
907 Some(&first_failures),
908 1,
909 artifacts.clone(),
910 )
911 .unwrap();
912 let second_key = mutation_execution_cache_key_from_parts_with_rerun_failures(
913 &config,
914 &evm_opts,
915 None,
916 &filter_args,
917 Some(&second_failures),
918 1,
919 artifacts,
920 )
921 .unwrap();
922
923 assert_ne!(first_key, second_key);
924 }
925
926 #[test]
927 fn mutation_compile_sources_only_include_selected_link_reference_closure() {
928 let sources = mutation_compile_sources(
929 [PathBuf::from("test/Selected.t.sol")],
930 [
931 (
932 PathBuf::from("test/Selected.t.sol"),
933 BTreeSet::from([PathBuf::from("test/SelectedLinkedHelper.sol")]),
934 ),
935 (
936 PathBuf::from("test/SelectedLinkedHelper.sol"),
937 BTreeSet::from([PathBuf::from("test/TransitiveLinkedHelper.sol")]),
938 ),
939 (
940 PathBuf::from("test/Unrelated.t.sol"),
941 BTreeSet::from([PathBuf::from("test/UnusedLinkedHelper.sol")]),
942 ),
943 ],
944 );
945
946 assert_eq!(
947 sources,
948 vec![
949 PathBuf::from("test/Selected.t.sol"),
950 PathBuf::from("test/SelectedLinkedHelper.sol"),
951 PathBuf::from("test/TransitiveLinkedHelper.sol"),
952 ]
953 );
954 }
955
956 #[test]
957 fn resumed_adaptive_skips_are_reported_as_skipped_results() {
958 let mut handler =
959 MutationHandler::new(PathBuf::from("src/Counter.sol"), Arc::new(Config::default()));
960 handler.mark_span_survived(Span::new(BytePos(10), BytePos(20)));
961
962 let exact_survivor = mutant(10, 20);
963 let skipped_child = mutant(12, 18);
964 let unrelated = mutant(30, 40);
965 let (mutants_to_test, skipped_results) = partition_adaptively_skipped_mutants(
966 &mut handler,
967 &[exact_survivor.clone(), skipped_child.clone(), unrelated.clone()],
968 );
969
970 assert_eq!(mutants_to_test.len(), 2);
971 assert_eq!(mutants_to_test[0].span, exact_survivor.span);
972 assert_eq!(mutants_to_test[1].span, unrelated.span);
973 assert_eq!(skipped_results.len(), 1);
974 assert!(matches!(skipped_results[0].1, MutationResult::Skipped));
975 assert_eq!(skipped_results[0].0.span, skipped_child.span);
976 assert_eq!(handler.get_report().total_skipped(), 1);
977 assert_eq!(handler.get_report().total_mutants(), 1);
978 }
979}