use crate::record::write_record;
use crate::stage_recover::{Recovery, recover};
use crate::stage_ring::{HEADER, StageRing};
use crate::tests::temp_file;
const CAP: u64 = 64 * 1024;
fn record(payload: &[u8]) -> Vec<u8> {
let mut v = Vec::new();
write_record(&mut v, payload).unwrap();
v
}
fn push(ring: &mut StageRing, rec: &[u8]) -> bool {
ring.push(rec.len(), |slot| slot.copy_from_slice(rec))
}
fn pending(ring: &StageRing) -> Vec<u8> {
let mut out = Vec::new();
ring.for_each_pending(|run| out.extend_from_slice(run));
out
}
#[test]
fn pushed_records_come_back_in_order_as_one_run() {
let mut ring = StageRing::create(&temp_file("stage-order"), CAP, 7, 9).unwrap();
let (a, b) = (record(b"first"), record(b"second"));
assert!(push(&mut ring, &a) && push(&mut ring, &b));
let mut runs = 0;
let end = ring.for_each_pending(|_| runs += 1);
assert_eq!(runs, 1);
assert_eq!(pending(&ring), [a.clone(), b.clone()].concat());
assert_eq!(end, (a.len() + b.len()) as u64);
assert_eq!(ring.head().commit, end);
}
#[test]
fn a_record_that_would_straddle_the_end_starts_again_at_the_front() {
use std::os::unix::fs::FileExt;
for gap in [20u64, 5] {
let path = temp_file(&format!("stage-wrap-{gap}"));
let mut ring = StageRing::create(&path, CAP, 1, 9).unwrap();
let filler = record(&vec![b'f'; (CAP - gap) as usize - 8]);
assert!(push(&mut ring, &filler));
ring.mark_drained(ring.head().commit, 100, ring.head().log_id);
let f = std::fs::File::options().write(true).open(&path).unwrap();
f.write_all_at(&vec![0xff; gap as usize], HEADER as u64 + CAP - gap).unwrap();
let rec = record(b"after the wrap");
assert!(rec.len() as u64 > gap, "the record must not fit in the gap");
assert!(push(&mut ring, &rec));
assert_eq!(pending(&ring), rec, "gap {gap}");
assert_eq!(ring.head().commit, CAP + rec.len() as u64);
}
}
#[test]
fn a_full_ring_refuses_until_it_is_drained() {
let mut ring = StageRing::create(&temp_file("stage-full"), CAP, 1, 9).unwrap();
let rec = record(&[b'x'; 1000]);
let mut n = 0;
while push(&mut ring, &rec) {
n += 1;
}
assert_eq!(n, CAP as usize / rec.len());
let end = ring.for_each_pending(|_| {});
ring.mark_drained(end, 9, ring.head().log_id);
assert!(push(&mut ring, &rec));
assert!(!ring.push(CAP as usize + 1, |_| unreachable!()), "a record larger than the ring");
}
#[test]
fn an_existing_ring_reopens_as_it_was_left() {
let path = temp_file("stage-reopen");
let mut ring = StageRing::create(&path, CAP, 42, 900).unwrap();
let rec = record(b"kept");
assert!(push(&mut ring, &rec));
let head = ring.head();
drop(ring);
let (again, found) = StageRing::open_existing(&path).unwrap().unwrap();
assert_eq!(found, head);
assert_eq!((found.log_id, found.aof_len), (42, 900));
assert_eq!(pending(&again), rec);
std::fs::write(&path, b"not a ring").unwrap();
assert!(StageRing::open_existing(&path).unwrap().is_none());
assert!(StageRing::open_existing(&temp_file("stage-none")).unwrap().is_none());
}
#[test]
fn a_file_the_size_of_a_ring_without_its_magic_is_not_one() {
let path = temp_file("stage-foreign");
std::fs::write(&path, vec![0xa5u8; HEADER + CAP as usize]).unwrap();
assert!(StageRing::open_existing(&path).unwrap().is_none());
}
#[test]
fn a_directory_is_neither_opened_nor_created_as_a_ring() {
let dir = kevy_tmpdir::TmpDir::new("stage-dir");
assert!(StageRing::open_existing(dir.path()).is_err(), "only a missing file is no ring");
assert!(StageRing::create(dir.path(), CAP, 1, 9).is_err());
}
#[test]
fn a_record_is_found_only_whole_before_the_end_it_is_asked_about() {
let rec = record(b"a record of some length");
let ring = ring_with("stage-record-at", std::slice::from_ref(&rec));
let len = rec.len() as u64;
assert_eq!(ring.record_at(0, len), Some((0, len)));
assert_eq!(ring.record_at(0, 4), None, "not even its header before the end");
assert_eq!(ring.record_at(0, len - 1), None, "its header but not all of it");
}
fn ring_with(name: &str, recs: &[Vec<u8>]) -> StageRing {
let mut ring = StageRing::create(&temp_file(name), CAP, 5, 100).unwrap();
for r in recs {
assert!(push(&mut ring, r));
}
ring
}
#[test]
fn recovery_replays_what_the_log_does_not_hold() {
let recs = [record(b"a"), record(b"bb"), record(b"ccc")];
let ring = ring_with("stage-rec-all", &recs);
let head = ring.head();
assert_eq!(
recover(&ring, head, 5, 100, &[]),
Recovery::Replay { records: recs.to_vec(), torn: false }
);
assert_eq!(
recover(&ring, head, 5, 100 + recs[0].len() as u64, &recs[0]),
Recovery::Replay { records: recs[1..].to_vec(), torn: false }
);
let all = recs.concat();
assert_eq!(
recover(&ring, head, 5, 100 + all.len() as u64, &all),
Recovery::Replay { records: vec![], torn: false }
);
}
#[test]
fn recovery_discards_a_ring_that_cannot_prove_its_place() {
let recs = [record(b"a"), record(b"bb")];
let ring = ring_with("stage-rec-discard", &recs);
let head = ring.head();
let discarded = |r: Recovery| matches!(r, Recovery::Discard(_));
assert!(discarded(recover(&ring, head, 6, 100, &[])), "another log");
assert!(discarded(recover(&ring, head, 5, 99, &[])), "a shorter log");
assert_eq!(
recover(&ring, head, 5, 110, &[]),
Recovery::Discard("the log's tail was not handed over whole")
);
let other = record(b"z"); assert!(discarded(recover(&ring, head, 5, 100 + other.len() as u64, &other)), "a foreign tail");
let long = [recs.concat(), record(b"more")].concat();
assert!(discarded(recover(&ring, head, 5, 100 + long.len() as u64, &long)), "a longer log");
}
#[test]
fn recovery_stops_at_a_record_that_fails_its_checksum() {
let path = temp_file("stage-rec-torn");
let recs = [record(b"good"), record(b"flipped"), record(b"after")];
let mut ring = StageRing::create(&path, CAP, 5, 100).unwrap();
for r in &recs {
assert!(push(&mut ring, r));
}
let head = ring.head();
drop(ring);
let mut bytes = std::fs::read(&path).unwrap();
bytes[HEADER + recs[0].len() + 8] ^= 0x20;
std::fs::write(&path, &bytes).unwrap();
let (ring, found) = StageRing::open_existing(&path).unwrap().unwrap();
assert_eq!(found, head);
assert_eq!(
recover(&ring, found, 5, 100, &[]),
Recovery::Replay { records: recs[..1].to_vec(), torn: true }
);
}
#[test]
fn a_state_written_but_not_selected_is_not_the_head() {
let path = temp_file("stage-select");
let mut ring = StageRing::create(&path, CAP, 5, 100).unwrap();
assert!(push(&mut ring, &record(b"one")));
ring.mark_drained(ring.head().commit, 140, ring.head().log_id);
let head = ring.head();
ring.write_unselected([5, 999, 0]);
drop(ring);
let (_, found) = StageRing::open_existing(&path).unwrap().unwrap();
assert_eq!(found, head, "the half-written slot must not be read");
assert_eq!((found.aof_len, found.drained), (140, head.commit));
}
#[test]
fn a_record_ending_at_the_ring_end_does_not_run_into_the_next_one() {
let mut ring = StageRing::create(&temp_file("stage-exact-end"), CAP, 1, 9).unwrap();
let rec = |i: u8| record(&[i; 1016]);
for i in 0..60 {
assert!(push(&mut ring, &rec(i)));
}
let end = ring.for_each_pending(|_| {});
ring.mark_drained(end, 9, ring.head().log_id);
let second: Vec<Vec<u8>> = (60..68).map(rec).collect();
for r in &second {
assert!(push(&mut ring, r));
}
assert_eq!(ring.head().commit, CAP + 4 * 1024, "the fifth record starts at the front");
let mut runs = Vec::new();
ring.for_each_pending(|run| runs.push(run.to_vec()));
assert_eq!(runs, [second[..4].concat(), second[4..].concat()]);
}
#[test]
fn pending_runs_match_a_reference_queue_across_many_laps() {
let mut ring = StageRing::create(&temp_file("stage-laps"), CAP, 1, 9).unwrap();
let mut queue = std::collections::VecDeque::new();
let mut seed = 0x9e37_79b9_7f4a_7c15u64;
let mut next = move || {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
seed
};
for step in 0..20_000u64 {
let to_end = CAP - ring.head().commit % CAP;
let len = match next() % 4 {
0 if (9..=4096).contains(&to_end) => to_end,
_ => 9 + next() % 3000,
};
let rec = record(&vec![step as u8; len as usize - 8]);
if next() % 8 == 0 || !push(&mut ring, &rec) {
let mut got = Vec::new();
let end = ring.for_each_pending(|run| got.extend_from_slice(run));
let want: Vec<u8> = queue.drain(..).flat_map(|r: Vec<u8>| r).collect();
assert_eq!(got, want, "step {step}");
ring.mark_drained(end, 9, ring.head().log_id);
continue;
}
queue.push_back(rec);
}
assert!(ring.head().commit > 100 * CAP, "the ring went round many times");
}