use super::*;
use crate::mutation::result_cache::KeyFiles;
fn header() -> JournalHeader {
JournalHeader {
journal_format: JOURNAL_FORMAT,
fallow_version: "2.0.0".to_owned(),
runner: "vitest".to_owned(),
runtime: "node 24.1.0".to_owned(),
run_dir: "0123456789abcdef0123".to_owned(),
runner_config: None,
scope: "all".to_owned(),
operators: None,
allow_side_effects: false,
}
}
fn keys(dir: &Path) -> Arc<ResultCache> {
Arc::new(ResultCache::keys_only(
dir,
dir,
"vitest",
KeyFiles::default(),
))
}
fn open(dir: &Path, header: &JournalHeader, resume: bool) -> Result<Opened, JournalMismatch> {
Journal::open(dir, header, keys(dir), resume)
}
fn killed(test: &str) -> CachedResult {
CachedResult {
status: MutantStatus::Killed,
killed_by: Some(test.to_owned()),
duration_ms: Some(5),
}
}
fn lines(dir: &Path) -> Vec<String> {
std::fs::read_to_string(Journal::path(dir))
.unwrap()
.lines()
.map(str::to_owned)
.collect()
}
#[test]
fn a_run_without_resume_starts_a_new_journal_and_appends_each_result() {
let dir = tempfile::tempdir().unwrap();
let opened = open(dir.path(), &header(), false).unwrap();
opened.journal.append("m1", "k1", &killed("t1"));
assert!(opened.note.is_none());
assert_eq!(lines(dir.path()).len(), 2);
let next = open(dir.path(), &header(), false).unwrap();
assert_eq!(
lines(dir.path()),
vec![serde_json::to_string(&header()).unwrap()],
"a run without --resume replaces the journal"
);
assert_eq!(next.journal.resumed("m1", "k1"), None);
}
#[test]
fn a_resume_reads_the_final_results_and_ignores_a_cut_last_line() {
let dir = tempfile::tempdir().unwrap();
let first = open(dir.path(), &header(), false).unwrap();
first.journal.append("m1", "k1", &killed("t1"));
first.journal.append(
"m2",
"k2",
&CachedResult {
status: MutantStatus::Survived,
killed_by: None,
duration_ms: Some(9),
},
);
first.journal.append(
"m3",
"k3",
&CachedResult {
status: MutantStatus::Timeout,
killed_by: None,
duration_ms: None,
},
);
drop(first);
let mut file = OpenOptions::new()
.append(true)
.open(Journal::path(dir.path()))
.unwrap();
file.write_all(br#"{"id":"m4","key":"k4","sta"#).unwrap();
drop(file);
let resumed = open(dir.path(), &header(), true).unwrap();
assert!(resumed.note.is_none());
let journal = resumed.journal;
assert_eq!(journal.resumed("m1", "k1"), Some(killed("t1")));
assert_eq!(
journal.resumed("m2", "k2").map(|result| result.status),
Some(MutantStatus::Survived)
);
assert_eq!(journal.resumed("m3", "k3"), None, "a timeout is not final");
assert_eq!(journal.resumed("m4", "k4"), None, "a cut line is ignored");
journal.append("m5", "k5", &killed("t5"));
let after = lines(dir.path());
assert_eq!(after.len(), 4, "{after:#?}");
for line in &after[1..] {
assert!(serde_json::from_str::<Line>(line).is_ok(), "{line}");
}
}
#[test]
fn a_changed_key_is_rejected_and_counted_once() {
let dir = tempfile::tempdir().unwrap();
let first = open(dir.path(), &header(), false).unwrap();
first.journal.append("m1", "k1", &killed("t1"));
drop(first);
let journal = open(dir.path(), &header(), true).unwrap().journal;
assert_eq!(journal.resumed("m1", "another-key"), None);
assert_eq!(journal.resumed("m1", "another-key"), None);
assert_eq!(journal.rejected(), 1);
assert_eq!(journal.resumed("m1", "k1"), Some(killed("t1")));
}
#[test]
fn a_resume_with_another_fingerprint_names_each_input_that_differs() {
let dir = tempfile::tempdir().unwrap();
drop(open(dir.path(), &header(), false).unwrap());
let mut changed = header();
changed.runtime = "node 22.18.0".to_owned();
changed.run_dir = "fedcba9876543210fedc".to_owned();
changed.operators = Some(vec!["arithmetic-operator".to_owned()]);
let Err(mismatch) = open(dir.path(), &changed, true) else {
panic!("a different fingerprint is refused");
};
assert_eq!(
mismatch.differences,
vec![
"runtime (journal: node 24.1.0, this run: node 22.18.0)".to_owned(),
"planned source and instrumentation (journal: 0123456789ab, this run: fedcba987654)"
.to_owned(),
"operators (journal: each operator, this run: arithmetic-operator)".to_owned(),
]
);
assert_eq!(
lines(dir.path()),
vec![serde_json::to_string(&header()).unwrap()],
"a refused resume keeps the journal"
);
}
#[test]
fn a_resume_without_a_usable_journal_starts_from_the_beginning_with_a_note() {
let dir = tempfile::tempdir().unwrap();
let opened = open(dir.path(), &header(), true).unwrap();
assert_eq!(
opened.note.as_deref(),
Some("the run started from the beginning, because no journal exists")
);
assert_eq!(lines(dir.path()).len(), 1, "the run starts its own journal");
std::fs::write(Journal::path(dir.path()), "{\"journal_for").unwrap();
let opened = open(dir.path(), &header(), true).unwrap();
assert_eq!(
opened.note.as_deref(),
Some("the run started from the beginning, because the journal has no valid header")
);
}
#[test]
fn a_failed_write_is_reported_and_never_stops_the_run() {
let dir = tempfile::tempdir().unwrap();
let blocked = dir.path().join("file");
std::fs::write(&blocked, "").unwrap();
let opened = open(&blocked, &header(), false).unwrap();
opened.journal.append("m1", "k1", &killed("t1"));
assert!(
opened
.journal
.write_error()
.is_some_and(|error| error.contains("cannot write")),
"{:?}",
opened.journal.write_error()
);
}
#[test]
fn a_last_line_cut_inside_a_multibyte_character_keeps_the_earlier_results() {
let dir = tempfile::tempdir().unwrap();
let first = open(dir.path(), &header(), false).unwrap();
first.journal.append("m1", "k1", &killed("t1"));
drop(first);
let line = serde_json::to_string(&Line {
id: "m2".to_owned(),
key: "k2".to_owned(),
status: MutantStatus::Killed,
killed_by: Some("test/\u{eb}.test.ts::adds".to_owned()),
duration_ms: None,
})
.unwrap();
let cut = line.find('\u{eb}').unwrap() + 1;
let mut file = OpenOptions::new()
.append(true)
.open(Journal::path(dir.path()))
.unwrap();
file.write_all(&line.as_bytes()[..cut]).unwrap();
drop(file);
let resumed = open(dir.path(), &header(), true).unwrap();
assert!(resumed.note.is_none(), "{:?}", resumed.note);
assert_eq!(resumed.journal.resumed("m1", "k1"), Some(killed("t1")));
assert_eq!(lines(dir.path()).len(), 2, "the cut line is dropped");
}
#[test]
fn a_journal_over_the_size_cap_is_not_read_and_not_erased() {
let dir = tempfile::tempdir().unwrap();
let first = open(dir.path(), &header(), false).unwrap();
first.journal.append("m1", "k1", &killed("t1"));
drop(first);
let before = std::fs::read(Journal::path(dir.path())).unwrap();
let opened = Journal::open_capped(dir.path(), &header(), keys(dir.path()), true, 16).unwrap();
assert_eq!(
opened.note.as_deref(),
Some(
"the run started from the beginning, because the journal is larger than 16 bytes; the run moved it to journal.jsonl.unread"
)
);
assert_eq!(opened.journal.resumed("m1", "k1"), None);
assert_eq!(
std::fs::read(dir.path().join("journal.jsonl.unread")).unwrap(),
before,
"the unread journal is kept"
);
assert_eq!(lines(dir.path()).len(), 1, "the run starts its own journal");
}
#[test]
fn a_stale_temporary_file_is_removed_and_a_failed_start_leaves_none() {
let dir = tempfile::tempdir().unwrap();
let stale = dir.path().join("journal.jsonl.999999.tmp");
std::fs::write(&stale, "{").unwrap();
File::options()
.write(true)
.open(&stale)
.unwrap()
.set_modified(std::time::SystemTime::now() - std::time::Duration::from_secs(7200))
.unwrap();
let fresh = dir.path().join("journal.jsonl.999998.tmp");
std::fs::write(&fresh, "{").unwrap();
drop(open(dir.path(), &header(), false).unwrap());
assert!(!stale.exists(), "a temporary file of a dead run is removed");
assert!(
fresh.exists(),
"a recent temporary file can belong to a live run"
);
let blocked = tempfile::tempdir().unwrap();
std::fs::create_dir_all(Journal::path(blocked.path()).join("child")).unwrap();
let opened = open(blocked.path(), &header(), false).unwrap();
assert!(opened.journal.write_error().is_some());
let leftovers: Vec<String> = std::fs::read_dir(blocked.path())
.unwrap()
.filter_map(Result::ok)
.map(|entry| entry.file_name().to_string_lossy().into_owned())
.filter(|name| {
Path::new(name)
.extension()
.is_some_and(|extension| extension == "tmp")
})
.collect();
assert!(leftovers.is_empty(), "{leftovers:?}");
}