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 visitor;
76
77pub struct MutationsSummary {
78 dead: Vec<Mutant>,
79 survived: Vec<Mutant>,
80 invalid: Vec<Mutant>,
81 skipped: Vec<Mutant>,
82 timed_out: Vec<Mutant>,
85}
86
87impl Default for MutationsSummary {
88 fn default() -> Self {
89 Self::new()
90 }
91}
92
93impl MutationsSummary {
94 pub const fn new() -> Self {
95 Self {
96 dead: Vec::new(),
97 survived: Vec::new(),
98 invalid: Vec::new(),
99 skipped: Vec::new(),
100 timed_out: Vec::new(),
101 }
102 }
103
104 pub fn update_invalid_mutant(&mut self, mutant: Mutant) {
105 self.invalid.push(mutant);
106 }
107
108 pub fn add_dead_mutant(&mut self, mutant: Mutant) {
109 self.dead.push(mutant);
110 }
111
112 pub fn add_survived_mutant(&mut self, mutant: Mutant) {
113 self.survived.push(mutant);
114 }
115
116 pub fn add_skipped_mutant(&mut self, mutant: Mutant) {
117 self.skipped.push(mutant);
118 }
119
120 pub fn add_timed_out_mutant(&mut self, mutant: Mutant) {
121 self.timed_out.push(mutant);
122 }
123
124 pub const fn total_mutants(&self) -> usize {
125 self.dead.len()
126 + self.survived.len()
127 + self.invalid.len()
128 + self.skipped.len()
129 + self.timed_out.len()
130 }
131
132 pub const fn total_dead(&self) -> usize {
133 self.dead.len()
134 }
135
136 pub const fn total_survived(&self) -> usize {
137 self.survived.len()
138 }
139
140 pub const fn total_invalid(&self) -> usize {
141 self.invalid.len()
142 }
143
144 pub const fn total_skipped(&self) -> usize {
145 self.skipped.len()
146 }
147
148 pub const fn total_timed_out(&self) -> usize {
149 self.timed_out.len()
150 }
151
152 pub const fn get_dead(&self) -> &Vec<Mutant> {
153 &self.dead
154 }
155
156 pub const fn get_survived(&self) -> &Vec<Mutant> {
157 &self.survived
158 }
159
160 pub const fn get_invalid(&self) -> &Vec<Mutant> {
161 &self.invalid
162 }
163
164 pub const fn get_timed_out(&self) -> &Vec<Mutant> {
165 &self.timed_out
166 }
167
168 pub fn merge(&mut self, other: &Self) {
170 self.dead.extend(other.dead.clone());
171 self.survived.extend(other.survived.clone());
172 self.invalid.extend(other.invalid.clone());
173 self.skipped.extend(other.skipped.clone());
174 self.timed_out.extend(other.timed_out.clone());
175 }
176
177 pub fn mutation_score(&self) -> f64 {
180 let valid_mutants = self.dead.len() + self.survived.len();
181 if valid_mutants == 0 { 0.0 } else { self.dead.len() as f64 / valid_mutants as f64 * 100.0 }
182 }
183
184 pub const fn total_evaluated(&self) -> usize {
186 self.dead.len() + self.survived.len()
187 }
188
189 pub const fn has_reliable_score(&self) -> bool {
191 self.total_evaluated() > 0 && self.timed_out.len() < self.total_evaluated()
192 }
193
194 pub fn to_json_output(&self, duration_secs: f64) -> MutationJsonOutput {
202 let mut survived_mutants: BTreeMap<String, Vec<SurvivedMutantJson>> = BTreeMap::new();
203
204 for mutant in &self.survived {
205 let file_path = mutant.relative_path();
206 let entry = survived_mutants.entry(file_path).or_default();
207 entry.push(SurvivedMutantJson::from_mutant(mutant));
208 }
209
210 for entries in survived_mutants.values_mut() {
211 entries.sort_by(|a, b| {
212 (a.line, a.column, &a.original, &a.mutant).cmp(&(
213 b.line,
214 b.column,
215 &b.original,
216 &b.mutant,
217 ))
218 });
219 }
220
221 MutationJsonOutput {
222 summary: MutationSummaryJson {
223 total: self.total_mutants(),
224 killed: self.total_dead(),
225 survived: self.total_survived(),
226 invalid: self.total_invalid(),
227 skipped: self.total_skipped(),
228 timed_out: self.total_timed_out(),
229 mutation_score: self.mutation_score(),
230 duration_secs,
231 },
232 survived_mutants,
233 }
234 }
235}
236
237#[derive(Debug, Clone, Serialize)]
242pub struct MutationJsonOutput {
243 pub summary: MutationSummaryJson,
244 pub survived_mutants: BTreeMap<String, Vec<SurvivedMutantJson>>,
245}
246
247#[derive(Debug, Clone, Serialize)]
249pub struct MutationSummaryJson {
250 pub total: usize,
251 pub killed: usize,
252 pub survived: usize,
253 pub invalid: usize,
254 pub skipped: usize,
255 pub timed_out: usize,
256 pub mutation_score: f64,
257 pub duration_secs: f64,
258}
259
260#[derive(Debug, Clone, Serialize)]
262pub struct SurvivedMutantJson {
263 pub line: usize,
264 pub column: usize,
265 pub original: String,
266 pub mutant: String,
267}
268
269impl SurvivedMutantJson {
270 pub fn from_mutant(mutant: &Mutant) -> Self {
272 Self {
273 line: mutant.line_number,
274 column: mutant.column_number,
275 original: mutant.original.clone(),
276 mutant: mutant.mutation.to_string(),
277 }
278 }
279}
280
281#[derive(Debug, Clone, Default)]
284pub struct SurvivedSpans {
285 spans: HashSet<(u32, u32)>, }
287
288impl SurvivedSpans {
289 pub fn new() -> Self {
290 Self { spans: HashSet::new() }
291 }
292
293 pub fn mark_survived(&mut self, span: Span) {
295 self.spans.insert((span.lo().0, span.hi().0));
296 }
297
298 pub fn should_skip(&self, span: Span) -> bool {
305 let (lo, hi) = (span.lo().0, span.hi().0);
306
307 self.spans.iter().any(|&(parent_lo, parent_hi)| {
308 parent_lo <= lo && hi <= parent_hi && (parent_lo != lo || parent_hi != hi)
309 })
310 }
311
312 pub fn should_skip_in_live_run(&self, span: Span) -> bool {
317 let (lo, hi) = (span.lo().0, span.hi().0);
318
319 self.spans.iter().any(|&(parent_lo, parent_hi)| parent_lo <= lo && hi <= parent_hi)
320 }
321
322 fn to_vec(&self) -> Vec<(u32, u32)> {
324 self.spans.iter().copied().collect()
325 }
326
327 fn from_vec(pairs: Vec<(u32, u32)>) -> Self {
329 Self { spans: pairs.into_iter().collect() }
330 }
331}
332
333pub struct MutationHandler {
334 contract_to_mutate: PathBuf,
335 pub src: Arc<String>,
336 pub mutations: Vec<Mutant>,
337 config: Arc<foundry_config::Config>,
338 report: MutationsSummary,
339 survived_spans: SurvivedSpans,
340 contract_filter: Option<regex::Regex>,
343}
344
345impl MutationHandler {
346 pub fn new(contract_to_mutate: PathBuf, config: Arc<foundry_config::Config>) -> Self {
347 Self {
348 contract_to_mutate,
349 src: Arc::default(),
350 mutations: vec![],
351 config,
352 report: MutationsSummary::new(),
353 survived_spans: SurvivedSpans::new(),
354 contract_filter: None,
355 }
356 }
357
358 pub fn with_contract_filter(mut self, filter: regex::Regex) -> Self {
360 self.contract_filter = Some(filter);
361 self
362 }
363
364 pub fn read_source_contract(&mut self) -> Result<(), std::io::Error> {
365 let content = std::fs::read_to_string(&self.contract_to_mutate)?;
366 self.src = Arc::new(content);
367 Ok(())
368 }
369
370 pub fn add_dead_mutant(&mut self, mutant: Mutant) {
372 self.report.add_dead_mutant(mutant);
373 }
374
375 pub fn add_survived_mutant(&mut self, mutant: Mutant) {
377 self.report.add_survived_mutant(mutant);
378 }
379
380 pub fn add_invalid_mutant(&mut self, mutant: Mutant) {
382 self.report.update_invalid_mutant(mutant);
383 }
384
385 pub fn add_skipped_mutant(&mut self, mutant: Mutant) {
386 self.report.add_skipped_mutant(mutant);
387 }
388
389 pub fn add_timed_out_mutant(&mut self, mutant: Mutant) {
390 self.report.add_timed_out_mutant(mutant);
391 }
392
393 pub const fn get_report(&self) -> &MutationsSummary {
395 &self.report
396 }
397
398 fn cache_file_path(&self, hash: &str, kind: CacheKind<'_>) -> PathBuf {
407 let mut hasher = DefaultHasher::new();
408 self.contract_to_mutate.hash(&mut hasher);
409 let path_hash = hasher.finish();
410
411 let mut mutant_cfg_hasher = DefaultHasher::new();
419 "mutant-set-v2".hash(&mut mutant_cfg_hasher);
422 for op in self.config.mutation.enabled_operators() {
423 op.to_string().hash(&mut mutant_cfg_hasher);
424 }
425 match self.contract_filter.as_ref() {
426 Some(re) => {
427 "filter:".hash(&mut mutant_cfg_hasher);
428 re.as_str().hash(&mut mutant_cfg_hasher);
429 }
430 None => "nofilter".hash(&mut mutant_cfg_hasher),
431 }
432 let mutant_cfg_hash = mutant_cfg_hasher.finish();
433
434 let (ext, execution_suffix) = match kind {
435 CacheKind::Mutants => ("mutants", String::new()),
436 CacheKind::Results { execution_key } => ("results", format!("_{execution_key}")),
437 CacheKind::Survived { execution_key } => ("survived", format!("_{execution_key}")),
438 };
439
440 let stem =
441 self.contract_to_mutate.file_stem().and_then(|s| s.to_str()).unwrap_or("unknown");
442 self.config.root.join(&self.config.mutation_dir).join(format!(
443 "{hash}_{stem}_{path_hash:x}_{mutant_cfg_hash:x}{execution_suffix}.{ext}"
444 ))
445 }
446
447 pub fn persist_cached_mutants(&self, hash: &str, mutants: &[Mutant]) -> std::io::Result<()> {
449 let cache_file = self.cache_file_path(hash, CacheKind::Mutants);
450 if let Some(dir) = cache_file.parent() {
451 std::fs::create_dir_all(dir)?;
452 }
453 let json = serde_json::to_string_pretty(mutants).map_err(std::io::Error::other)?;
454 std::fs::write(cache_file, json)
455 }
456
457 pub fn persist_cached_results(
459 &self,
460 hash: &str,
461 execution_key: &str,
462 mutants: &[Mutant],
463 results: &[(Mutant, crate::mutation::mutant::MutationResult)],
464 ) -> std::io::Result<()> {
465 let cache_file = self.cache_file_path(hash, CacheKind::Results { execution_key });
466 if let Some(dir) = cache_file.parent() {
467 std::fs::create_dir_all(dir)?;
468 }
469 let cached = CachedMutationResults {
470 mutant_count: mutants.len(),
471 mutant_hash: mutant_set_hash(mutants),
472 results: results.to_vec(),
473 };
474 let json = serde_json::to_string_pretty(&cached).map_err(std::io::Error::other)?;
475 std::fs::write(cache_file, json)
476 }
477
478 pub async fn generate_ast(&mut self) -> eyre::Result<()> {
481 let path = &self.contract_to_mutate;
482 let target_content = Arc::clone(&self.src);
483 let sess = Session::builder().with_silent_emitter(None).build();
484
485 let contract_filter = self.contract_filter.clone();
486
487 let result = sess.enter(|| -> eyre::Result<Vec<Mutant>> {
488 let arena = solar::ast::Arena::new();
489 let mut parser =
490 Parser::from_lazy_source_code(&sess, &arena, FileName::from(path.clone()), || {
491 Ok((*target_content).clone())
492 })
493 .map_err(|_e| failed_to_parse(path))?;
494
495 let ast = parser.parse_file().map_err(|e| {
496 e.emit();
497 failed_to_parse(path)
498 })?;
499 drop(parser);
500
501 let operators = self.config.mutation.enabled_operators();
502 let mut mutant_visitor = MutantVisitor::with_operators(path.clone(), &operators)
503 .with_source(&target_content);
504
505 if let Some(filter) = contract_filter {
506 mutant_visitor =
507 mutant_visitor.with_contract_filter(move |name| filter.is_match(name));
508 }
509 let _ = mutant_visitor.visit_source_unit(&ast);
510
511 for err in mutant_visitor.take_errors() {
512 let _ = sh_warn!("{err:?}");
513 }
514
515 Ok(mutant_visitor.mutation_to_conduct)
516 });
517
518 match result {
519 Ok(mutations) => {
520 self.mutations.extend(mutations);
521 Ok(())
522 }
523 Err(err) => Err(err),
524 }
525 }
526
527 pub fn retrieve_cached_mutants(&self, hash: &str) -> Option<Vec<Mutant>> {
529 let cache_file = self.cache_file_path(hash, CacheKind::Mutants);
530 let data = std::fs::read_to_string(cache_file).ok()?;
531 serde_json::from_str(&data).ok()
532 }
533
534 pub fn retrieve_cached_mutant_results(
536 &self,
537 hash: &str,
538 execution_key: &str,
539 mutants: &[Mutant],
540 ) -> Option<Vec<(Mutant, MutationResult)>> {
541 let cache_file = self.cache_file_path(hash, CacheKind::Results { execution_key });
542 let data = std::fs::read_to_string(cache_file).ok()?;
543 let cached: CachedMutationResults = serde_json::from_str(&data).ok()?;
544 (cached.mutant_count == mutants.len() && cached.mutant_hash == mutant_set_hash(mutants))
545 .then_some(cached.results)
546 }
547
548 pub fn mark_span_survived(&mut self, span: Span) {
550 self.survived_spans.mark_survived(span);
551 }
552
553 pub fn should_skip_span(&self, span: Span) -> bool {
555 self.survived_spans.should_skip(span)
556 }
557
558 pub fn persist_survived_spans(&self, hash: &str, execution_key: &str) -> std::io::Result<()> {
560 let cache_file = self.cache_file_path(hash, CacheKind::Survived { execution_key });
561 if let Some(dir) = cache_file.parent() {
562 std::fs::create_dir_all(dir)?;
563 }
564 let spans = self.survived_spans.to_vec();
565 let json = serde_json::to_string_pretty(&spans).map_err(std::io::Error::other)?;
566 std::fs::write(cache_file, json)
567 }
568
569 pub fn retrieve_survived_spans(&mut self, hash: &str, execution_key: &str) -> bool {
571 let cache_file = self.cache_file_path(hash, CacheKind::Survived { execution_key });
572
573 if let Ok(data) = std::fs::read_to_string(cache_file)
574 && let Ok(pairs) = serde_json::from_str::<Vec<(u32, u32)>>(&data)
575 {
576 self.survived_spans = SurvivedSpans::from_vec(pairs);
577 return true;
578 }
579
580 false
581 }
582}
583
584#[cfg(test)]
585mod tests {
586 use super::*;
587 use foundry_config::Config;
588 use solar::ast::interface::BytePos;
589 use tempfile::TempDir;
590
591 fn test_handler(config: Config) -> MutationHandler {
592 let source = config.root.join("src").join("Counter.sol");
593 MutationHandler::new(source, Arc::new(config))
594 }
595
596 fn test_config() -> (TempDir, Config) {
597 let temp = TempDir::new().unwrap();
598 let config = Config {
599 root: temp.path().to_path_buf(),
600 mutation_dir: "cache/mutation".into(),
601 ..Default::default()
602 };
603 (temp, config)
604 }
605
606 fn mutant(lo: u32, hi: u32, original: &str) -> Mutant {
607 Mutant {
608 path: PathBuf::from("src/Counter.sol"),
609 span: Span::new(BytePos(lo), BytePos(hi)),
610 mutation: mutant::MutationType::DeleteExpression,
611 original: original.to_string(),
612 source_line: "number++;".to_string(),
613 line_number: 1,
614 column_number: 1,
615 }
616 }
617
618 #[test]
619 fn result_cache_path_includes_execution_key() {
620 let (_temp, config) = test_config();
621 let handler = test_handler(config);
622
623 let first =
624 handler.cache_file_path("build", CacheKind::Results { execution_key: "exec-a" });
625 let second =
626 handler.cache_file_path("build", CacheKind::Results { execution_key: "exec-b" });
627 let mutants = handler.cache_file_path("build", CacheKind::Mutants);
628
629 assert_ne!(first, second);
630 assert_ne!(first, mutants);
631 assert_ne!(second, mutants);
632 }
633
634 #[test]
635 fn survived_span_cache_path_includes_execution_key() {
636 let (_temp, config) = test_config();
637 let handler = test_handler(config);
638
639 let first =
640 handler.cache_file_path("build", CacheKind::Survived { execution_key: "exec-a" });
641 let second =
642 handler.cache_file_path("build", CacheKind::Survived { execution_key: "exec-b" });
643
644 assert_ne!(first, second);
645 }
646
647 #[test]
648 fn mutant_cache_path_ignores_execution_only_timeout() {
649 let (_temp, mut first_config) = test_config();
650 let mut second_config = first_config.clone();
651
652 first_config.mutation.timeout = Some(1);
653 second_config.mutation.timeout = Some(99);
654
655 let first = test_handler(first_config).cache_file_path("build", CacheKind::Mutants);
656 let second = test_handler(second_config).cache_file_path("build", CacheKind::Mutants);
657
658 assert_eq!(first, second);
659 }
660
661 #[test]
662 fn result_cache_validates_current_mutant_set() {
663 let (_temp, config) = test_config();
664 let handler = test_handler(config);
665 let mutants = vec![mutant(10, 20, "number++")];
666 let results = vec![(mutants[0].clone(), MutationResult::Dead)];
667
668 handler.persist_cached_results("build", "exec", &mutants, &results).unwrap();
669
670 assert!(handler.retrieve_cached_mutant_results("build", "exec", &mutants).is_some());
671
672 let changed_mutants = vec![mutant(10, 20, "number--")];
673 assert!(
674 handler.retrieve_cached_mutant_results("build", "exec", &changed_mutants).is_none()
675 );
676 }
677
678 #[test]
679 fn mutation_score_is_unreliable_when_evaluated_mutants_equal_timeouts() {
680 let mut summary = MutationsSummary::new();
681 summary.add_dead_mutant(mutant(10, 20, "number++"));
682 summary.add_timed_out_mutant(mutant(30, 40, "number--"));
683
684 assert_eq!(summary.total_evaluated(), 1);
685 assert!(!summary.has_reliable_score());
686 }
687
688 #[test]
689 fn mutation_score_is_unreliable_when_timeouts_dominate() {
690 let mut summary = MutationsSummary::new();
691 summary.add_dead_mutant(mutant(10, 20, "number++"));
692 summary.add_timed_out_mutant(mutant(30, 40, "number--"));
693 summary.add_timed_out_mutant(mutant(50, 60, "number += 1"));
694
695 assert_eq!(summary.total_evaluated(), 1);
696 assert!(!summary.has_reliable_score());
697 }
698
699 #[test]
700 fn mutation_score_is_unreliable_with_no_evaluated_mutants() {
701 let mut summary = MutationsSummary::new();
702 summary.add_timed_out_mutant(mutant(10, 20, "number++"));
703
704 assert_eq!(summary.total_evaluated(), 0);
705 assert!(!summary.has_reliable_score());
706 assert_eq!(summary.mutation_score(), 0.0);
707 }
708}