# powercut
> A dependency-free Rust crate that proves an acknowledged write survives a
> power cut, inside `cargo test`. It runs a workload to its ack point, captures
> only the bytes a successful `fsync` certified, reboots the store from that
> copy, and checks that the ack was honoured. Database-agnostic: the user
> supplies the store directory, a sync witness, a workload closure and a
> read-back closure.
`powercut` is a testing harness, not a runtime dependency. Its central claim is
that the durable set is **derived** from the syncs that actually happened rather
than declared by the test author, so deleting an `fsync` from a write path turns
a test red with no separate mutation run. Its second claim is that a durability
harness must be able to say "I could not measure this" and "I am the broken
thing here" — hence four verdicts (`Survived`, `Lost`, `NotMeasured`,
`HarnessBroken`) rather than pass/fail.
License: MIT OR Apache-2.0. Zero runtime dependencies. `#![forbid(unsafe_code)]`.
MSRV 1.82. Platforms: macOS, Linux, Windows.
## Start here
- [AGENTS.md](AGENTS.md): the complete brief for a coding agent — the tier
model, the four verdicts, the exact minimal test to copy, how to choose
`Coverage`, the nine mistakes agents make, the full API surface, and the
modelling assumptions. If you read one file, read this one.
- [README.md](README.md): what it is, the 10-line quickstart, the tier model,
the durability primitive and its one honest residual, and the non-goals.
- [docs.rs/powercut](https://docs.rs/powercut): every public item carries its
own compiling doctest — 113 of them across 109 public items, run in CI on
Linux, macOS and Windows. If an example there does not compile, that is a bug.
## Key concepts
- Tier model: `Tier::ProcessCrash` keeps every byte on the filesystem (models
SIGKILL/OS crash); `Tier::PowerCut` keeps only `fsync`-certified bytes, as of
the `fsync` (models a real power cut). Tier 2 is the default.
- `Coverage::Complete` vs `Coverage::Partial`: a witness must declare how much
sync activity it can see. Under partial coverage a passing check is still
sound; a failing one becomes `NotMeasured`, never `Lost`.
- `Acked::with_teeth_control`: an anti-vacuity control run that must pass. If it
does not, the verdict is `HarnessBroken`, so a broken harness cannot accuse a
database.
- `assert_no_worse_than(reports, baseline)`: the CI ratchet. Engines
legitimately differ; what CI wants is "nothing started differing in a new way".
Baselines are recorded with `POWERCUT_UPDATE_BASELINE=1`, which is **ignored
when `CI` is set** — recording accepts today's run as correct, so honouring it
in CI would silently retire the gate. Never offer it as a way to green a build.
## Examples
- [examples/generic_file_store.rs](examples/generic_file_store.rs): a
hand-written store, no engine dependencies, all the mechanics visible.
- [examples/sqlite_durable_ack.rs](examples/sqlite_durable_ack.rs): rusqlite —
`Lost`, `Survived` and `NotMeasured` on one engine, and how to get a fully
measured result from an FFI engine.
- [examples/redb_backend.rs](examples/redb_backend.rs): redb observed through
its own `StorageBackend` trait, with complete coverage.
- [examples/read_fork_close_tear.rs](examples/read_fork_close_tear.rs): the
flagship — a second handle opening and closing destroys the durability of
already-`fsync`'d data, run as a matrix across all three engines.
## Non-goals
- Not a filesystem crash-consistency tester (see CrashMonkey/ACE).
- Not a block-layer fault injector (see LazyFS, which is stronger and
Linux-only; `powercut` trades that for running in `cargo test`).
- Not a distributed-systems simulator or a concurrency checker.
- Cannot detect a drive that lies about honouring a flush. Nothing in user
space can.
- No syscall interposition in 0.1: an FFI engine's internal `fsync` calls are
unobservable, which is what `Coverage::Partial` and `Finding::NotMeasured`
exist to express honestly.
- On Windows, cannot capture a store whose engine holds an exclusive whole-file
lock (redb does; SQLite does not). Capture returns `Error::Locked` rather than
skipping the unreadable file, because a missing file would fake a `Lost`. The
message names the file and says to measure that engine on macOS or Linux.
## Contributing
Arguments are welcome, including arguments that a core design decision is
wrong. See [CONTRIBUTING.md](CONTRIBUTING.md).