#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::wildcard_enum_match_arm,
reason = "test code"
)]
use kernel::ledger::chain_hash;
use kernel::{EventDraft, EventKind, EventRecord, GENESIS_PREV, Payload, RunId, Seq, TimeMs};
use std::collections::VecDeque;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use crate::fault_fs::{FaultFs, FaultPlan, TornTail};
use crate::jsonl::JsonlLedger;
use crate::real_fs::RealFs;
use crate::vfs::Vfs;
fn draft(who: &str, t: u64) -> EventDraft {
EventDraft {
run: RunId::CITY,
t: TimeMs::new(t),
who: who.to_string(),
addr: None,
kind: EventKind::GateChecked,
data: Payload::empty(),
ig: false,
}
}
#[test]
fn a_failed_write_leaves_no_bytes_the_next_wave_builds_on() {
let dir = tempfile::tempdir().unwrap();
let fs = FaultFs::new(FaultPlan {
cut_at_op: None,
cut_on_write: Some("doomed"),
torn_tail: TornTail::KeepBytes(7),
});
let (mut ledger, _) = JsonlLedger::open_faulty(fs.clone(), dir.path(), TimeMs::new(0)).unwrap();
ledger.append_all(vec![draft("city", 1)]).unwrap();
assert!(ledger.append_all(vec![draft("doomed", 2)]).is_err());
ledger.append_all(vec![draft("city", 3)]).unwrap();
drop(ledger);
let (reopened, report) = JsonlLedger::open_faulty(fs, dir.path(), TimeMs::new(9))
.expect("the ledger reopens after a failed write");
let whos: Vec<(Seq, String)> = reopened
.read_raw_lines()
.unwrap()
.iter()
.scan(GENESIS_PREV, |prev, line| {
let record = EventRecord::parse_line(line).unwrap();
assert_eq!(
record.prev(),
*prev,
"the chain breaks before seq {:?}",
record.seq()
);
*prev = chain_hash(line);
Some((record.seq(), record.who().to_string()))
})
.collect();
assert!(report.recovered.is_none(), "reopen had to cut torn bytes");
assert_eq!(
whos,
vec![
(Seq::new(0), "city".to_string()),
(Seq::new(1), "city".to_string())
]
);
}
#[test]
fn a_restored_tail_survives_a_power_cut() {
let dir = tempfile::tempdir().unwrap();
let fs = FaultFs::new(FaultPlan {
cut_at_op: None,
cut_on_write: Some("doomed"),
torn_tail: TornTail::KeepBytes(7),
});
let (mut ledger, _) = JsonlLedger::open_faulty(fs.clone(), dir.path(), TimeMs::new(0)).unwrap();
ledger.append_all(vec![draft("city", 1)]).unwrap();
assert!(ledger.append_all(vec![draft("doomed", 2)]).is_err());
drop(ledger);
fs.power_cut();
let (reopened, report) = JsonlLedger::open_faulty(fs, dir.path(), TimeMs::new(9)).unwrap();
assert!(
report.recovered.is_none(),
"the restore's truncation was lost with the power"
);
assert_eq!(reopened.position(), Seq::new(1));
}
#[derive(Clone, Copy, Debug)]
enum Fault {
Tear(usize),
Unsynced,
RefuseRestore,
}
struct Scripted {
real: RealFs,
faults: Arc<Mutex<VecDeque<Fault>>>,
}
impl Scripted {
fn take(&self, wanted: fn(Fault) -> bool) -> Option<Fault> {
let mut faults = self.faults.lock().unwrap();
let hit = faults.front().copied().filter(|fault| wanted(*fault));
if hit.is_some() {
faults.pop_front();
}
hit
}
}
fn refused() -> io::Error {
io::Error::other("scripted fault")
}
impl Vfs for Scripted {
fn create_dir_all(&mut self, dir: &Path) -> io::Result<()> {
self.real.create_dir_all(dir)
}
fn list(&self, dir: &Path) -> io::Result<Vec<PathBuf>> {
self.real.list(dir)
}
fn list_dirs(&self, dir: &Path) -> io::Result<Vec<PathBuf>> {
self.real.list_dirs(dir)
}
fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
self.real.read(path)
}
fn size(&self, path: &Path) -> io::Result<u64> {
self.real.size(path)
}
fn read_at(&self, path: &Path, offset: u64, len: u64) -> io::Result<Vec<u8>> {
self.real.read_at(path, offset, len)
}
fn append(&mut self, path: &Path, bytes: &[u8]) -> io::Result<()> {
match self.take(|fault| matches!(fault, Fault::Tear(_))) {
Some(Fault::Tear(kept)) => {
self.real.append(path, &bytes[..kept])?;
Err(refused())
}
_ => self.real.append(path, bytes),
}
}
fn write_at(&mut self, path: &Path, offset: u64, bytes: &[u8]) -> io::Result<()> {
self.real.write_at(path, offset, bytes)
}
fn truncate(&mut self, path: &Path, len: u64) -> io::Result<()> {
match self.take(|fault| matches!(fault, Fault::RefuseRestore)) {
Some(_) => Err(refused()),
None => self.real.truncate(path, len),
}
}
fn sync_data(&mut self, path: &Path) -> io::Result<()> {
match self.take(|fault| matches!(fault, Fault::Unsynced)) {
Some(_) => Err(refused()),
None => self.real.sync_data(path),
}
}
fn rename(&mut self, from: &Path, to: &Path) -> io::Result<()> {
self.real.rename(from, to)
}
fn sync_dir(&mut self, dir: &Path) -> io::Result<()> {
self.real.sync_dir(dir)
}
fn remove_file(&mut self, path: &Path) -> io::Result<()> {
match self.take(|fault| matches!(fault, Fault::RefuseRestore)) {
Some(_) => Err(refused()),
None => self.real.remove_file(path),
}
}
fn copy_permissions(&mut self, from: &Path, to: &Path) -> io::Result<()> {
self.real.copy_permissions(from, to)
}
fn exists(&self, path: &Path) -> bool {
self.real.exists(path)
}
}
struct Wave {
faults: &'static [Fault],
answers_ok: bool,
}
const fn wave(faults: &'static [Fault], answers_ok: bool) -> Wave {
Wave { faults, answers_ok }
}
use Fault::{RefuseRestore, Tear, Unsynced};
const TRACES: &[&[Wave]] = &[
&[
wave(&[], true),
wave(&[Tear(7)], false),
wave(&[], true),
wave(&[], true),
],
&[wave(&[], true), wave(&[Unsynced], false), wave(&[], true)],
&[
wave(&[], true),
wave(&[Tear(7), RefuseRestore], false),
wave(&[], true),
],
&[
wave(&[], true),
wave(&[Tear(7), RefuseRestore], false),
wave(&[RefuseRestore], false),
wave(&[], true),
],
&[wave(&[], true), wave(&[Unsynced, RefuseRestore], false)],
&[wave(&[], true), wave(&[Tear(7), RefuseRestore], false)],
&[
wave(&[Unsynced], false),
wave(&[Tear(3), RefuseRestore], false),
wave(&[], true),
wave(&[Unsynced, RefuseRestore], false),
wave(&[], true),
],
];
#[test]
fn every_answered_seq_survives_reopen_on_every_trace() {
for (index, trace) in TRACES.iter().enumerate() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("ledger");
let faults = Arc::new(Mutex::new(VecDeque::new()));
let vfs = Scripted {
real: RealFs::new(),
faults: Arc::clone(&faults),
};
let (mut ledger, _) = JsonlLedger::open_with(Box::new(vfs), &dir, TimeMs::new(0)).unwrap();
let mut answered = Vec::new();
for (t, step) in trace.iter().enumerate() {
faults.lock().unwrap().extend(step.faults.iter().copied());
let refs = ledger.append_all(vec![draft("city", u64::try_from(t).unwrap())]);
assert_eq!(
refs.is_ok(),
step.answers_ok,
"trace {index}, wave {t}: {refs:?}"
);
answered.extend(refs.into_iter().flatten().map(|r| r.seq()));
assert!(
faults.lock().unwrap().is_empty(),
"trace {index}, wave {t} left a fault unmet"
);
}
drop(ledger);
let (reopened, _) = JsonlLedger::open(&dir, TimeMs::new(99)).unwrap();
let kept: Vec<Seq> = reopened
.read_raw_lines()
.unwrap()
.iter()
.map(|line| EventRecord::parse_line(line).unwrap().seq())
.collect();
for seq in &answered {
assert!(
kept.contains(seq),
"trace {index}: seq {seq:?} was answered and lost"
);
}
}
}