use camino::Utf8Path;
use crate::discover::{Hints, Killer, RunRecord};
use crate::model::{Mutant, MutantId};
use crate::{HashMap, HashSet};
#[derive(Debug, Default)]
pub(super) struct Killers {
entries: HashMap<MutantId, Killer>,
}
impl Killers {
pub(super) fn load(base: &Utf8Path, root: &Utf8Path) -> Self {
let mut entries = Hints::load(root).probes();
entries.extend(RunRecord::load(base).probes().clone());
Self { entries }
}
pub(super) fn hint(&self, id: &str) -> Option<&Killer> {
self.entries.get(id)
}
pub(super) fn record(&mut self, id: MutantId, killer: Killer) {
let _previous = self.entries.insert(id, killer);
}
pub(super) fn forget(&mut self, id: &str) {
let _previous = self.entries.remove(id);
}
pub(super) fn store(&self, base: &Utf8Path, population: &[Mutant]) {
let current: HashSet<&MutantId> = population.iter().map(|mutant| &mutant.id).collect();
let probes: HashMap<MutantId, Killer> = self
.entries
.iter()
.filter(|(id, _killer)| current.contains(id))
.map(|(id, killer)| (id.clone(), killer.clone()))
.collect();
RunRecord::store_probes(base, &probes);
}
#[cfg(test)]
fn len(&self) -> usize {
self.entries.len()
}
#[cfg(test)]
fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
#[cfg(test)]
#[cfg(not(miri))]
mod tests {
use super::*;
fn killer(test: &str) -> Killer {
Killer {
package: "alpha".to_owned(),
target: "lib".to_owned(),
test: test.to_owned(),
}
}
fn mutant(id: &str) -> Mutant {
Mutant {
id: id.into(),
..crate::fixtures::mutant()
}
}
#[test]
fn a_map_round_trips_through_the_record() {
let dir = tempfile::tempdir().expect("a temporary directory");
let base = Utf8Path::from_path(dir.path()).expect("a utf-8 path");
let mut killers = Killers::default();
killers.record("abc".into(), killer("tests::round_trip"));
killers.store(base, &[mutant("abc")]);
let read = Killers::load(base, base);
assert_eq!(read.len(), 1);
assert_eq!(read.hint("abc"), Some(&killer("tests::round_trip")));
}
#[test]
fn a_missing_record_is_an_empty_map() {
let dir = tempfile::tempdir().expect("a temporary directory");
let base = Utf8Path::from_path(dir.path()).expect("a utf-8 path");
assert!(Killers::load(base, base).is_empty());
}
#[test]
fn a_corrupt_record_is_an_empty_map() {
let dir = tempfile::tempdir().expect("a temporary directory");
let base = Utf8Path::from_path(dir.path()).expect("a utf-8 path");
std::fs::write(base.join("last-gamma-run.json").as_std_path(), "{ not json").expect("the file to be written");
assert!(Killers::load(base, base).is_empty());
}
#[test]
fn hints_survive_a_build_context_this_run_does_not_share() {
let dir = tempfile::tempdir().expect("a temporary directory");
let base = Utf8Path::from_path(dir.path()).expect("a utf-8 path");
let mut killers = Killers::default();
killers.record("abc".into(), killer("tests::round_trip"));
killers.store(base, &[mutant("abc")]);
let elsewhere = crate::discover::record_context(&crate::discover::RecordContext {
toolchain: Some("some other toolchain"),
..crate::discover::RecordContext::default()
})
.expect("a named toolchain gives a context");
RunRecord::from_run(base, &[], &elsewhere, &[]).store(base, base);
assert_eq!(Killers::load(base, base).hint("abc"), Some(&killer("tests::round_trip")));
}
#[test]
fn forgetting_a_mutant_drops_its_hint() {
let mut killers = Killers::default();
killers.record("abc".into(), killer("tests::round_trip"));
killers.forget("abc");
assert!(killers.hint("abc").is_none());
}
#[test]
fn a_hint_names_only_the_binary_it_was_recorded_against() {
let hint = killer("tests::round_trip");
assert!(hint.names("alpha", "lib"));
assert!(!hint.names("alpha", "integration"));
assert!(!hint.names("beta", "lib"));
}
#[test]
fn recording_a_mutant_twice_keeps_the_later_killer() {
let mut killers = Killers::default();
killers.record("abc".into(), killer("tests::first"));
killers.record("abc".into(), killer("tests::second"));
assert_eq!(killers.len(), 1);
assert_eq!(killers.hint("abc").map(|found| found.test.as_str()), Some("tests::second"));
}
#[test]
fn storing_prunes_hints_outside_the_current_population() {
let dir = tempfile::tempdir().expect("a temporary directory");
let base = Utf8Path::from_path(dir.path()).expect("a UTF-8 path");
let mut killers = Killers::default();
killers.record("old".into(), killer("tests::old"));
killers.record("new".into(), killer("tests::new"));
killers.store(base, &[mutant("new")]);
let read = Killers::load(base, base);
assert!(read.hint("old").is_none());
assert_eq!(read.hint("new"), Some(&killer("tests::new")));
}
}