#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![doc = include_str!("../README.md")]
mod baseline;
mod capture;
mod report;
mod scenario;
mod scratch;
mod witness;
pub use baseline::{
assert_no_worse_than, compare_to_baseline, Baseline, BaselineEntry, UPDATE_ENV,
};
pub use capture::{CapturedFile, CrashState, Tier, Warrant};
pub use report::{Checker, Finding, IsAbsent, Report, Verdict, Violation, ViolationKind};
pub use scenario::{Acked, Scenario};
pub use scratch::ScratchDir;
pub use witness::{
AssumedDurable, Barrier, Coverage, DurabilityWitness, SyncJournal, SyncKind, SyncRecord,
};
use std::path::PathBuf;
pub type Result<T> = std::result::Result<T, Error>;
#[non_exhaustive]
#[derive(Debug)]
pub enum Error {
StoreMissing(PathBuf),
Locked {
path: PathBuf,
source: std::io::Error,
},
Io {
path: Option<PathBuf>,
source: std::io::Error,
},
}
impl Error {
pub(crate) fn io(path: Option<PathBuf>, source: std::io::Error) -> Self {
Error::Io { path, source }
}
pub(crate) fn from_capture_read(path: &std::path::Path, source: std::io::Error) -> Self {
#[cfg(windows)]
{
const WINDOWS_LOCK_CODES: [i32; 2] = [32, 33];
if source
.raw_os_error()
.is_some_and(|code| WINDOWS_LOCK_CODES.contains(&code))
{
return Error::Locked {
path: path.to_path_buf(),
source,
};
}
}
Error::io(Some(path.to_path_buf()), source)
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::StoreMissing(p) => write!(
f,
"the store directory `{}` does not exist. `Scenario::new` wants the directory \
your engine keeps its files in, and it must exist before the workload runs.",
p.display()
),
Error::Locked { path, source } => write!(
f,
"cannot read `{}` for the capture: another process holds an exclusive lock over \
it ({source}). powercut copies a store's bytes while the engine is still open, \
and a byte range under an exclusive Windows lock is unreadable through any \
other handle — redb takes one for as long as the database is open; SQLite does \
not, because its locks sit beyond EOF. The file is NOT skipped, because a store \
file missing from a capture would manufacture a false `Lost`. Measure this \
engine on macOS or Linux, or drive it so that no handle is open at the ack.",
path.display()
),
Error::Io {
path: Some(p),
source,
} => write!(f, "i/o error at `{}`: {source}", p.display()),
Error::Io { path: None, source } => write!(f, "i/o error: {source}"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Io { source, .. } | Error::Locked { source, .. } => Some(source),
Error::StoreMissing(_) => None,
}
}
}
impl From<std::io::Error> for Error {
fn from(source: std::io::Error) -> Self {
Error::Io { path: None, source }
}
}
#[must_use]
pub fn to_markdown(reports: &[Report]) -> String {
let mut out = String::from("| case | tier | verdict | detail |\n|---|---|---|---|\n");
for report in reports {
out.push_str(&report.to_markdown_row());
out.push('\n');
}
out
}
pub fn assert_all_survived(reports: &[Report]) {
for report in reports {
println!("{}", report.to_text());
}
let failed: Vec<&Report> = reports.iter().filter(|r| !r.verdict().is_pass()).collect();
assert!(
failed.is_empty(),
"\npowercut: {} of {} case(s) did not survive:\n\n{}\n",
failed.len(),
reports.len(),
failed
.iter()
.map(|r| r.to_text())
.collect::<Vec<_>>()
.join("\n")
);
}