1use std::{
2 collections::{BTreeMap, HashSet, hash_map::DefaultHasher},
3 hash::{Hash, Hasher},
4 path::{Path, PathBuf},
5 sync::Arc,
6};
7
8use crate::mutation::{
9 mutant::{Mutant, MutationResult},
10 orchestrator::project_relative_path,
11 visitor::MutantVisitor,
12};
13pub use crate::mutation::{
14 orchestrator::{MutationRunConfig, MutationRunResult, run_mutation_testing},
15 progress::MutationProgress,
16 reporter::MutationReporter,
17 runner::run_mutations_parallel_with_progress,
18};
19use eyre::eyre;
20use foundry_common::sh_warn;
21use serde::{Deserialize, Serialize};
22use solar::{
23 ast::{
24 Span,
25 interface::{Session, source_map::FileName},
26 visit::Visit,
27 },
28 parse::Parser,
29};
30
31#[derive(Clone, Copy)]
32enum CacheKind<'a> {
33 Mutants,
34 Results { execution_key: &'a str },
35 Survived { execution_key: &'a str },
36}
37
38#[derive(Serialize, Deserialize)]
39struct CachedMutationResults {
40 mutant_count: usize,
41 mutant_hash: u64,
42 results: Vec<(Mutant, MutationResult)>,
43}
44
45pub mod mutant;
46mod mutators;
47pub mod orchestrator;
48pub mod progress;
49mod reporter;
50pub mod runner;
51mod type_analysis;
52mod visitor;
53
54pub struct MutationsSummary {
55 dead: Vec<Mutant>,
56 survived: Vec<Mutant>,
57 invalid: Vec<Mutant>,
58 skipped: Vec<Mutant>,
59 timed_out: Vec<Mutant>,
62}
63
64impl Default for MutationsSummary {
65 fn default() -> Self {
66 Self::new()
67 }
68}
69
70impl MutationsSummary {
71 pub const fn new() -> Self {
72 Self {
73 dead: Vec::new(),
74 survived: Vec::new(),
75 invalid: Vec::new(),
76 skipped: Vec::new(),
77 timed_out: Vec::new(),
78 }
79 }
80
81 pub fn update_invalid_mutant(&mut self, mutant: Mutant) {
82 self.invalid.push(mutant);
83 }
84
85 pub fn add_dead_mutant(&mut self, mutant: Mutant) {
86 self.dead.push(mutant);
87 }
88
89 pub fn add_survived_mutant(&mut self, mutant: Mutant) {
90 self.survived.push(mutant);
91 }
92
93 pub fn add_skipped_mutant(&mut self, mutant: Mutant) {
94 self.skipped.push(mutant);
95 }
96
97 pub fn add_timed_out_mutant(&mut self, mutant: Mutant) {
98 self.timed_out.push(mutant);
99 }
100
101 pub const fn total_mutants(&self) -> usize {
102 self.dead.len()
103 + self.survived.len()
104 + self.invalid.len()
105 + self.skipped.len()
106 + self.timed_out.len()
107 }
108
109 pub const fn total_dead(&self) -> usize {
110 self.dead.len()
111 }
112
113 pub const fn total_survived(&self) -> usize {
114 self.survived.len()
115 }
116
117 pub const fn total_invalid(&self) -> usize {
118 self.invalid.len()
119 }
120
121 pub const fn total_skipped(&self) -> usize {
122 self.skipped.len()
123 }
124
125 pub const fn total_timed_out(&self) -> usize {
126 self.timed_out.len()
127 }
128
129 pub const fn get_dead(&self) -> &Vec<Mutant> {
130 &self.dead
131 }
132
133 pub const fn get_survived(&self) -> &Vec<Mutant> {
134 &self.survived
135 }
136
137 pub const fn get_invalid(&self) -> &Vec<Mutant> {
138 &self.invalid
139 }
140
141 pub const fn get_timed_out(&self) -> &Vec<Mutant> {
142 &self.timed_out
143 }
144
145 pub fn merge(&mut self, other: &Self) {
147 self.dead.extend(other.dead.clone());
148 self.survived.extend(other.survived.clone());
149 self.invalid.extend(other.invalid.clone());
150 self.skipped.extend(other.skipped.clone());
151 self.timed_out.extend(other.timed_out.clone());
152 }
153
154 pub fn mutation_score(&self) -> f64 {
157 let valid_mutants = self.dead.len() + self.survived.len();
158 if valid_mutants == 0 { 0.0 } else { self.dead.len() as f64 / valid_mutants as f64 * 100.0 }
159 }
160
161 pub const fn total_evaluated(&self) -> usize {
163 self.dead.len() + self.survived.len()
164 }
165
166 pub const fn has_reliable_score(&self) -> bool {
168 self.total_evaluated() > 0 && self.timed_out.len() < self.total_evaluated()
169 }
170
171 pub fn to_json_output(&self, duration_secs: f64) -> MutationJsonOutput {
179 let mut survived_mutants: BTreeMap<String, Vec<SurvivedMutantJson>> = BTreeMap::new();
180
181 for mutant in &self.survived {
182 let file_path = mutant.relative_path();
183 let entry = survived_mutants.entry(file_path).or_default();
184 entry.push(SurvivedMutantJson::from_mutant(mutant));
185 }
186
187 for entries in survived_mutants.values_mut() {
188 entries.sort_by(|a, b| {
189 (a.line, a.column, &a.original, &a.mutant).cmp(&(
190 b.line,
191 b.column,
192 &b.original,
193 &b.mutant,
194 ))
195 });
196 }
197
198 MutationJsonOutput {
199 summary: MutationSummaryJson {
200 total: self.total_mutants(),
201 killed: self.total_dead(),
202 survived: self.total_survived(),
203 invalid: self.total_invalid(),
204 skipped: self.total_skipped(),
205 timed_out: self.total_timed_out(),
206 mutation_score: self.mutation_score(),
207 duration_secs,
208 },
209 survived_mutants,
210 }
211 }
212}
213
214#[derive(Debug, Clone, Serialize)]
219pub struct MutationJsonOutput {
220 pub summary: MutationSummaryJson,
221 pub survived_mutants: BTreeMap<String, Vec<SurvivedMutantJson>>,
222}
223
224#[derive(Debug, Clone, Serialize)]
226pub struct MutationSummaryJson {
227 pub total: usize,
228 pub killed: usize,
229 pub survived: usize,
230 pub invalid: usize,
231 pub skipped: usize,
232 pub timed_out: usize,
233 pub mutation_score: f64,
234 pub duration_secs: f64,
235}
236
237#[derive(Debug, Clone, Serialize)]
239pub struct SurvivedMutantJson {
240 pub line: usize,
241 pub column: usize,
242 pub original: String,
243 pub mutant: String,
244}
245
246impl SurvivedMutantJson {
247 pub fn from_mutant(mutant: &Mutant) -> Self {
249 Self {
250 line: mutant.line_number,
251 column: mutant.column_number,
252 original: mutant.original.clone(),
253 mutant: mutant.mutation.to_string(),
254 }
255 }
256}
257
258#[derive(Debug, Clone, Default)]
261pub struct SurvivedSpans {
262 spans: HashSet<(u32, u32)>, }
264
265impl SurvivedSpans {
266 pub fn new() -> Self {
267 Self { spans: HashSet::new() }
268 }
269
270 pub fn mark_survived(&mut self, span: Span) {
272 self.spans.insert((span.lo().0, span.hi().0));
273 }
274
275 pub fn should_skip(&self, span: Span) -> bool {
282 let (lo, hi) = (span.lo().0, span.hi().0);
283
284 self.spans.iter().any(|&(parent_lo, parent_hi)| {
285 parent_lo <= lo && hi <= parent_hi && (parent_lo != lo || parent_hi != hi)
286 })
287 }
288
289 fn to_vec(&self) -> Vec<(u32, u32)> {
291 self.spans.iter().copied().collect()
292 }
293
294 fn from_vec(pairs: Vec<(u32, u32)>) -> Self {
296 Self { spans: pairs.into_iter().collect() }
297 }
298}
299
300pub struct MutationHandler {
301 contract_to_mutate: PathBuf,
302 pub src: Arc<String>,
303 pub mutations: Vec<Mutant>,
304 config: Arc<foundry_config::Config>,
305 report: MutationsSummary,
306 survived_spans: SurvivedSpans,
307 contract_filter: Option<regex::Regex>,
310 mutation_exclusions: type_analysis::MutationExclusionSet,
311}
312
313impl MutationHandler {
314 pub fn new(contract_to_mutate: PathBuf, config: Arc<foundry_config::Config>) -> Self {
315 Self {
316 contract_to_mutate,
317 src: Arc::default(),
318 mutations: vec![],
319 config,
320 report: MutationsSummary::new(),
321 survived_spans: SurvivedSpans::new(),
322 contract_filter: None,
323 mutation_exclusions: type_analysis::MutationExclusionSet::new(),
324 }
325 }
326
327 pub fn with_contract_filter(mut self, filter: regex::Regex) -> Self {
329 self.contract_filter = Some(filter);
330 self
331 }
332
333 pub(crate) fn with_mutation_exclusions(
335 mut self,
336 mutations: type_analysis::MutationExclusionSet,
337 ) -> Self {
338 self.mutation_exclusions = mutations;
339 self
340 }
341
342 pub fn read_source_contract(&mut self) -> Result<(), std::io::Error> {
343 let content = std::fs::read_to_string(&self.contract_to_mutate)?;
344 self.src = Arc::new(content);
345 Ok(())
346 }
347
348 pub fn add_dead_mutant(&mut self, mutant: Mutant) {
350 self.report.add_dead_mutant(mutant);
351 }
352
353 pub fn add_survived_mutant(&mut self, mutant: Mutant) {
355 self.report.add_survived_mutant(mutant);
356 }
357
358 pub fn add_invalid_mutant(&mut self, mutant: Mutant) {
360 self.report.update_invalid_mutant(mutant);
361 }
362
363 pub fn add_skipped_mutant(&mut self, mutant: Mutant) {
364 self.report.add_skipped_mutant(mutant);
365 }
366
367 pub fn add_timed_out_mutant(&mut self, mutant: Mutant) {
368 self.report.add_timed_out_mutant(mutant);
369 }
370
371 pub const fn get_report(&self) -> &MutationsSummary {
373 &self.report
374 }
375
376 fn cache_file_path(&self, hash: &str, kind: CacheKind<'_>) -> PathBuf {
385 let mut hasher = DefaultHasher::new();
386 self.contract_to_mutate.hash(&mut hasher);
387 let path_hash = hasher.finish();
388
389 let mut mutant_cfg_hasher = DefaultHasher::new();
397 "mutant-set-v8".hash(&mut mutant_cfg_hasher);
400 for op in self.config.mutation.enabled_operators() {
401 op.to_string().hash(&mut mutant_cfg_hasher);
402 }
403 match self.contract_filter.as_ref() {
404 Some(re) => {
405 "filter:".hash(&mut mutant_cfg_hasher);
406 re.as_str().hash(&mut mutant_cfg_hasher);
407 }
408 None => "nofilter".hash(&mut mutant_cfg_hasher),
409 }
410 let mut exclusion_hashes = self
411 .mutation_exclusions
412 .iter()
413 .map(|exclusion| {
414 let mut hasher = DefaultHasher::new();
415 exclusion.hash(&mut hasher);
416 hasher.finish()
417 })
418 .collect::<Vec<_>>();
419 exclusion_hashes.sort_unstable();
420 "exclusions:".hash(&mut mutant_cfg_hasher);
421 exclusion_hashes.len().hash(&mut mutant_cfg_hasher);
422 for exclusion_hash in exclusion_hashes {
423 exclusion_hash.hash(&mut mutant_cfg_hasher);
424 }
425 let mutant_cfg_hash = mutant_cfg_hasher.finish();
426
427 let (ext, execution_suffix) = match kind {
428 CacheKind::Mutants => ("mutants", String::new()),
429 CacheKind::Results { execution_key } => ("results", format!("_{execution_key}")),
430 CacheKind::Survived { execution_key } => ("survived", format!("_{execution_key}")),
431 };
432
433 let stem =
434 self.contract_to_mutate.file_stem().and_then(|s| s.to_str()).unwrap_or("unknown");
435 self.config.root.join(&self.config.mutation_dir).join(format!(
436 "{hash}_{stem}_{path_hash:x}_{mutant_cfg_hash:x}{execution_suffix}.{ext}"
437 ))
438 }
439
440 pub fn persist_cached_mutants(&self, hash: &str, mutants: &[Mutant]) -> std::io::Result<()> {
442 let cache_file = self.cache_file_path(hash, CacheKind::Mutants);
443 if let Some(dir) = cache_file.parent() {
444 std::fs::create_dir_all(dir)?;
445 }
446 let json = serde_json::to_string_pretty(mutants).map_err(std::io::Error::other)?;
447 std::fs::write(cache_file, json)
448 }
449
450 pub fn persist_cached_results(
452 &self,
453 hash: &str,
454 execution_key: &str,
455 mutants: &[Mutant],
456 results: &[(Mutant, crate::mutation::mutant::MutationResult)],
457 ) -> std::io::Result<()> {
458 let cache_file = self.cache_file_path(hash, CacheKind::Results { execution_key });
459 if let Some(dir) = cache_file.parent() {
460 std::fs::create_dir_all(dir)?;
461 }
462 let cached = CachedMutationResults {
463 mutant_count: mutants.len(),
464 mutant_hash: mutant_set_hash(mutants),
465 results: results.to_vec(),
466 };
467 let json = serde_json::to_string_pretty(&cached).map_err(std::io::Error::other)?;
468 std::fs::write(cache_file, json)
469 }
470
471 pub async fn generate_ast(&mut self) -> eyre::Result<()> {
474 let path = &self.contract_to_mutate;
475 let target_content = Arc::clone(&self.src);
476 let sess = Session::builder().with_silent_emitter(None).build();
477
478 let contract_filter = self.contract_filter.clone();
479
480 let result = sess.enter(|| -> eyre::Result<Vec<Mutant>> {
481 let arena = solar::ast::Arena::new();
482 let mut parser =
483 Parser::from_lazy_source_code(&sess, &arena, FileName::from(path.clone()), || {
484 Ok((*target_content).clone())
485 })
486 .map_err(|_e| failed_to_parse(path))?;
487
488 let ast = parser.parse_file().map_err(|e| {
489 e.emit();
490 failed_to_parse(path)
491 })?;
492 drop(parser);
493
494 let mutant_path =
497 project_relative_path(&self.config.root, path).unwrap_or_else(|| path.clone());
498 let operators = self.config.mutation.enabled_operators();
499 let mut mutant_visitor = MutantVisitor::with_operators(mutant_path, &operators)
500 .with_source(&target_content)
501 .with_mutation_exclusions(self.mutation_exclusions.clone());
502
503 if let Some(filter) = contract_filter {
504 mutant_visitor =
505 mutant_visitor.with_contract_filter(move |name| filter.is_match(name));
506 }
507 let _ = mutant_visitor.visit_source_unit(&ast);
508
509 for err in mutant_visitor.take_errors() {
510 let _ = sh_warn!("{err:?}");
511 }
512
513 Ok(mutant_visitor.mutation_to_conduct)
514 });
515
516 match result {
517 Ok(mutations) => {
518 self.mutations.extend(mutations);
519 Ok(())
520 }
521 Err(err) => Err(err),
522 }
523 }
524
525 pub fn retrieve_cached_mutants(&self, hash: &str) -> Option<Vec<Mutant>> {
527 let cache_file = self.cache_file_path(hash, CacheKind::Mutants);
528 let data = std::fs::read_to_string(cache_file).ok()?;
529 serde_json::from_str(&data).ok()
530 }
531
532 pub fn retrieve_cached_mutant_results(
534 &self,
535 hash: &str,
536 execution_key: &str,
537 mutants: &[Mutant],
538 ) -> Option<Vec<(Mutant, MutationResult)>> {
539 let cache_file = self.cache_file_path(hash, CacheKind::Results { execution_key });
540 let data = std::fs::read_to_string(cache_file).ok()?;
541 let cached: CachedMutationResults = serde_json::from_str(&data).ok()?;
542 (cached.mutant_count == mutants.len() && cached.mutant_hash == mutant_set_hash(mutants))
543 .then_some(cached.results)
544 }
545
546 pub fn mark_span_survived(&mut self, span: Span) {
548 self.survived_spans.mark_survived(span);
549 }
550
551 pub fn should_skip_span(&self, span: Span) -> bool {
553 self.survived_spans.should_skip(span)
554 }
555
556 pub fn persist_survived_spans(&self, hash: &str, execution_key: &str) -> std::io::Result<()> {
558 let cache_file = self.cache_file_path(hash, CacheKind::Survived { execution_key });
559 if let Some(dir) = cache_file.parent() {
560 std::fs::create_dir_all(dir)?;
561 }
562 let spans = self.survived_spans.to_vec();
563 let json = serde_json::to_string_pretty(&spans).map_err(std::io::Error::other)?;
564 std::fs::write(cache_file, json)
565 }
566
567 pub fn retrieve_survived_spans(&mut self, hash: &str, execution_key: &str) -> bool {
569 let cache_file = self.cache_file_path(hash, CacheKind::Survived { execution_key });
570
571 if let Ok(data) = std::fs::read_to_string(cache_file)
572 && let Ok(pairs) = serde_json::from_str::<Vec<(u32, u32)>>(&data)
573 {
574 self.survived_spans = SurvivedSpans::from_vec(pairs);
575 return true;
576 }
577
578 false
579 }
580}
581
582fn failed_to_parse(path: &Path) -> eyre::Report {
583 eyre!("failed to parse {}", path.display())
584}
585
586fn mutant_set_hash(mutants: &[Mutant]) -> u64 {
587 let mut entries: Vec<_> = mutants
588 .iter()
589 .map(|mutant| {
590 (
591 mutant.span.lo().0,
592 mutant.span.hi().0,
593 mutant.mutation.to_string(),
594 mutant.original.clone(),
595 )
596 })
597 .collect();
598 entries.sort();
599
600 let mut hasher = DefaultHasher::new();
601 for entry in entries {
602 entry.hash(&mut hasher);
603 }
604 hasher.finish()
605}
606
607#[cfg(test)]
608mod tests {
609 use super::*;
610 use crate::mutation::type_analysis::{AssignmentReplacement, MutationExclusion};
611 use foundry_config::Config;
612 use solar::ast::interface::BytePos;
613 use tempfile::TempDir;
614
615 fn test_handler(config: Config) -> MutationHandler {
616 let source = config.root.join("src").join("Counter.sol");
617 MutationHandler::new(source, Arc::new(config))
618 }
619
620 fn test_config() -> (TempDir, Config) {
621 let temp = TempDir::new().unwrap();
622 let config = Config {
623 root: temp.path().to_path_buf(),
624 mutation_dir: "cache/mutation".into(),
625 ..Default::default()
626 };
627 (temp, config)
628 }
629
630 fn mutant(lo: u32, hi: u32, original: &str) -> Mutant {
631 Mutant {
632 path: PathBuf::from("src/Counter.sol"),
633 span: Span::new(BytePos(lo), BytePos(hi)),
634 mutation: mutant::MutationType::DeleteExpression,
635 original: original.to_string(),
636 source_line: "number++;".to_string(),
637 line_number: 1,
638 column_number: 1,
639 }
640 }
641
642 #[test]
643 fn result_cache_path_includes_execution_key() {
644 let (_temp, config) = test_config();
645 let handler = test_handler(config);
646
647 let first =
648 handler.cache_file_path("build", CacheKind::Results { execution_key: "exec-a" });
649 let second =
650 handler.cache_file_path("build", CacheKind::Results { execution_key: "exec-b" });
651 let mutants = handler.cache_file_path("build", CacheKind::Mutants);
652
653 assert_ne!(first, second);
654 assert_ne!(first, mutants);
655 assert_ne!(second, mutants);
656 }
657
658 #[test]
659 fn survived_span_cache_path_includes_execution_key() {
660 let (_temp, config) = test_config();
661 let handler = test_handler(config);
662
663 let first =
664 handler.cache_file_path("build", CacheKind::Survived { execution_key: "exec-a" });
665 let second =
666 handler.cache_file_path("build", CacheKind::Survived { execution_key: "exec-b" });
667
668 assert_ne!(first, second);
669 }
670
671 #[test]
672 fn mutant_cache_path_ignores_execution_only_timeout() {
673 let (_temp, mut first_config) = test_config();
674 let mut second_config = first_config.clone();
675
676 first_config.mutation.timeout = Some(1);
677 second_config.mutation.timeout = Some(99);
678
679 let first = test_handler(first_config).cache_file_path("build", CacheKind::Mutants);
680 let second = test_handler(second_config).cache_file_path("build", CacheKind::Mutants);
681
682 assert_eq!(first, second);
683 }
684
685 #[test]
686 fn mutation_cache_paths_include_type_analysis_exclusions() {
687 let (_temp, config) = test_config();
688 let without_exclusions = test_handler(config.clone());
689 let exclusion = MutationExclusion::assignment(
690 Span::new(BytePos(10), BytePos(20)),
691 AssignmentReplacement::Zero,
692 );
693 let with_exclusions = test_handler(config).with_mutation_exclusions([exclusion].into());
694 without_exclusions.persist_cached_mutants("build", &[mutant(10, 20, "account")]).unwrap();
695
696 assert!(with_exclusions.retrieve_cached_mutants("build").is_none());
697
698 for kind in [
699 CacheKind::Mutants,
700 CacheKind::Results { execution_key: "exec" },
701 CacheKind::Survived { execution_key: "exec" },
702 ] {
703 assert_ne!(
704 without_exclusions.cache_file_path("build", kind),
705 with_exclusions.cache_file_path("build", kind),
706 );
707 }
708 }
709
710 #[test]
711 fn result_cache_validates_current_mutant_set() {
712 let (_temp, config) = test_config();
713 let handler = test_handler(config);
714 let mutants = vec![mutant(10, 20, "number++")];
715 let results = vec![(mutants[0].clone(), MutationResult::Dead)];
716
717 handler.persist_cached_results("build", "exec", &mutants, &results).unwrap();
718
719 assert!(handler.retrieve_cached_mutant_results("build", "exec", &mutants).is_some());
720
721 let changed_mutants = vec![mutant(10, 20, "number--")];
722 assert!(
723 handler.retrieve_cached_mutant_results("build", "exec", &changed_mutants).is_none()
724 );
725 }
726
727 #[test]
728 fn mutation_score_is_unreliable_when_evaluated_mutants_equal_timeouts() {
729 let mut summary = MutationsSummary::new();
730 summary.add_dead_mutant(mutant(10, 20, "number++"));
731 summary.add_timed_out_mutant(mutant(30, 40, "number--"));
732
733 assert_eq!(summary.total_evaluated(), 1);
734 assert!(!summary.has_reliable_score());
735 }
736
737 #[test]
738 fn mutation_score_is_unreliable_when_timeouts_dominate() {
739 let mut summary = MutationsSummary::new();
740 summary.add_dead_mutant(mutant(10, 20, "number++"));
741 summary.add_timed_out_mutant(mutant(30, 40, "number--"));
742 summary.add_timed_out_mutant(mutant(50, 60, "number += 1"));
743
744 assert_eq!(summary.total_evaluated(), 1);
745 assert!(!summary.has_reliable_score());
746 }
747
748 #[test]
749 fn mutation_score_is_unreliable_with_no_evaluated_mutants() {
750 let mut summary = MutationsSummary::new();
751 summary.add_timed_out_mutant(mutant(10, 20, "number++"));
752
753 assert_eq!(summary.total_evaluated(), 0);
754 assert!(!summary.has_reliable_score());
755 assert_eq!(summary.mutation_score(), 0.0);
756 }
757}