use std::fmt;
use crate::capture::{CapturedFile, Tier};
use crate::witness::Coverage;
#[non_exhaustive]
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ViolationKind {
LostData,
Divergent,
Baseline,
}
impl ViolationKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
ViolationKind::LostData => "lost-data",
ViolationKind::Divergent => "divergent",
ViolationKind::Baseline => "baseline",
}
}
#[must_use]
pub fn parse(s: &str) -> Option<Self> {
match s {
"lost-data" => Some(ViolationKind::LostData),
"divergent" => Some(ViolationKind::Divergent),
"baseline" => Some(ViolationKind::Baseline),
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Violation {
pub kind: ViolationKind,
pub detail: String,
}
impl Violation {
#[must_use]
pub fn lost(detail: impl Into<String>) -> Self {
Self {
kind: ViolationKind::LostData,
detail: detail.into(),
}
}
#[must_use]
pub fn divergent(detail: impl Into<String>) -> Self {
Self {
kind: ViolationKind::Divergent,
detail: detail.into(),
}
}
#[must_use]
pub fn baseline(detail: impl Into<String>) -> Self {
Self {
kind: ViolationKind::Baseline,
detail: detail.into(),
}
}
}
impl fmt::Display for Violation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let prefix = match self.kind {
ViolationKind::LostData => "LOST",
ViolationKind::Divergent => "DIVERGENT",
ViolationKind::Baseline => "HARNESS/BASELINE",
};
write!(f, "{prefix}: {}", self.detail)
}
}
#[derive(Debug, Default)]
pub struct Checker {
baseline: Vec<Violation>,
rest: Vec<Violation>,
}
impl Checker {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn baseline<T>(&mut self, what: &str, expected: T, actual: T)
where
T: PartialEq + fmt::Debug,
{
if expected != actual {
self.baseline.push(Violation::baseline(format!(
"{what}: the captured copy has {actual:?}, but this was already true \
BEFORE the workload ran (expected {expected:?}). The copy is not a usable \
store, so nothing else in this report is a durability measurement."
)));
}
}
pub fn survives<T>(&mut self, what: &str, expected: T, actual: T)
where
T: PartialEq + fmt::Debug + IsAbsent,
{
if expected == actual {
return;
}
let detail = format!("{what}: acked {expected:?}, the durable set has {actual:?}");
self.rest.push(if actual.is_absent() {
Violation::lost(detail)
} else {
Violation::divergent(detail)
});
}
pub fn push(&mut self, violation: Violation) {
if violation.kind == ViolationKind::Baseline {
self.baseline.push(violation);
} else {
self.rest.push(violation);
}
}
#[must_use]
pub fn finish(mut self) -> Vec<Violation> {
self.baseline.append(&mut self.rest);
self.baseline
}
}
pub trait IsAbsent {
fn is_absent(&self) -> bool;
}
impl<T> IsAbsent for Option<T> {
fn is_absent(&self) -> bool {
self.is_none()
}
}
impl<T> IsAbsent for Vec<T> {
fn is_absent(&self) -> bool {
self.is_empty()
}
}
macro_rules! not_absent {
($($t:ty),*) => {$(
impl IsAbsent for $t {
fn is_absent(&self) -> bool { false }
}
)*};
}
not_absent!(i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, bool, String, &str);
#[non_exhaustive]
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Verdict {
Survived,
NotMeasured,
Lost,
HarnessBroken,
}
impl Verdict {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Verdict::Survived => "survived",
Verdict::NotMeasured => "not-measured",
Verdict::Lost => "lost",
Verdict::HarnessBroken => "harness-broken",
}
}
#[must_use]
pub fn parse(s: &str) -> Option<Self> {
match s {
"survived" => Some(Verdict::Survived),
"not-measured" => Some(Verdict::NotMeasured),
"lost" => Some(Verdict::Lost),
"harness-broken" => Some(Verdict::HarnessBroken),
_ => None,
}
}
#[must_use]
pub fn is_pass(self) -> bool {
matches!(self, Verdict::Survived)
}
#[must_use]
pub const fn severity(self) -> u8 {
match self {
Verdict::Survived => 0,
Verdict::NotMeasured => 1,
Verdict::Lost => 2,
Verdict::HarnessBroken => 3,
}
}
}
impl fmt::Display for Verdict {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[non_exhaustive]
#[derive(Clone, Debug)]
pub enum Finding {
Survived,
Lost {
violations: Vec<Violation>,
},
NotMeasured {
why: String,
},
HarnessBroken {
why: String,
violations: Vec<Violation>,
},
}
impl Finding {
#[must_use]
pub fn verdict(&self) -> Verdict {
match self {
Finding::Survived => Verdict::Survived,
Finding::Lost { .. } => Verdict::Lost,
Finding::NotMeasured { .. } => Verdict::NotMeasured,
Finding::HarnessBroken { .. } => Verdict::HarnessBroken,
}
}
#[must_use]
pub fn violations(&self) -> &[Violation] {
match self {
Finding::Lost { violations } | Finding::HarnessBroken { violations, .. } => violations,
Finding::Survived | Finding::NotMeasured { .. } => &[],
}
}
}
#[derive(Debug)]
pub struct Report {
pub(crate) label: String,
pub(crate) tier: Tier,
pub(crate) finding: Finding,
pub(crate) coverage: Coverage,
pub(crate) manifest: Vec<CapturedFile>,
pub(crate) manifest_text: String,
pub(crate) notes: Vec<String>,
pub(crate) reference_violations: Option<Vec<Violation>>,
pub(crate) control_violations: Option<Vec<Violation>>,
pub(crate) used_assumption: bool,
}
impl Report {
#[must_use]
pub fn label(&self) -> &str {
&self.label
}
#[must_use]
pub fn tier(&self) -> Tier {
self.tier
}
#[must_use]
pub fn finding(&self) -> &Finding {
&self.finding
}
#[must_use]
pub fn verdict(&self) -> Verdict {
self.finding.verdict()
}
#[must_use]
pub fn coverage(&self) -> &Coverage {
&self.coverage
}
#[must_use]
pub fn manifest(&self) -> &[CapturedFile] {
&self.manifest
}
#[must_use]
pub fn reached_filesystem(&self) -> Option<bool> {
self.reference_violations.as_ref().map(|v| v.is_empty())
}
#[must_use]
pub fn to_text(&self) -> String {
let mut out = String::new();
out.push_str(&format!(
"powercut: {} [tier {}] => {}\n",
self.label,
self.tier.as_str(),
self.verdict()
));
out.push_str(&format!(" tier models {}\n", self.tier.models()));
match &self.coverage {
Coverage::Complete => {
out.push_str(" witness coverage: COMPLETE\n");
}
Coverage::Partial { unobserved } => {
out.push_str(&format!(" witness coverage: PARTIAL — {unobserved}\n"));
}
}
if self.used_assumption {
out.push_str(
" !! this run used an ASSUMED-durable file. Assumptions only ever ENLARGE \
the durable set, so a pass here is weaker than a measured one.\n",
);
}
match self
.reached_filesystem()
.filter(|_| !self.verdict().is_pass())
{
Some(true) => out.push_str(
" asymmetry: the acked data IS present in the tier-1 byte copy taken at the \
same instant. It reached the filesystem and no further — the missing thing \
is an fsync, not a write.\n",
),
Some(false) => out.push_str(
" asymmetry: the acked data is ALSO absent from the tier-1 byte copy, which \
keeps every byte that reached the filesystem. So the write never reached the \
filesystem at all — it is still in the engine's user-space buffers — or the \
store paths / checker are wrong. A teeth control tells those apart.\n",
),
None => {}
}
if let Some(control) = &self.control_violations {
if control.is_empty() {
out.push_str(
" teeth control: PASSED — with an explicit flush the same copy and the \
same checker do find the data, so the red above is about durability.\n",
);
} else {
out.push_str(" teeth control: FAILED — see below.\n");
}
} else if self.verdict() == Verdict::Lost {
out.push_str(
" teeth control: NOT RUN. Without one, a red cannot be distinguished from a \
broken harness. Add `.with_teeth_control(...)`.\n",
);
}
out.push_str(&self.manifest_text);
for note in &self.notes {
out.push_str(&format!(" note: {note}\n"));
}
let violations = self.finding.violations();
if !violations.is_empty() {
out.push_str(" findings:\n");
for v in violations {
out.push_str(&format!(" {v}\n"));
}
}
if let Finding::NotMeasured { why } = &self.finding {
out.push_str(&format!(" NOT MEASURED: {why}\n"));
}
out
}
#[must_use]
pub fn to_markdown_row(&self) -> String {
let detail = match &self.finding {
Finding::Survived => "—".to_string(),
Finding::NotMeasured { why } => escape(why),
Finding::Lost { violations } | Finding::HarnessBroken { violations, .. } => violations
.iter()
.map(|v| escape(&v.to_string()))
.collect::<Vec<_>>()
.join("; "),
};
format!(
"| `{}` | {} | **{}** | {} |",
self.label,
self.tier.as_str(),
self.verdict(),
detail
)
}
pub fn assert_survived(&self) {
assert!(
self.verdict().is_pass(),
"\n{}\n{}",
self.to_text(),
self.control_detail()
);
}
fn control_detail(&self) -> String {
match &self.control_violations {
Some(v) if !v.is_empty() => {
let mut s = String::from(
" the teeth control FAILED. With everything explicitly flushed the \
checker still could not find the data, so this run measured the harness, \
not the store:\n",
);
for violation in v {
s.push_str(&format!(" {violation}\n"));
}
s
}
_ => String::new(),
}
}
}
pub(crate) fn escape(s: &str) -> String {
s.replace('|', "\\|").replace('\n', " ")
}