use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use rustc_hash::{FxHashMap, FxHashSet};
use super::super::protocol::{RunResult, RunStatus, TestStatus};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct ControlFault {
pub(super) no_tests: bool,
pub(super) failed_tests: Vec<FailedTest>,
pub(super) detail: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct FailedTest {
pub(super) test_id: String,
pub(super) error: Option<String>,
}
impl FailedTest {
pub(super) fn message(&self, reason: &str) -> String {
let Some(error) = self
.error
.as_deref()
.map(str::trim)
.filter(|e| !e.is_empty())
else {
return reason.to_owned();
};
let kept = &error[..error.floor_char_boundary(MAX_TEST_ERROR_BYTES)];
let ellipsis = if kept.len() < error.len() { "..." } else { "" };
format!("{reason}: {kept}{ellipsis}")
}
}
impl ControlFault {
pub(super) fn untrusted(detail: String) -> Self {
Self {
no_tests: false,
failed_tests: Vec::new(),
detail,
}
}
}
pub(super) fn control_fault(tests: &[String], result: &RunResult) -> Option<ControlFault> {
let first_error = result
.unhandled_errors
.first()
.map(|error| format!("; first error: {}", first_line(&error.message)));
if result.tests_executed == 0 {
return Some(ControlFault {
no_tests: true,
failed_tests: Vec::new(),
detail: format!(
"0 of {} requested tests ran{}",
tests.len(),
first_error.unwrap_or_default()
),
});
}
if let Some(id) = &result.mutant_id {
return Some(ControlFault::untrusted(format!(
"the run result names mutant `{id}`, expected none"
)));
}
let requested: FxHashSet<&str> = tests.iter().map(String::as_str).collect();
let failed_tests: Vec<FailedTest> = result
.tests
.iter()
.filter(|test| test.status == TestStatus::Fail && requested.contains(test.test_id.as_str()))
.map(|test| FailedTest {
test_id: test.test_id.clone(),
error: test.message.clone(),
})
.collect();
if let Some(first) = failed_tests.first() {
return Some(ControlFault {
no_tests: false,
detail: format!("the test `{}` failed", first.test_id),
failed_tests,
});
}
if let Some(first_error) = first_error {
return Some(ControlFault::untrusted(format!(
"the run reported an error{first_error}"
)));
}
let passed: FxHashSet<&str> = result
.tests
.iter()
.filter(|test| test.status == TestStatus::Pass)
.map(|test| test.test_id.as_str())
.collect();
if result.status != RunStatus::Survived
|| result.tests_executed != tests.len()
|| tests.iter().any(|test| !passed.contains(test.as_str()))
{
return Some(ControlFault::untrusted(format!(
"{} of {} requested tests ran and passed",
passed.len(),
tests.len()
)));
}
None
}
pub(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";
pub(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";
const MAX_TEST_ERROR_BYTES: usize = 8 * 1024;
#[must_use]
pub fn split_control_run_failure(message: &str) -> Option<(&str, &str)> {
[CONTROL_RUN_FAILURE, CONFIRMATION_RUN_FAILURE]
.into_iter()
.find_map(|reason| {
let error = message.strip_prefix(reason)?.strip_prefix(": ")?;
Some((reason, error))
})
}
pub(super) fn first_line(message: &str) -> &str {
message.lines().next().unwrap_or_default()
}
pub(super) enum ControlOutcome {
Passed,
Failed(ControlFault),
Fault(String),
TimedOut(String),
}
pub(super) fn sorted_tests(tests: &[String]) -> Vec<&str> {
let mut sorted: Vec<&str> = tests.iter().map(String::as_str).collect();
sorted.sort_unstable();
sorted
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum ControlVerdict {
Passed,
Failed,
TimedOut(String),
}
pub(super) enum ControlCheck {
Passed,
Failed { detail: String, settled: bool },
NoTime,
}
pub(super) type ControlCell = Arc<Mutex<Option<ControlVerdict>>>;
#[derive(Default)]
pub(super) struct ControlMemo {
entries: Mutex<FxHashMap<Vec<String>, ControlCell>>,
pub(super) failed_tests: Mutex<BTreeMap<String, String>>,
}
impl ControlMemo {
pub(super) fn cell(&self, tests: &[String]) -> ControlCell {
let mut key = tests.to_vec();
key.sort_unstable();
let Ok(mut entries) = self.entries.lock() else {
return Arc::new(Mutex::new(None));
};
Arc::clone(entries.entry(key).or_default())
}
pub(super) fn store(&self, tests: &[String], verdict: ControlVerdict) {
if let Ok(mut cell) = self.cell(tests).lock() {
*cell = Some(verdict);
}
}
pub(super) fn mark_failed(&self, tests: &[FailedTest], reason: &str) {
if let Ok(mut failed) = self.failed_tests.lock() {
for test in tests {
failed
.entry(test.test_id.clone())
.or_insert_with(|| test.message(reason));
}
}
}
pub(super) fn is_failed(&self, test: &str) -> bool {
self.failed_tests
.lock()
.is_ok_and(|failed| failed.contains_key(test))
}
pub(super) fn failed(&self) -> BTreeMap<String, String> {
self.failed_tests
.lock()
.map(|failed| failed.clone())
.unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_control_run_failure_message_splits_into_its_reason_and_the_test_error() {
let error = "AssertionError: expected 1 to be 2\n at test/a.test.ts:3:5";
let test = FailedTest {
test_id: "test/a.test.ts::adds".to_owned(),
error: Some(error.to_owned()),
};
for reason in [CONTROL_RUN_FAILURE, CONFIRMATION_RUN_FAILURE] {
let message = test.message(reason);
assert_eq!(split_control_run_failure(&message), Some((reason, error)));
}
let no_error = FailedTest {
error: None,
..test
};
assert_eq!(
split_control_run_failure(&no_error.message(CONTROL_RUN_FAILURE)),
None
);
assert_eq!(
split_control_run_failure("Cannot find module './setup'"),
None
);
}
}