use core::fmt::Write as _;
use core::ops::Range;
use std::process::{Command, Stdio};
use std::sync::mpsc;
use std::{fs, io, thread};
use camino::Utf8Path;
use serde_json::Value;
use super::complaints::Diagnostic;
use super::invoke::{
OutputLimits, Stream, compile, drained, finish_readers, is_progress, read_pipe, read_pipe_with_limits, rendered_diagnostic,
spawn_failure, supervise, supervise_with_limits,
};
use super::messages::dep_files;
use super::*;
use crate::discover::TargetFile;
use crate::schema::Position;
fn ordinals_blamed(stdout: &str, root: &Utf8Path, guards: &Guards) -> HashSet<u32> {
blame(stdout, root, guards).into_keys().collect()
}
fn artifact_message(path: &Utf8Path) -> String {
serde_json::json!({
"reason": "compiler-artifact",
"filenames": [path.as_str()],
})
.to_string()
}
#[test]
fn cargos_progress_bar_is_told_apart_from_the_things_it_wanted_to_say() {
assert!(is_progress(" Building [====> ] 4/17: serde_core, quote"));
assert!(is_progress(" Compiling [=> ] 1/9: syn"));
assert!(is_progress("\u{1b}[1;36m Building\u{1b}[0m [====> ] 4/17: serde_core, quote"));
assert!(!is_progress(" Compiling serde v1.0.229"));
assert!(!is_progress(
" Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.14s"
));
assert!(!is_progress(""));
}
#[test]
fn a_diagnostic_is_surfaced_the_way_the_compiler_rendered_it() {
let message = |level: &str| {
format!(
r#"{{"reason":"compiler-message","message":{{"level":"{level}","message":"mismatched types","rendered":"error[E0308]: mismatched types\n --> src/lib.rs:2:5\n"}}}}"#
)
};
assert_eq!(
rendered_diagnostic(&message("error")).as_deref(),
Some("error[E0308]: mismatched types\n --> src/lib.rs:2:5\n")
);
assert!(rendered_diagnostic(&message("warning")).is_some());
}
#[test]
fn a_line_that_is_not_a_compiler_message_yields_no_diagnostic() {
assert!(rendered_diagnostic(r#"{"reason":"compiler-artifact","target":{"name":"serde"}}"#).is_none());
assert!(rendered_diagnostic(" Building [====> ] 4/17: serde").is_none());
assert!(rendered_diagnostic("").is_none());
}
#[test]
fn a_message_without_a_rendering_yields_nothing() {
let bare = r#"{"reason":"compiler-message","message":{"level":"error","message":"could not find `nope`","spans":[]}}"#;
assert!(rendered_diagnostic(bare).is_none());
assert!(rendered_diagnostic(r#"{"reason":"compiler-message","message":{"level":"error","rendered":" "}}"#).is_none());
}
#[test]
fn the_reader_splits_on_carriage_returns_as_well_as_newlines() {
let (sender, lines) = mpsc::sync_channel(64);
let text = "one\rtwo\nthree\r\nfour";
let collected = read_pipe(io::Cursor::new(text), Stream::Prose, &sender)
.expect("spawn reader")
.join()
.expect("reader");
drop(sender);
let seen: Vec<String> = lines.into_iter().map(|(_, line)| line).collect();
assert_eq!(seen, ["one", "two", "three", "four"]);
assert_eq!(collected.text, text.as_bytes());
assert!(collected.complete, "a stream read to its end is the whole of it");
assert!(collected.within_limits, "short output stays within the normal output limit");
}
#[test]
fn a_pipe_that_fails_part_way_is_not_read_as_the_end_of_the_stream() {
struct Faltering(bool);
impl io::Read for Faltering {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if self.0 {
return Err(io::Error::other("the read a test asked to fail"));
}
self.0 = true;
let said = b"{\"reason\":\"compiler-artifact\"}\n";
buf[..said.len()].copy_from_slice(said);
Ok(said.len())
}
}
let (sender, _lines) = mpsc::sync_channel(64);
let reader = read_pipe(Faltering(false), Stream::Json, &sender).expect("spawn reader");
assert!(
drained(Some(reader), Instant::now() + Duration::from_secs(5)).is_none(),
"a reader that stopped on an error must not pass its bytes off as the whole stream"
);
}
#[test]
fn a_build_past_its_output_limit_fails_without_retaining_the_excess() {
let directory = crate::testing::workdir("build-output-limit-");
let root = Utf8PathBuf::from_path_buf(directory.path().to_path_buf()).expect("utf8");
let work = Workspace::adopt(root.clone(), root.join("target"));
let mut command = Command::new(crate::testing::helper_binary_path().as_std_path());
let _configured = command
.arg(crate::testing::directive("print:the compiler said more than sixteen bytes"))
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let error = supervise_with_limits(
command,
&work,
Some(Duration::from_secs(30)),
&mut crate::testing::Recorder::default(),
OutputLimits {
retained: 16,
line: 64,
backlog: 1,
},
)
.expect_err("output above the configured limit is not parsed as a complete build");
assert!(
error
.to_string()
.contains("configured 16-byte retained or 64-byte per-line build-output limit"),
"{error}"
);
}
#[test]
fn an_over_limit_pipe_keeps_only_its_configured_prefix() {
let (sender, _lines) = mpsc::sync_channel(4);
let pipe = read_pipe_with_limits(
io::Cursor::new("0123456789\n"),
Stream::Prose,
&sender,
OutputLimits {
retained: 4,
line: 16,
backlog: 4,
},
)
.expect("spawn reader")
.join()
.expect("reader");
assert_eq!(pipe.text, b"0123");
assert!(pipe.complete, "the pipe was still drained to EOF");
assert!(!pipe.within_limits, "the capped output is explicitly marked incomplete");
}
#[test]
fn reader_thread_creation_failure_is_reported_as_incomplete_output() {
let (sender, lines) = mpsc::sync_channel(1);
let _refused = crate::exec::faults::arm(crate::exec::faults::Fault::Thread);
let failure = read_pipe(io::Cursor::new("cargo output"), Stream::Prose, &sender).expect_err("thread creation was asked to fail");
drop(sender);
let mut events = crate::testing::Recorder::default();
let (stdout, _stderr) = finish_readers(
Some(Err(failure)),
None,
&lines,
&mut events,
Instant::now() + Duration::from_secs(1),
);
assert!(stdout.is_none(), "a reader that could not be created was treated as complete");
}
#[test]
fn finishing_readers_drains_a_backpressured_narration_channel() {
let (sender, lines) = mpsc::sync_channel(0);
let stdout = read_pipe_with_limits(
io::Cursor::new("first\nsecond\n"),
Stream::Prose,
&sender,
OutputLimits {
retained: 64,
line: 64,
backlog: 0,
},
);
let mut events = crate::testing::Recorder::default();
let (stdout, stderr) = finish_readers(Some(stdout), None, &lines, &mut events, Instant::now() + Duration::from_secs(5));
assert_eq!(stdout.expect("the reader was drained").text, b"first\nsecond\n");
assert!(stderr.is_some(), "an absent stderr pipe is an empty complete stream");
}
use crate::ops::collect::Shape;
fn stuck_reason(convergence: Convergence) -> String {
match convergence {
Convergence::Built(_stdout) => panic!("the build was expected not to compile"),
Convergence::Stuck(reason) => reason.to_string(),
}
}
fn trivial_workspace(prefix: &str) -> (tempfile::TempDir, Workspace) {
let dir = crate::testing::workdir(prefix);
let root = Utf8PathBuf::from_path_buf(dir.path().join("src")).expect("utf8");
fs::create_dir_all(root.join("src").as_std_path()).expect("src");
fs::write(
root.join("Cargo.toml").as_std_path(),
"[package]\nname = \"trivial\"\nversion = \"0.0.0\"\nedition = \"2024\"\n\n[workspace]\n",
)
.expect("manifest");
fs::write(root.join("src/lib.rs").as_std_path(), "pub const A: i32 = 1;\n").expect("lib");
let target = Utf8PathBuf::from_path_buf(dir.path().join("target")).expect("utf8");
let work = Workspace::adopt(root, target);
(dir, work)
}
#[test]
fn a_build_that_no_guard_explains_stops_with_the_compiler_output() {
let (_dir, work) = trivial_workspace("build-unattributed-");
fs::write(
work.root.join("src/lib.rs").as_std_path(),
"pub const A: i32 = \"not an integer\";\n",
)
.expect("lib");
let plan = empty_plan(&work);
let limits = BuildLimits::default();
let convergence = Converger::default()
.converge(
&work,
&plan,
None,
&["build", "--tests", "--keep-going"],
limits,
&mut crate::testing::Recorder::default(),
)
.expect("the build ran, however badly");
assert!(stuck_reason(convergence).contains("could not be attributed"));
}
#[test]
fn a_build_that_never_reached_the_compiler_says_so_rather_than_blaming_the_tree() {
let (_dir, work) = trivial_workspace("build-no-diagnostics-");
fs::write(
work.root.join("build.rs").as_std_path(),
"fn main() { panic!(\"the build script refused\"); }\n",
)
.expect("build script");
let plan = empty_plan(&work);
let convergence = Converger::default()
.converge(
&work,
&plan,
None,
&["build", "--tests", "--keep-going"],
BuildLimits::default(),
&mut crate::testing::Recorder::default(),
)
.expect("the build ran, however badly");
let reason = stuck_reason(convergence);
assert!(reason.contains("the compiler reported nothing"), "{reason}");
assert!(
!reason.contains("does not compile"),
"the message for a tree rustc rejected must not be reused here: {reason}"
);
assert!(
reason.contains("the build script refused"),
"cargo's own words are the only account of this failure: {reason}"
);
}
#[test]
fn a_narrowed_build_that_fails_is_retried_across_the_whole_workspace() {
let (_dir, work) = trivial_workspace("build-widen-");
let mut plan = empty_plan(&work);
let select = vec!["no-such-package".to_owned()];
let build = Converger::default()
.finish(
&work,
&mut plan,
Some(&select),
BuildLimits::default(),
&mut crate::testing::Recorder::default(),
)
.expect("widening to the whole workspace must succeed");
assert!(build.widened, "the build should have reported that it widened");
}
#[test]
fn the_final_build_still_compiles_examples() {
let (_dir, work) = trivial_workspace("build-example-");
fs::create_dir_all(work.root.join("examples").as_std_path()).expect("examples");
fs::write(
work.root.join("examples/broken.rs").as_std_path(),
"fn main() { let _: i32 = \"not an integer\"; }\n",
)
.expect("example");
let mut plan = empty_plan(&work);
let build = Converger::default()
.finish(
&work,
&mut plan,
Some(&["trivial".to_owned()]),
BuildLimits::default(),
&mut crate::testing::Recorder::default(),
)
.expect("the build reports the example failure");
assert!(
build.stuck.is_some(),
"a broken example must not disappear from the compilation oracle"
);
assert!(build.binaries.is_empty(), "a failed compilation produces no runnable oracle");
}
fn split_workspace(prefix: &str) -> (tempfile::TempDir, Workspace) {
let dir = crate::testing::workdir(prefix);
let root = Utf8PathBuf::from_path_buf(dir.path().join("src")).expect("utf8");
fs::create_dir_all(root.join("good/src").as_std_path()).expect("good");
fs::create_dir_all(root.join("broken/src").as_std_path()).expect("broken");
fs::write(
root.join("Cargo.toml").as_std_path(),
"[workspace]\nmembers = [\"good\", \"broken\"]\nresolver = \"3\"\n",
)
.expect("workspace manifest");
for member in ["good", "broken"] {
fs::write(
root.join(member).join("Cargo.toml").as_std_path(),
format!("[package]\nname = \"{member}\"\nversion = \"0.0.0\"\nedition = \"2024\"\n"),
)
.expect("member manifest");
}
fs::write(root.join("good/src/lib.rs").as_std_path(), "pub const A: i32 = 1;\n").expect("good lib");
fs::write(
root.join("broken/src/lib.rs").as_std_path(),
"pub const B: i32 = \"gamma-broken-marker\";\n",
)
.expect("broken lib");
let target = Utf8PathBuf::from_path_buf(dir.path().join("target")).expect("utf8");
let work = Workspace::adopt(root, target);
(dir, work)
}
fn unified_workspace(prefix: &str) -> (tempfile::TempDir, Workspace) {
let dir = crate::testing::workdir(prefix);
let root = Utf8PathBuf::from_path_buf(dir.path().join("src")).expect("utf8");
fs::create_dir_all(root.join("app/src").as_std_path()).expect("app");
fs::create_dir_all(root.join("leaf/src").as_std_path()).expect("leaf");
fs::write(
root.join("Cargo.toml").as_std_path(),
"[workspace]\nmembers = [\"app\", \"leaf\"]\nresolver = \"3\"\n",
)
.expect("workspace manifest");
fs::write(
root.join("app/Cargo.toml").as_std_path(),
"[package]\nname = \"app\"\nversion = \"0.0.0\"\nedition = \"2024\"\n\n\
[dependencies]\nleaf = { path = \"../leaf\", features = [\"wide\"] }\n",
)
.expect("app manifest");
fs::write(
root.join("leaf/Cargo.toml").as_std_path(),
"[package]\nname = \"leaf\"\nversion = \"0.0.0\"\nedition = \"2024\"\n\n[features]\nwide = []\n",
)
.expect("leaf manifest");
fs::write(
root.join("leaf/src/lib.rs").as_std_path(),
"#[cfg(feature = \"wide\")]\npub const WIDE: i32 = 1;\n\n\
pub fn value() -> i32 {\n WIDE\n}\n",
)
.expect("leaf lib");
fs::write(
root.join("app/src/lib.rs").as_std_path(),
"pub fn value() -> i32 {\n leaf::value()\n}\n",
)
.expect("app lib");
let target = Utf8PathBuf::from_path_buf(dir.path().join("target")).expect("utf8");
let work = Workspace::adopt(root, target);
(dir, work)
}
#[test]
fn a_check_that_only_the_whole_workspace_passes_says_so() {
let (_dir, work) = unified_workspace("build-preflight-unified-");
let plan = empty_plan(&work);
let select = vec!["leaf".to_owned()];
let cleared = Converger::preflight(
&work,
&plan,
Some(&select),
&select,
BuildLimits::default(),
&mut crate::testing::Recorder::default(),
)
.expect("the workspace compiles when its features are unified");
assert!(
cleared.whole_workspace,
"the check widened to succeed and then reported nothing about it"
);
assert!(cleared.dropped.is_empty(), "{:?}", cleared.dropped);
}
#[test]
fn a_whole_workspace_requirement_survives_into_the_staged_builds() {
let (_dir, work) = unified_workspace("build-stage-unified-");
let mut plan = empty_plan(&work);
let stage = vec!["leaf".to_owned()];
let mut narrowing = Converger::default();
assert!(
narrowing
.stage(
&work,
&mut plan,
&stage,
BuildLimits::default(),
&mut crate::testing::Recorder::default(),
)
.expect("the build runs")
.is_some(),
"the fixture is meant to be one a narrowed build cannot compile"
);
let mut whole = Converger::default();
whole.require_whole_workspace();
assert!(
whole
.stage(
&work,
&mut plan,
&stage,
BuildLimits::default(),
&mut crate::testing::Recorder::default(),
)
.expect("the build runs")
.is_none(),
"the scope the preflight validated was not carried into the staged build"
);
}
#[test]
fn a_broken_package_nobody_is_mutating_is_dropped_rather_than_failing_the_run() {
let (_dir, work) = split_workspace("build-preflight-retreat-");
let plan = empty_plan(&work);
let select = vec!["broken".to_owned(), "good".to_owned()];
let mutating = vec!["good".to_owned()];
let dropped = Converger::preflight(
&work,
&plan,
Some(&select),
&mutating,
BuildLimits::default(),
&mut crate::testing::Recorder::default(),
)
.expect("the package being mutated compiles on its own");
assert_eq!(dropped.dropped, vec!["broken".to_owned()]);
assert!(
!dropped.whole_workspace,
"a retreat narrows the scope, so it cannot be reporting that only the whole workspace built"
);
}
#[test]
fn a_broken_package_that_is_being_mutated_still_stops_the_run() {
let (_dir, work) = split_workspace("build-preflight-noretreat-");
fs::write(
work.root.join("good/src/lib.rs").as_std_path(),
"pub const A: i32 = \"gamma-selected-marker\";\n",
)
.expect("good lib");
let plan = empty_plan(&work);
let select = vec!["broken".to_owned(), "good".to_owned()];
let mutating = vec!["good".to_owned()];
let error = Converger::preflight(
&work,
&plan,
Some(&select),
&mutating,
BuildLimits::default(),
&mut crate::testing::Recorder::default(),
)
.expect_err("no narrowing makes the selected package compile");
assert!(error.to_string().contains("gamma-selected-marker"), "{error}");
}
#[test]
fn a_preflight_that_fails_both_ways_reports_the_selected_packages_errors() {
let (_dir, work) = trivial_workspace("build-preflight-both-");
fs::write(
work.root.join("src/lib.rs").as_std_path(),
"pub const A: i32 = \"gamma-narrow-marker\";\n",
)
.expect("lib");
let plan = empty_plan(&work);
let select = vec!["trivial".to_owned()];
let error = Converger::preflight(
&work,
&plan,
Some(&select),
&select,
BuildLimits::default(),
&mut crate::testing::Recorder::default(),
)
.expect_err("neither check can succeed");
assert!(error.to_string().contains("gamma-narrow-marker"), "{error}");
}
#[test]
fn a_whole_workspace_build_that_fails_is_reported_rather_than_retried() {
let (_dir, work) = trivial_workspace("build-nowiden-");
fs::write(
work.root.join("src/lib.rs").as_std_path(),
"pub const A: i32 = \"not an integer\";\n",
)
.expect("lib");
let mut plan = empty_plan(&work);
let build = Converger::default()
.finish(
&work,
&mut plan,
None,
BuildLimits::default(),
&mut crate::testing::Recorder::default(),
)
.expect("the build ran, however badly");
let stuck = build.stuck.expect("the build could not be made to compile");
assert!(stuck.reason.contains("could not be attributed"), "{}", stuck.reason);
assert!(build.binaries.is_empty(), "a build that never compiled has nothing to run");
}
#[test]
fn hitting_the_rollback_round_limit_is_reported_rather_than_retried_forever() {
let (_dir, work) = trivial_workspace("build-rollback-limit-");
let text = "pub const A: i32 = 1;\n";
let start = text.find('1').expect("the literal is in the fixture");
let mutant = Mutant {
span: start..start + 1,
replacement: "2".to_owned().into(),
..mutant()
};
let mut plan = empty_plan(&work);
plan.files.push(target_file(&work.root, "src/lib.rs"));
plan.mutants.push(mutant);
let limits = BuildLimits {
timeout: None,
multiplier: None,
rollback_rounds: 1,
};
let convergence = Converger::default()
.converge(
&work,
&plan,
None,
&["build", "--keep-going"],
limits,
&mut crate::testing::Recorder::default(),
)
.expect("a guard around a const initializer cannot compile");
assert!(stuck_reason(convergence).contains("rollback"));
}
#[test]
fn a_stage_that_cannot_be_converged_abandons_its_mutants_as_never_built() {
let (_dir, work) = trivial_workspace("build-stage-abandons-");
let mut plan = unguardable_plan(&work, 1);
plan.mutants[0].package = ("trivial".to_owned()).into();
let limits = BuildLimits {
timeout: None,
multiplier: None,
rollback_rounds: 1,
};
let mut converger = Converger::default();
let abandoned = converger
.stage(
&work,
&mut plan,
&["trivial".to_owned()],
limits,
&mut crate::testing::Recorder::default(),
)
.expect("the stage ran, however badly")
.expect("a budget of one round cannot survive a failing round");
assert_eq!(abandoned.ordinals, vec![1]);
assert!(abandoned.reason.contains("rollback rounds"), "{}", abandoned.reason);
assert!(
abandoned.reason.contains("Mutants blamed in the last rounds of this build: 1"),
"{}",
abandoned.reason
);
assert_eq!(plan.mutants[0].outcome, Outcome::NotBuilt);
assert!(plan.mutants[0].note.is_some(), "the mutant should say why it never ran");
converger.settle(&mut plan);
assert_eq!(plan.mutants[0].outcome, Outcome::NotBuilt, "settling must not overwrite it");
}
#[test]
fn a_stage_the_run_gave_up_on_leaves_a_tree_the_next_build_can_still_compile() {
let (_dir, work) = trivial_workspace("build-stage-carries-on-");
let mut plan = unguardable_plan(&work, 1);
plan.mutants[0].package = ("trivial".to_owned()).into();
let limits = BuildLimits {
timeout: None,
multiplier: None,
rollback_rounds: 1,
};
let mut converger = Converger::default();
let packages = ["trivial".to_owned()];
let _abandoned = converger
.stage(&work, &mut plan, &packages, limits, &mut crate::testing::Recorder::default())
.expect("the stage ran, however badly")
.expect("a budget of one round cannot survive a failing round");
let again = converger
.stage(&work, &mut plan, &packages, limits, &mut crate::testing::Recorder::default())
.expect("the stage ran");
assert!(again.is_none(), "the tree compiles once the abandoned mutants are out of it");
}
#[test]
fn an_unattributed_failure_is_isolated_to_the_mutant_that_provably_breaks_the_build() {
let (_dir, work) = trivial_workspace("build-isolates-unattributed-");
let text = "pub fn bad() -> i32 { 1 }\n\
pub fn good(x: i32) -> i32 { x + 1 }\n";
let pristine = work.root.parent().expect("tree parent").join("pristine");
fs::create_dir_all(pristine.as_std_path()).expect("pristine");
fs::write(work.root.join("src/lib.rs").as_std_path(), text).expect("working source");
fs::write(pristine.join("lib.rs").as_std_path(), text).expect("pristine source");
let bad_start = text.find("{ 1 }").expect("bad body") + 2;
let good_start = text.find("x + 1").expect("addition");
let mut bad = mutant();
bad.ordinal = 1;
bad.span = bad_start..bad_start + 1;
bad.original = "1".into();
bad.replacement = "()".into();
bad.item_path = ("trivial::bad".to_owned()).into();
bad.package = ("trivial".to_owned()).into();
let mut good = mutant();
good.ordinal = 2;
good.span = good_start..good_start + "x + 1".len();
good.original = "x + 1".into();
good.replacement = "x - 1".into();
good.item_path = ("trivial::good".to_owned()).into();
good.package = ("trivial".to_owned()).into();
let mut plan = empty_plan(&work);
plan.files.push(TargetFile {
path: Utf8PathBuf::from("src/lib.rs"),
absolute: pristine.join("lib.rs"),
package: "trivial".to_owned(),
});
plan.mutants.extend([bad, good]);
fs::create_dir_all(work.root.join("gamma-rt/src").as_std_path()).expect("runtime source");
fs::write(
work.root.join("gamma-rt/Cargo.toml").as_std_path(),
"[package]\nname = \"cargo-gamma-rt\"\nversion = \"0.0.0\"\nedition = \"2024\"\n\n\
[lib]\nname = \"gamma_rt\"\n",
)
.expect("runtime manifest");
fs::write(
work.root.join("gamma-rt/src/lib.rs").as_std_path(),
"pub const fn a(_: u32) -> bool { false }\n",
)
.expect("runtime library");
work.link_runtime("trivial", &plan.files).expect("runtime linked");
let isolated = Converger::default()
.isolate(
&work,
&plan,
Some(&["trivial".to_owned()]),
&["build", "--keep-going"],
BuildLimits::default(),
&mut crate::testing::Recorder::default(),
)
.expect("isolation builds ran")
.expect("the bad mutant was isolated");
assert!(matches!(isolated, Isolation::Blamed(ordinals) if ordinals == vec![1]));
}
#[test]
fn an_item_only_interaction_settles_its_mutants_to_not_built() {
#[expect(
clippy::unnecessary_wraps,
reason = "the oracle matches subset_fails's Option<bool> verdict, where None is an indeterminate build"
)]
fn only_the_pair_fails(active: &HashSet<u32>) -> Option<bool> {
Some(active.contains(&1) && active.contains(&2))
}
let (_dir, work) = trivial_workspace("build-isolates-interaction-");
let mut plan = empty_plan(&work);
plan.mutants = vec![Mutant { ordinal: 1, ..mutant() }, Mutant { ordinal: 2, ..mutant() }];
let mut converger = Converger {
subset_oracle: Some(only_the_pair_fails),
..Converger::default()
};
let isolated = converger
.isolate(
&work,
&plan,
None,
&["build", "--keep-going"],
BuildLimits::default(),
&mut crate::testing::Recorder::default(),
)
.expect("the scripted proof builds ran")
.expect("the interaction was isolated to its item");
assert!(
matches!(&isolated, Isolation::Item(ordinals) if *ordinals == vec![1, 2]),
"an interaction with no single culprit must be an item isolation, not a blamed one"
);
let ordinals = match &isolated {
Isolation::Blamed(ordinals) | Isolation::Item(ordinals) => ordinals.clone(),
};
for ordinal in &ordinals {
let _ = converger.withdrawn.insert(*ordinal);
}
if let Isolation::Item(ordinals) = &isolated {
converger.abandoned.extend(ordinals.iter().copied());
}
converger.settle(&mut plan);
assert_eq!(
plan.mutants[0].outcome,
Outcome::NotBuilt,
"an item-only interaction is never judged"
);
assert_eq!(
plan.mutants[1].outcome,
Outcome::NotBuilt,
"an item-only interaction is never judged"
);
}
fn unguardable_plan(work: &Workspace, ordinal: u32) -> Plan {
let text = "pub const A: i32 = 1;\n";
let start = text.find('1').expect("the literal is in the fixture");
let pristine = work.root.parent().expect("the tree has a parent").join("pristine");
fs::create_dir_all(pristine.as_std_path()).expect("pristine");
fs::write(pristine.join("lib.rs").as_std_path(), text).expect("pristine source");
let mut plan = empty_plan(work);
plan.files.push(TargetFile {
path: Utf8PathBuf::from("src/lib.rs"),
absolute: pristine.join("lib.rs"),
package: "trivial".to_owned(),
});
plan.mutants.push(Mutant {
ordinal,
span: start..start + 1,
replacement: "2".to_owned().into(),
..mutant()
});
plan
}
#[test]
fn each_build_gets_the_whole_round_budget_rather_than_what_earlier_builds_left() {
let (_dir, work) = trivial_workspace("build-budget-per-build-");
let limits = BuildLimits {
timeout: None,
multiplier: None,
rollback_rounds: 2,
};
let mut converger = Converger::default();
let first = unguardable_plan(&work, 1);
let first_built = converger
.converge(
&work,
&first,
None,
&["build", "--keep-going"],
limits,
&mut crate::testing::Recorder::default(),
)
.expect("one withdrawal is enough to make the tree compile");
assert!(matches!(first_built, Convergence::Built(_)), "the tree must converge");
assert_eq!(converger.rounds, 2, "one failed round and one that succeeded");
let second = unguardable_plan(&work, 2);
let second_built = converger
.converge(
&work,
&second,
None,
&["build", "--keep-going"],
limits,
&mut crate::testing::Recorder::default(),
)
.expect("the second build must get a budget of its own");
assert!(matches!(second_built, Convergence::Built(_)), "the tree must converge");
assert_eq!(converger.rounds, 2, "the counter is per build, not per run");
assert_eq!(converger.total_rounds, 4, "the run still reports what it spent in total");
assert_eq!(converger.withdrawn(), 2, "withdrawals carry across builds");
}
#[test]
fn the_rollback_limit_error_describes_only_the_build_that_hit_it() {
let (_dir, work) = trivial_workspace("build-limit-series-");
let mut converger = Converger {
rounds: 9,
total_rounds: 9,
per_round: vec![41],
..Converger::default()
};
let limits = BuildLimits {
timeout: None,
multiplier: None,
rollback_rounds: 1,
};
let plan = unguardable_plan(&work, 1);
let error = stuck_reason(
converger
.converge(
&work,
&plan,
None,
&["build", "--keep-going"],
limits,
&mut crate::testing::Recorder::default(),
)
.expect("a budget of one round cannot survive a failing round"),
);
assert!(error.contains("1 of the 1 rollback rounds"), "{error}");
assert!(!error.contains("41 blamed during this build"), "{error}");
assert!(!error.contains("last rounds of this build: 41"), "{error}");
assert!(!error.contains("10 of"), "{error}");
}
#[test]
fn converging_when_the_tree_is_missing_reports_the_failure_rather_than_looping() {
let work = Workspace::adopt(
Utf8PathBuf::from("/gamma/definitely/not/a/directory"),
Utf8PathBuf::from("/gamma/definitely/not/a/directory/target"),
);
let plan = empty_plan(&work);
let error = Converger::default()
.converge(
&work,
&plan,
None,
&["build", "--keep-going"],
BuildLimits::default(),
&mut crate::testing::Recorder::default(),
)
.expect_err("cargo cannot be spawned in a directory that does not exist");
assert!(error.to_string().contains("disappeared"), "{error}");
}
#[test]
fn converging_a_mutant_whose_span_no_longer_fits_the_file_reports_the_internal_error() {
let (_dir, work) = trivial_workspace("build-converge-missing-guard-");
let text = fs::read_to_string(work.root.join("src/lib.rs").as_std_path()).expect("lib");
let mutant = Mutant {
span: text.len() + 10..text.len() + 11,
..mutant()
};
let mut plan = empty_plan(&work);
plan.files.push(target_file(&work.root, "src/lib.rs"));
plan.mutants.push(mutant);
let error = Converger::default()
.converge(
&work,
&plan,
None,
&["build", "--keep-going"],
BuildLimits::default(),
&mut crate::testing::Recorder::default(),
)
.expect_err("the span is out of range");
assert!(error.to_string().contains("no guard was emitted"), "{error}");
}
#[test]
fn a_stage_that_fails_to_compile_reports_the_failure() {
let (_dir, work) = trivial_workspace("build-stage-fails-");
fs::write(
work.root.join("src/lib.rs").as_std_path(),
"pub const A: i32 = \"not an integer\";\n",
)
.expect("lib");
let mut plan = empty_plan(&work);
let abandoned = Converger::default()
.stage(
&work,
&mut plan,
&["trivial".to_owned()],
BuildLimits::default(),
&mut crate::testing::Recorder::default(),
)
.expect("the stage ran, however badly")
.expect("the package does not compile");
assert!(abandoned.reason.contains("could not be attributed"), "{}", abandoned.reason);
}
#[test]
fn widening_to_the_whole_workspace_that_also_fails_reports_the_original_narrow_error() {
let (_dir, work) = trivial_workspace("build-widen-fails-");
fs::write(
work.root.join("src/lib.rs").as_std_path(),
"pub const A: i32 = \"not an integer\";\n",
)
.expect("lib");
let mut plan = empty_plan(&work);
let select = vec!["trivial".to_owned()];
let stuck = Converger::default()
.finish(
&work,
&mut plan,
Some(&select),
BuildLimits::default(),
&mut crate::testing::Recorder::default(),
)
.expect("the build ran, however badly")
.stuck
.expect("neither the narrowed nor the widened build compiles");
assert!(stuck.reason.contains("could not be attributed"), "{}", stuck.reason);
}
#[test]
fn a_live_mutant_whose_span_no_longer_fits_the_file_is_an_internal_error_rather_than_a_silent_survivor() {
let (_dir, work) = trivial_workspace("build-missing-guard-");
let text = fs::read_to_string(work.root.join("src/lib.rs").as_std_path()).expect("lib");
let mutant = Mutant {
span: text.len() + 10..text.len() + 11,
..mutant()
};
let mut plan = empty_plan(&work);
plan.files.push(target_file(&work.root, "src/lib.rs"));
plan.mutants.push(mutant);
let error = Splices::default()
.instrument(&work, &plan, &HashSet::default())
.expect_err("the span is out of range");
assert!(error.to_string().contains("no guard was emitted"), "{error}");
}
#[test]
fn a_survey_file_that_can_no_longer_be_read_stops_instrumentation_with_a_named_error() {
let (_dir, work) = trivial_workspace("build-unreadable-file-");
let mut plan = empty_plan(&work);
plan.files.push(target_file(&work.root, "src/gone.rs"));
let error = Splices::default()
.instrument(&work, &plan, &HashSet::default())
.expect_err("the file was never created");
assert!(error.to_string().contains("could not read"), "{error}");
}
#[test]
fn overlapping_mutant_spans_are_reported_as_an_ambiguous_splice() {
let (_dir, work) = trivial_workspace("build-overlap-");
let text = "fn f(a: i32, b: i32, c: i32) -> i32 { a + b + c }";
fs::write(work.root.join("src/lib.rs").as_std_path(), text).expect("lib");
let left = text.find("a + b").expect("the fixture contains this text");
let right = text.find("b + c").expect("the fixture contains this text");
let mut plan = empty_plan(&work);
plan.files.push(target_file(&work.root, "src/lib.rs"));
plan.mutants.push(Mutant {
ordinal: 1,
span: left..left + "a + b".len(),
..mutant()
});
plan.mutants.push(Mutant {
ordinal: 2,
span: right..right + "b + c".len(),
..mutant()
});
let error = Splices::default()
.instrument(&work, &plan, &HashSet::default())
.expect_err("the spans overlap without nesting");
assert!(error.to_string().contains("overlap"), "{error}");
}
#[test]
fn sorting_a_staged_plan_reindexes_mutants_before_the_baseline_splice() {
let (_dir, work) = trivial_workspace("build-sort-reindexes-");
let a_text = "pub fn a() -> bool { true }\n";
let b_text = "pub fn b() -> bool { true }\n";
fs::write(work.root.join("src/a.rs").as_std_path(), a_text).expect("a");
fs::write(work.root.join("src/b.rs").as_std_path(), b_text).expect("b");
let mut plan = empty_plan(&work);
plan.files
.extend([target_file(&work.root, "src/a.rs"), target_file(&work.root, "src/b.rs")]);
let mut b = mutant();
b.file = Utf8PathBuf::from("src/b.rs").into();
b.span = b_text.find("true").expect("true")..b_text.find("true").expect("true") + 4;
plan.mutants.push(b);
let mut splices = Splices::default();
let _guards = splices
.instrument(&work, &plan, &HashSet::default())
.expect("the first stage is instrumented");
let mut a = mutant();
a.ordinal = 2;
a.file = Utf8PathBuf::from("src/a.rs").into();
a.span = a_text.find("true").expect("true")..a_text.find("true").expect("true") + 4;
plan.mutants.push(a);
let _guards = splices
.instrument(&work, &plan, &HashSet::default())
.expect("the second stage is instrumented");
plan.sort();
splices.plan_reordered();
let guards = splices
.instrument(&work, &plan, &HashSet::from_iter([1]))
.expect("the baseline withdrawal reindexes the sorted plan");
assert_eq!(guards.get(&2).map(|(path, _guard)| path.as_str()), Some("src/a.rs"));
assert!(!guards.contains_key(&1), "the withdrawn b mutant has no guard");
}
#[test]
fn a_mutants_file_the_copy_never_created_reports_the_write_failure() {
let (_dir, work) = trivial_workspace("build-uncopied-destination-");
let mut plan = empty_plan(&work);
plan.files.push(TargetFile {
path: Utf8PathBuf::from("src/never_copied.rs"),
absolute: work.root.join("src/lib.rs"),
package: "trivial".to_owned(),
});
let error = Splices::default()
.instrument(&work, &plan, &HashSet::default())
.expect_err("the destination was never copied");
assert!(error.to_string().contains("which the copy did not create"), "{error}");
}
fn target_file(root: &Utf8Path, path: &str) -> TargetFile {
TargetFile {
path: Utf8PathBuf::from(path),
absolute: root.join(path),
package: "trivial".to_owned(),
}
}
fn empty_plan(work: &Workspace) -> Plan {
Plan {
skipped: Vec::new(),
digests: HashMap::default(),
root: work.root.clone(),
files: Vec::new(),
mutants: Vec::new(),
suppressed: 0,
idle: Vec::new(),
sharded_out: 0,
settled_out: 0,
reach: HashMap::default(),
specs: HashMap::default(),
}
}
#[test]
fn a_build_that_outstays_its_budget_is_stopped() {
let (_dir, work) = trivial_workspace("build-budget-");
let outcome = compile(
&work,
&["check".to_owned()],
Some(Duration::ZERO),
&mut crate::testing::Recorder::default(),
)
.expect("spawn");
assert!(outcome.is_none(), "a build past its budget should report no output");
}
#[test]
fn a_build_stopped_by_its_budget_reports_no_stdout() {
let (_dir, work) = trivial_workspace("build-nostdout-");
let limits = BuildLimits {
timeout: Some(Duration::ZERO),
multiplier: None,
rollback_rounds: 0,
};
let compiled = run_cargo(
&work,
&empty_plan(&work),
&["check"],
None,
limits,
None,
&mut crate::testing::Recorder::default(),
)
.expect("spawn");
assert!(!compiled.succeeded);
assert!(compiled.stdout.is_none());
}
#[test]
fn converging_on_a_build_that_never_finishes_stops_with_the_budget() {
let (_dir, work) = trivial_workspace("build-converge-budget-");
let plan = Plan {
skipped: Vec::new(),
digests: HashMap::default(),
root: work.root.clone(),
files: Vec::new(),
mutants: Vec::new(),
suppressed: 0,
idle: Vec::new(),
sharded_out: 0,
settled_out: 0,
reach: HashMap::default(),
specs: HashMap::default(),
};
let limits = BuildLimits {
timeout: Some(Duration::ZERO),
multiplier: None,
rollback_rounds: 0,
};
let error = Converger::default()
.converge(&work, &plan, None, &["check"], limits, &mut crate::testing::Recorder::default())
.expect_err("the build never finishes");
assert!(error.to_string().contains("was still running"), "{error}");
assert!(error.to_string().contains("--build-timeout"), "{error}");
}
#[test]
fn a_build_inside_its_budget_is_collected() {
let (_dir, work) = trivial_workspace("build-collected-");
let outcome = compile(
&work,
&["--version".to_owned()],
Some(Duration::from_mins(2)),
&mut crate::testing::Recorder::default(),
)
.expect("spawn")
.expect("cargo should finish well inside two minutes");
assert!(outcome.status.success(), "{outcome:?}");
}
#[test]
fn a_build_stopped_by_its_budget_takes_its_descendants_with_it() {
crate::testing::within(crate::testing::WATCHDOG, "a build with a descendant", || {
let started = crate::testing::workdir("build-descendant-");
let root = Utf8PathBuf::from_path_buf(started.path().to_path_buf()).expect("a UTF-8 scratch path");
let (running, survived) = (root.join("running"), root.join("survived"));
let mut command = Command::new(crate::testing::helper_binary_path().as_std_path());
let _configured = command
.arg(crate::testing::directive(format_args!(
"spawn:touch:{running}|sleep:3000|touch:{survived}"
)))
.arg(crate::testing::directive("sleep:30000"))
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let work = Workspace::adopt(root.clone(), root.join("target"));
let outcome = supervise(
command,
&work,
Some(Duration::from_millis(750)),
&mut crate::testing::Recorder::default(),
)
.expect("the build was supervised");
assert!(outcome.is_none(), "a build past its budget reports no output");
assert!(
running.as_std_path().exists(),
"the descendant never started, so this test proves nothing about killing it"
);
thread::sleep(Duration::from_millis(3500));
assert!(
!survived.as_std_path().exists(),
"the build's descendant outlived the kill that was aimed at the build"
);
});
}
#[test]
fn a_finished_build_is_collected_without_waiting_for_its_survivors() {
crate::testing::within(crate::testing::WATCHDOG, "a build with a survivor", || {
let started = crate::testing::workdir("build-survivor-");
let root = Utf8PathBuf::from_path_buf(started.path().to_path_buf()).expect("a UTF-8 scratch path");
let running = root.join("running");
let mut command = Command::new(crate::testing::helper_binary_path().as_std_path());
let _configured = command
.arg(crate::testing::directive(format_args!("spawn:touch:{running}|sleep:20000")))
.arg(crate::testing::directive(format_args!("wait-file:{running}|5000")))
.arg(crate::testing::directive("print:the build said this"))
.arg(crate::testing::directive("exit:0"))
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let work = Workspace::adopt(root.clone(), root.join("target"));
let began = Instant::now();
let output = supervise(
command,
&work,
Some(Duration::from_secs(45)),
&mut crate::testing::Recorder::default(),
)
.expect("the build was supervised")
.expect("the build finished well inside its budget");
assert!(output.status.success(), "{output:?}");
assert!(
running.as_std_path().exists(),
"the survivor never started, so this test proves nothing about outliving it"
);
assert!(
began.elapsed() < Duration::from_secs(10),
"collecting the build waited for the survivor holding its pipes"
);
assert!(
String::from_utf8_lossy(&output.stdout).contains("the build said this"),
"what the build itself said has to survive the sweep: {output:?}"
);
});
}
#[test]
fn a_missing_scratch_tree_is_named_rather_than_blamed_on_cargo() {
let work = Workspace::adopt(
Utf8PathBuf::from("/gamma/definitely/not/a/directory"),
Utf8PathBuf::from("/gamma/definitely/not/a/directory/target"),
);
let error = compile(&work, &["--version".to_owned()], None, &mut crate::testing::Recorder::default()).expect_err("no such directory");
assert!(error.to_string().contains("disappeared"), "{error}");
}
#[test]
fn a_cargo_that_cannot_be_found_names_the_program_and_where_it_came_from() {
let root = tempfile::TempDir::new().expect("temp");
let root = Utf8PathBuf::from_path_buf(root.path().to_path_buf()).expect("utf8");
let work = Workspace::adopt(root.clone(), root.join("target"));
let cause = io::Error::from(io::ErrorKind::NotFound);
let error = spawn_failure("gamma-no-such-cargo-binary", &work, cause);
assert!(error.to_string().contains("gamma-no-such-cargo-binary"), "{error}");
assert!(error.to_string().contains("CARGO"), "{error}");
}
fn at(line: u32, column: u32) -> Position {
Position::new(line, column).expect("a position written into a test is one-based by construction")
}
fn guard(site: Range<Position>, mutated: Option<Range<Position>>) -> Guard {
Guard { site, mutated }
}
fn span(file: &str, line_start: u32, column_start: u32, line_end: u32, column_end: u32, primary: bool) -> Value {
serde_json::json!({
"file_name": file,
"line_start": line_start,
"column_start": column_start,
"line_end": line_end,
"column_end": column_end,
"is_primary": primary,
})
}
fn compiler_message(spans: &[Value]) -> String {
serde_json::json!({
"reason": "compiler-message",
"message": {
"level": "error",
"rendered": "error: boom\n",
"spans": spans,
},
})
.to_string()
}
fn coded_message(code: &str, primary: &[Value], notes: &[Value]) -> String {
serde_json::json!({
"reason": "compiler-message",
"message": {
"level": "error",
"code": { "code": code },
"rendered": "error: boom\n",
"spans": primary,
"children": [{ "level": "note", "spans": notes }],
},
})
.to_string()
}
fn mutant() -> Mutant {
Mutant {
id: "deadbeefcafe".to_owned().into(),
ordinal: 1,
file: (Utf8PathBuf::from("src/lib.rs")).into(),
package: ("pkg".to_owned()).into(),
span: 0..1,
line: 7,
end_line: 7,
column: 3,
mutator: ("lit.true_to_false".to_owned()).into(),
item_path: ("pkg::f".to_owned()).into(),
trait_impl: None,
occurrence: 0,
replacement_index: 0,
original: "true".to_owned().into(),
replacement: "false".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,
}
}
#[test]
fn a_move_error_is_blamed_on_the_deletion_that_changed_which_paths_exist() {
let mut guards = Guards::default();
let _ = guards.insert(7, (Utf8PathBuf::from("src/codegen.rs"), guard(at(396, 9)..at(396, 17), None)));
let _ = guards.insert(
8,
(
Utf8PathBuf::from("src/codegen.rs"),
guard(at(400, 5)..at(400, 9), Some(at(400, 5)..at(400, 9))),
),
);
let stdout = coded_message(
"E0382",
&[span("src/codegen.rs", 432, 9, 432, 13, true)],
&[
span("src/codegen.rs", 372, 5, 372, 20, false),
span("src/codegen.rs", 383, 9, 383, 13, false),
],
);
let blamed = ordinals_blamed(&stdout, Utf8Path::new(""), &guards);
assert_eq!(blamed, HashSet::from_iter([7]), "the deletion is the only reachability change");
}
#[test]
fn a_withdrawal_remembers_the_error_code_that_caused_it() {
let mut guards = Guards::default();
let _ = guards.insert(
7,
(
Utf8PathBuf::from("src/lib.rs"),
guard(at(10, 5)..at(10, 9), Some(at(10, 5)..at(10, 9))),
),
);
let stdout = coded_message("E0308", &[span("src/lib.rs", 10, 5, 10, 9, true)], &[]);
assert_eq!(
blame(&stdout, Utf8Path::new(""), &guards).get(&7).map(String::as_str),
Some("E0308")
);
}
#[test]
fn a_secondary_span_intersecting_generated_text_blames_that_mutant() {
let mut guards = Guards::default();
let _ = guards.insert(
7,
(
Utf8PathBuf::from("src/lib.rs"),
guard(at(10, 5)..at(10, 45), Some(at(10, 20)..at(10, 25))),
),
);
let stdout = compiler_message(&[span("src/lib.rs", 12, 5, 12, 20, true), span("src/lib.rs", 10, 5, 10, 40, false)]);
assert_eq!(ordinals_blamed(&stdout, Utf8Path::new(""), &guards), HashSet::from_iter([7]));
}
#[test]
fn a_secondary_span_on_innocent_original_text_blames_nothing() {
let mut guards = Guards::default();
let _ = guards.insert(
7,
(
Utf8PathBuf::from("src/lib.rs"),
guard(at(10, 5)..at(10, 45), Some(at(10, 20)..at(10, 25))),
),
);
let stdout = compiler_message(&[span("src/lib.rs", 12, 5, 12, 20, true), span("src/lib.rs", 10, 30, 10, 40, false)]);
assert!(ordinals_blamed(&stdout, Utf8Path::new(""), &guards).is_empty());
}
#[test]
fn the_census_counts_mutants_rather_than_diagnostics_and_leads_with_the_densest_pair() {
let mut converger = Converger::default();
let plan = plan_of(&[(1, "lit.true_to_false"), (2, "lit.true_to_false"), (3, "expr.delete")]);
let _ = converger.census.insert(1, "E0308".to_owned());
let _ = converger.census.insert(2, "E0308".to_owned());
let _ = converger.census.insert(3, "E0382".to_owned());
let _ = converger.census.insert(1, "E0308".to_owned());
assert_eq!(
converger.tally(&plan),
vec![
Withdrawal {
code: "E0308".to_owned(),
mutator: "lit.true_to_false".to_owned(),
mutants: 2,
},
Withdrawal {
code: "E0382".to_owned(),
mutator: "expr.delete".to_owned(),
mutants: 1,
},
]
);
}
fn plan_of(entries: &[(u32, &str)]) -> Plan {
Plan {
skipped: Vec::new(),
digests: HashMap::default(),
root: Utf8PathBuf::new(),
files: Vec::new(),
mutants: entries
.iter()
.map(|(ordinal, mutator)| Mutant {
ordinal: *ordinal,
mutator: ((*mutator).to_owned()).into(),
..mutant()
})
.collect(),
suppressed: 0,
idle: Vec::new(),
sharded_out: 0,
settled_out: 0,
reach: HashMap::default(),
specs: HashMap::default(),
}
}
#[test]
fn an_error_that_is_not_flow_sensitive_is_never_blamed_by_region() {
let mut guards = Guards::default();
let _ = guards.insert(7, (Utf8PathBuf::from("src/codegen.rs"), guard(at(396, 9)..at(396, 17), None)));
let stdout = coded_message("E0432", &[span("src/codegen.rs", 432, 9, 432, 13, true)], &[]);
assert!(ordinals_blamed(&stdout, Utf8Path::new(""), &guards).is_empty());
let uncoded = compiler_message(&[span("src/codegen.rs", 432, 9, 432, 13, true)]);
assert!(ordinals_blamed(&uncoded, Utf8Path::new(""), &guards).is_empty());
}
#[test]
fn a_region_with_no_deletion_falls_back_to_every_guard_in_it() {
let mut guards = Guards::default();
let _ = guards.insert(
3,
(
Utf8PathBuf::from("src/codegen.rs"),
guard(at(400, 5)..at(400, 9), Some(at(400, 5)..at(400, 9))),
),
);
let _ = guards.insert(
4,
(
Utf8PathBuf::from("src/codegen.rs"),
guard(at(900, 5)..at(900, 9), Some(at(900, 5)..at(900, 9))),
),
);
let stdout = coded_message(
"E0499",
&[span("src/codegen.rs", 432, 9, 432, 13, true)],
&[span("src/codegen.rs", 383, 9, 383, 13, false)],
);
assert_eq!(ordinals_blamed(&stdout, Utf8Path::new(""), &guards), HashSet::from_iter([3]));
}
#[test]
fn a_diagnostic_that_lands_on_a_guard_is_still_blamed_on_that_guard_alone() {
let mut guards = Guards::default();
let _ = guards.insert(
5,
(
Utf8PathBuf::from("src/codegen.rs"),
guard(at(432, 9)..at(432, 13), Some(at(432, 9)..at(432, 13))),
),
);
let _ = guards.insert(6, (Utf8PathBuf::from("src/codegen.rs"), guard(at(396, 9)..at(396, 17), None)));
let stdout = coded_message(
"E0382",
&[span("src/codegen.rs", 432, 9, 432, 13, true)],
&[span("src/codegen.rs", 383, 9, 383, 13, false)],
);
assert_eq!(ordinals_blamed(&stdout, Utf8Path::new(""), &guards), HashSet::from_iter([5]));
}
#[test]
fn diagnostics_are_read_from_the_json_stream() {
let stdout = concat!(
r#"{"reason":"compiler-message","message":{"level":"error","rendered":"error[E0308]: boom"}}"#,
"\n",
r#"{"reason":"compiler-message","message":{"level":"warning","rendered":"just a warning"}}"#,
"\n",
r#"{"reason":"compiler-message"}"#,
"\n",
r#"{"reason":"compiler-message","message":{}}"#,
"\n",
r#"{"reason":"compiler-artifact","profile":{"test":true},"executable":"/tmp/x"}"#,
"\n",
"warning: unused manifest key\n",
);
let rendered = diagnostics(stdout).into_iter().map(|found| found.rendered).collect::<String>();
assert!(rendered.contains("E0308"));
assert!(!rendered.contains("just a warning"));
assert!(!rendered.contains("unused manifest key"));
}
#[test]
fn an_error_message_with_no_rendered_text_contributes_nothing_rather_than_panicking() {
let stdout = r#"{"reason":"compiler-message","message":{"level":"error"}}"#;
assert!(diagnostics(stdout).is_empty());
}
#[test]
fn a_long_diagnostic_list_keeps_the_first_errors_whole_and_counts_the_rest() {
let rendered: Vec<Diagnostic> = (0..8)
.map(|index| reported(None, &format!("error[E{index:04}]: something\n --> src/lib.rs:{index}\n")))
.collect();
let shown = leading(&rendered, 3);
assert!(shown.starts_with("error[E0000]"), "{shown}");
assert!(shown.contains("error[E0002]"), "{shown}");
assert!(!shown.contains("error[E0003]"), "{shown}");
assert!(shown.contains("and 5 further errors not shown"), "{shown}");
}
#[test]
fn a_diagnostic_list_within_the_limit_is_quoted_whole_and_counts_nothing() {
let rendered = vec![reported(None, "error[E0001]: something\n")];
assert_eq!(leading(&rendered, 3), "error[E0001]: something\n");
}
#[test]
fn errors_in_the_mutated_packages_are_quoted_before_anyone_elses() {
let mut found = vec![
reported(Some("/w/other/Cargo.toml"), "error: in the reverse dependency\n"),
reported(Some("/w/mine/Cargo.toml"), "error: in my own crate\n"),
reported(None, "error: from nowhere in particular\n"),
];
prioritize(&mut found, &core::iter::once("/w/mine/Cargo.toml".to_owned()).collect());
assert!(leading(&found, 1).starts_with("error: in my own crate\n"), "{}", leading(&found, 1));
assert!(
leading(&found, 3).contains("reverse dependency\nerror: from nowhere"),
"{}",
leading(&found, 3)
);
}
fn reported(manifest: Option<&str>, rendered: &str) -> Diagnostic {
Diagnostic {
manifest: manifest.map(ToOwned::to_owned),
rendered: rendered.to_owned(),
}
}
#[test]
fn diagnostic_spans_inside_mutated_text_name_that_mutant_exactly() {
let mut guards = Guards::default();
let file = Utf8PathBuf::from("src/lib.rs");
let _old = guards.insert(1, (file.clone(), guard(at(10, 1)..at(20, 1), Some(at(12, 5)..at(12, 10)))));
let _old = guards.insert(2, (file, guard(at(10, 1)..at(20, 1), None)));
let blamed = ordinals_blamed(
&compiler_message(&[span("src/lib.rs", 12, 6, 12, 8, true)]),
Utf8Path::new("/work"),
&guards,
);
assert_eq!(blamed, HashSet::from_iter([1]));
}
#[test]
fn a_diagnostic_inside_a_guard_blames_the_innermost_enclosing_site() {
let mut guards = Guards::default();
for ordinal in [1, 2, 3] {
let site = if ordinal == 1 { at(1, 1)..at(50, 1) } else { at(10, 1)..at(15, 1) };
let _old = guards.insert(ordinal, (Utf8PathBuf::from("src/lib.rs"), guard(site, None)));
}
let blamed = ordinals_blamed(
&compiler_message(&[span("src/lib.rs", 12, 2, 12, 4, true)]),
Utf8Path::new("/work"),
&guards,
);
assert_eq!(blamed, HashSet::from_iter([2, 3]));
}
#[test]
fn a_diagnostic_that_encloses_guards_keeps_only_the_smallest_reported_region() {
let mut guards = Guards::default();
let _old = guards.insert(1, (Utf8PathBuf::from("src/lib.rs"), guard(at(20, 1)..at(21, 1), None)));
let _old = guards.insert(2, (Utf8PathBuf::from("src/lib.rs"), guard(at(40, 1)..at(41, 1), None)));
let stdout = compiler_message(&[span("src/lib.rs", 1, 1, 100, 1, false), span("src/lib.rs", 35, 1, 45, 1, false)]);
let blamed = ordinals_blamed(&stdout, Utf8Path::new("/work"), &guards);
assert_eq!(blamed, HashSet::from_iter([2]));
}
#[test]
fn a_wider_enclosing_diagnostic_processed_after_a_narrower_one_does_not_replace_its_attribution() {
let mut guards = Guards::default();
let _old = guards.insert(1, (Utf8PathBuf::from("src/lib.rs"), guard(at(10, 1)..at(11, 1), None)));
let _old = guards.insert(2, (Utf8PathBuf::from("src/lib.rs"), guard(at(60, 1)..at(61, 1), None)));
let stdout = compiler_message(&[span("src/lib.rs", 5, 1, 15, 1, false), span("src/lib.rs", 1, 1, 100, 1, false)]);
let blamed = ordinals_blamed(&stdout, Utf8Path::new("/work"), &guards);
assert_eq!(blamed, HashSet::from_iter([1]));
}
#[test]
fn diagnostics_are_matched_by_suffix_when_cargo_spells_paths_differently() {
let mut guards = Guards::default();
let _old = guards.insert(
1,
(
Utf8PathBuf::from("crates/pkg/src/lib.rs"),
guard(at(5, 1)..at(6, 1), Some(at(5, 5)..at(5, 10))),
),
);
let blamed = ordinals_blamed(
&compiler_message(&[span("/elsewhere/crates/pkg/src/lib.rs", 5, 6, 5, 8, true)]),
Utf8Path::new("/scratch/tree"),
&guards,
);
assert_eq!(blamed, HashSet::from_iter([1]));
}
#[test]
fn non_error_messages_and_malformed_spans_are_ignored_for_ordinals_blamed() {
let mut guards = Guards::default();
let _old = guards.insert(1, (Utf8PathBuf::from("src/lib.rs"), guard(at(1, 1)..at(2, 1), None)));
let stdout = [
"not json".to_owned(),
serde_json::json!({"reason": "compiler-artifact"}).to_string(),
serde_json::json!({"reason": "compiler-message", "message": {"level": "warning"}}).to_string(),
serde_json::json!({"reason": "compiler-message", "message": {"level": "error"}}).to_string(),
serde_json::json!({
"reason": "compiler-message",
"message": {"level": "error", "spans": [{}, {"file_name": "src/lib.rs"}]},
})
.to_string(),
]
.join("\n");
assert!(ordinals_blamed(&stdout, Utf8Path::new("/work"), &guards).is_empty());
}
#[test]
fn a_span_with_a_line_but_no_column_is_not_read_as_a_position() {
let mut guards = Guards::default();
let _old = guards.insert(1, (Utf8PathBuf::from("src/lib.rs"), guard(at(1, 1)..at(2, 1), None)));
let span = serde_json::json!({
"file_name": "src/lib.rs",
"line_start": 1,
"line_end": 1,
"column_end": 2,
"is_primary": true,
});
let stdout = compiler_message(&[span]);
assert!(ordinals_blamed(&stdout, Utf8Path::new("/work"), &guards).is_empty());
}
#[test]
fn a_span_with_a_complete_start_but_no_end_is_not_read_as_a_position() {
let mut guards = Guards::default();
let _old = guards.insert(1, (Utf8PathBuf::from("src/lib.rs"), guard(at(1, 1)..at(2, 1), None)));
let span = serde_json::json!({
"file_name": "src/lib.rs",
"line_start": 1,
"column_start": 1,
"line_end": 1,
"is_primary": true,
});
let stdout = compiler_message(&[span]);
assert!(ordinals_blamed(&stdout, Utf8Path::new("/work"), &guards).is_empty());
}
#[test]
fn build_errors_are_formatted_without_running_cargo() {
let timeout = Converger::build_timeout_error(Duration::from_secs(2)).to_string();
let stdout = compiler_message(&[span("src/lib.rs", 1, 1, 1, 2, true)]);
let (_dir, mut work) = trivial_workspace("build-errors-");
work.leak = true;
let unattributed = Converger::unattributed_build_error(&work, &stdout, "").to_string();
let limited = Converger::rollback_limit_error(32, 32, &[9, 5, 2], &work, &stdout).to_string();
let missing = Converger::missing_guard_error(&mutant()).to_string();
assert!(timeout.contains("after 2s"));
assert!(unattributed.contains("could not be attributed"));
assert!(limited.contains("32 of the 32 rollback rounds"), "{limited}");
assert!(limited.contains("16 blamed during this build"), "{limited}");
assert!(limited.contains("9, 5, 2"), "{limited}");
let single = Converger::rollback_limit_error(1, 1, &[41], &work, &stdout).to_string();
assert!(single.contains("1 of the 1 rollback rounds"), "{single}");
assert!(single.contains("41 blamed during this build"), "{single}");
assert!(single.contains("blamed in the last rounds of this build: 41"), "{single}");
assert!(single.contains("If those counts are falling"), "{single}");
assert!(missing.contains("no guard was emitted"));
assert!(missing.contains("src/lib.rs:7"));
assert!(unattributed.contains(work.root.as_str()), "{unattributed}");
work.leak = false;
let swept = Converger::unattributed_build_error(&work, &stdout, "").to_string();
assert!(swept.contains("--leak-dirs"), "{swept}");
assert!(!swept.contains(work.root.as_str()), "{swept}");
}
#[test]
fn a_build_that_reported_nothing_is_not_described_as_a_compile_failure() {
let (_dir, work) = trivial_workspace("silent-build-");
let stderr = " Compiling tonic v0.14.0\n\
error: failed to run custom build command for `codegen v0.1.0`\n\n\
Caused by:\n \
process didn't exit successfully: exit status: 101\n";
let message = Converger::unattributed_build_error(&work, "", stderr).to_string();
assert!(message.contains("the compiler reported nothing"), "{message}");
assert!(!message.contains("does not compile"), "{message}");
assert!(message.contains("failed to run custom build command"), "{message}");
assert!(message.contains("Caused by"), "{message}");
assert!(!message.contains("Compiling tonic"), "{message}");
}
#[test]
fn a_failure_with_nothing_on_either_stream_says_so_rather_than_showing_a_blank() {
let (_dir, work) = trivial_workspace("silent-both-");
let message = Converger::unattributed_build_error(&work, "", " Compiling x v0.1.0\n").to_string();
assert!(message.contains("cargo said nothing on stderr either"), "{message}");
}
#[test]
fn cargo_progress_is_told_apart_from_a_word_that_merely_starts_the_same_way() {
let kept = complaints(" Compiling x v0.1.0\nCompilationfailed for a reason\n");
assert!(kept.contains("Compilationfailed"), "{kept}");
assert!(!kept.contains("Compiling x"), "{kept}");
}
#[test]
fn a_word_sharing_a_progress_verbs_prefix_but_continuing_without_a_space_is_kept() {
let kept = complaints("Compilingx and y do not unify\n");
assert!(kept.contains("Compilingx and y do not unify"), "{kept}");
}
#[test]
fn a_carriage_return_progress_bar_does_not_bury_the_error_it_was_drawn_around() {
let bar = " Building [==> ] 2/17: serde_core\r\
\u{1b}[1;36m Building\u{1b}[0m [====> ] 4/17: quote, syn\r\
\u{1b}[1;36m Compiling\u{1b}[0m [======> ] 9/17: trivial\r";
let stderr =
format!(" Compiling serde v1.0.229\n{bar}\rerror: linking with `cc` failed: exit status: 1\r{bar}\n Building [=>] 1/2\r");
let kept = complaints(&stderr);
assert_eq!(kept, "error: linking with `cc` failed: exit status: 1\n", "{kept}");
}
#[test]
fn a_leading_blank_line_on_stderr_is_dropped_rather_than_kept() {
let kept = complaints("\nerror: something is wrong\n");
assert_eq!(kept, "error: something is wrong\n");
}
#[test]
fn only_the_dep_info_belonging_to_this_build_is_read() {
let dir = crate::testing::workdir("dep-files-");
let deps = Utf8PathBuf::from_path_buf(dir.path().join("deps")).expect("utf8");
fs::create_dir_all(deps.as_std_path()).expect("deps");
fs::write(deps.join("mine-aaaa.d").as_std_path(), "x: src/kept.rs\n").expect("mine");
fs::write(deps.join("stale-bbbb.d").as_std_path(), "x: src/gone.rs\n").expect("stale");
let stdout = artifact_message(&deps.join("libmine-aaaa.rmeta"));
let compiled = compiled_sources(&stdout, Utf8Path::new("/nowhere")).expect("read something");
assert!(compiled.contains(Utf8Path::new("src/kept.rs")), "{compiled:?}");
assert!(!compiled.contains(Utf8Path::new("src/gone.rs")), "{compiled:?}");
}
#[test]
fn a_dependency_path_containing_an_escaped_space_is_read_as_one_path() {
let dir = crate::testing::workdir("dep-spaces-");
let deps = Utf8PathBuf::from_path_buf(dir.path().join("deps")).expect("utf8");
fs::create_dir_all(deps.as_std_path()).expect("deps");
fs::write(deps.join("mine-aaaa.d").as_std_path(), "x.rmeta: src/lib.rs src/my\\ file.rs\n").expect("mine");
let stdout = artifact_message(&deps.join("libmine-aaaa.rmeta"));
let compiled = compiled_sources(&stdout, Utf8Path::new("/nowhere")).expect("read something");
assert!(compiled.contains(Utf8Path::new("src/lib.rs")), "{compiled:?}");
assert!(compiled.contains(Utf8Path::new("src/my file.rs")), "{compiled:?}");
assert_eq!(compiled.len(), 2, "the escaped space split the path: {compiled:?}");
}
#[test]
fn a_backslash_that_is_not_an_escape_survives_into_the_path() {
let dir = crate::testing::workdir("dep-separators-");
let deps = Utf8PathBuf::from_path_buf(dir.path().join("deps")).expect("utf8");
fs::create_dir_all(deps.as_std_path()).expect("deps");
fs::write(deps.join("mine-aaaa.d").as_std_path(), "x.rmeta: src\\my\\ file.rs\n").expect("mine");
let stdout = artifact_message(&deps.join("libmine-aaaa.rmeta"));
let compiled = compiled_sources(&stdout, Utf8Path::new("/nowhere")).expect("read something");
assert!(compiled.contains(Utf8Path::new("src/my file.rs")), "{compiled:?}");
}
#[test]
fn a_mutant_in_a_file_whose_path_contains_a_space_is_not_excused_as_unbuilt() {
let dir = crate::testing::workdir("withdraw-spaces-");
let deps = Utf8PathBuf::from_path_buf(dir.path().join("deps")).expect("utf8");
fs::create_dir_all(deps.as_std_path()).expect("deps");
fs::write(deps.join("mine-aaaa.d").as_std_path(), "x.rmeta: src/lib.rs src/my\\ file.rs\n").expect("mine");
let stdout = artifact_message(&deps.join("libmine-aaaa.rmeta"));
let compiled = compiled_sources(&stdout, Utf8Path::new("/nowhere")).expect("read something");
let target = |path: &str| TargetFile {
path: Utf8PathBuf::from(path),
absolute: Utf8PathBuf::from("/nowhere").join(path),
package: "pkg".to_owned(),
};
let mutant_in = |path: &str, ordinal: u32| Mutant {
ordinal,
file: (Utf8PathBuf::from(path)).into(),
..mutant()
};
let mut plan = plan_of(&[]);
plan.files = vec![target("src/lib.rs"), target("src/my file.rs"), target("src/absent.rs")];
plan.mutants = vec![
mutant_in("src/lib.rs", 1),
mutant_in("src/my file.rs", 2),
mutant_in("src/absent.rs", 3),
];
withdraw_uncompiled(&mut plan, &compiled);
assert_eq!(plan.mutants[0].outcome, Outcome::Pending, "the plain file was withdrawn");
assert_eq!(plan.mutants[1].outcome, Outcome::Pending, "the spaced file was withdrawn");
assert_eq!(plan.mutants[2].outcome, Outcome::NotBuilt, "an uncompiled file was kept");
}
#[test]
fn a_compiled_set_disjoint_from_the_survey_withdraws_nothing() {
let target = |path: &str| TargetFile {
path: Utf8PathBuf::from(path),
absolute: Utf8PathBuf::from("/nowhere").join(path),
package: "pkg".to_owned(),
};
let mutant_in = |path: &str, ordinal: u32| Mutant {
ordinal,
file: (Utf8PathBuf::from(path)).into(),
..mutant()
};
let mut plan = plan_of(&[]);
plan.files = vec![target("src/lib.rs"), target("src/main.rs")];
plan.mutants = vec![mutant_in("src/lib.rs", 1), mutant_in("src/main.rs", 2)];
let mut compiled = HashSet::default();
let _ = compiled.insert(Utf8PathBuf::from("build/generated.rs"));
let _ = compiled.insert(Utf8PathBuf::from("vendor/other.rs"));
withdraw_uncompiled(&mut plan, &compiled);
assert_eq!(
plan.mutants[0].outcome,
Outcome::Pending,
"a disjoint compiled set must not excuse a mutant"
);
assert_eq!(
plan.mutants[1].outcome,
Outcome::Pending,
"a disjoint compiled set must not excuse a mutant"
);
}
#[test]
fn dep_info_that_cannot_be_read_yields_no_conclusion() {
let stdout = r#"{"reason":"compiler-artifact","filenames":["/nowhere/deps/libmine-aaaa.rmeta"]}"#;
assert!(compiled_sources(stdout, Utf8Path::new("/nowhere")).is_none());
}
#[test]
fn a_dep_info_path_that_cannot_be_read_as_text_is_skipped() {
let dir = crate::testing::workdir("dep-not-a-file-");
let deps = Utf8PathBuf::from_path_buf(dir.path().join("deps")).expect("utf8");
fs::create_dir_all(deps.as_std_path()).expect("deps");
fs::create_dir_all(deps.join("mine-aaaa.d").as_std_path()).expect("a directory standing in for the dep file");
let stdout = artifact_message(&deps.join("libmine-aaaa.rmeta"));
let compiled = compiled_sources(&stdout, Utf8Path::new("/nowhere"));
assert!(compiled.is_none(), "{compiled:?}");
}
#[test]
fn a_dep_info_line_without_a_rule_separator_is_ignored() {
let dir = crate::testing::workdir("dep-no-colon-");
let deps = Utf8PathBuf::from_path_buf(dir.path().join("deps")).expect("utf8");
fs::create_dir_all(deps.as_std_path()).expect("deps");
fs::write(deps.join("mine-aaaa.d").as_std_path(), "just some text with no colon\n").expect("mine");
let stdout = artifact_message(&deps.join("libmine-aaaa.rmeta"));
let compiled = compiled_sources(&stdout, Utf8Path::new("/nowhere")).expect("the file was read");
assert!(compiled.is_empty(), "{compiled:?}");
}
#[test]
fn a_line_that_is_not_json_does_not_stop_dep_file_discovery() {
let dir = crate::testing::workdir("dep-files-bad-json-");
let deps = Utf8PathBuf::from_path_buf(dir.path().join("deps")).expect("utf8");
fs::create_dir_all(deps.as_std_path()).expect("deps");
fs::write(deps.join("mine-aaaa.d").as_std_path(), "x: src/kept.rs\n").expect("mine");
let stdout = format!("not json at all\n{}\n", artifact_message(&deps.join("libmine-aaaa.rmeta")));
assert_eq!(dep_files(&stdout), vec![deps.join("mine-aaaa.d")]);
}
#[test]
fn messages_with_a_different_reason_are_not_read_as_artifacts() {
let stdout = r#"{"reason":"build-script-executed","filenames":["/gamma/deps/libmine-aaaa.rmeta"]}"#;
assert!(dep_files(stdout).is_empty());
}
#[test]
fn a_filename_with_no_parent_or_stem_is_skipped() {
let stdout = r#"{"reason":"compiler-artifact","filenames":[".rlib"]}"#;
assert!(dep_files(stdout).is_empty());
}
#[test]
fn a_completely_empty_filename_is_skipped() {
let stdout = r#"{"reason":"compiler-artifact","filenames":[""]}"#;
assert!(dep_files(stdout).is_empty());
}
#[test]
fn a_dep_info_directory_that_does_not_exist_is_skipped() {
let stdout = r#"{"reason":"compiler-artifact","filenames":["/gamma/definitely/not/a/directory/libmine-aaaa.rmeta"]}"#;
assert!(dep_files(stdout).is_empty());
}
#[test]
#[cfg(unix)]
fn a_directory_entry_with_a_non_utf8_name_is_skipped_without_panicking() {
use std::os::unix::ffi::OsStrExt;
let dir = crate::testing::workdir("dep-files-non-utf8-");
let deps = Utf8PathBuf::from_path_buf(dir.path().join("deps")).expect("utf8");
fs::create_dir_all(deps.as_std_path()).expect("deps");
fs::write(deps.join("mine-aaaa.d").as_std_path(), "x: src/kept.rs\n").expect("mine");
let invalid_name = std::ffi::OsStr::from_bytes(b"mine-\xFF\xFE.d");
fs::write(dir.path().join("deps").join(invalid_name), "x: src/ignored.rs\n").expect("non-utf8 entry");
let stdout = artifact_message(&deps.join("libmine-aaaa.rmeta"));
let found = dep_files(&stdout);
assert_eq!(found, vec![deps.join("mine-aaaa.d")], "{found:?}");
}
#[test]
fn absolute_and_relative_dependency_paths_both_land_relative_to_the_tree() {
let dir = crate::testing::workdir("dep-paths-");
let deps = Utf8PathBuf::from_path_buf(dir.path().join("deps")).expect("utf8");
fs::create_dir_all(deps.as_std_path()).expect("deps");
fs::write(deps.join("mine-aaaa.d").as_std_path(), "x: /tree/src/one.rs src/two.rs\n").expect("mine");
let stdout = artifact_message(&deps.join("mine-aaaa.rlib"));
let compiled = compiled_sources(&stdout, Utf8Path::new("/tree")).expect("read something");
assert!(compiled.contains(Utf8Path::new("src/one.rs")), "{compiled:?}");
assert!(compiled.contains(Utf8Path::new("src/two.rs")), "{compiled:?}");
}
#[test]
fn a_rendered_diagnostic_survives_the_escapes_it_arrives_with() {
let line = "{\"reason\": \"compiler-message\", \"message\": {\"level\": \"error\", \"rendered\": \"error[E0308]: mismatched types\\n --> src/x.rs:1:5\\n |\\n = note: \\\"quoted\\\"\\n\"}}";
let rendered = rendered_diagnostic(line).expect("the line carries a rendered diagnostic");
assert!(rendered.contains('\n'), "the newlines are real ones: {rendered:?}");
assert!(rendered.contains("= note: \"quoted\""), "the quotes came through: {rendered:?}");
assert!(rendered.starts_with("error[E0308]"));
}
#[test]
fn a_second_round_rewrites_only_the_files_it_withdrew_from() {
let dir = crate::testing::workdir("build-incremental-splice-");
let origin = Utf8PathBuf::from_path_buf(dir.path().join("origin")).expect("utf8");
let root = Utf8PathBuf::from_path_buf(dir.path().join("copy")).expect("utf8");
let source = "pub const A: i32 = 1;\n";
for base in [&origin, &root] {
fs::create_dir_all(base.join("src").as_std_path()).expect("src");
fs::write(base.join("src/a.rs").as_std_path(), source).expect("a");
fs::write(base.join("src/b.rs").as_std_path(), source).expect("b");
}
let target = Utf8PathBuf::from_path_buf(dir.path().join("target")).expect("utf8");
let work = Workspace::adopt(root.clone(), target);
let mut plan = empty_plan(&work);
for name in ["src/a.rs", "src/b.rs"] {
plan.files.push(TargetFile {
path: Utf8PathBuf::from(name),
absolute: origin.join(name),
package: "trivial".to_owned(),
});
}
plan.mutants.push(Mutant {
ordinal: 1,
file: (Utf8PathBuf::from("src/a.rs")).into(),
span: 19..20,
..mutant()
});
plan.mutants.push(Mutant {
ordinal: 2,
file: (Utf8PathBuf::from("src/b.rs")).into(),
span: 19..20,
..mutant()
});
let mut splices = Splices::default();
let first = splices
.instrument(&work, &plan, &HashSet::default())
.expect("the first round splices");
assert_eq!(first.len(), 2, "both mutants were guarded");
let sentinel = "// this round never touched me\n";
fs::write(root.join("src/b.rs").as_std_path(), sentinel).expect("sentinel");
let second = splices
.instrument(&work, &plan, &HashSet::from_iter([1]))
.expect("the second round splices");
assert_eq!(
fs::read_to_string(root.join("src/b.rs").as_std_path()).expect("b"),
sentinel,
"the untouched file was re-spliced"
);
assert_eq!(
fs::read_to_string(root.join("src/a.rs").as_std_path()).expect("a"),
source,
"the withdrawn file was not put back"
);
assert!(second.contains_key(&2), "{second:?}");
assert!(!second.contains_key(&1), "{second:?}");
}
fn guarded_workspace(prefix: &str) -> (tempfile::TempDir, Workspace) {
let dir = crate::testing::workdir(prefix);
let root = Utf8PathBuf::from_path_buf(dir.path().join("src")).expect("utf8");
let runtime = root.join("gamma-rt");
fs::create_dir_all(root.join("src").as_std_path()).expect("src");
fs::create_dir_all(runtime.join("src").as_std_path()).expect("runtime src");
fs::write(
runtime.join("Cargo.toml").as_std_path(),
"[package]\nname = \"cargo-gamma-rt\"\nversion = \"0.0.0\"\nedition = \"2021\"\npublish = false\n\n\
[lib]\nname = \"gamma_rt\"\npath = \"src/lib.rs\"\n\n[workspace]\n",
)
.expect("runtime manifest");
for (name, source) in [
("lib.rs", include_str!("../../../../cargo-gamma-rt/src/lib.rs")),
("either.rs", include_str!("../../../../cargo-gamma-rt/src/either.rs")),
("runtime.rs", include_str!("../../../../cargo-gamma-rt/src/runtime.rs")),
] {
fs::write(runtime.join("src").join(name).as_std_path(), source).expect("runtime source");
}
fs::write(
root.join("Cargo.toml").as_std_path(),
"[package]\nname = \"trivial\"\nversion = \"0.0.0\"\nedition = \"2024\"\n\n\
[dependencies]\ngamma_rt = { path = \"gamma-rt\", package = \"cargo-gamma-rt\" }\n\n\
[workspace]\nexclude = [\"gamma-rt\"]\n",
)
.expect("manifest");
fs::write(root.join("src/lib.rs").as_std_path(), "pub const A: i32 = 1;\n").expect("lib");
let target = Utf8PathBuf::from_path_buf(dir.path().join("target")).expect("utf8");
let work = Workspace::adopt(root, target);
(dir, work)
}
fn probe_plan(work: &Workspace, unviable: usize, viable: usize) -> Plan {
let mut text = String::new();
for index in 0..unviable {
let _ = writeln!(text, "pub const C{index}: i32 = 1;");
}
for index in 0..viable {
let _ = writeln!(text, "pub fn f{index}() -> i32 {{ let value = 1; value }}");
}
let pristine = work.root.parent().expect("the tree has a parent").join("pristine");
fs::create_dir_all(pristine.as_std_path()).expect("pristine");
fs::write(pristine.join("lib.rs").as_std_path(), &text).expect("pristine source");
fs::write(work.root.join("src/lib.rs").as_std_path(), &text).expect("tree source");
let mut plan = empty_plan(work);
plan.files.push(TargetFile {
path: Utf8PathBuf::from("src/lib.rs"),
absolute: pristine.join("lib.rs"),
package: "trivial".to_owned(),
});
let mut offset = 0;
for index in 0..unviable.saturating_add(viable) {
let start = text
.get(offset..)
.and_then(|rest| rest.find("= 1"))
.expect("every item holds a literal")
+ offset
+ 2;
offset = start + 1;
plan.mutants.push(Mutant {
id: format!("mutant-{index}").into(),
ordinal: u32::try_from(index).expect("the fixture is small") + 1,
span: start..start + 1,
replacement: "2".to_owned().into(),
..mutant()
});
}
plan
}
fn hinted(plan: &Plan, indices: impl IntoIterator<Item = usize>) -> HashSet<crate::model::MutantId> {
indices.into_iter().map(|index| plan.mutants[index].id.clone()).collect()
}
fn population(plan: &Plan) -> Vec<(u32, Outcome)> {
let mut outcomes: Vec<(u32, Outcome)> = plan.mutants.iter().map(|mutant| (mutant.ordinal, mutant.outcome)).collect();
outcomes.sort_unstable_by_key(|(ordinal, _outcome)| *ordinal);
outcomes
}
#[test]
fn a_context_mismatch_changes_the_order_and_never_the_population() {
let (_blind_dir, blind_work) = guarded_workspace("build-probe-blind-");
let mut blind_plan = probe_plan(&blind_work, 5, 2);
let blind = Converger::default()
.finish(
&blind_work,
&mut blind_plan,
None,
BuildLimits::default(),
&mut crate::testing::Recorder::default(),
)
.expect("the unguided build converges");
let (_guided_dir, guided_work) = guarded_workspace("build-probe-guided-");
let mut guided_plan = probe_plan(&guided_work, 5, 2);
let guided = Converger::guided(hinted(&guided_plan, 0..5))
.finish(
&guided_work,
&mut guided_plan,
None,
BuildLimits::default(),
&mut crate::testing::Recorder::default(),
)
.expect("the guided build converges");
assert_eq!(
population(&blind_plan),
population(&guided_plan),
"a hint that changes any verdict is filtering, not ordering"
);
assert_eq!(blind.withdrawn, guided.withdrawn, "the same mutants must be withdrawn either way");
assert_eq!(blind.ordering.rounds, 0, "an unguided build takes no probe round");
assert_eq!(guided.ordering.offered, 5, "every hinted mutant was front-loaded");
assert_eq!(guided.ordering.confirmed, 5, "the compiler agreed with every hint");
assert_eq!(guided.ordering.rounds, 1, "one probe round, taken once");
}
#[test]
fn a_hint_the_compiler_disagrees_with_leaves_its_mutant_live_and_judged() {
let (_dir, work) = guarded_workspace("build-probe-wrong-");
let mut plan = probe_plan(&work, 0, 5);
let build = Converger::guided(hinted(&plan, 0..5))
.finish(
&work,
&mut plan,
None,
BuildLimits::default(),
&mut crate::testing::Recorder::default(),
)
.expect("the build converges");
assert_eq!(build.ordering.offered, 5, "the hints were taken at face value and probed");
assert_eq!(build.ordering.confirmed, 0, "no hint was confirmed, because none was right");
assert_eq!(build.withdrawn, 0, "a wrong hint must never withdraw a mutant");
for mutant in &plan.mutants {
assert_eq!(
mutant.outcome,
Outcome::Pending,
"a hinted mutant that compiles has to be left for the sweep to judge: {mutant:?}"
);
}
}
#[test]
fn a_hint_the_compiler_agrees_with_is_settled_by_the_compiler() {
let (_dir, work) = guarded_workspace("build-probe-right-");
let mut plan = probe_plan(&work, 5, 1);
let build = Converger::guided(hinted(&plan, 0..5))
.finish(
&work,
&mut plan,
None,
BuildLimits::default(),
&mut crate::testing::Recorder::default(),
)
.expect("the build converges");
assert_eq!(build.ordering.confirmed, 5, "every hinted mutant was refused by the compiler");
for mutant in plan.mutants.iter().take(5) {
assert_eq!(mutant.outcome, Outcome::CompileError, "{mutant:?}");
}
assert_eq!(
plan.mutants[5].outcome,
Outcome::Pending,
"the unhinted mutant compiles and is judged"
);
}
#[test]
fn a_handful_of_hints_is_not_worth_a_probe_round() {
let (_dir, work) = guarded_workspace("build-probe-floor-");
let mut plan = probe_plan(&work, 3, 1);
let build = Converger::guided(hinted(&plan, 0..3))
.finish(
&work,
&mut plan,
None,
BuildLimits::default(),
&mut crate::testing::Recorder::default(),
)
.expect("the build converges");
assert_eq!(build.ordering.rounds, 0, "a probe below the floor costs a build and buys nothing");
assert_eq!(build.ordering.offered, 0, "nothing was front-loaded");
assert_eq!(build.withdrawn, 3, "the ordinary rounds still find every unviable mutant");
}
#[test]
fn no_mutant_is_probed_twice_however_many_builds_run() {
let (_dir, work) = guarded_workspace("build-probe-once-");
let mut plan = probe_plan(&work, 5, 1);
let mut converger = Converger::guided(hinted(&plan, 0..5));
let staged = converger
.converge(
&work,
&plan,
None,
&["build", "--keep-going"],
BuildLimits::default(),
&mut crate::testing::Recorder::default(),
)
.expect("the staged build runs");
assert!(
matches!(staged, Convergence::Built(_)),
"the tree converges once the consts are out"
);
let build = converger
.finish(
&work,
&mut plan,
None,
BuildLimits::default(),
&mut crate::testing::Recorder::default(),
)
.expect("the deciding build converges");
assert_eq!(
build.ordering.rounds, 1,
"the second build must not re-probe what the first already did"
);
assert_eq!(build.ordering.offered, 5, "each hinted mutant is offered exactly once");
}
#[test]
fn the_probe_set_is_ordered_by_the_plan_rather_than_by_the_hint_set() {
let (_dir, work) = guarded_workspace("build-probe-order-");
let plan = probe_plan(&work, 5, 2);
let forwards = Converger::guided(hinted(&plan, 0..5));
let backwards = Converger::guided(hinted(&plan, (0..5).rev()));
let (first, deferred) = forwards.probe_sets(&plan, None);
let (second, also_deferred) = backwards.probe_sets(&plan, None);
assert_eq!(first, vec![1, 2, 3, 4, 5], "the candidates follow the plan's ordinals");
assert_eq!(first, second, "the hint set's own order may not reach the build");
assert_eq!(deferred, also_deferred);
let held: Vec<u32> = {
let mut ordinals: Vec<u32> = deferred.into_iter().collect();
ordinals.sort_unstable();
ordinals
};
assert_eq!(held, vec![6, 7], "only the unhinted mutants are held back from the probe round");
}
#[test]
fn a_probe_leaves_mutants_outside_the_selection_where_they_are() {
let (_dir, work) = guarded_workspace("build-probe-selection-");
let mut plan = probe_plan(&work, 5, 2);
for mutant in plan.mutants.iter_mut().skip(5) {
mutant.package = "elsewhere".to_owned().into();
}
let converger = Converger::guided(hinted(&plan, 0..5));
let select = vec![plan.mutants[0].package.to_string()];
let (candidates, deferred) = converger.probe_sets(&plan, Some(&select));
assert_eq!(candidates, vec![1, 2, 3, 4, 5]);
assert!(deferred.is_empty(), "another package's mutants stay spliced: {deferred:?}");
}