use std::io;
use std::path::{Path, PathBuf};
use crate::capture::{CrashState, Tier};
use crate::report::{Finding, Report, Violation, ViolationKind};
use crate::witness::{Coverage, DurabilityWitness};
use crate::Result;
#[derive(Debug)]
pub struct Scenario<W> {
store: PathBuf,
witness: W,
tier: Tier,
label: String,
}
impl<W: DurabilityWitness> Scenario<W> {
pub fn new(store: impl AsRef<Path>, witness: W) -> Self {
let store = store.as_ref().to_path_buf();
let label = store
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "unnamed".to_string());
Self {
store,
witness,
tier: Tier::PowerCut,
label,
}
}
#[must_use]
pub fn tier(mut self, tier: Tier) -> Self {
self.tier = tier;
self
}
#[must_use]
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = label.into();
self
}
#[must_use]
pub fn selected_tier(&self) -> Tier {
self.tier
}
#[must_use]
pub fn selected_label(&self) -> &str {
&self.label
}
pub fn ack<T>(self, workload: impl FnOnce() -> T) -> Result<Acked<T, W>> {
let ack = workload();
let measured = CrashState::capture(&self.store, self.tier, &self.witness)?;
let reference = if self.tier == Tier::PowerCut {
Some(CrashState::capture(
&self.store,
Tier::ProcessCrash,
&self.witness,
)?)
} else {
None
};
Ok(Acked {
store: self.store,
witness: self.witness,
tier: self.tier,
label: self.label,
ack,
measured,
reference,
control: None,
})
}
}
#[derive(Debug)]
pub struct Acked<T, W> {
store: PathBuf,
witness: W,
tier: Tier,
label: String,
ack: T,
measured: CrashState,
reference: Option<CrashState>,
control: Option<CrashState>,
}
impl<T, W: DurabilityWitness> Acked<T, W> {
#[must_use]
pub fn ack(&self) -> &T {
&self.ack
}
#[must_use]
pub fn crash_state(&self) -> &CrashState {
&self.measured
}
pub fn with_teeth_control(mut self, flush: impl FnOnce() -> io::Result<()>) -> Result<Self> {
flush().map_err(|e| crate::Error::io(Some(self.store.clone()), e))?;
self.control = Some(CrashState::capture(&self.store, self.tier, &self.witness)?);
Ok(self)
}
#[must_use]
pub fn check(self, check: impl Fn(&CrashState, &T) -> Vec<Violation>) -> Report {
let violations = check(&self.measured, &self.ack);
let reference_violations = self.reference.as_ref().map(|r| check(r, &self.ack));
let control_violations = self.control.as_ref().map(|c| check(c, &self.ack));
let coverage = self.witness.coverage();
let finding = decide(
&violations,
control_violations.as_deref(),
&coverage,
self.tier,
);
Report {
label: self.label,
tier: self.tier,
finding,
coverage,
manifest: self.measured.manifest().to_vec(),
manifest_text: self.measured.manifest_text(),
notes: self.measured.notes().to_vec(),
reference_violations,
control_violations,
used_assumption: self.measured.used_assumption(),
}
}
}
fn decide(
violations: &[Violation],
control: Option<&[Violation]>,
coverage: &Coverage,
tier: Tier,
) -> Finding {
if let Some(control) = control {
if !control.is_empty() {
return Finding::HarnessBroken {
why: "the teeth control failed: with the store explicitly flushed, the same \
capture and the same checker still could not find the acked data, so \
this run says nothing about durability. In order of how often it is the \
cause: (1) the flush closure does not actually force the engine's own \
buffers — an fsync cannot make user-space state durable, so the flush \
has to be whatever YOUR system's `really persist it` operation is; \
(2) the store path list is missing a file the engine needs to reopen; \
(3) the checker is looking in the wrong place."
.to_string(),
violations: control.to_vec(),
};
}
}
if violations.iter().any(|v| v.kind == ViolationKind::Baseline) {
return Finding::HarnessBroken {
why: "the captured copy is missing state that already existed BEFORE the workload \
ran, so it is not a usable store. Make the setup state durable before the \
measured workload begins (write it, sync it, and record that sync)."
.to_string(),
violations: violations.to_vec(),
};
}
if violations.is_empty() {
return Finding::Survived;
}
if let Coverage::Partial { unobserved } = coverage {
if tier == Tier::PowerCut {
return Finding::NotMeasured {
why: format!(
"the durable set is incomplete, so this loss is not attributable: \
{unobserved}. powercut reports NotMeasured rather than a loss, because an \
unobserved sync may well have saved this data. Give the witness complete \
coverage — or configure the engine to perform no syncs of its own and do \
them yourself — before treating this as a finding."
),
};
}
}
Finding::Lost {
violations: violations.to_vec(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::report::{Checker, Verdict};
use crate::witness::SyncJournal;
use crate::ScratchDir;
fn read_back(crash: &CrashState) -> Option<Vec<u8>> {
std::fs::read(crash.dir().join("app.log")).ok()
}
#[test]
fn partial_coverage_downgrades_a_red_to_not_measured() {
let store = ScratchDir::new("partial").unwrap();
let db = store.path().join("app.log");
let journal = SyncJournal::partial("the engine also syncs internally");
let report = Scenario::new(store.path(), journal)
.ack(|| std::fs::write(&db, b"acked").unwrap())
.unwrap()
.check(|crash, ()| {
let mut c = Checker::new();
c.survives("app.log", Some(b"acked".to_vec()), read_back(crash));
c.finish()
});
assert_eq!(report.verdict(), Verdict::NotMeasured);
}
#[test]
fn partial_coverage_still_allows_a_pass() {
let store = ScratchDir::new("partial-pass").unwrap();
let db = store.path().join("app.log");
let journal = SyncJournal::partial("the engine also syncs internally");
let report = Scenario::new(store.path(), journal.clone())
.ack(|| {
std::fs::write(&db, b"acked").unwrap();
journal.sync_path(&db).unwrap();
})
.unwrap()
.check(|crash, ()| {
let mut c = Checker::new();
c.survives("app.log", Some(b"acked".to_vec()), read_back(crash));
c.finish()
});
assert_eq!(
report.verdict(),
Verdict::Survived,
"a subset of the true durable set that still honours the ack is a sound pass"
);
}
#[test]
fn a_failing_teeth_control_is_harness_broken_not_lost() {
let store = ScratchDir::new("broken").unwrap();
let db = store.path().join("app.log");
let journal = SyncJournal::complete();
let report = Scenario::new(store.path(), journal.clone())
.ack(|| std::fs::write(&db, b"acked").unwrap())
.unwrap()
.with_teeth_control(|| {
journal.sync_path(&db)?;
Ok(())
})
.unwrap()
.check(|crash, ()| {
let mut c = Checker::new();
let wrong = std::fs::read(crash.dir().join("typo.log")).ok();
c.survives("typo.log", Some(b"acked".to_vec()), wrong);
c.finish()
});
assert_eq!(report.verdict(), Verdict::HarnessBroken);
}
}