fallow_api/mutation/scheduler/
control.rs1use std::collections::BTreeMap;
6use std::sync::{Arc, Mutex};
7
8use rustc_hash::{FxHashMap, FxHashSet};
9
10use super::super::protocol::{RunResult, RunStatus, TestStatus};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub(super) struct ControlFault {
15 pub(super) no_tests: bool,
17 pub(super) failed_tests: Vec<FailedTest>,
20 pub(super) detail: String,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub(super) struct FailedTest {
26 pub(super) test_id: String,
27 pub(super) error: Option<String>,
30}
31
32impl FailedTest {
33 pub(super) fn message(&self, reason: &str) -> String {
38 let Some(error) = self
39 .error
40 .as_deref()
41 .map(str::trim)
42 .filter(|e| !e.is_empty())
43 else {
44 return reason.to_owned();
45 };
46 let kept = &error[..error.floor_char_boundary(MAX_TEST_ERROR_BYTES)];
47 let ellipsis = if kept.len() < error.len() { "..." } else { "" };
48 format!("{reason}: {kept}{ellipsis}")
49 }
50}
51
52impl ControlFault {
53 pub(super) fn untrusted(detail: String) -> Self {
54 Self {
55 no_tests: false,
56 failed_tests: Vec::new(),
57 detail,
58 }
59 }
60}
61
62pub(super) fn control_fault(tests: &[String], result: &RunResult) -> Option<ControlFault> {
65 let first_error = result
66 .unhandled_errors
67 .first()
68 .map(|error| format!("; first error: {}", first_line(&error.message)));
69 if result.tests_executed == 0 {
70 return Some(ControlFault {
71 no_tests: true,
72 failed_tests: Vec::new(),
73 detail: format!(
74 "0 of {} requested tests ran{}",
75 tests.len(),
76 first_error.unwrap_or_default()
77 ),
78 });
79 }
80 if let Some(id) = &result.mutant_id {
81 return Some(ControlFault::untrusted(format!(
82 "the run result names mutant `{id}`, expected none"
83 )));
84 }
85 let requested: FxHashSet<&str> = tests.iter().map(String::as_str).collect();
86 let failed_tests: Vec<FailedTest> = result
87 .tests
88 .iter()
89 .filter(|test| test.status == TestStatus::Fail && requested.contains(test.test_id.as_str()))
90 .map(|test| FailedTest {
91 test_id: test.test_id.clone(),
92 error: test.message.clone(),
93 })
94 .collect();
95 if let Some(first) = failed_tests.first() {
96 return Some(ControlFault {
97 no_tests: false,
98 detail: format!("the test `{}` failed", first.test_id),
99 failed_tests,
100 });
101 }
102 if let Some(first_error) = first_error {
103 return Some(ControlFault::untrusted(format!(
104 "the run reported an error{first_error}"
105 )));
106 }
107 let passed: FxHashSet<&str> = result
108 .tests
109 .iter()
110 .filter(|test| test.status == TestStatus::Pass)
111 .map(|test| test.test_id.as_str())
112 .collect();
113 if result.status != RunStatus::Survived
114 || result.tests_executed != tests.len()
115 || tests.iter().any(|test| !passed.contains(test.as_str()))
116 {
117 return Some(ControlFault::untrusted(format!(
118 "{} of {} requested tests ran and passed",
119 passed.len(),
120 tests.len()
121 )));
122 }
123 None
124}
125
126pub(super) const CONTROL_RUN_FAILURE: &str = "the test fails in a mutant run with no active mutant, but passed in the dry run of its whole file; it can depend on another test of its file";
129
130pub(super) const CONFIRMATION_RUN_FAILURE: &str = "the test fails with no active mutant in a run of each test of its file that passed in the dry run, but passed in the dry run of its whole file; it can depend on a test of its file that failed or was skipped in the dry run, or it can be flaky";
134
135const MAX_TEST_ERROR_BYTES: usize = 8 * 1024;
139
140#[must_use]
144pub fn split_control_run_failure(message: &str) -> Option<(&str, &str)> {
145 [CONTROL_RUN_FAILURE, CONFIRMATION_RUN_FAILURE]
146 .into_iter()
147 .find_map(|reason| {
148 let error = message.strip_prefix(reason)?.strip_prefix(": ")?;
149 Some((reason, error))
150 })
151}
152
153pub(super) fn first_line(message: &str) -> &str {
155 message.lines().next().unwrap_or_default()
156}
157
158pub(super) enum ControlOutcome {
160 Passed,
161 Failed(ControlFault),
162 Fault(String),
164 TimedOut(String),
166}
167
168pub(super) fn sorted_tests(tests: &[String]) -> Vec<&str> {
170 let mut sorted: Vec<&str> = tests.iter().map(String::as_str).collect();
171 sorted.sort_unstable();
172 sorted
173}
174
175#[derive(Debug, Clone, PartialEq, Eq)]
177pub(super) enum ControlVerdict {
178 Passed,
180 Failed,
182 TimedOut(String),
185}
186
187pub(super) enum ControlCheck {
190 Passed,
192 Failed { detail: String, settled: bool },
196 NoTime,
198}
199
200pub(super) type ControlCell = Arc<Mutex<Option<ControlVerdict>>>;
203
204#[derive(Default)]
212pub(super) struct ControlMemo {
213 entries: Mutex<FxHashMap<Vec<String>, ControlCell>>,
214 pub(super) failed_tests: Mutex<BTreeMap<String, String>>,
215}
216
217impl ControlMemo {
218 pub(super) fn cell(&self, tests: &[String]) -> ControlCell {
221 let mut key = tests.to_vec();
222 key.sort_unstable();
223 let Ok(mut entries) = self.entries.lock() else {
224 return Arc::new(Mutex::new(None));
225 };
226 Arc::clone(entries.entry(key).or_default())
227 }
228
229 pub(super) fn store(&self, tests: &[String], verdict: ControlVerdict) {
230 if let Ok(mut cell) = self.cell(tests).lock() {
231 *cell = Some(verdict);
232 }
233 }
234
235 pub(super) fn mark_failed(&self, tests: &[FailedTest], reason: &str) {
238 if let Ok(mut failed) = self.failed_tests.lock() {
239 for test in tests {
240 failed
241 .entry(test.test_id.clone())
242 .or_insert_with(|| test.message(reason));
243 }
244 }
245 }
246
247 pub(super) fn is_failed(&self, test: &str) -> bool {
248 self.failed_tests
249 .lock()
250 .is_ok_and(|failed| failed.contains_key(test))
251 }
252
253 pub(super) fn failed(&self) -> BTreeMap<String, String> {
255 self.failed_tests
256 .lock()
257 .map(|failed| failed.clone())
258 .unwrap_or_default()
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 #[test]
267 fn a_control_run_failure_message_splits_into_its_reason_and_the_test_error() {
268 let error = "AssertionError: expected 1 to be 2\n at test/a.test.ts:3:5";
269 let test = FailedTest {
270 test_id: "test/a.test.ts::adds".to_owned(),
271 error: Some(error.to_owned()),
272 };
273 for reason in [CONTROL_RUN_FAILURE, CONFIRMATION_RUN_FAILURE] {
274 let message = test.message(reason);
275 assert_eq!(split_control_run_failure(&message), Some((reason, error)));
276 }
277 let no_error = FailedTest {
278 error: None,
279 ..test
280 };
281 assert_eq!(
282 split_control_run_failure(&no_error.message(CONTROL_RUN_FAILURE)),
283 None
284 );
285 assert_eq!(
286 split_control_run_failure("Cannot find module './setup'"),
287 None
288 );
289 }
290}