Skip to main content

forge/mutation/
mod.rs

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    /// Mutants whose compile-and-test work exceeded the configured timeout.
83    /// Tracked separately so they are not counted toward survived/killed.
84    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    /// Merge another MutationsSummary into this one
169    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    /// Calculate mutation score (percentage of dead mutants out of valid mutants)
178    /// Higher scores indicate better test coverage
179    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    /// Mutants that reached a test verdict and can contribute to the score.
185    pub const fn total_evaluated(&self) -> usize {
186        self.dead.len() + self.survived.len()
187    }
188
189    /// Whether the score is useful enough to present as a coverage signal.
190    pub const fn has_reliable_score(&self) -> bool {
191        self.total_evaluated() > 0 && self.timed_out.len() < self.total_evaluated()
192    }
193
194    /// Convert to JSON output format.
195    ///
196    /// Output is sorted deterministically: files in lexicographic order
197    /// (`BTreeMap` keys), and survived mutants within each file sorted by
198    /// `(line, column, original, mutant)`. Without this, parallel worker
199    /// completion order leaks into the JSON and breaks downstream diffing,
200    /// snapshot tests, and reproducibility.
201    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/// JSON output for mutation testing results.
238///
239/// Uses [`BTreeMap`] for `survived_mutants` so file ordering in the emitted
240/// JSON is deterministic.
241#[derive(Debug, Clone, Serialize)]
242pub struct MutationJsonOutput {
243    pub summary: MutationSummaryJson,
244    pub survived_mutants: BTreeMap<String, Vec<SurvivedMutantJson>>,
245}
246
247/// Summary section of JSON output
248#[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/// Individual survived mutant in JSON output
261#[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    /// Create from a Mutant, using the full original expression
271    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/// Tracks spans where mutations have survived (weren't killed by tests).
282/// Used for adaptive mutation testing to skip redundant mutations.
283#[derive(Debug, Clone, Default)]
284pub struct SurvivedSpans {
285    spans: HashSet<(u32, u32)>, // (lo, hi) byte positions
286}
287
288impl SurvivedSpans {
289    pub fn new() -> Self {
290        Self { spans: HashSet::new() }
291    }
292
293    /// Mark a span as having a surviving mutation
294    pub fn mark_survived(&mut self, span: Span) {
295        self.spans.insert((span.lo().0, span.hi().0));
296    }
297
298    /// Check if any survived parent span contains this span.
299    ///
300    /// Exact span matches are not skipped: a persisted survived-span cache only
301    /// records byte ranges, not which mutant at that range survived. Re-testing
302    /// exact spans after an interrupted run keeps known survivors from being
303    /// converted into `Skipped` results in the next complete cache.
304    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    /// Check if any survived span contains this span, including exact matches.
313    ///
314    /// Live workers know exact same-span mutants are siblings in the current
315    /// run, so once one survives the remaining siblings can be skipped.
316    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    /// Serialize to a list of (lo, hi) pairs for caching
323    fn to_vec(&self) -> Vec<(u32, u32)> {
324        self.spans.iter().copied().collect()
325    }
326
327    /// Deserialize from a list of (lo, hi) pairs
328    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    /// Optional regex used to restrict mutation to specific contracts within
341    /// the file (matches against contract name).
342    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    /// Restrict mutation to contracts whose name matches `filter`.
359    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    /// Add a dead mutant to the report
371    pub fn add_dead_mutant(&mut self, mutant: Mutant) {
372        self.report.add_dead_mutant(mutant);
373    }
374
375    /// Add a survived mutant to the report
376    pub fn add_survived_mutant(&mut self, mutant: Mutant) {
377        self.report.add_survived_mutant(mutant);
378    }
379
380    /// Add an invalid mutant to the report
381    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    /// Get a reference to the current report
394    pub const fn get_report(&self) -> &MutationsSummary {
395        &self.report
396    }
397
398    // Note: we now get the build hash directly from the recent compile output (see test flow)
399
400    /// Returns the cache file path for the given build hash and cache kind.
401    /// The filename encodes a hash of the full contract path to prevent collisions
402    /// between files with the same stem in different directories, and a hash of
403    /// the active mutation config so changes to enabled operators invalidate
404    /// previously cached mutants. Result-like caches also include an execution
405    /// key so stale outcomes are not reused after test/config/EVM changes.
406    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        // Hash the effective set of enabled mutation operators so mutant cache
412        // entries are invalidated when the user changes `include_operators` /
413        // `exclude_operators` in their config.
414        //
415        // Also fold in the active `--mutate-contract` regex pattern, because
416        // running with vs. without that filter produces a different mutant set
417        // for the same file.
418        let mut mutant_cfg_hasher = DefaultHasher::new();
419        // Version salt for this mutant-set cache schema. Bump this if the
420        // inputs that define generated mutants change.
421        "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    /// Persists cached mutants using build hash for cache invalidation.
448    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    /// Persists results for mutants using build hash for cache invalidation.
458    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    /// Read a source string, and for each contract found, gets its ast and visit it to list
479    /// all mutations to conduct.
480    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    /// Retrieves cached mutants using build hash.
528    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    /// Retrieves cached results using build hash.
535    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    /// Mark a span as having a surviving mutation
549    pub fn mark_span_survived(&mut self, span: Span) {
550        self.survived_spans.mark_survived(span);
551    }
552
553    /// Check if a span should be skipped (has survived mutation or is child of survived span)
554    pub fn should_skip_span(&self, span: Span) -> bool {
555        self.survived_spans.should_skip(span)
556    }
557
558    /// Persist survived spans to cache for adaptive mutation testing.
559    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    /// Retrieve survived spans from cache.
570    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}