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