use core::fmt::Write as _;
use core::time::Duration;
use std::time::Instant;
use cargo_gamma_process::MemoryRequest;
use super::baseline::{Baseline, measure_baseline};
use super::build::{Abandoned, Converger};
use super::census::{self, Census};
use super::config::Config;
use super::events::Events;
use super::killers::Killers;
use super::memory;
use super::memory::MemoryPolicy;
use super::session::{CensusCost, Phases, Session, SweepCost};
use super::stall::Stall;
use super::sweep::{Sweep, test_all};
use super::test_binary::{
Reachability, TestBinary, TestScope, admits_target, build_packages, calibrate, oracle_packages, reaches, reaching_packages, restrict,
unmatched_test, workload,
};
use super::workspace::{Workspace, gamma_base};
use crate::discover::{CompileFailTarget, Plan, Survey, compile_fail_advice};
use crate::error::error;
use crate::estimate::project;
use crate::model::Outcome;
use crate::ops::registry::Selection;
use crate::report::encode_controls;
use crate::{HashMap, HashSet, Result};
const GROUP_LIMIT: usize = 5;
pub fn run(survey: &Survey, selection: &Selection, config: &Config, events: &mut impl Events) -> Result<Measured> {
run_with_locks(survey, selection, config, events, None)
}
pub(crate) fn run_with_locks(
survey: &Survey,
selection: &Selection,
config: &Config,
events: &mut impl Events,
locks: Option<super::workspace::CacheLocks>,
) -> Result<Measured> {
let Measured {
mut plan,
built,
stuck,
dropped,
} = measure_with_locks(survey, selection, config, events, locks)?;
let Some(mut built) = built else {
return Ok(Measured {
plan,
built: None,
stuck,
dropped,
});
};
let stall = Stall {
budget: built.session.stall,
};
let oracle = built.oracle.clone();
let scope = oracle.scope();
let base = gamma_base(&survey.root, config.cache_dir.as_deref());
let mut killers = if config.incremental.is_enabled() {
Killers::load(&base, &survey.root)
} else {
Killers::default()
};
let reach = Reachability::build(&plan, &built.session.binaries, &scope);
let (census_targets, maximum_census_savings) = census_targets(&plan, &reach, &killers);
let census_requested = !config.whole_test_binaries && !census_targets.is_empty() && !maximum_census_savings.is_zero();
let census_started = Instant::now();
let census = if census_requested {
census::take(
&built.work,
&built.session.binaries,
&census_targets,
maximum_census_savings,
config.jobs,
stall,
events,
)
} else {
Census::default()
};
if census_requested {
built.session.phases.census = Some(CensusCost {
elapsed: census_started.elapsed(),
walked: census.walked(),
binaries: census_targets.len(),
});
}
let work = workload(&plan.mutants, &reach, (!config.whole_test_binaries).then_some(&census));
let projection = project(&plan.mutants, work, built.session.baseline_wall, built.session.build, config.jobs);
events.measured(&plan, &built.session, &projection);
let sweep = Sweep {
timeout_floor: config.timeout_floor,
stall,
jobs: config.jobs,
meter: built.session.metered,
confirm: config.confirm,
census: &census,
};
let sweep_started = Instant::now();
let swept = test_all(&built.work, &mut plan, &reach, sweep, &mut killers, events);
let sweep_elapsed = sweep_started.elapsed();
if config.incremental.is_enabled() {
killers.store(&base, &plan.mutants);
}
let spent = swept?;
built.session.phases.sweep = spent.map(|spent| SweepCost {
elapsed: sweep_elapsed,
launches: spent.launches,
probes: spent.probes,
});
Ok(Measured {
plan,
built: Some(built),
stuck,
dropped,
})
}
fn census_targets(plan: &Plan, reach: &Reachability<'_>, killers: &Killers) -> (HashMap<camino::Utf8PathBuf, HashSet<u32>>, Duration) {
let mut targets: HashMap<camino::Utf8PathBuf, HashSet<u32>> = HashMap::default();
let mut maximum_savings = Duration::ZERO;
let pending = || {
plan.mutants
.iter()
.filter(|mutant| mutant.ordinal > 0 && mutant.outcome == Outcome::Pending)
};
let eligibly_hinted = |mutant: &crate::model::Mutant| {
killers.hint(&mutant.id).is_some_and(|hint| {
reach
.reachable(&mutant.package)
.is_some_and(|reachable| reachable.iter().any(|binary| hint.names(&binary.package, &binary.target)))
})
};
for mutant in pending().filter(|mutant| !eligibly_hinted(mutant)) {
let Some(reachable) = reach.reachable(&mutant.package) else {
continue;
};
for binary in reachable {
let _new = targets.entry(binary.path.clone()).or_default().insert(mutant.ordinal);
maximum_savings = maximum_savings.saturating_add(binary.baseline);
}
}
for mutant in pending().filter(|mutant| eligibly_hinted(mutant)) {
let Some(reachable) = reach.reachable(&mutant.package) else {
continue;
};
for binary in reachable {
if let Some(sites) = targets.get_mut(&binary.path) {
let _new = sites.insert(mutant.ordinal);
}
}
}
(targets, maximum_savings)
}
#[derive(Debug)]
pub struct Measured {
pub plan: Plan,
pub built: Option<Built>,
pub stuck: Vec<String>,
pub dropped: Vec<String>,
}
#[derive(Debug)]
pub struct Built {
pub work: Workspace,
pub session: Session,
pub oracle: Oracle,
}
#[derive(Debug, Clone)]
pub struct Oracle {
pub packages: Vec<String>,
pub package_local: bool,
pub whole_workspace: bool,
}
impl Oracle {
fn new(packages: Vec<String>, package_local: bool, whole_workspace: bool) -> Self {
Self {
packages,
package_local,
whole_workspace,
}
}
fn scope(&self) -> TestScope<'_> {
TestScope {
packages: &self.packages,
package_local: self.package_local,
whole_workspace: self.whole_workspace,
}
}
}
fn preflight(
survey: &Survey,
plan: &Plan,
work: &Workspace,
config: &Config,
converger: &mut Converger,
events: &mut impl Events,
) -> Result<Cleared> {
events.begin("Validating", "Validated", "workspace");
let requested: Vec<String> = oracle_packages(&survey.selected, config);
let package_local = config.test_packages.is_empty() && !config.test_workspace;
let scope = TestScope {
packages: &requested,
package_local,
whole_workspace: config.test_workspace,
};
let intended = survey.packages();
let intending: crate::HashSet<&str> = intended.iter().map(String::as_str).collect();
let checking = reaching_packages(&survey.reach, &intending, &scope);
let cleared = Converger::preflight(work, plan, checking.as_deref(), &intended, config.build, events)?;
events.end("");
let dropped = cleared.dropped;
if cleared.whole_workspace {
converger.require_whole_workspace();
}
if dropped.is_empty() {
return Ok(Cleared {
packages: requested,
whole_workspace: config.test_workspace,
dropped,
});
}
Ok(Cleared {
packages: narrowed_oracle(requested, intended, &dropped)?,
whole_workspace: config.test_workspace,
dropped,
})
}
fn narrowed_oracle(requested: Vec<String>, intended: Vec<String>, dropped: &[String]) -> Result<Vec<String>> {
if requested.is_empty() {
return Ok(intended);
}
let kept: Vec<String> = requested.into_iter().filter(|package| !dropped.contains(package)).collect();
if kept.is_empty() {
let named = dropped.join("`, `");
return Err(error!(
"none of the packages this run's tests would come from could be compiled: `{named}`.\n\
Fix the build there, or name a package that does build with --test-package."
));
}
Ok(kept)
}
struct Cleared {
packages: Vec<String>,
whole_workspace: bool,
dropped: Vec<String>,
}
pub fn measure(survey: &Survey, selection: &Selection, config: &Config, events: &mut impl Events) -> Result<Measured> {
measure_with_locks(survey, selection, config, events, None)
}
#[expect(
clippy::too_many_lines,
reason = "one ordered pass: admission, copy, build, and baseline share borrowed state"
)]
fn measure_with_locks(
survey: &Survey,
selection: &Selection,
config: &Config,
events: &mut impl Events,
locks: Option<super::workspace::CacheLocks>,
) -> Result<Measured> {
let started = Instant::now();
let (memory, unbounded) = admit_memory_control(config)?;
if let Err(reason) = cargo_gamma_process::containment() {
let reason = reason.to_string();
events.warn(&format!(
"this host cannot fully contain a test's descendants, so cleanup is best-effort: {}",
encode_controls(&reason)
));
}
if let Some(pattern) = unmatched_test(&survey.tests, &config.include_tests, &config.exclude_tests) {
return Err(error!("no test target matches `{pattern}`; patterns match cargo target names, not test function names").usage());
}
warn_about_compile_fail_targets(&survey.compile_fail, &survey.selected, config, events);
let mut plan = survey.skeleton();
let mut converger = Converger::guided(ordering_hints(survey, config));
let copy_started = Instant::now();
let mut work = Workspace::prepare_with_locks(&plan.root, config, events, locks)?;
let copy = copy_started.elapsed();
work.calibrate_harness(config.jobs);
let preflight_started = Instant::now();
let Cleared {
packages,
whole_workspace,
dropped,
} = preflight(survey, &plan, &work, config, &mut converger, events)?;
let preflight_elapsed = preflight_started.elapsed();
let oracle = Oracle::new(packages, config.test_packages.is_empty() && !config.test_workspace, whole_workspace);
let scope = oracle.scope();
let Staged { anything_live, mut stuck } = converge_stages(survey, selection, &mut plan, &mut converger, &work, config, events)?;
plan.sort();
converger.plan_reordered();
if !anything_live {
converger.settle(&mut plan);
return Ok(Measured {
plan,
built: None,
stuck,
dropped,
});
}
events.begin("Baselining", "Baseline", "building the test binaries and running the suite");
let select = build_packages(&plan, &scope);
let mut build = converger.finish(&work, &mut plan, select.as_deref(), config.build, events)?;
if let Some(abandoned) = build.stuck {
let count = abandoned.ordinals.len();
stuck.push(describe_stuck(&plan, select.as_deref().unwrap_or_default(), &abandoned));
events.complete(&format!(
"the build could not be made to compile, {} not run",
crate::report::quantity(count, "mutant")
));
return Ok(Measured {
plan,
built: None,
stuck,
dropped,
});
}
let present = build.binaries.len();
restrict(&mut build.binaries, &config.include_tests, &config.exclude_tests);
let filtered = present.saturating_sub(build.binaries.len());
if build.binaries.is_empty() && present > 0 {
return Err(error!("`--include-test` and `--exclude-test` left no test target to decide a verdict").usage());
}
let build_time = started.elapsed();
if config.nextest {
work.arm_nextest(&build.binaries)?;
}
let baseline = take_baseline(&work, &mut build.binaries, config, &memory, events)?;
warn_about_an_empty_oracle(&plan, &build.binaries, &scope, config.test_packages.is_empty(), &dropped, events);
let stall = calibrate_stall(&baseline, config);
calibrate(&mut build.binaries, config, &memory);
let session = Session {
baseline: baseline.elapsed,
baseline_wall: baseline.wall,
tests: baseline.tests,
quiet: baseline.quiet,
stall: stall.budget,
build: build_time,
peak: baseline.peak,
metered: memory.measuring(),
unbounded,
withdrawn: build.withdrawn,
census: build.census,
rounds: build.rounds,
rounds_taken: build.history,
binaries: build.binaries,
scratch: work.base().to_owned(),
filtered,
widened: build.widened,
ordering: build.ordering,
phases: Phases {
copy,
preflight: preflight_elapsed,
census: None,
sweep: None,
},
};
work.settle();
Ok(Measured {
plan,
built: Some(Built { work, session, oracle }),
stuck,
dropped,
})
}
fn describe_stuck(plan: &Plan, packages: &[String], abandoned: &Abandoned) -> String {
let ordinals: crate::HashSet<u32> = abandoned.ordinals.iter().copied().collect();
let affected: Vec<&crate::model::Mutant> = plan.mutants.iter().filter(|mutant| ordinals.contains(&mutant.ordinal)).collect();
let where_ = if packages.is_empty() {
"the workspace".to_owned()
} else {
format!("{} {}", crate::report::quantity(packages.len(), "package"), packages.join(", "))
};
let mut described = format!(
"the build for {where_} could not be made to compile, so {} never ran and {} reported as `notbuilt` rather than as survivors.",
crate::report::quantity(abandoned.ordinals.len(), "mutant"),
if abandoned.ordinals.len() == 1 { "is" } else { "are" }
);
for (label, counted) in [
("mutator", tally(&affected, |mutant| mutant.mutator.to_string())),
("scope", tally(&affected, |mutant| mutant.item_path.to_string())),
] {
if !counted.is_empty() {
let _ = write!(described, "\nNot run, by {label}: {}.", counted.join(", "));
}
}
described.push('\n');
described.push_str(&abandoned.reason);
described
}
fn tally(mutants: &[&crate::model::Mutant], key: impl Fn(&crate::model::Mutant) -> String) -> Vec<String> {
let mut counts: crate::HashMap<String, usize> = crate::HashMap::default();
for mutant in mutants {
*counts.entry(key(mutant)).or_default() += 1;
}
let mut ranked: Vec<(String, usize)> = counts.into_iter().collect();
ranked.sort_by(|left, right| right.1.cmp(&left.1).then_with(|| left.0.cmp(&right.0)));
let rest = ranked.len().saturating_sub(GROUP_LIMIT);
let mut rendered: Vec<String> = ranked
.iter()
.take(GROUP_LIMIT)
.map(|(name, count)| format!("{name} ({count})"))
.collect();
if rest > 0 {
rendered.push(format!("and {rest} more"));
}
rendered
}
struct Staged {
anything_live: bool,
stuck: Vec<String>,
}
fn ordering_hints(survey: &Survey, config: &Config) -> crate::HashSet<crate::model::MutantId> {
if !config.incremental.is_enabled() {
return crate::HashSet::default();
}
let base = gamma_base(&survey.root, config.cache_dir.as_deref());
let record = crate::discover::RunRecord::load(&base);
let checked_in = crate::discover::Hints::load(&survey.root);
record
.ordering()
.into_iter()
.map(crate::model::MutantId::new)
.chain(checked_in.ordering().into_iter().map(crate::model::MutantId::new))
.collect()
}
fn converge_stages(
survey: &Survey,
selection: &Selection,
plan: &mut Plan,
converger: &mut Converger,
work: &Workspace,
config: &Config,
events: &mut impl Events,
) -> Result<Staged> {
let mut ordinals = 0_u32;
let mut staged = Staged {
anything_live: false,
stuck: Vec::new(),
};
for stage in &crate::discover::stages(&survey.packages(), &survey.reach) {
let name = stage.join(", ");
events.begin("Mutating", "Mutated", &name);
let mut live = 0_usize;
for package in stage {
let scanned = survey.scan(Some(package), selection, &mut ordinals)?;
live = live.saturating_add(scanned.mutants.iter().filter(|mutant| mutant.ordinal > 0).count());
plan.absorb(scanned);
}
if live == 0 {
events.end(", no mutants");
continue;
}
for package in stage {
work.link_runtime(package, &plan.files)?;
}
let before = converger.withdrawn();
if let Some(abandoned) = converger.stage(work, plan, stage, config.build, events)? {
let count = abandoned.ordinals.len();
staged.stuck.push(describe_stuck(plan, stage, &abandoned));
events.end(&format!(
", the build could not be made to compile, {} not run",
crate::report::quantity(count, "mutant")
));
continue;
}
staged.anything_live = true;
let viable = live.saturating_sub(converger.withdrawn().saturating_sub(before));
events.end(&format!(", {}", crate::report::quantity(viable, "viable mutant")));
}
Ok(staged)
}
fn warn_about_compile_fail_targets(targets: &[CompileFailTarget], selected: &[String], config: &Config, events: &mut dyn Events) {
let oracle = oracle_packages(selected, config);
let admitted: Vec<_> = targets
.iter()
.filter(|target| {
(config.test_workspace || oracle.contains(&target.package))
&& admits_target(&target.target, &config.include_tests, &config.exclude_tests)
})
.cloned()
.collect();
if let Some(warning) = compile_fail_advice(&admitted) {
events.warn(&warning);
}
}
const NAMED_HELPERS: usize = 3;
fn warn_about_an_empty_oracle(
plan: &Plan,
binaries: &[TestBinary],
scope: &TestScope<'_>,
inferred: bool,
dropped: &[String],
events: &mut dyn Events,
) {
if dropped.is_empty() && (!inferred || scope.whole_workspace) {
return;
}
let mutated: crate::HashSet<&str> = plan
.mutants
.iter()
.filter(|mutant| mutant.ordinal > 0 && mutant.outcome == Outcome::Pending)
.map(|mutant| &*mutant.package)
.collect();
if mutated.is_empty() {
return;
}
let judged = mutated.iter().any(|package| {
binaries
.iter()
.any(|binary| binary.tests != Some(0) && reaches(binary, package, plan, scope))
});
if judged {
return;
}
if !dropped.is_empty() {
let named = dropped.join("`, `");
events.warn(&format!(
"no test in this run's scope reaches the code being mutated, so every mutant will be reported as uncovered. The preflight could not compile `{named}`, so their tests were withdrawn from the oracle. Fixing the build there restores them"
));
return;
}
let mut helpers: Vec<&str> = plan
.reach
.iter()
.filter(|(package, reachable)| !scope.admits(package) && reachable.iter().any(|name| mutated.contains(name.as_str())))
.map(|(package, _reachable)| package.as_str())
.collect();
if helpers.is_empty() {
return;
}
helpers.sort_unstable();
let shown = helpers.len().min(NAMED_HELPERS);
let named = helpers[..shown].join("`, `");
let rest = helpers.len() - shown;
let others = if rest == 0 { String::new() } else { format!(" and {rest} more") };
events.warn(&format!(
"no test in this run's scope reaches the code being mutated, so every mutant will be reported as uncovered. By default each mutant is judged only by its own package, and `{named}`{others} also links this code. Add `--test-package {}` to let its tests decide a verdict, or `--test-workspace` for every package's",
helpers[0]
));
}
fn take_baseline(
work: &Workspace,
binaries: &mut [TestBinary],
config: &Config,
memory: &MemoryPolicy,
events: &mut impl Events,
) -> Result<Baseline> {
if !config.baseline {
events.complete("no baseline was measured");
return Ok(Baseline {
elapsed: Duration::ZERO,
wall: Duration::ZERO,
quiet: Duration::ZERO,
tests: None,
peak: None,
});
}
let request = MemoryRequest {
meter: memory.measuring(),
limit: memory.baseline_limit,
};
let total = binaries.len();
let unit = if total == 1 { "test binary" } else { "test binaries" };
let mut completed = 0_usize;
events.phase_progress(completed, total, unit);
let measured = measure_baseline(work, binaries, request, config.jobs, || {
completed += 1;
events.phase_progress(completed, total, unit);
})?;
events.complete(&describe(&measured));
Ok(measured)
}
fn admit_memory_control(config: &Config) -> Result<(MemoryPolicy, Option<String>)> {
settle_memory_control(config, memory::support().map_err(|reason| reason.to_string()))
}
fn settle_memory_control(config: &Config, support: Result<(), String>) -> Result<(MemoryPolicy, Option<String>)> {
let policy = config.memory;
if !policy.measuring() {
return Ok((policy, None));
}
if let Err(reason) = support {
if policy.insisted() {
return Err(error!(
"memory control was asked for, but it is not available here: {reason}.\n\
Run with `--memory off` to continue without it."
));
}
return Ok((policy.disabled(), Some(reason)));
}
if policy.enforcing() && !config.baseline && policy.limit.is_none() {
if policy.insisted() {
return Err(error!(
"`--memory enforce` derives each test binary's ceiling from what it used during the \
baseline, and `--no-baseline` means there is no such measurement.\n\
Pass `--memory-limit` to state a ceiling outright, or drop `--no-baseline`."
));
}
return Ok((
policy.disabled(),
Some("`--no-baseline` leaves no measurement to derive a ceiling from".to_owned()),
));
}
Ok((policy, None))
}
fn describe(baseline: &Baseline) -> String {
let duration = format!("{:.1?}", baseline.wall);
let ran = baseline.tests.map_or_else(
|| format!("the suite passed in {duration}"),
|tests| format!("{} ran in {duration}", crate::report::quantity(tests, "test")),
);
match baseline.peak {
Some(peak) => format!("{ran} with a peak of {}", crate::report::bytes(peak)),
None => ran,
}
}
fn calibrate_stall(baseline: &Baseline, config: &Config) -> Stall {
if config.stall && config.baseline {
Stall::calibrated(baseline.quiet, config.stall_factor, config.stall_floor)
} else {
Stall::NONE
}
}
#[cfg(test)]
mod tests {
use camino::{Utf8Path, Utf8PathBuf};
use super::*;
use crate::discover::Killer;
use crate::exec::memory::{Demand, MemoryControl};
use crate::fixtures;
use crate::model::Mutant;
use crate::testing::Recorder;
const fn measured(elapsed: Duration, quiet: Duration) -> Baseline {
Baseline {
elapsed,
wall: Duration::ZERO,
quiet,
tests: None,
peak: None,
}
}
#[test]
fn the_default_stall_budget_is_ten_times_the_measured_quiet_period() {
let config = Config::default();
let baseline = measured(Duration::from_mins(10), Duration::from_secs(3));
let stall = calibrate_stall(&baseline, &config);
assert_eq!(
stall.budget,
Some(Duration::from_secs(30)),
"3s quiet x {} factor should be 30s; the default factor is now {}",
config.stall_factor,
config.stall_factor
);
}
#[test]
fn a_suite_that_never_goes_quiet_gets_the_stall_floor_rather_than_zero() {
let config = Config::default();
let baseline = measured(Duration::from_mins(10), Duration::ZERO);
let stall = calibrate_stall(&baseline, &config);
assert_eq!(
stall.budget,
Some(Duration::from_secs(5)),
"a zero quiet period scaled by any factor is still zero, so the floor must win; \
the default floor is now {:?}",
config.stall_floor
);
}
#[test]
fn stall_detection_stands_down_when_there_is_no_baseline_to_calibrate_from() {
let config = Config {
baseline: false,
..Config::default()
};
let stall = calibrate_stall(&measured(Duration::from_secs(100), Duration::from_secs(1)), &config);
assert_eq!(stall.budget, None, "a budget derived from a baseline that never ran is a guess");
}
#[test]
fn stall_detection_stands_down_when_it_is_switched_off() {
let config = Config {
stall: false,
..Config::default()
};
let stall = calibrate_stall(&measured(Duration::from_secs(100), Duration::from_secs(1)), &config);
assert_eq!(stall.budget, None, "--no-stall must disable the detector outright");
}
fn stuck_mutant(ordinal: u32, mutator: &str, item_path: &str) -> Mutant {
Mutant {
id: format!("m{ordinal}").into(),
ordinal,
mutator: (mutator.to_owned()).into(),
item_path: (item_path.to_owned()).into(),
original: "a < b".to_owned().into(),
replacement: "a <= b".to_owned().into(),
outcome: Outcome::NotBuilt,
..fixtures::mutant()
}
}
fn stuck_plan(mutants: Vec<Mutant>) -> Plan {
Plan {
skipped: Vec::new(),
digests: crate::HashMap::default(),
root: Utf8PathBuf::from("/workspace"),
files: Vec::new(),
mutants,
suppressed: 0,
idle: Vec::new(),
sharded_out: 0,
settled_out: 0,
reach: crate::HashMap::default(),
specs: crate::HashMap::default(),
}
}
#[test]
fn a_stuck_build_is_described_by_mutator_and_by_scope() {
let plan = stuck_plan(vec![
stuck_mutant(1, "relational.lt_to_le", "subject::less"),
stuck_mutant(2, "relational.lt_to_le", "subject::less"),
stuck_mutant(3, "arith.add_to_sub", "subject::sum"),
stuck_mutant(4, "relational.lt_to_le", "subject::sum"),
]);
let abandoned = Abandoned {
reason: "Mutants blamed in the last rounds of this build: 9, 9, 9.".to_owned(),
ordinals: vec![1, 2, 3, 4],
};
let described = describe_stuck(&plan, &["subject".to_owned()], &abandoned);
assert!(described.contains("4 mutants"), "{described}");
assert!(described.contains("subject"), "{described}");
assert!(
described.contains("Not run, by mutator: relational.lt_to_le (3), arith.add_to_sub (1)."),
"{described}"
);
assert!(
described.contains("Not run, by scope: subject::less (2), subject::sum (2)."),
"{described}"
);
assert!(
described.contains("Mutants blamed in the last rounds of this build: 9, 9, 9."),
"{described}"
);
}
#[test]
fn a_stuck_whole_workspace_build_says_so() {
let plan = stuck_plan(vec![stuck_mutant(1, "relational.lt_to_le", "subject::less")]);
let abandoned = Abandoned {
reason: "the instrumented tree does not compile".to_owned(),
ordinals: vec![1],
};
let described = describe_stuck(&plan, &[], &abandoned);
assert!(described.contains("the build for the workspace"), "{described}");
assert!(described.contains("1 mutant never ran"), "{described}");
}
#[test]
fn a_stuck_build_counts_only_what_it_gave_up_on() {
let plan = stuck_plan(vec![
stuck_mutant(1, "relational.lt_to_le", "subject::less"),
stuck_mutant(2, "arith.add_to_sub", "subject::sum"),
]);
let abandoned = Abandoned {
reason: "stuck".to_owned(),
ordinals: vec![2],
};
let described = describe_stuck(&plan, &["subject".to_owned()], &abandoned);
assert!(described.contains("Not run, by mutator: arith.add_to_sub (1)."), "{described}");
assert!(!described.contains("relational.lt_to_le"), "{described}");
}
#[test]
fn a_long_list_of_groups_is_capped_and_the_rest_counted() {
let mutants: Vec<Mutant> = (1..=8)
.map(|ordinal| stuck_mutant(ordinal, &format!("op.{ordinal}"), "subject::f"))
.collect();
let plan = stuck_plan(mutants);
let abandoned = Abandoned {
reason: "stuck".to_owned(),
ordinals: (1..=8).collect(),
};
let described = describe_stuck(&plan, &["subject".to_owned()], &abandoned);
assert!(described.contains("and 3 more"), "{described}");
}
#[test]
fn a_baseline_without_a_harness_count_is_described_by_elapsed_time_only() {
let baseline = Baseline {
elapsed: Duration::from_millis(1500),
wall: Duration::from_millis(500),
quiet: Duration::ZERO,
tests: None,
peak: None,
};
assert_eq!(describe(&baseline), "the suite passed in 500.0ms");
}
#[test]
fn a_baseline_with_a_measured_peak_is_described_with_it() {
let baseline = Baseline {
elapsed: Duration::from_millis(500),
wall: Duration::from_millis(500),
quiet: Duration::ZERO,
tests: Some(4),
peak: Some(1024 * 1024),
};
assert_eq!(describe(&baseline), "4 tests ran in 500.0ms with a peak of 1.0 MB");
}
#[test]
fn memory_control_that_was_never_asked_for_needs_no_host_support() {
let config = Config {
memory: MemoryPolicy {
control: MemoryControl::Off,
..MemoryPolicy::default()
},
..Config::default()
};
let (settled, note) =
settle_memory_control(&config, Err("no cgroup here".to_owned())).expect("a policy that asks for nothing cannot fail");
assert!(!settled.measuring());
assert_eq!(note, None);
}
#[test]
fn a_stated_memory_policy_this_host_cannot_deliver_is_an_error() {
let config = Config {
memory: MemoryPolicy {
control: MemoryControl::Measure,
demand: Demand::Stated,
..MemoryPolicy::default()
},
..Config::default()
};
let failure = settle_memory_control(&config, Err("no cgroup here".to_owned()))
.expect_err("a stated policy this host cannot deliver must error");
assert!(failure.to_string().contains("--memory off"), "{failure}");
assert!(
failure.to_string().contains("no cgroup here"),
"the error must repeat what the host said, {failure}"
);
}
#[test]
fn a_defaulted_memory_policy_this_host_cannot_deliver_degrades_with_a_note() {
let (settled, note) = settle_memory_control(&Config::default(), Err("no cgroup here".to_owned()))
.expect("a defaulted policy degrades rather than errors");
assert!(!settled.measuring());
assert_eq!(
note.as_deref(),
Some("no cgroup here"),
"a degraded policy must say why, not merely that it degraded"
);
}
#[test]
fn a_stated_policy_this_host_can_deliver_still_errors_with_no_baseline_to_derive_a_ceiling_from() {
let config = Config {
memory: MemoryPolicy {
control: MemoryControl::Enforce,
demand: Demand::Stated,
..MemoryPolicy::default()
},
baseline: false,
..Config::default()
};
let failure =
settle_memory_control(&config, Ok(())).expect_err("a stated policy with no baseline to derive a ceiling from must error");
assert!(failure.to_string().contains("--memory-limit"), "{failure}");
}
#[test]
fn a_defaulted_policy_this_host_can_deliver_degrades_with_no_baseline_to_derive_a_ceiling_from() {
let config = Config {
memory: MemoryPolicy {
control: MemoryControl::Enforce,
..MemoryPolicy::default()
},
baseline: false,
..Config::default()
};
let (settled, note) = settle_memory_control(&config, Ok(())).expect("a defaulted policy degrades rather than errors");
assert!(!settled.measuring());
assert!(note.is_some_and(|note| note.contains("--no-baseline")), "the note must say why");
}
#[test]
fn a_stated_ceiling_needs_no_baseline_to_derive_one_from() {
let config = Config {
memory: MemoryPolicy {
control: MemoryControl::Enforce,
limit: Some(1 << 30),
..MemoryPolicy::default()
},
baseline: false,
..Config::default()
};
let (settled, note) = settle_memory_control(&config, Ok(())).expect("a stated ceiling needs no baseline");
assert!(settled.measuring());
assert_eq!(note, None);
}
fn target_in(package: &str, name: &str) -> CompileFailTarget {
CompileFailTarget {
package: package.to_owned(),
target: name.to_owned(),
harness: "trybuild".to_owned(),
}
}
fn target(name: &str) -> CompileFailTarget {
target_in("routerama", name)
}
fn config_with(exclude: &[&str]) -> Config {
Config {
exclude_tests: exclude.iter().map(|pattern| (*pattern).to_owned()).collect(),
..Config::default()
}
}
#[test]
fn a_compile_fail_target_is_named_before_anything_is_built() {
let mut events = Recorder::default();
warn_about_compile_fail_targets(
&[target("router_compile_fail")],
&["routerama".to_owned()],
&config_with(&[]),
&mut events,
);
let [warning] = events.warnings.as_slice() else {
panic!("one admitted target is one warning, got {:?}", events.warnings);
};
assert!(warning.contains("router_compile_fail"), "{warning}");
assert!(warning.contains("--exclude-test router_compile_fail"), "{warning}");
}
#[test]
fn a_target_already_excluded_is_not_warned_about() {
let mut events = Recorder::default();
warn_about_compile_fail_targets(
&[target("router_compile_fail")],
&["routerama".to_owned()],
&config_with(&["router_compile_fail"]),
&mut events,
);
assert!(events.warnings.is_empty(), "{:?}", events.warnings);
}
#[test]
fn a_target_excluded_by_a_glob_is_not_warned_about() {
let mut events = Recorder::default();
warn_about_compile_fail_targets(
&[target("router_compile_fail")],
&["routerama".to_owned()],
&config_with(&["*_compile_fail"]),
&mut events,
);
assert!(events.warnings.is_empty(), "{:?}", events.warnings);
}
#[test]
fn a_compile_fail_target_outside_the_oracle_is_not_warned_about() {
let mut events = Recorder::default();
warn_about_compile_fail_targets(
&[target("router_compile_fail"), target_in("internity", "compile_fail")],
&["routerama".to_owned()],
&config_with(&[]),
&mut events,
);
let [warning] = events.warnings.as_slice() else {
panic!("only the selected package should produce a warning, got {:?}", events.warnings);
};
assert!(warning.contains("router_compile_fail"), "{warning}");
assert!(!warning.contains("internity"), "{warning}");
assert!(!warning.contains("--exclude-test compile_fail"), "{warning}");
}
#[test]
fn a_workspace_without_one_is_silent() {
let mut events = Recorder::default();
warn_about_compile_fail_targets(&[], &["routerama".to_owned()], &config_with(&[]), &mut events);
assert!(events.warnings.is_empty(), "{:?}", events.warnings);
}
fn oracle_plan(mutating: &str) -> Plan {
let mut reach: crate::HashMap<String, crate::HashSet<String>> = crate::HashMap::default();
let _app = reach.insert("app".to_owned(), ["app".to_owned(), "core".to_owned()].into_iter().collect());
let _core = reach.insert("core".to_owned(), core::iter::once("core".to_owned()).collect());
let mut pending = Mutant {
ordinal: 1,
outcome: Outcome::Pending,
..crate::testing::advise_fixture::mutant("a.rs", "arith.add_to_sub", Outcome::Pending, 0)
};
pending.package = mutating.to_owned().into();
Plan {
skipped: Vec::new(),
digests: crate::HashMap::default(),
root: Utf8PathBuf::from("/w"),
files: Vec::new(),
mutants: vec![pending],
suppressed: 0,
idle: Vec::new(),
sharded_out: 0,
settled_out: 0,
reach,
specs: crate::HashMap::default(),
}
}
fn oracle_binary(package: &str, tests: Option<usize>) -> TestBinary {
TestBinary {
package: package.to_owned(),
tests,
..crate::testing::test_binary("/tmp/t")
}
}
fn capped(packages: &[String]) -> TestScope<'_> {
TestScope {
packages,
package_local: false,
whole_workspace: false,
}
}
#[test]
fn census_targets_only_selected_pending_sites_and_relevant_binaries() {
let mut plan = oracle_plan("core");
let mut settled = plan.mutants[0].clone();
settled.ordinal = 2;
settled.outcome = Outcome::Killed;
plan.mutants.push(settled);
let mut core = oracle_binary("core", Some(1));
core.path = "/tmp/core".into();
core.baseline = Duration::from_secs(2);
let mut app = oracle_binary("app", Some(1));
app.path = "/tmp/app".into();
app.baseline = Duration::from_secs(3);
let mut empty = oracle_binary("app", Some(0));
empty.path = "/tmp/empty".into();
empty.baseline = Duration::from_secs(100);
let packages = [String::from("app")];
let scope = capped(&packages);
let binaries = [core, app, empty];
let reach = Reachability::build(&plan, &binaries, &scope);
let (targets, savings) = census_targets(&plan, &reach, &Killers::default());
assert_eq!(targets.len(), 1);
let selected: HashSet<u32> = core::iter::once(1).collect();
assert_eq!(targets.get(Utf8Path::new("/tmp/app")), Some(&selected));
assert_eq!(savings, Duration::from_secs(3));
}
#[test]
fn a_hinted_mutant_rides_along_on_a_binary_an_unhinted_mutant_already_justifies() {
let mut plan = oracle_plan("core");
let mut hinted = crate::testing::advise_fixture::mutant("a.rs", "arith.add_to_sub", Outcome::Pending, 1);
hinted.package = "core".to_owned().into();
hinted.ordinal = 2;
plan.mutants.push(hinted);
let mut killers = Killers::default();
killers.record(
plan.mutants[1].id.clone(),
Killer {
package: "core".to_owned(),
target: String::new(),
test: "tests::hint".to_owned(),
},
);
let mut core = oracle_binary("core", Some(1));
core.path = "/tmp/core".into();
core.baseline = Duration::from_secs(2);
let binaries = [core];
let scope = TestScope {
packages: &[],
package_local: false,
whole_workspace: true,
};
let reach = Reachability::build(&plan, &binaries, &scope);
let (targets, savings) = census_targets(&plan, &reach, &killers);
assert_eq!(targets.len(), 1, "one binary is targeted, shared by both mutants");
let expected: HashSet<u32> = [1, 2].into_iter().collect();
assert_eq!(
targets.get(Utf8Path::new("/tmp/core")),
Some(&expected),
"the hinted ordinal rides along with the unhinted one"
);
assert_eq!(
savings,
Duration::from_secs(2),
"only the unhinted mutant pays the binary's baseline into the census's economics"
);
}
#[test]
fn a_mutant_with_a_hint_and_no_unhinted_sibling_never_targets_its_own_binary() {
let mut plan = oracle_plan("core");
plan.mutants[0].ordinal = 1;
let mut killers = Killers::default();
killers.record(
plan.mutants[0].id.clone(),
Killer {
package: "core".to_owned(),
target: String::new(),
test: "tests::hint".to_owned(),
},
);
let mut core = oracle_binary("core", Some(1));
core.path = "/tmp/core".into();
core.baseline = Duration::from_secs(2);
let binaries = [core];
let scope = TestScope {
packages: &[],
package_local: false,
whole_workspace: true,
};
let reach = Reachability::build(&plan, &binaries, &scope);
let (targets, savings) = census_targets(&plan, &reach, &killers);
assert!(targets.is_empty(), "a hinted mutant alone must not pay to census its own binary");
assert_eq!(savings, Duration::ZERO, "no census was justified, so it has nothing to be worth");
}
#[test]
fn a_stale_hints_binary_stays_out_of_targets_when_nothing_else_justifies_it() {
let mut plan = oracle_plan("core");
plan.mutants[0].ordinal = 1;
let _extra_reach = plan
.reach
.insert("extra".to_owned(), core::iter::once("extra".to_owned()).collect());
let mut extra = crate::testing::advise_fixture::mutant("b.rs", "arith.add_to_sub", Outcome::Pending, 2);
extra.package = "extra".to_owned().into();
extra.ordinal = 5;
plan.mutants.push(extra);
let mut killers = Killers::default();
killers.record(
plan.mutants[1].id.clone(),
Killer {
package: "extra".to_owned(),
target: String::new(),
test: "tests::hint".to_owned(),
},
);
let mut core = oracle_binary("core", Some(1));
core.path = "/tmp/core".into();
core.baseline = Duration::from_secs(2);
let mut extra_binary = oracle_binary("extra", Some(1));
extra_binary.path = "/tmp/extra".into();
extra_binary.baseline = Duration::from_secs(4);
let binaries = [core, extra_binary];
let scope = TestScope {
packages: &[],
package_local: false,
whole_workspace: true,
};
let reach = Reachability::build(&plan, &binaries, &scope);
let (targets, savings) = census_targets(&plan, &reach, &killers);
assert_eq!(targets.len(), 1, "only the unhinted mutant's own binary is targeted");
assert!(
targets.contains_key(Utf8Path::new("/tmp/core")),
"the unhinted mutant's binary is censused"
);
assert!(
!targets.contains_key(Utf8Path::new("/tmp/extra")),
"the hinted mutant's own binary, which nothing else justifies, must stay absent"
);
assert_eq!(
savings,
Duration::from_secs(2),
"only the unhinted mutant's binary counts toward savings"
);
}
#[test]
fn a_hint_naming_no_reachable_binary_is_treated_as_unhinted() {
let mut plan = oracle_plan("core");
plan.mutants[0].ordinal = 1;
let mut killers = Killers::default();
killers.record(
plan.mutants[0].id.clone(),
Killer {
package: "core".to_owned(),
target: "a-target-no-binary-carries".to_owned(),
test: "tests::hint".to_owned(),
},
);
let mut core = oracle_binary("core", Some(1));
core.path = "/tmp/core".into();
core.baseline = Duration::from_secs(2);
let binaries = [core];
let scope = TestScope {
packages: &[],
package_local: false,
whole_workspace: true,
};
let reach = Reachability::build(&plan, &binaries, &scope);
let (targets, savings) = census_targets(&plan, &reach, &killers);
let expected: HashSet<u32> = core::iter::once(1).collect();
assert_eq!(
targets.get(Utf8Path::new("/tmp/core")),
Some(&expected),
"a hint naming no reachable binary must not exempt the mutant from justifying its own census"
);
assert_eq!(
savings,
Duration::from_secs(2),
"an ineligible hint pays the baseline into savings exactly like an unhinted mutant"
);
}
#[test]
fn a_cap_that_leaves_the_mutants_unjudged_names_the_package_that_could_judge_them() {
let mut events = Recorder::default();
let plan = oracle_plan("core");
let named = [String::from("core")];
let scope = capped(&named);
warn_about_an_empty_oracle(&plan, &[oracle_binary("core", Some(0))], &scope, true, &[], &mut events);
let [warning] = events.warnings.as_slice() else {
panic!("one unjudged package is one warning, got {:?}", events.warnings);
};
assert!(warning.contains("`app`"), "the package that could help is not named: {warning}");
assert!(warning.contains("--test-package"), "the remedy is not named: {warning}");
assert!(warning.contains("--test-workspace"), "the wider remedy is not named: {warning}");
}
#[test]
fn a_retreat_does_not_substitute_the_mutated_packages_for_the_oracle_the_caller_named() {
let requested = vec![String::from("p"), String::from("r")];
let intended = vec![String::from("q")];
let kept = narrowed_oracle(requested, intended, &[String::from("p")]).unwrap();
assert_eq!(kept, vec![String::from("r")], "the retreat may only subtract");
}
#[test]
fn a_retreat_that_empties_the_oracle_is_reported_rather_than_run() {
let requested = vec![String::from("p")];
let intended = vec![String::from("q")];
let cause = narrowed_oracle(requested, intended, &[String::from("p")]).unwrap_err();
assert!(cause.to_string().contains("`p`"), "{cause}");
assert!(cause.to_string().contains("none of the packages"), "{cause}");
}
#[test]
fn a_retreat_from_an_unrestricted_oracle_names_what_was_checked() {
let intended = vec![String::from("q")];
let kept = narrowed_oracle(Vec::new(), intended, &[String::from("p")]).unwrap();
assert_eq!(kept, vec![String::from("q")]);
}
#[test]
fn an_oracle_emptied_by_a_retreat_blames_the_retreat_rather_than_the_cap() {
let mut events = Recorder::default();
let plan = oracle_plan("core");
let named = [String::from("core")];
let scope = capped(&named);
warn_about_an_empty_oracle(
&plan,
&[oracle_binary("core", Some(0))],
&scope,
false,
&[String::from("app")],
&mut events,
);
let [warning] = events.warnings.as_slice() else {
panic!("a retreat that empties the oracle is one warning, got {:?}", events.warnings);
};
assert!(warning.contains("could not compile `app`"), "{warning}");
assert!(
!warning.contains("--test-package"),
"the remedy must not name what just failed: {warning}"
);
}
#[test]
fn a_cap_whose_own_tests_reach_the_mutants_says_nothing() {
let mut events = Recorder::default();
let plan = oracle_plan("core");
let named = [String::from("core")];
let scope = capped(&named);
warn_about_an_empty_oracle(&plan, &[oracle_binary("core", Some(4))], &scope, true, &[], &mut events);
assert!(events.warnings.is_empty(), "{:?}", events.warnings);
}
#[test]
fn an_uncounted_suite_is_not_reported_as_an_empty_one() {
let mut events = Recorder::default();
let plan = oracle_plan("core");
let named = [String::from("core")];
let scope = capped(&named);
warn_about_an_empty_oracle(&plan, &[oracle_binary("core", None)], &scope, true, &[], &mut events);
assert!(events.warnings.is_empty(), "{:?}", events.warnings);
}
#[test]
fn a_cap_the_caller_chose_is_left_to_stand() {
let mut events = Recorder::default();
let plan = oracle_plan("core");
let named = [String::from("core")];
let scope = capped(&named);
warn_about_an_empty_oracle(&plan, &[oracle_binary("core", Some(0))], &scope, false, &[], &mut events);
assert!(events.warnings.is_empty(), "{:?}", events.warnings);
}
#[test]
fn a_package_nothing_else_links_gets_no_advice() {
let mut events = Recorder::default();
let plan = oracle_plan("app");
let named = [String::from("app")];
let scope = capped(&named);
warn_about_an_empty_oracle(&plan, &[oracle_binary("app", Some(0))], &scope, true, &[], &mut events);
assert!(events.warnings.is_empty(), "{:?}", events.warnings);
}
#[test]
fn a_whole_workspace_oracle_is_never_told_to_widen() {
let mut events = Recorder::default();
let plan = oracle_plan("core");
let named = [String::from("harness")];
let scope = TestScope {
packages: &named,
package_local: false,
whole_workspace: true,
};
warn_about_an_empty_oracle(&plan, &[oracle_binary("harness", Some(0))], &scope, true, &[], &mut events);
assert!(events.warnings.is_empty(), "{:?}", events.warnings);
}
}