1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use crate::Result;
use crate::error::error;
use crate::model::Outcome;
/// A verdict that may be suppressed automatically.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Eligible {
/// The mutant exceeded its time budget.
///
/// Eligible by default, and still the second-best answer: a timeout that is cached keeps the
/// mutant in the score and costs nothing on re-runs, whereas suppressing it removes it from the
/// denominator. This is for sites that are permanently un-mutatable — a hand-written spin loop,
/// a driver poll, a reactor — where the team wants that recorded where the next reader sees it.
Timeout,
/// The mutant's test run passed its memory ceiling.
///
/// Eligible by default for the same reason as [`Self::Timeout`], and it is the same defect
/// wearing different clothes: a mutant that turns a bounded loop into an unbounded one is
/// stopped by whichever ceiling it reaches first, and which one that is depends on the machine
/// rather than on the code. Making one suppressible and not the other would mean a directive
/// that works on the maintainer's laptop and not in CI.
OutOfMemory,
/// The mutant did not compile.
Unviable,
}
impl Eligible {
/// Parses the `--eligible` list.
///
/// `missed` and `survived` are named explicitly so the refusal can explain itself. Falling
/// through to "unknown verdict" would read like a typo, and the person who typed it would try
/// harder rather than reconsidering.
pub fn parse(list: &str) -> Result<Vec<Self>> {
let mut out = Vec::new();
for entry in list.split(',').map(str::trim).filter(|entry| !entry.is_empty()) {
match entry {
"timeout" => out.push(Self::Timeout),
"outofmem" | "oom" | "out-of-memory" => out.push(Self::OutOfMemory),
"unviable" | "compile-error" => out.push(Self::Unviable),
"missed" | "survived" | "survivor" => {
return Err(error!(
"`{entry}` is not eligible for `suppress`, and cannot be made eligible: a surviving mutant is a gap in the test suite, and suppressing it would remove that gap from the score rather than from the code"
)
.usage());
}
other => {
return Err(error!("unknown verdict `{other}`; --eligible accepts `timeout`, `outofmem` and `unviable`").usage());
}
}
}
out.sort_unstable();
out.dedup();
Ok(out)
}
/// The tag written into generated directives.
#[must_use]
pub const fn tag(self) -> &'static str {
match self {
Self::Timeout => "timeout",
Self::OutOfMemory => "outofmem",
Self::Unviable => "unviable",
}
}
/// Returns the verdict this covers.
#[must_use]
pub const fn outcome(self) -> Outcome {
match self {
Self::Timeout => Outcome::Timeout,
Self::OutOfMemory => Outcome::OutOfMemory,
Self::Unviable => Outcome::CompileError,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_survivor_cannot_be_made_eligible() {
// The single most important test in the module. If this ever passes, every mutation score
// the tool reports becomes a number that can be improved by editing comments.
let cause = Eligible::parse("missed").expect_err("survivors must be refused");
assert!(cause.is_usage(), "{cause}");
assert!(cause.to_string().contains("gap in the test suite"), "{cause}");
}
#[test]
fn eligible_lists_are_trimmed_sorted_and_deduplicated() {
let parsed = Eligible::parse(" unviable, timeout, compile-error, timeout ").unwrap();
assert_eq!(parsed, vec![Eligible::Timeout, Eligible::Unviable]);
}
/// Both ceilings a runaway mutant can hit are suppressible, under every spelling.
///
/// `outofmem` is the canonical form because it is what [`Outcome::as_str`] prints and therefore
/// what a reader sees in the report they are acting on; `oom` and `out-of-memory` are what they
/// are liable to type instead.
#[test]
fn out_of_memory_is_eligible_under_each_of_its_spellings() {
for spelling in ["outofmem", "oom", "out-of-memory"] {
let parsed = Eligible::parse(spelling).unwrap_or_else(|_| panic!("`{spelling}` must parse"));
assert_eq!(parsed, vec![Eligible::OutOfMemory], "{spelling}");
assert_eq!(parsed[0].outcome(), Outcome::OutOfMemory, "{spelling}");
assert_eq!(parsed[0].tag(), "outofmem", "every spelling writes one tag");
}
}
/// Every eligible verdict maps to a distinct outcome and a distinct tag.
///
/// Two variants sharing an outcome would make `plan`'s first-match lookup pick arbitrarily, and
/// two sharing a tag would merge suppressions that were written for different reasons.
#[test]
fn each_eligible_verdict_has_its_own_outcome_and_tag() {
let all = [Eligible::Timeout, Eligible::OutOfMemory, Eligible::Unviable];
for (index, entry) in all.iter().enumerate() {
for other in &all[index + 1..] {
assert_ne!(entry.outcome(), other.outcome(), "{entry:?} and {other:?} share an outcome");
assert_ne!(entry.tag(), other.tag(), "{entry:?} and {other:?} share a tag");
}
}
}
#[test]
fn unknown_eligible_verdicts_are_errors() {
let cause = Eligible::parse("killed").expect_err("unknown verdict should be rejected");
assert!(cause.is_usage(), "{cause}");
assert!(cause.to_string().contains("timeout"), "{cause}");
assert!(
cause.to_string().contains("outofmem"),
"the message must name every accepted verdict: {cause}"
);
}
}