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