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