use core::sync::atomic::{AtomicUsize, Ordering};
use core::time::Duration;
use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock, PoisonError, mpsc};
use std::thread;
use std::time::Instant;
use camino::Utf8Path;
use cargo_gamma_process::MemoryRequest;
use super::census::{Census, CensusSelection, CensusWork};
use super::events::Events;
use super::killers::Killers;
use super::stall::Stall;
use super::test_binary::{Reachability, TestBinary};
#[cfg(test)]
use super::test_binary::{TestScope, order_reachable, reaches};
use super::verdict::{Attempt, Only, Verdict, run_binary, tail};
use super::workspace::Workspace;
use crate::Result;
use crate::discover::{Killer, Plan};
use crate::error::error;
use crate::model::Outcome;
fn memory_note(binary: &Utf8Path, peak: Option<u64>, limit: u64) -> String {
let name = binary.file_name().unwrap_or(binary.as_str());
let Some(peak) = peak else {
return format!("`{name}` reached the {} this run allowed it", crate::report::bytes(limit));
};
let where_it_landed = match peak.cmp(&limit) {
core::cmp::Ordering::Greater => "past",
core::cmp::Ordering::Equal => "at",
core::cmp::Ordering::Less => "against",
};
let (reached, allowed) = {
let rounded = (crate::report::bytes(peak), crate::report::bytes(limit));
if rounded.0 == rounded.1 && peak != limit {
(format!("{peak} bytes"), format!("{limit} bytes"))
} else {
rounded
}
};
format!("`{name}` reached {reached}, {where_it_landed} the {allowed} this run allowed it")
}
fn stall_note(test: Option<&str>) -> String {
test.map_or_else(
|| "stalled before the harness named a test".to_owned(),
|name| format!("stalled, last test named was `{name}`"),
)
}
fn flaky_note(binary: &Utf8Path, test: Option<&str>) -> String {
let which = test.map_or_else(|| "a test".to_owned(), |name| format!("test `{name}`"));
format!("{which} in `{binary}` fails with no mutant active as well as with one, so this mutant was never judged")
}
fn enumeration_note(binary: &Utf8Path, output: &str) -> String {
if !output.trim().is_empty() {
let binary = crate::report::encode_controls(binary.as_str());
let tail = tail(output, 20);
let output = crate::report::encode_preserving_color(&tail);
crate::notes::note(format!(
"`cargo nextest` could not enumerate tests in `{binary}` with a mutant active; its \
output, kept out of every published report, was:\n{output}"
));
}
format!(
"`cargo nextest` could not enumerate tests in `{binary}` with this mutant active; the same selection succeeded with no mutant active"
)
}
type Completed = (usize, Outcome, u64, Option<Killer>, Option<String>);
fn mutant_cost(position: usize, plan: &Plan, reach: &Reachability<'_>, census: &Census, killers: &Killers) -> Duration {
let mutant = &plan.mutants[position];
if let Some(hint) = killers.hint(&mutant.id)
&& let Some(binaries) = reach.reachable(&mutant.package)
&& let Some(binary) = binaries.iter().find(|b| hint.names(&b.package, &b.target))
{
return binary.baseline;
}
let Some(binaries) = reach.reachable(&mutant.package) else {
return Duration::ZERO;
};
let mut total = Duration::ZERO;
let mut used_census = false;
for binary in binaries {
match census.work(binary, mutant.ordinal) {
CensusWork::Selected(duration) => {
total += duration;
used_census = true;
}
CensusWork::Whole => total += binary.baseline,
CensusWork::Uncovered => {}
CensusWork::Hinted(_duration) => total += binary.baseline,
}
}
let _ = used_census;
total
}
fn schedule(pending: &mut [usize], plan: &Plan, reach: &Reachability<'_>, census: &Census, killers: &Killers) {
pending.sort_by_cached_key(|position| (core::cmp::Reverse(mutant_cost(*position, plan, reach, census, killers)), *position));
let mut queues: Vec<Vec<usize>> = Vec::new();
let mut by_package: crate::HashMap<String, usize> = crate::HashMap::default();
for position in pending.iter().copied() {
let package = &*plan.mutants[position].package;
if let Some(index) = by_package.get(package).copied() {
queues[index].push(position);
} else {
let index = queues.len();
let _fresh = by_package.insert(package.to_owned(), index);
queues.push(vec![position]);
}
}
let mut next = vec![0_usize; queues.len()];
let mut out = 0_usize;
loop {
let mut moved = false;
for (queue, cursor) in queues.iter().zip(&mut next) {
if let Some(position) = queue.get(*cursor).copied() {
pending[out] = position;
out += 1;
*cursor += 1;
moved = true;
}
}
if !moved {
break;
}
}
}
#[derive(Debug, Default)]
struct Tally {
launches: AtomicUsize,
probes: AtomicUsize,
}
#[derive(Debug, Clone, Copy, Default)]
pub(super) struct Spent {
pub(super) launches: usize,
pub(super) probes: usize,
}
#[expect(clippy::too_many_lines, reason = "sweep setup and its scoped worker loop share borrowed state")]
pub(super) fn test_all(
work: &Workspace,
plan: &mut Plan,
reach: &Reachability<'_>,
sweep: Sweep<'_>,
killers: &mut Killers,
events: &mut impl Events,
) -> Result<Option<Spent>> {
let jobs = sweep.jobs;
let mut pending: Vec<usize> = plan
.mutants
.iter()
.enumerate()
.filter(|(_position, mutant)| mutant.ordinal > 0 && mutant.outcome == Outcome::Pending)
.map(|(position, _mutant)| position)
.collect();
if pending.is_empty() {
return Ok(None);
}
schedule(&mut pending, plan, reach, sweep.census, killers);
let next = AtomicUsize::new(0);
let tally = Tally::default();
let abandoned: OnceLock<String> = OnceLock::new();
let ordinals: Vec<(u32, Option<f64>)> = pending
.iter()
.map(|position| (plan.mutants[*position].ordinal, plan.mutants[*position].test_timeout_multiplier))
.collect();
let reachable: Vec<&[&TestBinary]> = pending
.iter()
.map(|position| {
reach
.reachable(&plan.mutants[*position].package)
.expect("the shared reachability index was built from these same pending mutants")
})
.collect();
let mut files: crate::HashMap<_, usize> = crate::HashMap::default();
let file_slots: Vec<usize> = pending
.iter()
.map(|position| {
let file = Arc::clone(&plan.mutants[*position].file);
let next = files.len();
*files.entry(file).or_insert(next)
})
.collect();
let file_killers: Vec<FileLearning> = (0..files.len()).map(|_file| FileLearning::new()).collect();
let (sender, receiver) = mpsc::channel::<Completed>();
let hints: Vec<Option<Killer>> = pending
.iter()
.map(|position| killers.hint(&plan.mutants[*position].id).cloned())
.collect();
let notes = crate::notes::current();
thread::scope(|scope| {
for _worker in 0..jobs.max(1) {
let sender = sender.clone();
let next = &next;
let ordinals = &ordinals;
let reachable = &reachable;
let hints = &hints;
let pending = &pending;
let file_slots = &file_slots;
let file_killers = &file_killers;
let abandoned = &abandoned;
let tally = &tally;
let notes = notes.clone();
let _handle = scope.spawn(move || {
let _notes = crate::notes::enter(notes.as_ref());
loop {
if abandoned.get().is_some() {
break;
}
let index = next.fetch_add(1, Ordering::Relaxed);
let Some(position) = pending.get(index).copied() else {
break;
};
let (ordinal, timeout_multiplier) = ordinals[index];
let started = Instant::now();
let reachable = &reachable[index];
let judged = judge_learning(
work,
ordinal,
reachable,
hints[index].as_ref(),
&file_killers[file_slots[index]],
timeout_multiplier,
sweep,
tally,
);
let (outcome, killer, note) = match judged {
Judgement::Reached(outcome, killer, note) => (outcome, killer, note),
Judgement::Abandoned(reason) => {
let _first = abandoned.set(reason);
break;
}
};
let elapsed = u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX);
let _sent = sender.send((position, outcome, elapsed, killer, note));
}
});
}
drop(sender);
for (position, outcome, elapsed, killer, note) in receiver {
if let Some(mutant) = plan.mutants.get_mut(position) {
mutant.outcome = outcome;
mutant.elapsed_ms = elapsed;
mutant.killed_by = killer.as_ref().map(|killer| killer.test.clone());
mutant.note = note;
match killer {
Some(killer) => killers.record(mutant.id.clone(), killer),
None => killers.forget(&mutant.id),
}
events.mutant(mutant);
}
}
});
abandoned.into_inner().map_or_else(
|| {
Ok(Some(Spent {
launches: tally.launches.into_inner(),
probes: tally.probes.into_inner(),
}))
},
|reason| {
Err(error!(
"the run cannot be trusted to judge anything further: {reason}.\n\
It stops here rather than reach verdicts that would each have to be taken on faith."
))
},
)
}
#[derive(Debug, Clone, Copy)]
pub(super) struct Sweep<'run> {
pub(super) timeout_floor: Duration,
pub(super) stall: Stall,
pub(super) jobs: usize,
pub(super) meter: bool,
pub(super) confirm: bool,
pub(super) census: &'run Census,
}
enum Judgement {
Reached(Outcome, Option<Killer>, Option<String>),
Abandoned(String),
}
fn probe(
work: &Workspace,
ordinal: u32,
binary: &TestBinary,
hint: &Killer,
timeout_multiplier: Option<f64>,
sweep: Sweep<'_>,
tally: &Tally,
) -> Option<Killer> {
let request = MemoryRequest {
meter: sweep.meter,
limit: binary.memory,
};
let attempt = Attempt {
active: Some(ordinal),
timeout: binary.budget_for(timeout_multiplier, sweep.timeout_floor),
stall: sweep.stall,
request,
only: Only::One(&hint.test),
census: None,
};
let _launches = tally.launches.fetch_add(1, Ordering::Relaxed);
let _probes = tally.probes.fetch_add(1, Ordering::Relaxed);
let verdict = run_binary(work, binary, attempt, sweep.confirm);
match verdict {
Verdict::Failed(named) => Some(Killer {
package: hint.package.clone(),
target: hint.target.clone(),
test: named.unwrap_or_else(|| hint.test.clone()),
}),
_inconclusive => None,
}
}
fn filtered_kill_is_final(binary: &TestBinary, timeout_multiplier: Option<f64>, sweep: Sweep<'_>) -> bool {
binary.budget_for(timeout_multiplier, sweep.timeout_floor).is_none() && sweep.stall.budget.is_none() && !sweep.meter && !sweep.confirm
}
fn probe_cases(
work: &Workspace,
ordinal: u32,
binary: &TestBinary,
names: &[&str],
timeout_multiplier: Option<f64>,
sweep: Sweep<'_>,
tally: &Tally,
) -> Option<Killer> {
let attempt = Attempt {
active: Some(ordinal),
timeout: binary.budget_for(timeout_multiplier, sweep.timeout_floor),
stall: sweep.stall,
request: MemoryRequest {
meter: sweep.meter,
limit: binary.memory,
},
only: Only::These(names),
census: None,
};
let _launches = tally.launches.fetch_add(1, Ordering::Relaxed);
let _probes = tally.probes.fetch_add(1, Ordering::Relaxed);
match run_binary(work, binary, attempt, sweep.confirm) {
Verdict::Failed(name) => Some(Killer {
package: binary.package.clone(),
target: binary.target.clone(),
test: name.unwrap_or_else(|| {
names
.first()
.copied()
.expect("an incomplete census hint always names at least one test")
.to_owned()
}),
}),
_inconclusive => None,
}
}
#[cfg(test)]
fn judge(
work: &Workspace,
ordinal: u32,
reachable: &[&TestBinary],
hint: Option<&Killer>,
timeout_multiplier: Option<f64>,
sweep: Sweep<'_>,
tally: &Tally,
) -> Judgement {
judge_ordered(work, ordinal, reachable, hint, None, timeout_multiplier, sweep, tally)
}
#[expect(clippy::too_many_arguments, reason = "the hot verdict path keeps execution state borrowed")]
fn judge_ordered(
work: &Workspace,
ordinal: u32,
reachable: &[&TestBinary],
hint: Option<&Killer>,
file_hint: Option<&Killer>,
timeout_multiplier: Option<f64>,
sweep: Sweep<'_>,
tally: &Tally,
) -> Judgement {
let mut ran = false;
for binary in reachable.iter().copied() {
let selection = sweep.census.selection(binary, ordinal);
let killer_hint = hint
.filter(|hint| hint.names(&binary.package, &binary.target))
.or_else(|| file_hint.filter(|hint| hint.names(&binary.package, &binary.target)));
if let Some(hint) = killer_hint
&& filtered_kill_is_final(binary, timeout_multiplier, sweep)
&& let Some(killer) = probe(work, ordinal, binary, hint, timeout_multiplier, sweep, tally)
{
return Judgement::Reached(Outcome::Killed, Some(killer), None);
}
if let CensusSelection::Hinted(names) = &selection
&& filtered_kill_is_final(binary, timeout_multiplier, sweep)
&& let Some(killer) = probe_cases(work, ordinal, binary, names, timeout_multiplier, sweep, tally)
{
return Judgement::Reached(Outcome::Killed, Some(killer), None);
}
let only = match &selection {
CensusSelection::Whole | CensusSelection::Hinted(_) => Only::All,
CensusSelection::Uncovered => continue,
CensusSelection::Selected(names) => Only::These(names),
};
ran = true;
let request = MemoryRequest {
meter: sweep.meter,
limit: binary.memory,
};
let attempt = Attempt {
active: Some(ordinal),
timeout: binary.budget_for(timeout_multiplier, sweep.timeout_floor),
stall: sweep.stall,
request,
only,
census: None,
};
let _launches = tally.launches.fetch_add(1, Ordering::Relaxed);
let mut verdict = run_binary(work, binary, attempt, sweep.confirm);
if matches!(selection, CensusSelection::Selected(_)) && !matches!(verdict, Verdict::Passed) {
let _launches = tally.launches.fetch_add(1, Ordering::Relaxed);
verdict = run_binary(
work,
binary,
Attempt {
only: Only::All,
..attempt
},
sweep.confirm,
);
}
match verdict {
Verdict::Passed => {}
Verdict::Failed(name) => {
let killer = name.map(|test| Killer {
package: binary.package.clone(),
target: binary.target.clone(),
test,
});
return Judgement::Reached(Outcome::Killed, killer, None);
}
Verdict::TestEnumerationFailed(output) => {
return Judgement::Reached(Outcome::Killed, None, Some(enumeration_note(&binary.path, &output)));
}
Verdict::TimedOut => return Judgement::Reached(Outcome::Timeout, None, None),
Verdict::Stalled(test) => {
return Judgement::Reached(Outcome::Timeout, None, Some(stall_note(test.as_deref())));
}
Verdict::MemoryLimit { peak, limit } => {
return Judgement::Reached(Outcome::OutOfMemory, None, Some(memory_note(&binary.path, peak, limit)));
}
Verdict::Flaky(test) => return Judgement::Reached(Outcome::Flaky, None, Some(flaky_note(&binary.path, test.as_deref()))),
Verdict::Unmetered(reason) => return Judgement::Abandoned(reason),
Verdict::Unjudged(reason) => return Judgement::Reached(Outcome::Pending, None, Some(reason)),
}
}
Judgement::Reached(if ran { Outcome::Survived } else { Outcome::NoCoverage }, None, None)
}
#[expect(clippy::too_many_arguments, reason = "adds one file-local state cell to the verdict path")]
fn judge_learning(
work: &Workspace,
ordinal: u32,
reachable: &[&TestBinary],
hint: Option<&Killer>,
observed: &FileLearning,
timeout_multiplier: Option<f64>,
sweep: Sweep<'_>,
tally: &Tally,
) -> Judgement {
if hint.is_some() {
let judged = judge_ordered(work, ordinal, reachable, hint, None, timeout_multiplier, sweep, tally);
observed.publish(&judged);
return judged;
}
let state = {
let mut learned = observed.locked();
match &*learned {
Learning::Learned(killer) => Some(Ok(killer.clone())),
Learning::Untried => {
*learned = Learning::InProgress;
None
}
Learning::InProgress => {
let (guard, _timed_out) = observed
.notify
.wait_timeout_while(learned, LEARNING_WAIT, |state| matches!(state, Learning::InProgress))
.unwrap_or_else(PoisonError::into_inner);
learned = guard;
match &*learned {
Learning::Learned(killer) => Some(Ok(killer.clone())),
_unlearned => Some(Err(())),
}
}
Learning::Exhausted => Some(Err(())),
}
};
if let Some(state) = state {
return match state {
Ok(killer) => judge_ordered(work, ordinal, reachable, None, Some(&killer), timeout_multiplier, sweep, tally),
Err(()) => judge_ordered(work, ordinal, reachable, None, None, timeout_multiplier, sweep, tally),
};
}
let judged = judge_ordered(work, ordinal, reachable, None, None, timeout_multiplier, sweep, tally);
observed.publish(&judged);
judged
}
const LEARNING_WAIT: Duration = Duration::from_millis(50);
struct FileLearning {
state: Mutex<Learning>,
notify: Condvar,
}
impl FileLearning {
const fn new() -> Self {
Self {
state: Mutex::new(Learning::Untried),
notify: Condvar::new(),
}
}
fn locked(&self) -> MutexGuard<'_, Learning> {
self.state.lock().unwrap_or_else(PoisonError::into_inner)
}
fn publish(&self, judged: &Judgement) {
let found = match judged {
Judgement::Reached(_outcome, Some(killer), _note) => Some(killer),
_nothing_learned => None,
};
{
let mut state = self.locked();
match (&*state, found) {
(Learning::Learned(_known), _any) => {}
(_unknown, Some(killer)) => *state = Learning::Learned(killer.clone()),
(Learning::Untried | Learning::InProgress, None) => *state = Learning::Exhausted,
(Learning::Exhausted, None) => {}
}
}
self.notify.notify_all();
}
}
#[derive(Debug)]
enum Learning {
Untried,
InProgress,
Learned(Killer),
Exhausted,
}
#[cfg(test)]
#[cfg(not(miri))]
mod tests {
use core::iter::once;
#[cfg(unix)]
use std::fs;
use camino::Utf8PathBuf;
use super::*;
#[cfg(unix)]
use crate::exec::faults::{self, Fault};
#[cfg(unix)]
use crate::exec::memory;
use crate::ops::collect::Shape;
fn blind() -> &'static Census {
static BLIND: OnceLock<Census> = OnceLock::new();
BLIND.get_or_init(Census::default)
}
fn sweep(stall: Stall) -> Sweep<'static> {
Sweep {
timeout_floor: Duration::ZERO,
stall,
jobs: 1,
meter: false,
confirm: true,
census: blind(),
}
}
#[test]
fn a_stall_does_not_claim_the_named_test_is_the_one_that_hung() {
let note = stall_note(Some("tests::round_trip"));
assert!(note.contains("last test named was `tests::round_trip`"), "{note}");
assert!(!note.contains("during"), "{note}");
assert!(!note.contains(" in `"), "{note}");
}
#[test]
fn a_stall_before_any_test_was_named_says_so() {
let note = stall_note(None);
assert_eq!(note, "stalled before the harness named a test");
}
#[cfg(unix)]
fn harness(body: &str, budget: Duration) -> (tempfile::TempDir, Workspace, Plan, Vec<TestBinary>) {
let (directory, work) = crate::testing::shell_workspace("test-all", body);
let plan = one_mutant_plan(work.root.clone());
let binaries = vec![TestBinary {
package: "subject".to_owned(),
baseline: Duration::from_millis(1),
budget: Some(budget),
..crate::testing::test_binary("/bin/sh")
}];
(directory, work, plan, binaries)
}
fn unreachable_harness(binaries: Vec<TestBinary>) -> (tempfile::TempDir, Workspace, Plan, Vec<TestBinary>) {
let directory = crate::testing::workdir("test-all-uncovered");
let root = Utf8PathBuf::from_path_buf(directory.path().to_path_buf()).expect("the scratch path is UTF-8");
let work = Workspace::adopt(root.clone(), root.join("target"));
let plan = one_mutant_plan(root);
(directory, work, plan, binaries)
}
fn one_mutant_plan(root: Utf8PathBuf) -> Plan {
let mutant = crate::model::Mutant {
id: "m1".to_owned().into(),
ordinal: 1,
file: (Utf8PathBuf::from("src/a.rs")).into(),
package: ("subject".to_owned()).into(),
span: 0..1,
line: 1,
end_line: 1,
column: 1,
mutator: ("relational.gt_to_ge".to_owned()).into(),
item_path: ("subject::f".to_owned()).into(),
trait_impl: None,
occurrence: 0,
replacement_index: 0,
original: "a > b".to_owned().into(),
replacement: "a >= b".to_owned().into(),
shape: Shape::Expr,
outcome: Outcome::Pending,
suppression: None,
expectation: None,
test_timeout_multiplier: None,
elapsed_ms: 0,
killed_by: None,
note: None,
};
Plan {
skipped: Vec::new(),
digests: crate::HashMap::default(),
root,
files: Vec::new(),
mutants: vec![mutant],
suppressed: 0,
idle: Vec::new(),
sharded_out: 0,
settled_out: 0,
reach: crate::HashMap::default(),
specs: crate::HashMap::default(),
}
}
#[cfg(unix)]
fn harness_n(body: &str, budget: Duration, count: usize) -> (tempfile::TempDir, Workspace, Plan, Vec<TestBinary>) {
let (directory, work, mut plan, binaries) = harness(body, budget);
let template = plan.mutants[0].clone();
plan.mutants = (0..count)
.map(|index| {
let mut mutant = template.clone();
mutant.id = format!("m{index}").into();
mutant.ordinal = u32::try_from(index + 1).expect("test counts stay well under u32::MAX");
mutant
})
.collect();
(directory, work, plan, binaries)
}
#[test]
#[cfg(unix)]
fn file_local_killer_is_probed_only_after_earlier_binaries() {
let (_directory, work, mut plan, mut binaries) =
harness_n("echo 'test tests::caught ... FAILED'; exit 1", Duration::from_secs(30), 2);
binaries[0].target = "killer".to_owned();
binaries[0].budget = None;
let mut passing = binaries[0].clone();
passing.path = "/bin/true".into();
passing.target = "passing".to_owned();
passing.baseline = Duration::ZERO;
binaries.insert(0, passing);
let scope = TestScope {
packages: &[],
package_local: false,
whole_workspace: true,
};
let reach = Reachability::build(&plan, &binaries, &scope);
let spent = test_all(
&work,
&mut plan,
&reach,
Sweep {
timeout_floor: Duration::ZERO,
stall: Stall::NONE,
jobs: 1,
meter: false,
confirm: false,
census: blind(),
},
&mut Killers::default(),
&mut crate::testing::Recorder::default(),
)
.expect("the sweep should finish")
.expect("two mutants should be swept");
assert!(plan.mutants.iter().all(|mutant| mutant.outcome == Outcome::Killed));
assert_eq!(
spent.launches, 5,
"both mutants must run the earlier binary before the learned hint is checked"
);
assert_eq!(spent.probes, 1, "the learned killer is checked only after the earlier binary");
}
#[test]
#[cfg(unix)]
fn same_file_survivors_run_in_parallel_after_one_learning_attempt_starts() {
let (_directory, work, mut plan, binaries) = harness_n("sleep 0.20; exit 0", Duration::from_secs(30), 2);
let started = Instant::now();
let scope = TestScope {
packages: &[],
package_local: false,
whole_workspace: true,
};
let reach = Reachability::build(&plan, &binaries, &scope);
let _spent = test_all(
&work,
&mut plan,
&reach,
Sweep {
timeout_floor: Duration::ZERO,
stall: Stall::NONE,
jobs: 2,
meter: false,
confirm: false,
census: blind(),
},
&mut Killers::default(),
&mut crate::testing::Recorder::default(),
)
.expect("the sweep should finish");
assert!(plan.mutants.iter().all(|mutant| mutant.outcome == Outcome::Survived));
assert!(started.elapsed() < Duration::from_millis(350), "{:?}", started.elapsed());
}
fn plan_over(packages: &[&str]) -> Plan {
let mut plan = one_mutant_plan(Utf8PathBuf::from("/nowhere"));
let template = plan.mutants[0].clone();
plan.mutants = packages
.iter()
.enumerate()
.map(|(index, package)| crate::model::Mutant {
id: format!("m{index}").into(),
ordinal: u32::try_from(index + 1).expect("test counts stay well under u32::MAX"),
package: ((*package).to_owned()).into(),
..template.clone()
})
.collect();
plan.reach = packages
.iter()
.map(|package| ((*package).to_owned(), once((*package).to_owned()).collect()))
.collect();
plan
}
const NARROW: TestScope<'static> = TestScope {
packages: &[],
package_local: false,
whole_workspace: false,
};
fn binary_of(package: &str, baseline: Duration) -> TestBinary {
TestBinary {
package: package.to_owned(),
baseline,
budget: Some(Duration::from_mins(1)),
..crate::testing::test_binary("/bin/sh")
}
}
#[test]
fn cold_runs_put_own_package_binaries_before_cheaper_dependents() {
let mut plan = plan_over(&["subject", "facade"]);
let inserted = plan
.reach
.get_mut("facade")
.expect("the fixture records every package")
.insert("subject".to_owned());
assert!(inserted);
let mut own_slow = binary_of("subject", Duration::from_secs(20));
own_slow.target = "subject-tests".to_owned();
let mut dependent_fast = binary_of("facade", Duration::from_millis(1));
dependent_fast.target = "facade-smoke".to_owned();
let binaries = vec![dependent_fast, own_slow];
let sets = Reachability::build(&plan, &binaries, &NARROW);
let ordered = sets.reachable("subject").expect("subject holds the plan's one pending mutant");
assert_eq!(
ordered.iter().map(|binary| binary.package.as_str()).collect::<Vec<_>>(),
["subject", "facade"]
);
}
#[test]
fn cold_run_tiers_use_baseline_then_stable_identity() {
let mut dependent_z = binary_of("z-helper", Duration::from_secs(2));
dependent_z.target = "z".to_owned();
dependent_z.path = "/tests/z".into();
let mut own_slow = binary_of("subject", Duration::from_secs(3));
own_slow.target = "slow".to_owned();
own_slow.path = "/tests/own-slow".into();
let mut dependent_a = binary_of("a-helper", Duration::from_secs(2));
dependent_a.target = "a".to_owned();
dependent_a.path = "/tests/a".into();
let mut own_fast = binary_of("subject", Duration::from_secs(1));
own_fast.target = "fast".to_owned();
own_fast.path = "/tests/own-fast".into();
let mut ordered = vec![&dependent_z, &own_slow, &dependent_a, &own_fast];
order_reachable(&mut ordered, "subject");
assert_eq!(
ordered.iter().map(|binary| binary.path.as_str()).collect::<Vec<_>>(),
["/tests/own-fast", "/tests/own-slow", "/tests/a", "/tests/z"]
);
}
#[test]
fn cold_run_ordering_never_drops_a_reachable_binary() {
let own = binary_of("subject", Duration::from_secs(3));
let helper = binary_of("helper", Duration::from_secs(2));
let facade = binary_of("facade", Duration::from_secs(1));
let mut ordered = vec![&helper, &own, &facade];
order_reachable(&mut ordered, "subject");
assert_eq!(
ordered.iter().map(|binary| binary.package.as_str()).collect::<Vec<_>>(),
["subject", "facade", "helper"]
);
}
#[test]
fn the_queue_puts_the_most_expensive_mutants_first() {
let plan = plan_over(&["cheap", "dear", "middling"]);
let binaries = vec![
binary_of("cheap", Duration::from_millis(1)),
binary_of("dear", Duration::from_secs(30)),
binary_of("middling", Duration::from_secs(2)),
];
let mut pending: Vec<usize> = (0..plan.mutants.len()).collect();
let sets = Reachability::build(&plan, &binaries, &NARROW);
schedule(&mut pending, &plan, &sets, blind(), &Killers::default());
let order: Vec<&str> = pending.iter().map(|position| &*plan.mutants[*position].package).collect();
assert_eq!(order, vec!["dear", "middling", "cheap"]);
}
#[test]
fn mutants_that_cost_the_same_keep_the_order_the_plan_gave_them() {
let plan = plan_over(&["a", "b", "c", "d"]);
let binaries: Vec<TestBinary> = ["a", "b", "c", "d"]
.iter()
.map(|package| binary_of(package, Duration::from_millis(7)))
.collect();
let mut pending: Vec<usize> = (0..plan.mutants.len()).collect();
let sets = Reachability::build(&plan, &binaries, &NARROW);
schedule(&mut pending, &plan, &sets, blind(), &Killers::default());
assert_eq!(pending, vec![0, 1, 2, 3]);
}
#[test]
fn package_queues_are_interleaved_instead_of_forming_worker_convoys() {
let plan = plan_over(&["dear", "dear", "cheap", "cheap", "middle", "middle"]);
let binaries = vec![
binary_of("dear", Duration::from_secs(30)),
binary_of("cheap", Duration::from_millis(1)),
binary_of("middle", Duration::from_secs(2)),
];
let mut pending: Vec<usize> = (0..plan.mutants.len()).collect();
let sets = Reachability::build(&plan, &binaries, &NARROW);
schedule(&mut pending, &plan, &sets, blind(), &Killers::default());
let order: Vec<&str> = pending.iter().map(|position| &*plan.mutants[*position].package).collect();
assert_eq!(order, vec!["dear", "middle", "cheap", "dear", "middle", "cheap"]);
}
#[test]
fn every_mutant_of_a_package_gets_one_reachability_answer() {
let plan = plan_over(&["left", "right", "left", "right", "left"]);
let binaries = vec![
binary_of("left", Duration::from_millis(1)),
binary_of("right", Duration::from_millis(1)),
];
let pending: Vec<usize> = (0..plan.mutants.len()).collect();
let sets = Reachability::build(&plan, &binaries, &NARROW);
assert_eq!(sets.len(), 2, "one entry per distinct package, not one per mutant");
for position in &pending {
let package = &*plan.mutants[*position].package;
let found: Vec<&TestBinary> = binaries.iter().filter(|binary| reaches(binary, package, &plan, &NARROW)).collect();
assert_eq!(
sets.reachable(package).expect("every package in `pending` holds a pending mutant"),
found.as_slice(),
"the memoized set must equal the one a per-mutant filter would give"
);
}
}
#[test]
#[cfg(unix)]
fn a_mutant_that_exhausts_its_budget_is_recorded_as_a_timeout() {
let (_directory, work, mut plan, binaries) = harness("sleep 30", Duration::from_millis(50));
let scope = TestScope {
packages: &[],
package_local: false,
whole_workspace: true,
};
let reach = Reachability::build(&plan, &binaries, &scope);
let _spent = test_all(
&work,
&mut plan,
&reach,
sweep(Stall::NONE),
&mut Killers::default(),
&mut crate::testing::Recorder::default(),
)
.expect("the sweep completes");
assert_eq!(plan.mutants[0].outcome, Outcome::Timeout);
assert_eq!(plan.mutants[0].note, None);
}
#[test]
#[cfg(unix)]
fn a_mutant_that_stalls_is_recorded_as_a_timeout_naming_the_test() {
let (_directory, work, mut plan, binaries) = harness("echo 'test slow::case ... '\nsleep 30", Duration::from_mins(1));
let scope = TestScope {
packages: &[],
package_local: false,
whole_workspace: true,
};
let reach = Reachability::build(&plan, &binaries, &scope);
let stall = Stall {
budget: Some(Duration::from_millis(50)),
};
let _spent = test_all(
&work,
&mut plan,
&reach,
sweep(stall),
&mut Killers::default(),
&mut crate::testing::Recorder::default(),
)
.expect("the sweep completes");
assert_eq!(plan.mutants[0].outcome, Outcome::Timeout);
assert!(plan.mutants[0].note.is_some(), "{:?}", plan.mutants[0].note);
}
#[test]
fn a_mutant_no_binary_reaches_is_uncovered() {
let (_directory, work, mut plan, binaries) = unreachable_harness(vec![TestBinary {
package: "subject".to_owned(),
budget: Some(Duration::from_secs(30)),
..crate::testing::test_binary("does-not-exist")
}]);
let scope = TestScope {
packages: &["other".to_owned()],
package_local: false,
whole_workspace: false,
};
let reach = Reachability::build(&plan, &binaries, &scope);
let _spent = test_all(
&work,
&mut plan,
&reach,
sweep(Stall::NONE),
&mut Killers::default(),
&mut crate::testing::Recorder::default(),
)
.expect("the sweep completes");
assert_eq!(plan.mutants[0].outcome, Outcome::NoCoverage);
}
#[test]
fn a_mutant_whose_only_binary_announced_no_tests_is_uncovered() {
let (_directory, work, mut plan, binaries) = unreachable_harness(vec![TestBinary {
package: "subject".to_owned(),
budget: Some(Duration::from_secs(30)),
tests: Some(0),
..crate::testing::test_binary("does-not-exist")
}]);
let scope = TestScope {
packages: &[],
package_local: false,
whole_workspace: true,
};
let reach = Reachability::build(&plan, &binaries, &scope);
let _spent = test_all(
&work,
&mut plan,
&reach,
sweep(Stall::NONE),
&mut Killers::default(),
&mut crate::testing::Recorder::default(),
)
.expect("the sweep completes");
assert_eq!(plan.mutants[0].outcome, Outcome::NoCoverage);
}
#[test]
#[cfg(unix)]
fn a_site_most_of_the_suite_reaches_is_run_whole_rather_than_called_uncovered() {
let (_directory, work, mut plan, binaries) = harness("exit 0", Duration::from_mins(1));
let scope = TestScope {
packages: &[],
package_local: false,
whole_workspace: true,
};
let reach = Reachability::build(&plan, &binaries, &scope);
let census = Census::examined(&binaries[0].path, plan.mutants[0].ordinal, 5, 9);
let sweep = Sweep {
timeout_floor: Duration::ZERO,
stall: Stall::NONE,
jobs: 1,
meter: false,
confirm: true,
census: &census,
};
let spent = test_all(
&work,
&mut plan,
&reach,
sweep,
&mut Killers::default(),
&mut crate::testing::Recorder::default(),
)
.expect("the sweep completes")
.expect("a pending mutant means the sweep ran");
assert_eq!(plan.mutants[0].outcome, Outcome::Survived);
assert_eq!(spent.launches, 1, "the whole binary was run exactly once, not skipped as uncovered");
assert_eq!(spent.probes, 0, "no hint was given, so no probe was launched");
}
#[test]
fn a_plan_with_no_pending_mutants_sweeps_nothing_and_returns_none() {
let (_directory, work, mut plan, binaries) = unreachable_harness(Vec::new());
plan.mutants.clear();
let reach = Reachability::build(&plan, &binaries, &NARROW);
let swept = test_all(
&work,
&mut plan,
&reach,
sweep(Stall::NONE),
&mut Killers::default(),
&mut crate::testing::Recorder::default(),
)
.expect("an empty sweep is not a failure");
assert!(swept.is_none(), "nothing pending means there is no sweep phase, not a free one");
}
#[test]
fn memory_notes_say_how_far_past_the_ceiling_the_run_went() {
let note = memory_note(
Utf8Path::new("/workspace/target/debug/deps/unit-abc"),
Some(300 * 1024 * 1024),
256 * 1024 * 1024,
);
assert!(note.contains("unit-abc"), "{note}");
assert!(note.contains("300.0 MB"), "{note}");
assert!(note.contains("256.0 MB"), "{note}");
assert!(note.contains("past the"), "{note}");
}
#[test]
fn enumeration_notes_never_carry_the_raw_output_that_produced_them() {
crate::notes::alone(|| {
const SENTINEL: &str = "super-secret-token-3f9a1c";
let output = format!("error: could not list tests\n{SENTINEL}\n");
let note = enumeration_note(Utf8Path::new("/workspace/target/debug/deps/unit-abc"), &output);
assert!(!note.contains(SENTINEL), "the sentinel leaked into the durable note: {note}");
assert!(note.contains("unit-abc"), "{note}");
let raised = crate::notes::drain();
assert!(
raised.iter().any(|line| line.contains(SENTINEL)),
"the raw output was dropped rather than raised as a local diagnostic: {raised:?}"
);
});
}
#[test]
fn enumeration_diagnostics_encode_controls_but_preserve_color() {
crate::notes::alone(|| {
let _note = enumeration_note(Utf8Path::new("/workspace/\u{1b}[2Junit"), "\u{1b}[31mred\u{1b}[0m\u{1b}[2J\rforged");
let raised = crate::notes::drain().join("\n");
assert!(raised.contains("\u{1b}[31mred\u{1b}[0m"), "{raised:?}");
assert!(!raised.contains("\u{1b}[2J"), "{raised:?}");
assert!(!raised.contains('\r'), "{raised:?}");
assert!(raised.contains("/workspace/\\e[2Junit"), "{raised:?}");
});
}
#[test]
fn an_empty_enumeration_output_raises_no_diagnostic() {
crate::notes::alone(|| {
let note = enumeration_note(Utf8Path::new("/workspace/target/debug/deps/unit-abc"), "");
assert!(note.contains("unit-abc"), "{note}");
assert!(
crate::notes::drain().is_empty(),
"an empty output should not have raised a diagnostic"
);
});
}
#[test]
fn a_run_that_only_reached_its_ceiling_is_not_described_as_having_passed_it() {
let note = memory_note(
Utf8Path::new("/workspace/target/debug/deps/unit-abc"),
Some(256 * 1024 * 1024),
256 * 1024 * 1024,
);
assert!(note.contains("at the"), "{note}");
assert!(!note.contains("past"), "{note}");
assert!(note.contains("256.0 MB"), "{note}");
assert!(
!note.contains("bytes"),
"an exact count is disambiguation nobody needs here, {note}"
);
}
#[test]
fn a_reported_peak_below_the_ceiling_is_described_as_measured_against_it() {
let note = memory_note(
Utf8Path::new("/workspace/target/debug/deps/unit-abc"),
Some(100 * 1024 * 1024),
256 * 1024 * 1024,
);
assert!(note.contains("against the"), "{note}");
assert!(!note.contains("past"), "{note}");
}
#[test]
fn a_note_whose_figures_would_round_together_prints_the_exact_bytes() {
let limit = 512 * 1024 * 1024;
let note = memory_note(Utf8Path::new("/workspace/target/debug/deps/unit-abc"), Some(limit + 4096), limit);
assert!(note.contains(&format!("{} bytes", limit + 4096)), "{note}");
assert!(note.contains(&format!("{limit} bytes")), "{note}");
assert!(note.contains("past the"), "{note}");
}
#[test]
fn memory_notes_with_no_measured_peak_still_name_the_ceiling() {
let note = memory_note(Utf8Path::new("/workspace/target/debug/deps/unit-abc"), None, 256 * 1024 * 1024);
assert!(note.contains("unit-abc"), "{note}");
assert!(note.contains("256.0 MB"), "{note}");
assert!(
!note.contains("past"),
"a peak nobody measured must not be described as one, {note}"
);
}
#[test]
fn a_flaky_note_names_the_test_and_says_nothing_was_judged() {
let note = flaky_note(Utf8Path::new("target/debug/deps/unit-abc"), Some("a::b"));
assert!(note.contains("test `a::b`"), "{note}");
assert!(note.contains("unit-abc"), "{note}");
assert!(note.contains("no mutant active"), "{note}");
assert!(note.contains("never judged"), "{note}");
}
#[test]
fn a_flaky_note_without_a_test_name_still_reads() {
let note = flaky_note(Utf8Path::new("target/debug/deps/unit-abc"), None);
assert!(note.starts_with("a test in"), "{note}");
assert!(note.contains("never judged"), "{note}");
}
#[test]
#[cfg(unix)]
fn a_mutant_the_machine_would_not_run_is_recorded_rather_than_abandoning_the_sweep() {
let (_directory, work, _plan, binaries) = harness("exit 0", Duration::from_secs(30));
let reachable: Vec<&TestBinary> = binaries.iter().collect();
let _refusals: Vec<_> = (0..8).map(|_round| faults::arm(Fault::Spawn)).collect();
let judgement = judge(
&work,
1,
&reachable,
None,
None,
Sweep {
timeout_floor: Duration::ZERO,
stall: Stall::NONE,
jobs: 1,
meter: false,
confirm: true,
census: blind(),
},
&Tally::default(),
);
match judgement {
Judgement::Reached(outcome, _killer, note) => {
assert_eq!(outcome, Outcome::Pending, "an unjudgeable mutant is not a verdict about the mutant");
assert!(!outcome.is_valid(), "and must not be scored");
assert!(
note.is_some_and(|reason| reason.contains("could not be started")),
"the refusal has to travel"
);
}
Judgement::Abandoned(reason) => panic!("one refused spawn must not take the run with it: {reason}"),
}
}
#[test]
#[cfg(unix)]
fn a_binary_that_cannot_be_metered_abandons_the_mutant_it_was_judging() {
if memory::support().is_ok() {
return;
}
let (_directory, work, _plan, mut binaries) = harness("exit 0", Duration::from_secs(30));
binaries[0].memory = None;
let reachable: Vec<&TestBinary> = binaries.iter().collect();
let judgement = judge(
&work,
1,
&reachable,
None,
None,
Sweep {
timeout_floor: Duration::ZERO,
stall: Stall::NONE,
jobs: 1,
meter: true,
confirm: true,
census: blind(),
},
&Tally::default(),
);
match judgement {
Judgement::Abandoned(reason) => assert!(!reason.is_empty(), "a refusal has to say why"),
Judgement::Reached(..) => panic!("expected the mutant to be abandoned rather than judged unprotected"),
}
}
#[test]
#[cfg(unix)]
fn a_sweep_that_loses_its_memory_accounting_stops_rather_than_continue_unprotected() {
if memory::support().is_ok() {
return;
}
let (_directory, work, mut plan, binaries) = harness_n("exit 0", Duration::from_secs(30), 8);
let scope = TestScope {
packages: &[],
package_local: false,
whole_workspace: true,
};
let reach = Reachability::build(&plan, &binaries, &scope);
let sweep = Sweep {
timeout_floor: Duration::ZERO,
stall: Stall::NONE,
jobs: 4,
meter: true,
confirm: true,
census: blind(),
};
let failure = test_all(
&work,
&mut plan,
&reach,
sweep,
&mut Killers::default(),
&mut crate::testing::Recorder::default(),
)
.expect_err("a run that cannot be metered as asked must stop rather than continue");
assert!(failure.to_string().contains("judge anything further"), "{failure}");
assert!(failure.to_string().contains("cgroup"), "{failure}");
assert!(
plan.mutants.iter().any(|mutant| mutant.outcome == Outcome::Pending),
"{:?}",
plan.mutants.iter().map(|mutant| mutant.outcome).collect::<Vec<_>>()
);
}
#[test]
#[cfg(unix)]
fn a_binary_that_outgrows_its_ceiling_convicts_the_mutant_of_using_too_much_memory() {
if crate::testing::without_memory_support("a sweep asserting a ceiling is enforced") {
return;
}
let fill = format!("/dev/shm/gamma-judge.{}", std::process::id());
let (_directory, work, _plan, mut binaries) = harness(
&format!("dd if=/dev/zero of={fill} bs=1M count=512 2>/dev/null"),
Duration::from_mins(1),
);
binaries[0].memory = Some(32 * 1024 * 1024);
let reachable: Vec<&TestBinary> = binaries.iter().collect();
let judgement = judge(
&work,
1,
&reachable,
None,
None,
Sweep {
timeout_floor: Duration::ZERO,
stall: Stall::NONE,
jobs: 1,
meter: true,
confirm: true,
census: blind(),
},
&Tally::default(),
);
let _removed = fs::remove_file(&fill);
match judgement {
Judgement::Reached(Outcome::OutOfMemory, None, Some(note)) => {
assert!(note.contains("32.0 MB"), "{note}");
}
_ => panic!("expected the mutant to be convicted of using too much memory"),
}
}
const ONLY_WHEN_FILTERED: &[&str] = &[
"when-arg:tests::killer|print:test tests::killer ... FAILED",
"when-arg:tests::killer|exit:1",
"exit:0",
];
fn helper_harness(script: &[&str]) -> (tempfile::TempDir, Workspace, Plan, Vec<TestBinary>) {
let (directory, work) = crate::testing::helper_workspace("test-all-helper", script);
let plan = one_mutant_plan(work.root.clone());
let binaries = vec![TestBinary {
package: "subject".to_owned(),
baseline: Duration::from_millis(1),
budget: None,
..crate::testing::helper()
}];
(directory, work, plan, binaries)
}
#[test]
#[cfg(unix)]
fn the_test_that_caught_a_mutant_last_time_is_tried_first() {
let (_directory, work, _plan, binaries) = helper_harness(ONLY_WHEN_FILTERED);
let reachable: Vec<&TestBinary> = binaries.iter().collect();
let hint = Killer {
package: "subject".to_owned(),
target: String::new(),
test: "tests::killer".to_owned(),
};
let judgement = judge(
&work,
1,
&reachable,
Some(&hint),
None,
Sweep {
timeout_floor: Duration::ZERO,
stall: Stall::NONE,
jobs: 1,
meter: false,
confirm: false,
census: blind(),
},
&Tally::default(),
);
match judgement {
Judgement::Reached(Outcome::Killed, Some(killer), None) => {
assert_eq!(killer.test, "tests::killer");
assert_eq!(killer.package, "subject");
}
_ => panic!("expected the recorded test to convict the mutant on its own"),
}
}
#[test]
fn a_cached_killer_cannot_bypass_the_canonical_timeout() {
const SCRIPT: &[&str] = &[
"when-arg:tests::killer|print:test tests::killer ... FAILED",
"when-arg:tests::killer|exit:1",
"sleep:200",
"exit:0",
];
let (_directory, work, _plan, mut binaries) = helper_harness(SCRIPT);
binaries[0].budget = Some(Duration::from_millis(20));
let reachable: Vec<&TestBinary> = binaries.iter().collect();
let hint = Killer {
package: "subject".to_owned(),
target: String::new(),
test: "tests::killer".to_owned(),
};
let tally = Tally::default();
let judgement = judge(
&work,
1,
&reachable,
Some(&hint),
None,
Sweep {
timeout_floor: Duration::ZERO,
stall: Stall::NONE,
jobs: 1,
meter: false,
confirm: false,
census: blind(),
},
&tally,
);
assert!(matches!(judgement, Judgement::Reached(Outcome::Timeout, None, None)));
assert_eq!(tally.probes.load(Ordering::Relaxed), 0);
assert_eq!(tally.launches.load(Ordering::Relaxed), 1);
}
#[test]
fn a_partial_census_probe_cannot_bypass_the_whole_binary_timeout() {
const SCRIPT: &[&str] = &[
"when-arg:tests::t0|print:test tests::t0 ... FAILED",
"when-arg:tests::t0|exit:1",
"sleep:200",
"exit:0",
];
let (_directory, work, plan, mut binaries) = helper_harness(SCRIPT);
binaries[0].budget = Some(Duration::from_millis(20));
let reachable: Vec<&TestBinary> = binaries.iter().collect();
let census = Census::partial(&binaries[0].path, plan.mutants[0].ordinal, 1, 4);
let tally = Tally::default();
let judgement = judge(
&work,
plan.mutants[0].ordinal,
&reachable,
None,
None,
Sweep {
timeout_floor: Duration::ZERO,
stall: Stall::NONE,
jobs: 1,
meter: false,
confirm: false,
census: &census,
},
&tally,
);
assert!(matches!(judgement, Judgement::Reached(Outcome::Timeout, None, None)));
assert_eq!(tally.probes.load(Ordering::Relaxed), 0);
assert_eq!(tally.launches.load(Ordering::Relaxed), 1);
}
#[test]
#[cfg(unix)]
fn a_complete_census_failure_is_checked_against_the_whole_binary() {
const SCRIPT: &[&str] = &[
"when-arg:tests::t0|print:test tests::t0 ... FAILED",
"when-arg:tests::t0|exit:1",
"sleep:200",
"exit:0",
];
let (_directory, work, plan, mut binaries) = helper_harness(SCRIPT);
binaries[0].budget = Some(Duration::from_millis(20));
let reachable: Vec<&TestBinary> = binaries.iter().collect();
let census = Census::examined(&binaries[0].path, plan.mutants[0].ordinal, 1, 4);
let tally = Tally::default();
let judgement = judge(
&work,
plan.mutants[0].ordinal,
&reachable,
None,
None,
Sweep {
timeout_floor: Duration::ZERO,
stall: Stall::NONE,
jobs: 1,
meter: false,
confirm: false,
census: &census,
},
&tally,
);
assert!(matches!(judgement, Judgement::Reached(Outcome::Timeout, None, None)));
assert_eq!(tally.launches.load(Ordering::Relaxed), 2);
}
#[test]
fn a_cached_killer_cannot_bypass_confirmation_of_a_flaky_test() {
let (_directory, work, _plan, binaries) = helper_harness(&["print:test tests::killer ... FAILED", "exit:1"]);
let reachable: Vec<&TestBinary> = binaries.iter().collect();
let hint = Killer {
package: "subject".to_owned(),
target: String::new(),
test: "tests::killer".to_owned(),
};
let tally = Tally::default();
let judgement = judge(
&work,
1,
&reachable,
Some(&hint),
None,
Sweep {
timeout_floor: Duration::ZERO,
stall: Stall::NONE,
jobs: 1,
meter: false,
confirm: true,
census: blind(),
},
&tally,
);
assert!(matches!(judgement, Judgement::Reached(Outcome::Flaky, None, Some(_))));
assert_eq!(tally.probes.load(Ordering::Relaxed), 0);
assert_eq!(tally.launches.load(Ordering::Relaxed), 1);
}
#[test]
fn a_file_local_killer_cannot_move_a_later_binary_before_a_timeout() {
let (_directory, work, _plan, mut binaries) = helper_harness(&["sleep:200", "exit:0"]);
binaries[0].target = "canonical".to_owned();
binaries[0].budget = Some(Duration::from_millis(20));
let mut later = binaries[0].clone();
later.target = "hinted".to_owned();
later.budget = Some(Duration::from_secs(1));
binaries.push(later);
let reachable: Vec<&TestBinary> = binaries.iter().collect();
let hint = Killer {
package: "subject".to_owned(),
target: "hinted".to_owned(),
test: "tests::killer".to_owned(),
};
let tally = Tally::default();
let judgement = judge_ordered(
&work,
1,
&reachable,
None,
Some(&hint),
None,
Sweep {
timeout_floor: Duration::ZERO,
stall: Stall::NONE,
jobs: 1,
meter: false,
confirm: false,
census: blind(),
},
&tally,
);
assert!(matches!(judgement, Judgement::Reached(Outcome::Timeout, None, None)));
assert_eq!(tally.launches.load(Ordering::Relaxed), 1);
}
#[test]
#[cfg(unix)]
fn a_partial_census_hint_that_passes_falls_back_to_the_whole_binary() {
const SCRIPT: &[&str] = &["when-arg:tests::t0|exit:0", "print:test tests::whole ... FAILED", "exit:1"];
let (_directory, work, plan, binaries) = helper_harness(SCRIPT);
let reachable: Vec<&TestBinary> = binaries.iter().collect();
let census = Census::partial(&binaries[0].path, plan.mutants[0].ordinal, 1, 4);
let tally = Tally::default();
let judgement = judge(
&work,
plan.mutants[0].ordinal,
&reachable,
None,
None,
Sweep {
timeout_floor: Duration::ZERO,
stall: Stall::NONE,
jobs: 1,
meter: false,
confirm: false,
census: &census,
},
&tally,
);
assert!(matches!(judgement, Judgement::Reached(Outcome::Killed, _, None)));
assert_eq!(tally.probes.load(Ordering::Relaxed), 1);
assert_eq!(tally.launches.load(Ordering::Relaxed), 2);
}
#[test]
fn a_hint_naming_a_test_the_users_filter_excludes_does_not_convict() {
let (_directory, mut work, _plan, binaries) = helper_harness(ONLY_WHEN_FILTERED);
let reachable: Vec<&TestBinary> = binaries.iter().collect();
let mut arguments: Vec<String> = ONLY_WHEN_FILTERED.iter().map(crate::testing::directive).collect();
arguments.push("tests::other".to_owned());
work.set_test_args(arguments);
let hint = Killer {
package: "subject".to_owned(),
target: String::new(),
test: "tests::killer".to_owned(),
};
let judgement = judge(
&work,
1,
&reachable,
Some(&hint),
None,
Sweep {
timeout_floor: Duration::ZERO,
stall: Stall::NONE,
jobs: 1,
meter: false,
confirm: false,
census: blind(),
},
&Tally::default(),
);
assert!(
matches!(judgement, Judgement::Reached(Outcome::Survived, None, None)),
"an excluded test must not convict"
);
}
#[test]
#[cfg(unix)]
fn the_same_mutant_without_a_hint_is_judged_by_the_whole_binary() {
let (_directory, work, _plan, binaries) = helper_harness(ONLY_WHEN_FILTERED);
let reachable: Vec<&TestBinary> = binaries.iter().collect();
let judgement = judge(
&work,
1,
&reachable,
None,
None,
Sweep {
timeout_floor: Duration::ZERO,
stall: Stall::NONE,
jobs: 1,
meter: false,
confirm: false,
census: blind(),
},
&Tally::default(),
);
assert!(
matches!(judgement, Judgement::Reached(Outcome::Survived, None, None)),
"the unfiltered script passes, so this mutant survives"
);
}
#[test]
#[cfg(unix)]
fn a_hint_naming_an_unreachable_binary_is_ignored() {
let (_directory, work, _plan, binaries) = helper_harness(ONLY_WHEN_FILTERED);
let reachable: Vec<&TestBinary> = binaries.iter().collect();
let hint = Killer {
package: "elsewhere".to_owned(),
target: String::new(),
test: "tests::killer".to_owned(),
};
let judgement = judge(
&work,
1,
&reachable,
Some(&hint),
None,
Sweep {
timeout_floor: Duration::ZERO,
stall: Stall::NONE,
jobs: 1,
meter: false,
confirm: false,
census: blind(),
},
&Tally::default(),
);
assert!(
matches!(judgement, Judgement::Reached(Outcome::Survived, None, None)),
"a hint for another package must not decide anything"
);
}
#[cfg(unix)]
const FAILS_ONLY_UNFILTERED: &str = r#"if [ "$0" = "tests::gone" ]; then exit 0; fi; echo "test tests::other ... FAILED"; exit 1"#;
#[test]
#[cfg(unix)]
fn a_hint_that_no_longer_convicts_reaches_the_verdict_the_run_would_have_reached_anyway() {
let (_directory, work, _plan, binaries) = harness(FAILS_ONLY_UNFILTERED, Duration::from_secs(30));
let reachable: Vec<&TestBinary> = binaries.iter().collect();
let hint = Killer {
package: "subject".to_owned(),
target: String::new(),
test: "tests::gone".to_owned(),
};
let hinted = judge(
&work,
1,
&reachable,
Some(&hint),
None,
Sweep {
timeout_floor: Duration::ZERO,
stall: Stall::NONE,
jobs: 1,
meter: false,
confirm: false,
census: blind(),
},
&Tally::default(),
);
let unhinted = judge(
&work,
1,
&reachable,
None,
None,
Sweep {
timeout_floor: Duration::ZERO,
stall: Stall::NONE,
jobs: 1,
meter: false,
confirm: false,
census: blind(),
},
&Tally::default(),
);
for (judgement, described) in [(hinted, "with a wrong hint"), (unhinted, "with no hint")] {
match judgement {
Judgement::Reached(Outcome::Killed, Some(killer), None) => {
assert_eq!(killer.test, "tests::other", "{described}: the wrong test was credited");
}
Judgement::Reached(outcome, killer, note) => {
panic!("{described}: expected a kill by `tests::other`, got {outcome:?} / {killer:?} / {note:?}")
}
Judgement::Abandoned(reason) => panic!("{described}: the run was abandoned: {reason}"),
}
}
}
#[test]
#[cfg(unix)]
fn a_wrong_hint_cannot_turn_a_survivor_into_a_kill() {
let (_directory, work, _plan, binaries) = harness("exit 0", Duration::from_secs(30));
let reachable: Vec<&TestBinary> = binaries.iter().collect();
let hint = Killer {
package: "subject".to_owned(),
target: String::new(),
test: "tests::gone".to_owned(),
};
let hinted = judge(
&work,
1,
&reachable,
Some(&hint),
None,
Sweep {
timeout_floor: Duration::ZERO,
stall: Stall::NONE,
jobs: 1,
meter: false,
confirm: false,
census: blind(),
},
&Tally::default(),
);
let unhinted = judge(
&work,
1,
&reachable,
None,
None,
Sweep {
timeout_floor: Duration::ZERO,
stall: Stall::NONE,
jobs: 1,
meter: false,
confirm: false,
census: blind(),
},
&Tally::default(),
);
assert!(matches!(hinted, Judgement::Reached(Outcome::Survived, None, None)));
assert!(matches!(unhinted, Judgement::Reached(Outcome::Survived, None, None)));
}
#[test]
#[cfg(unix)]
fn a_sweep_records_the_killer_it_found_and_forgets_the_one_it_did_not() {
let (_directory, work, mut plan, binaries) = harness("echo 'test tests::caught ... FAILED'; exit 1", Duration::from_secs(30));
let scope = TestScope {
packages: &[],
package_local: false,
whole_workspace: true,
};
let reach = Reachability::build(&plan, &binaries, &scope);
let sweep = Sweep {
timeout_floor: Duration::ZERO,
stall: Stall::NONE,
jobs: 1,
meter: false,
confirm: false,
census: blind(),
};
let mut killers = Killers::default();
killers.record(
"m1".into(),
Killer {
package: "subject".to_owned(),
target: String::new(),
test: "tests::stale".to_owned(),
},
);
let _spent = test_all(
&work,
&mut plan,
&reach,
sweep,
&mut killers,
&mut crate::testing::Recorder::default(),
)
.expect("the sweep to finish");
assert_eq!(plan.mutants[0].outcome, Outcome::Killed);
assert_eq!(killers.hint("m1").map(|found| found.test.as_str()), Some("tests::caught"));
let (_directory, work, mut plan, binaries) = harness("exit 0", Duration::from_secs(30));
let reach = Reachability::build(&plan, &binaries, &scope);
let _spent = test_all(
&work,
&mut plan,
&reach,
sweep,
&mut killers,
&mut crate::testing::Recorder::default(),
)
.expect("the sweep to finish");
assert_eq!(plan.mutants[0].outcome, Outcome::Survived);
assert!(killers.hint("m1").is_none(), "a mutant nothing caught must not keep a killer");
drop(binaries);
}
#[test]
#[cfg(unix)]
fn a_mutant_with_a_timeout_multiplier_overrides_the_binary_budget() {
let (_directory, work, _plan, mut binaries) = harness("sleep 0.2; exit 0", Duration::from_millis(10));
binaries[0].baseline = Duration::from_millis(50);
let reachable: Vec<&TestBinary> = binaries.iter().collect();
let sweep = Sweep {
timeout_floor: Duration::ZERO,
stall: Stall::NONE,
jobs: 1,
meter: false,
confirm: false,
census: blind(),
};
let without_override = judge(&work, 1, &reachable, None, None, sweep, &Tally::default());
assert!(
matches!(without_override, Judgement::Reached(Outcome::Timeout, None, None)),
"default budget times out"
);
let with_override = judge(&work, 1, &reachable, None, Some(100.0), sweep, &Tally::default());
assert!(
matches!(with_override, Judgement::Reached(Outcome::Survived, None, None)),
"extended budget allows completion"
);
}
#[test]
fn census_based_scheduling_uses_per_site_measured_cost() {
use super::mutant_cost;
let plan = plan_over(&["subject", "subject"]);
let binaries = vec![binary_of("subject", Duration::from_secs(10))];
let sets = Reachability::build(&plan, &binaries, &NARROW);
let cost_a = mutant_cost(0, &plan, &sets, blind(), &Killers::default());
let cost_b = mutant_cost(1, &plan, &sets, blind(), &Killers::default());
assert_eq!(cost_a, cost_b);
assert_eq!(cost_a, Duration::from_secs(10));
}
#[test]
fn a_hinted_mutant_costs_less_than_an_unhinted_one() {
use super::mutant_cost;
let plan = plan_over(&["subject", "subject"]);
let binaries = vec![
binary_of("subject", Duration::from_secs(5)),
binary_of("subject", Duration::from_secs(20)),
];
let sets = Reachability::build(
&plan,
&binaries,
&TestScope {
packages: &[],
package_local: false,
whole_workspace: true,
},
);
let mut killers = Killers::default();
killers.record(
plan.mutants[0].id.clone(),
Killer {
package: "subject".to_owned(),
target: String::new(),
test: "tests::hint".to_owned(),
},
);
let cost_hinted = mutant_cost(0, &plan, &sets, blind(), &killers);
let cost_unhinted = mutant_cost(1, &plan, &sets, blind(), &Killers::default());
assert!(
cost_hinted < cost_unhinted,
"hinted={cost_hinted:?} should be less than unhinted={cost_unhinted:?}"
);
}
#[test]
fn file_learning_publishes_killer_to_waiting_siblings() {
let learning = FileLearning::new();
{
let mut state = learning.state.lock().unwrap();
assert!(matches!(*state, Learning::Untried));
*state = Learning::InProgress;
}
thread::scope(|scope| {
let _waiter = scope.spawn(|| {
let (guard_result, timed_out) = learning
.notify
.wait_timeout_while(learning.state.lock().unwrap(), Duration::from_millis(200), |s| {
matches!(s, Learning::InProgress)
})
.unwrap();
assert!(!timed_out.timed_out(), "should have been woken, not timed out");
match &*guard_result {
Learning::Learned(killer) => assert_eq!(killer.test, "tests::found"),
other => panic!("expected Learned, got {other:?}"),
}
drop(guard_result);
});
thread::sleep(Duration::from_millis(10));
{
let mut s = learning.state.lock().unwrap();
*s = Learning::Learned(Killer {
package: "pkg".to_owned(),
target: String::new(),
test: "tests::found".to_owned(),
});
drop(s);
learning.notify.notify_all();
}
});
}
#[test]
fn file_learning_wakes_waiters_on_exhaustion() {
let learning = FileLearning::new();
{
let mut state = learning.state.lock().unwrap();
*state = Learning::InProgress;
}
thread::scope(|scope| {
let _waiter = scope.spawn(|| {
let (guard_result, _timed_out) = learning
.notify
.wait_timeout_while(learning.state.lock().unwrap(), Duration::from_millis(200), |s| {
matches!(s, Learning::InProgress)
})
.unwrap();
assert!(matches!(*guard_result, Learning::Exhausted));
drop(guard_result);
});
thread::sleep(Duration::from_millis(10));
{
let mut s = learning.state.lock().unwrap();
*s = Learning::Exhausted;
drop(s);
learning.notify.notify_all();
}
});
}
#[test]
fn file_learning_wait_times_out_for_slow_learner() {
let learning = FileLearning::new();
{
let mut state = learning.state.lock().unwrap();
*state = Learning::InProgress;
}
let started = Instant::now();
{
let (guard_result, timed_out) = learning
.notify
.wait_timeout_while(learning.state.lock().unwrap(), Duration::from_millis(20), |s| {
matches!(s, Learning::InProgress)
})
.unwrap();
drop(guard_result);
assert!(timed_out.timed_out(), "should have timed out");
}
assert!(started.elapsed() < Duration::from_millis(100));
}
fn killer(test: &str) -> Killer {
Killer {
package: "subject".to_owned(),
target: "lib".to_owned(),
test: test.to_owned(),
}
}
#[test]
fn a_learned_killer_is_never_lost_to_a_later_worker_that_found_none() {
let learning = FileLearning::new();
learning.publish(&Judgement::Reached(Outcome::Killed, Some(killer("tests::found")), None));
learning.publish(&Judgement::Reached(Outcome::Survived, None, None));
match &*learning.locked() {
Learning::Learned(known) => assert_eq!(known.test, "tests::found"),
other => panic!("a survivor erased the file's killer: {other:?}"),
}
let contended = FileLearning::new();
thread::scope(|scope| {
for worker in 0..8_u32 {
let contended = &contended;
let _publisher = scope.spawn(move || {
let judged = if worker % 2 == 0 {
Judgement::Reached(Outcome::Killed, Some(killer("tests::found")), None)
} else {
Judgement::Reached(Outcome::Survived, None, None)
};
contended.publish(&judged);
});
}
});
match &*contended.locked() {
Learning::Learned(known) => assert_eq!(known.test, "tests::found"),
other => panic!("a concurrent survivor erased the file's killer: {other:?}"),
}
let barren = FileLearning::new();
barren.publish(&Judgement::Reached(Outcome::Survived, None, None));
let settled = barren.locked();
assert!(matches!(&*settled, Learning::Exhausted), "{settled:?}");
drop(settled);
}
#[test]
fn a_poisoned_file_local_killer_lock_is_recovered_rather_than_propagated() {
let learning = FileLearning::new();
let panicked = thread::scope(|scope| {
scope
.spawn(|| {
let mut state = learning.locked();
*state = Learning::InProgress;
panic!("a worker died holding the hint lock");
})
.join()
.is_err()
});
assert!(panicked, "the fixture must actually poison the lock");
assert!(learning.state.is_poisoned(), "the fixture must actually poison the lock");
let recovered = learning.locked();
assert!(matches!(&*recovered, Learning::InProgress), "{recovered:?}");
drop(recovered);
learning.publish(&Judgement::Reached(Outcome::Killed, Some(killer("tests::after")), None));
match &*learning.locked() {
Learning::Learned(known) => assert_eq!(known.test, "tests::after"),
other => panic!("a poisoned lock swallowed the publication: {other:?}"),
}
}
}