Skip to main content

planarian_gym/
conformance.rs

1//! `fake-assumptions-conformance` (SPEC §17a, §18.12): the fake backend as a
2//! second implementation of every docs/assumptions.md entry.
3//!
4//! Two halves. The contracts: one check per assumption the fake can hold,
5//! against a fresh fake machine. The matrix: each mutating backend call,
6//! with each outcome a real backend may give besides success (Partial: the
7//! effect landed but the call failed; Unknown: no answer; Fenced: refused
8//! before any effect), injected into a catalog entry that makes that call.
9//! Every transaction must still end in a terminal state (I12), and the
10//! journal must replay (I15; I6 is the ledger's own guard on replay).
11
12use crate::sim::Sim;
13use planarian_fake::{Fake, Injected, MemStore, gym_world};
14use planarian_schema::*;
15use planarian_txn::journal::{Journal, JournalError};
16use planarian_txn::ledger::Record;
17use planarian_txn::store::Store;
18use toml::Value as T;
19
20/// The mutating calls, and the catalog entry whose run makes each one.
21/// `unbind` is a failed driver bind's revert (`revert_device`); `cancel` is
22/// the kernel cancelling an unanswered activation's jobs before its drain.
23/// (`protect` and the runtime `set` are not made by any fake-harness entry:
24/// `protect` by no caller at all.)
25pub const MATRIX: &[(&str, &str)] = &[
26    ("restart", "svc-killed"),
27    ("stop", "intent-user-stops-service"),
28    ("activate_live", "gen-override-only-change"),
29    ("publish_boot", "gen-override-only-change"),
30    ("swap_override", "gen-override-only-change"),
31    ("bind", "drv-unbound-audio"),
32    ("collect_garbage", "storage-budget-unsatisfiable"),
33    ("publish_boot_once", "boot-only-kernel-confirmed"),
34    ("unbind", "drv-bind-fails-restores-previous"),
35    ("cancel", "crash-job-outlives-drain"),
36];
37
38pub const OUTCOMES: &[Injected] = &[Injected::Partial, Injected::Unknown, Injected::Fenced];
39
40fn fresh() -> Fake {
41    gym_world(MemStore::new(8 << 20))
42}
43
44fn op(txn: u64, epoch: u64) -> OpId {
45    OpId { txn: TxnId(txn), epoch, seq: 1 }
46}
47
48/// A fence at `epoch` for `txn`, as a lease holder would write it.
49fn fenced_at(f: &Fake, txn: u64, epoch: u64) {
50    f.store.set_fence(TxnId(txn), epoch).expect("fence");
51}
52
53/// One check per docs/assumptions.md row the fake can hold. Rows about the
54/// world outside the process (the kernel and systemd are correct) have no
55/// fake counterpart and are listed as such.
56pub fn contracts() -> Vec<(&'static str, Result<(), String>)> {
57    let web = UnitId::service("gym-web");
58    let mut out: Vec<(&'static str, Result<(), String>)> = vec![];
59
60    // Outcome is honest: Succeeded means applied.
61    out.push(("Backend Outcome is honest", {
62        let f = fresh();
63        fenced_at(&f, 1, 1);
64        f.w().units.get_mut(&web).expect("gym-web").observed = Observed::Failed(Reason::ExitCode(1));
65        let r = f.units().restart(&web, &op(1, 1));
66        let o = f.units().observe(&web);
67        let s = f.units().stop(&web, &op(1, 1));
68        let after_stop = f.units().observe(&web);
69        if r != Outcome::Succeeded || o != Ok(Observed::Active) {
70            Err(format!("restart said {r:?}, then observed {o:?}"))
71        } else if s != Outcome::Succeeded || after_stop == Ok(Observed::Active) {
72            Err(format!("stop said {s:?}, then observed {after_stop:?}"))
73        } else {
74            Ok(())
75        }
76    }));
77
78    // activate_live(revert_to) restores revert_to's activation state.
79    out.push(("activate_live(revert_to) restores revert_to", {
80        let f = fresh();
81        fenced_at(&f, 1, 1);
82        let (base, bad) = {
83            let mut w = f.w();
84            let base = w.current;
85            let mut o = OverrideSet::default();
86            o.assignments.insert("services.gym-web.listenAddress".into(), Value::Str("192.0.2.10".into()));
87            (base, w.add_generation(&o))
88        };
89        let a = f.generations().activate_live(bad, &op(1, 1));
90        let broke = f.units().observe(&web);
91        let b = f.generations().activate_live(base, &op(1, 1));
92        let back = f.units().observe(&web);
93        if a != Outcome::Succeeded || !matches!(broke, Ok(Observed::Failed(_))) {
94            Err(format!("the bad candidate did not take effect: {a:?}, {broke:?}"))
95        } else if b != Outcome::Succeeded || back != Ok(Observed::Active) || f.w().current != base {
96            Err(format!("revert left {back:?} on generation {}", f.w().current.0))
97        } else {
98            Ok(())
99        }
100    }));
101
102    // publish_boot and the override promotion are idempotent.
103    out.push(("publish_boot and the override promotion are idempotent", {
104        let f = fresh();
105        fenced_at(&f, 1, 1);
106        let g = f.w().current;
107        let r1 = f.generations().publish_boot(g, &op(1, 1));
108        let entries = f.w().boot_entries.clone();
109        let r2 = f.generations().publish_boot(g, &op(1, 1));
110        let (set, from) = f.generations().read_overrides().expect("overrides");
111        let mut next = set.clone();
112        next.assignments.insert("planarian.reference.fontScale".into(), Value::Str("1.25".into()));
113        let to = f.generations().remember_override(&next).expect("candidate");
114        let s1 = f.generations().swap_override(&from, &to, &op(1, 1));
115        let s2 = f.generations().swap_override(&from, &to, &op(1, 1));
116        if (r1.clone(), r2.clone()) != (Outcome::Succeeded, Outcome::Succeeded) || f.w().boot_entries != entries {
117            Err(format!("publish_boot twice: {r1:?}, {r2:?}"))
118        } else if (s1.clone(), s2.clone()) != (Outcome::Succeeded, Outcome::Succeeded)
119            || f.generations().override_hash() != Ok(to)
120        {
121            Err(format!("promotion twice: {s1:?}, {s2:?}"))
122        } else {
123            Ok(())
124        }
125    }));
126
127    // Journal fsync is durable: what a sync covered survives a crash.
128    out.push(("Journal fsync is durable", {
129        let store = MemStore::new(1 << 20);
130        let mut j = Journal::open(store.clone()).expect("open");
131        j.append(Timestamp(1), Record::KnownGood(GenerationId(7))).expect("append");
132        drop(j);
133        // A power loss: only what a sync covered is left (P21).
134        store.crash();
135        match Journal::open(store) {
136            Ok(j) if j.ledger().known_good == Some(GenerationId(7)) => Ok(()),
137            Ok(_) => Err("the synced record is gone".into()),
138            Err(e) => Err(format!("{e:?}")),
139        }
140    }));
141
142    // SHA-256 (I18): an edited retained record breaks the chain.
143    out.push(("SHA-256 is collision-resistant (I18)", {
144        let store = MemStore::new(1 << 20);
145        let mut j = Journal::open(store.clone()).expect("open");
146        for g in 1..=4 {
147            j.append(Timestamp(g), Record::KnownGood(GenerationId(g))).expect("append");
148        }
149        drop(j);
150        let _ = planarian_txn::journal::tamper(&store, 1, |p| {
151            if let Some(b) = p.iter_mut().find(|b| b.is_ascii_digit()) {
152                *b = if *b == b'9' { b'1' } else { *b + 1 };
153            }
154        });
155        match Journal::open(store) {
156            Err(JournalError::Integrity(_)) => Ok(()),
157            other => Err(format!("an edited record opened as {:?}", other.map(|_| ()))),
158        }
159    }));
160
161    // systemd reports state truthfully and honors X-RestartIfChanged: an
162    // activation that changes planariad's unit does not restart the daemon.
163    out.push(("systemd reports unit state truthfully; X-RestartIfChanged honored", {
164        let f = fresh();
165        fenced_at(&f, 1, 1);
166        f.w().alive.insert(4242);
167        f.w().units.get_mut(&web).expect("gym-web").observed = Observed::Bound;
168        let seen = f.units().observe(&web);
169        let g = {
170            let mut w = f.w();
171            let mut core = w.core_units.clone();
172            core.insert("planariad.service".into(), Hash::of(b"changed"));
173            w.core_units = core;
174            w.add_generation(&OverrideSet::default())
175        };
176        let r = f.generations().activate_live(g, &op(1, 1));
177        if seen != Ok(Observed::Bound) {
178            Err(format!("a stopped unit observed as {seen:?}"))
179        } else if r != Outcome::Succeeded || !f.w().alive.contains(&4242) {
180            Err("activation restarted the daemon".into())
181        } else {
182            Ok(())
183        }
184    }));
185
186    // Fencing: a stale epoch is refused before any effect.
187    out.push(("Fencing is effective", {
188        let f = fresh();
189        fenced_at(&f, 1, 2);
190        f.w().units.get_mut(&web).expect("gym-web").observed = Observed::Failed(Reason::ExitCode(1));
191        let r = f.units().restart(&web, &op(1, 1));
192        let o = f.units().observe(&web);
193        let logged = f.w().calls.last().is_some_and(|c| c.fenced);
194        if r.is_fenced() && matches!(o, Ok(Observed::Failed(_))) && logged {
195            Ok(())
196        } else {
197            Err(format!("stale epoch: {r:?}, unit {o:?}"))
198        }
199    }));
200
201    // The kernel and systemd are correct: the world outside the process.
202    out.push(("The kernel and systemd are correct", Ok(())));
203
204    // Progress (I12): backend calls terminate; jobs_tagged finds every job.
205    out.push(("Backend calls terminate; jobs_tagged finds every job an op started", {
206        let f = fresh();
207        fenced_at(&f, 1, 1);
208        f.w().slow_activation = Some((Millis::secs(60), true));
209        let g = f.w().add_generation(&OverrideSet::default());
210        let r = f.generations().activate_live(g, &op(1, 1));
211        let jobs = f.generations().jobs_tagged(&op(1, 1));
212        if !matches!(r, Outcome::Unknown(_)) {
213            Err(format!("a slow activation answered {r:?}"))
214        } else if !matches!(&jobs, Ok(Some(j)) if j.len() == 1) {
215            Err(format!("jobs_tagged: {jobs:?}"))
216        } else {
217            Ok(())
218        }
219    }));
220
221    // swap_override is a compare-and-swap.
222    out.push(("swap_override is a compare-and-swap", {
223        let f = fresh();
224        fenced_at(&f, 1, 1);
225        let (set, from) = f.generations().read_overrides().expect("overrides");
226        let mut next = set.clone();
227        next.assignments.insert("planarian.reference.fontScale".into(), Value::Str("1.5".into()));
228        let to = f.generations().remember_override(&next).expect("candidate");
229        let stale = Hash::of(b"someone else's file");
230        let r = f.generations().swap_override(&stale, &to, &op(1, 1));
231        if matches!(r, Outcome::Failed(_)) && f.generations().override_hash() == Ok(from) {
232            Ok(())
233        } else {
234            Err(format!("a swap from a stale hash: {r:?}"))
235        }
236    }));
237    out
238}
239
240/// Run `entry` with `outcome` injected into its first `call`; Ok if every
241/// transaction ends terminal, the journal replays, and the call was made.
242pub fn injected(entry: &T, call: &str, outcome: Injected) -> Result<(), String> {
243    let steps = |k: &str| match entry.get(k) {
244        Some(T::Array(a)) => a.clone(),
245        Some(t @ T::Table(_)) => vec![t.clone()],
246        _ => vec![],
247    };
248    let (setup, inject, after) = (steps("setup"), steps("inject"), steps("after"));
249    let mut sim = Sim::new();
250    for _ in 0..3 {
251        sim.tick();
252    }
253    sim.begin();
254    // Armed before the first step: the entry's first such call gets it.
255    sim.fake.w().inject.insert(call.into(), outcome);
256    for s in setup.iter().chain(inject.iter()).chain(after.iter()) {
257        sim.step(s).map_err(|u| format!("step: {}", u.0))?;
258    }
259    sim.run_window(crate::window(entry));
260    // Time for the watchdog and the incident manager to finish what is left.
261    sim.run_window(Millis::secs(3600));
262    if sim.fake.w().inject.contains_key(call) {
263        return Err(format!("{} never called {call}", entry.get("id").and_then(T::as_str).unwrap_or("?")));
264    }
265    let l = sim.ledger();
266    let open: Vec<String> = l
267        .live
268        .values()
269        .filter(|t| !matches!(t.txn.state, TxnState::AwaitingBoot | TxnState::Built))
270        .map(|t| format!("txn {} in {:?}", t.txn.id.0, t.txn.state))
271        .collect();
272    if !open.is_empty() {
273        return Err(format!("not terminal after an hour (I12): {}", open.join(", ")));
274    }
275    Journal::open(sim.fake.store.clone()).map(|_| ()).map_err(|e| format!("the journal does not replay (I15): {e:?}"))
276}
277
278/// The whole entry: contracts, then the matrix.
279pub fn run(catalog: &[T]) -> (bool, Vec<String>) {
280    let mut notes = vec![];
281    let mut ok = true;
282    for (name, r) in contracts() {
283        match r {
284            Ok(()) => notes.push(format!("AssumptionHeld({name})")),
285            Err(e) => {
286                ok = false;
287                notes.push(format!("assumption {name}: {e}"));
288            }
289        }
290    }
291    for (call, id) in MATRIX {
292        let Some(entry) = catalog.iter().find(|e| e.get("id").and_then(T::as_str) == Some(id)) else {
293            ok = false;
294            notes.push(format!("{call}: no catalog entry {id}"));
295            continue;
296        };
297        for o in OUTCOMES {
298            match injected(entry, call, *o) {
299                Ok(()) => notes.push(format!("{call} × {o:?} ({id}): terminal, journal replays")),
300                Err(e) => {
301                    ok = false;
302                    notes.push(format!("{call} × {o:?} ({id}): {e}"));
303                }
304            }
305        }
306    }
307    (ok, notes)
308}