1use 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
20pub 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
48fn fenced_at(f: &Fake, txn: u64, epoch: u64) {
50 f.store.set_fence(TxnId(txn), epoch).expect("fence");
51}
52
53pub 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 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 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 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 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 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 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 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 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 out.push(("The kernel and systemd are correct", Ok(())));
203
204 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 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
240pub 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 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 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
278pub 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}