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
30#[derive(Clone, Copy)]
31enum CacheKind<'a> {
32 Mutants,
33 Results { execution_key: &'a str },
34 Survived { execution_key: &'a str },
35}
36
37#[derive(Serialize, Deserialize)]
38struct CachedMutationResults {
39 mutant_count: usize,
40 mutant_hash: u64,
41 results: Vec<(Mutant, MutationResult)>,
42}
43
44pub mod mutant;
45mod mutators;
46pub mod orchestrator;
47pub mod progress;
48mod reporter;
49pub mod runner;
50mod type_analysis;
51mod visitor;
52
53pub struct MutationsSummary {
54 dead: Vec<Mutant>,
55 survived: Vec<Mutant>,
56 invalid: Vec<Mutant>,
57 skipped: Vec<Mutant>,
58 timed_out: Vec<Mutant>,
61}
62
63impl Default for MutationsSummary {
64 fn default() -> Self {
65 Self::new()
66 }
67}
68
69impl MutationsSummary {
70 pub const fn new() -> Self {
71 Self {
72 dead: Vec::new(),
73 survived: Vec::new(),
74 invalid: Vec::new(),
75 skipped: Vec::new(),
76 timed_out: Vec::new(),
77 }
78 }
79
80 pub fn update_invalid_mutant(&mut self, mutant: Mutant) {
81 self.invalid.push(mutant);
82 }
83
84 pub fn add_dead_mutant(&mut self, mutant: Mutant) {
85 self.dead.push(mutant);
86 }
87
88 pub fn add_survived_mutant(&mut self, mutant: Mutant) {
89 self.survived.push(mutant);
90 }
91
92 pub fn add_skipped_mutant(&mut self, mutant: Mutant) {
93 self.skipped.push(mutant);
94 }
95
96 pub fn add_timed_out_mutant(&mut self, mutant: Mutant) {
97 self.timed_out.push(mutant);
98 }
99
100 pub const fn total_mutants(&self) -> usize {
101 self.dead.len()
102 + self.survived.len()
103 + self.invalid.len()
104 + self.skipped.len()
105 + self.timed_out.len()
106 }
107
108 pub const fn total_dead(&self) -> usize {
109 self.dead.len()
110 }
111
112 pub const fn total_survived(&self) -> usize {
113 self.survived.len()
114 }
115
116 pub const fn total_invalid(&self) -> usize {
117 self.invalid.len()
118 }
119
120 pub const fn total_skipped(&self) -> usize {
121 self.skipped.len()
122 }
123
124 pub const fn total_timed_out(&self) -> usize {
125 self.timed_out.len()
126 }
127
128 pub const fn get_dead(&self) -> &Vec<Mutant> {
129 &self.dead
130 }
131
132 pub const fn get_survived(&self) -> &Vec<Mutant> {
133 &self.survived
134 }
135
136 pub const fn get_invalid(&self) -> &Vec<Mutant> {
137 &self.invalid
138 }
139
140 pub const fn get_timed_out(&self) -> &Vec<Mutant> {
141 &self.timed_out
142 }
143
144 pub fn merge(&mut self, other: &Self) {
146 self.dead.extend(other.dead.clone());
147 self.survived.extend(other.survived.clone());
148 self.invalid.extend(other.invalid.clone());
149 self.skipped.extend(other.skipped.clone());
150 self.timed_out.extend(other.timed_out.clone());
151 }
152
153 pub fn mutation_score(&self) -> f64 {
156 let valid_mutants = self.dead.len() + self.survived.len();
157 if valid_mutants == 0 { 0.0 } else { self.dead.len() as f64 / valid_mutants as f64 * 100.0 }
158 }
159
160 pub const fn total_evaluated(&self) -> usize {
162 self.dead.len() + self.survived.len()
163 }
164
165 pub const fn has_reliable_score(&self) -> bool {
167 self.total_evaluated() > 0 && self.timed_out.len() < self.total_evaluated()
168 }
169
170 pub fn to_json_output(&self, duration_secs: f64) -> MutationJsonOutput {
178 let mut survived_mutants: BTreeMap<String, Vec<SurvivedMutantJson>> = BTreeMap::new();
179
180 for mutant in &self.survived {
181 let file_path = mutant.relative_path();
182 let entry = survived_mutants.entry(file_path).or_default();
183 entry.push(SurvivedMutantJson::from_mutant(mutant));
184 }
185
186 for entries in survived_mutants.values_mut() {
187 entries.sort_by(|a, b| {
188 (a.line, a.column, &a.original, &a.mutant).cmp(&(
189 b.line,
190 b.column,
191 &b.original,
192 &b.mutant,
193 ))
194 });
195 }
196
197 MutationJsonOutput {
198 summary: MutationSummaryJson {
199 total: self.total_mutants(),
200 killed: self.total_dead(),
201 survived: self.total_survived(),
202 invalid: self.total_invalid(),
203 skipped: self.total_skipped(),
204 timed_out: self.total_timed_out(),
205 mutation_score: self.mutation_score(),
206 duration_secs,
207 },
208 survived_mutants,
209 }
210 }
211}
212
213#[derive(Debug, Clone, Serialize)]
218pub struct MutationJsonOutput {
219 pub summary: MutationSummaryJson,
220 pub survived_mutants: BTreeMap<String, Vec<SurvivedMutantJson>>,
221}
222
223#[derive(Debug, Clone, Serialize)]
225pub struct MutationSummaryJson {
226 pub total: usize,
227 pub killed: usize,
228 pub survived: usize,
229 pub invalid: usize,
230 pub skipped: usize,
231 pub timed_out: usize,
232 pub mutation_score: f64,
233 pub duration_secs: f64,
234}
235
236#[derive(Debug, Clone, Serialize)]
238pub struct SurvivedMutantJson {
239 pub line: usize,
240 pub column: usize,
241 pub original: String,
242 pub mutant: String,
243}
244
245impl SurvivedMutantJson {
246 pub fn from_mutant(mutant: &Mutant) -> Self {
248 Self {
249 line: mutant.line_number,
250 column: mutant.column_number,
251 original: mutant.original.clone(),
252 mutant: mutant.mutation.to_string(),
253 }
254 }
255}
256
257#[derive(Debug, Clone, Default)]
260pub struct SurvivedSpans {
261 spans: HashSet<(u32, u32)>, }
263
264impl SurvivedSpans {
265 pub fn new() -> Self {
266 Self { spans: HashSet::new() }
267 }
268
269 pub fn mark_survived(&mut self, span: Span) {
271 self.spans.insert((span.lo().0, span.hi().0));
272 }
273
274 pub fn should_skip(&self, span: Span) -> bool {
281 let (lo, hi) = (span.lo().0, span.hi().0);
282
283 self.spans.iter().any(|&(parent_lo, parent_hi)| {
284 parent_lo <= lo && hi <= parent_hi && (parent_lo != lo || parent_hi != hi)
285 })
286 }
287
288 pub fn should_skip_in_live_run(&self, span: Span) -> bool {
293 let (lo, hi) = (span.lo().0, span.hi().0);
294
295 self.spans.iter().any(|&(parent_lo, parent_hi)| parent_lo <= lo && hi <= parent_hi)
296 }
297
298 fn to_vec(&self) -> Vec<(u32, u32)> {
300 self.spans.iter().copied().collect()
301 }
302
303 fn from_vec(pairs: Vec<(u32, u32)>) -> Self {
305 Self { spans: pairs.into_iter().collect() }
306 }
307}
308
309pub struct MutationHandler {
310 contract_to_mutate: PathBuf,
311 pub src: Arc<String>,
312 pub mutations: Vec<Mutant>,
313 config: Arc<foundry_config::Config>,
314 report: MutationsSummary,
315 survived_spans: SurvivedSpans,
316 contract_filter: Option<regex::Regex>,
319 mutation_exclusions: type_analysis::MutationExclusionSet,
320}
321
322impl MutationHandler {
323 pub fn new(contract_to_mutate: PathBuf, config: Arc<foundry_config::Config>) -> Self {
324 Self {
325 contract_to_mutate,
326 src: Arc::default(),
327 mutations: vec![],
328 config,
329 report: MutationsSummary::new(),
330 survived_spans: SurvivedSpans::new(),
331 contract_filter: None,
332 mutation_exclusions: type_analysis::MutationExclusionSet::new(),
333 }
334 }
335
336 pub fn with_contract_filter(mut self, filter: regex::Regex) -> Self {
338 self.contract_filter = Some(filter);
339 self
340 }
341
342 pub(crate) fn with_mutation_exclusions(
344 mut self,
345 mutations: type_analysis::MutationExclusionSet,
346 ) -> Self {
347 self.mutation_exclusions = mutations;
348 self
349 }
350
351 pub fn read_source_contract(&mut self) -> Result<(), std::io::Error> {
352 let content = std::fs::read_to_string(&self.contract_to_mutate)?;
353 self.src = Arc::new(content);
354 Ok(())
355 }
356
357 pub fn add_dead_mutant(&mut self, mutant: Mutant) {
359 self.report.add_dead_mutant(mutant);
360 }
361
362 pub fn add_survived_mutant(&mut self, mutant: Mutant) {
364 self.report.add_survived_mutant(mutant);
365 }
366
367 pub fn add_invalid_mutant(&mut self, mutant: Mutant) {
369 self.report.update_invalid_mutant(mutant);
370 }
371
372 pub fn add_skipped_mutant(&mut self, mutant: Mutant) {
373 self.report.add_skipped_mutant(mutant);
374 }
375
376 pub fn add_timed_out_mutant(&mut self, mutant: Mutant) {
377 self.report.add_timed_out_mutant(mutant);
378 }
379
380 pub const fn get_report(&self) -> &MutationsSummary {
382 &self.report
383 }
384
385 fn cache_file_path(&self, hash: &str, kind: CacheKind<'_>) -> PathBuf {
394 let mut hasher = DefaultHasher::new();
395 self.contract_to_mutate.hash(&mut hasher);
396 let path_hash = hasher.finish();
397
398 let mut mutant_cfg_hasher = DefaultHasher::new();
406 "mutant-set-v7".hash(&mut mutant_cfg_hasher);
409 for op in self.config.mutation.enabled_operators() {
410 op.to_string().hash(&mut mutant_cfg_hasher);
411 }
412 match self.contract_filter.as_ref() {
413 Some(re) => {
414 "filter:".hash(&mut mutant_cfg_hasher);
415 re.as_str().hash(&mut mutant_cfg_hasher);
416 }
417 None => "nofilter".hash(&mut mutant_cfg_hasher),
418 }
419 let mut exclusion_hashes = self
420 .mutation_exclusions
421 .iter()
422 .map(|exclusion| {
423 let mut hasher = DefaultHasher::new();
424 exclusion.hash(&mut hasher);
425 hasher.finish()
426 })
427 .collect::<Vec<_>>();
428 exclusion_hashes.sort_unstable();
429 "exclusions:".hash(&mut mutant_cfg_hasher);
430 exclusion_hashes.len().hash(&mut mutant_cfg_hasher);
431 for exclusion_hash in exclusion_hashes {
432 exclusion_hash.hash(&mut mutant_cfg_hasher);
433 }
434 let mutant_cfg_hash = mutant_cfg_hasher.finish();
435
436 let (ext, execution_suffix) = match kind {
437 CacheKind::Mutants => ("mutants", String::new()),
438 CacheKind::Results { execution_key } => ("results", format!("_{execution_key}")),
439 CacheKind::Survived { execution_key } => ("survived", format!("_{execution_key}")),
440 };
441
442 let stem =
443 self.contract_to_mutate.file_stem().and_then(|s| s.to_str()).unwrap_or("unknown");
444 self.config.root.join(&self.config.mutation_dir).join(format!(
445 "{hash}_{stem}_{path_hash:x}_{mutant_cfg_hash:x}{execution_suffix}.{ext}"
446 ))
447 }
448
449 pub fn persist_cached_mutants(&self, hash: &str, mutants: &[Mutant]) -> std::io::Result<()> {
451 let cache_file = self.cache_file_path(hash, CacheKind::Mutants);
452 if let Some(dir) = cache_file.parent() {
453 std::fs::create_dir_all(dir)?;
454 }
455 let json = serde_json::to_string_pretty(mutants).map_err(std::io::Error::other)?;
456 std::fs::write(cache_file, json)
457 }
458
459 pub fn persist_cached_results(
461 &self,
462 hash: &str,
463 execution_key: &str,
464 mutants: &[Mutant],
465 results: &[(Mutant, crate::mutation::mutant::MutationResult)],
466 ) -> std::io::Result<()> {
467 let cache_file = self.cache_file_path(hash, CacheKind::Results { execution_key });
468 if let Some(dir) = cache_file.parent() {
469 std::fs::create_dir_all(dir)?;
470 }
471 let cached = CachedMutationResults {
472 mutant_count: mutants.len(),
473 mutant_hash: mutant_set_hash(mutants),
474 results: results.to_vec(),
475 };
476 let json = serde_json::to_string_pretty(&cached).map_err(std::io::Error::other)?;
477 std::fs::write(cache_file, json)
478 }
479
480 pub async fn generate_ast(&mut self) -> eyre::Result<()> {
483 let path = &self.contract_to_mutate;
484 let target_content = Arc::clone(&self.src);
485 let sess = Session::builder().with_silent_emitter(None).build();
486
487 let contract_filter = self.contract_filter.clone();
488
489 let result = sess.enter(|| -> eyre::Result<Vec<Mutant>> {
490 let arena = solar::ast::Arena::new();
491 let mut parser =
492 Parser::from_lazy_source_code(&sess, &arena, FileName::from(path.clone()), || {
493 Ok((*target_content).clone())
494 })
495 .map_err(|_e| failed_to_parse(path))?;
496
497 let ast = parser.parse_file().map_err(|e| {
498 e.emit();
499 failed_to_parse(path)
500 })?;
501 drop(parser);
502
503 let operators = self.config.mutation.enabled_operators();
504 let mut mutant_visitor = MutantVisitor::with_operators(path.clone(), &operators)
505 .with_source(&target_content)
506 .with_mutation_exclusions(self.mutation_exclusions.clone());
507
508 if let Some(filter) = contract_filter {
509 mutant_visitor =
510 mutant_visitor.with_contract_filter(move |name| filter.is_match(name));
511 }
512 let _ = mutant_visitor.visit_source_unit(&ast);
513
514 for err in mutant_visitor.take_errors() {
515 let _ = sh_warn!("{err:?}");
516 }
517
518 Ok(mutant_visitor.mutation_to_conduct)
519 });
520
521 match result {
522 Ok(mutations) => {
523 self.mutations.extend(mutations);
524 Ok(())
525 }
526 Err(err) => Err(err),
527 }
528 }
529
530 pub fn retrieve_cached_mutants(&self, hash: &str) -> Option<Vec<Mutant>> {
532 let cache_file = self.cache_file_path(hash, CacheKind::Mutants);
533 let data = std::fs::read_to_string(cache_file).ok()?;
534 serde_json::from_str(&data).ok()
535 }
536
537 pub fn retrieve_cached_mutant_results(
539 &self,
540 hash: &str,
541 execution_key: &str,
542 mutants: &[Mutant],
543 ) -> Option<Vec<(Mutant, MutationResult)>> {
544 let cache_file = self.cache_file_path(hash, CacheKind::Results { execution_key });
545 let data = std::fs::read_to_string(cache_file).ok()?;
546 let cached: CachedMutationResults = serde_json::from_str(&data).ok()?;
547 (cached.mutant_count == mutants.len() && cached.mutant_hash == mutant_set_hash(mutants))
548 .then_some(cached.results)
549 }
550
551 pub fn mark_span_survived(&mut self, span: Span) {
553 self.survived_spans.mark_survived(span);
554 }
555
556 pub fn should_skip_span(&self, span: Span) -> bool {
558 self.survived_spans.should_skip(span)
559 }
560
561 pub fn persist_survived_spans(&self, hash: &str, execution_key: &str) -> std::io::Result<()> {
563 let cache_file = self.cache_file_path(hash, CacheKind::Survived { execution_key });
564 if let Some(dir) = cache_file.parent() {
565 std::fs::create_dir_all(dir)?;
566 }
567 let spans = self.survived_spans.to_vec();
568 let json = serde_json::to_string_pretty(&spans).map_err(std::io::Error::other)?;
569 std::fs::write(cache_file, json)
570 }
571
572 pub fn retrieve_survived_spans(&mut self, hash: &str, execution_key: &str) -> bool {
574 let cache_file = self.cache_file_path(hash, CacheKind::Survived { execution_key });
575
576 if let Ok(data) = std::fs::read_to_string(cache_file)
577 && let Ok(pairs) = serde_json::from_str::<Vec<(u32, u32)>>(&data)
578 {
579 self.survived_spans = SurvivedSpans::from_vec(pairs);
580 return true;
581 }
582
583 false
584 }
585}
586
587fn failed_to_parse(path: &Path) -> eyre::Report {
588 eyre!("failed to parse {}", path.display())
589}
590
591fn mutant_set_hash(mutants: &[Mutant]) -> u64 {
592 let mut entries: Vec<_> = mutants
593 .iter()
594 .map(|mutant| {
595 (
596 mutant.span.lo().0,
597 mutant.span.hi().0,
598 mutant.mutation.to_string(),
599 mutant.original.clone(),
600 )
601 })
602 .collect();
603 entries.sort();
604
605 let mut hasher = DefaultHasher::new();
606 for entry in entries {
607 entry.hash(&mut hasher);
608 }
609 hasher.finish()
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}