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