//! Coverage bookkeeping for the golden tables. //! //! CLAUDE.md section 7 enumerates the scenarios that must be covered. A list //! nobody checks is a wish, so this module is the check: every mandatory //! scenario must be claimed by at least one case, and every `covers` tag a case //! uses must be one we know about. Both directions fail, because the second is //! how a case silently stops testing what its name claims. //! //! # Some scenarios are not table-shaped //! //! Clock tampering and the 24-hour cap cannot be expressed as data: proving //! them needs arithmetic that moves, or a bound that must hold across every //! lock level, not a decision at a point in time. Those live in //! `super::purity` as code fixtures. [`Location`] records which scenarios are //! covered where, so "covered" never quietly means "listed but not tested". use std::collections::BTreeMap; /// Where a scenario's coverage lives. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Location { /// A golden table case claims it. Table, /// A code fixture in `tests/src/purity.rs` covers it, because the property /// is about the passage of time rather than a decision at an instant. Fixture, } /// Every scenario CLAUDE.md section 7 (and section 1.2 / 1.5.4) requires. pub const REQUIRED: &[(&str, Location)] = &[ // Section 7's explicit list. ("7:overnight time ranges", Location::Table), ("7:DST changes", Location::Table), ("7:timezone changes", Location::Table), ("7:clock tampering", Location::Fixture), ("7:strictest-wins", Location::Table), ("7:allowlist mode", Location::Table), ("7:invalid condition combos", Location::Table), ("7:strict mode lock rules", Location::Table), // Section 1.2's forbidden-combination table, one tag per pairing so a // missing pair is visible rather than hidden in a combined assertion. ("1.2:time+usage rejected", Location::Table), ("1.2:time+launch rejected", Location::Table), ("1.2:usage+launch rejected", Location::Table), ("1.2:usage+location rejected", Location::Table), ("1.2:launch+location rejected", Location::Table), // Section 1.5.4's safety guarantees. ("1.5.4:never-block list is unblockable", Location::Table), ("1.5.4:worst case 24h", Location::Fixture), ]; /// Fixture names in `tests/src/purity.rs` that satisfy [`Location::Fixture`]. /// /// Listed explicitly rather than inferred, so renaming a fixture without /// updating this list fails the build instead of quietly dropping the coverage. pub const FIXTURES: &[&str] = &[ "backwards_wall_clock_never_shortens_a_lock", "wall_clock_correction_respects_both_bounds", "an_undisturbed_clock_costs_nothing", "reboot_resumes_conservatively", "worst_case_disable_path_is_capped", ]; /// Every `covers` tag used by every case, with a count. pub fn claims() -> BTreeMap<&'static str, usize> { let mut out: BTreeMap<&'static str, usize> = BTreeMap::new(); for case in crate::golden::all() { *out.entry(case.covers).or_insert(0) += 1; } out } /// Table-shaped scenarios with no case claiming them. /// /// [`Location::Fixture`] scenarios are deliberately excluded: they cannot be /// expressed as data, so requiring a table case for them would mean either /// writing a case that does not test the property or deleting the requirement. /// They are checked by [`fixtures_missing`] instead. pub fn uncovered() -> Vec<&'static str> { let claims = claims(); REQUIRED .iter() .filter(|(_, where_)| *where_ == Location::Table) .map(|(tag, _)| *tag) .filter(|tag| !claims.contains_key(tag)) .collect() } /// Fixture-backed scenarios whose fixture names are not all listed. /// /// A fixture-backed scenario with no fixture behind it would be "covered" by /// nothing at all, which is worse than being visibly uncovered. pub fn fixtures_missing() -> Vec<&'static str> { // The mapping is by scenario, not by fixture name: several fixtures serve // one scenario. Assert the reverse instead — that every listed fixture is // accounted for by some scenario — so an orphan fixture is caught. REQUIRED .iter() .filter(|(_, where_)| *where_ == Location::Fixture) .map(|(tag, _)| *tag) .collect() } /// Tags used by a case that are not in [`REQUIRED`]. /// /// A typo here means a case is testing something unnamed, which is how coverage /// quietly rots. pub fn unknown_claims() -> Vec<&'static str> { let known: Vec<&str> = REQUIRED.iter().map(|(tag, _)| *tag).collect(); claims().keys().filter(|tag| !known.contains(tag)).copied().collect() }