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