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