1use crate::kernel::{CheckPhase, Host, Verdict};
9use crate::store::{MemStore, Store};
10use planarian_schema::{
11 Built, DeviceInfo, DriverRef, EvaluatedGeneration, GenerationBackend, GenerationId, Hash, InventoryBackend, JobId,
12 Millis, Observed, OpId, OverrideSet, PinnedSource, Resources, StagedSource, StorePath, Timestamp, Transaction,
13 Transition, UnitId,
14};
15use std::collections::{BTreeMap, BTreeSet};
16use std::sync::{Arc, Mutex, MutexGuard};
17
18#[derive(Clone, Debug)]
19pub struct FakeJob {
20 pub op: OpId,
21 pub done_at: Timestamp,
22 pub cancellable: bool,
23 pub activates: Option<GenerationId>,
25}
26
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct CallLog {
29 pub at: Timestamp,
30 pub op: OpId,
31 pub call: String,
32 pub fenced: bool,
33 pub pending: usize,
35}
36
37#[derive(Debug, Default)]
38pub struct World {
39 pub now: Timestamp,
40 pub current: GenerationId,
41 pub boot_default: GenerationId,
42 pub boot_entries: BTreeSet<GenerationId>,
43 pub generations: BTreeMap<GenerationId, StorePath>,
44 pub bad: BTreeSet<GenerationId>,
46 pub env_fault: bool,
48 pub override_hash: Hash,
49 pub units: BTreeMap<UnitId, Observed>,
50 pub devices: BTreeMap<UnitId, Option<DriverRef>>,
51 pub bad_drivers: BTreeSet<String>,
53 pub jobs: BTreeMap<JobId, FakeJob>,
54 pub next_job: u64,
55 pub slow_activation: Option<(Millis, bool)>,
57 pub fail_activate: BTreeSet<GenerationId>,
59 pub fail_publish: bool,
60 pub jobs_tagged_unknown: bool,
61 pub calls: Vec<CallLog>,
62 pub alive: BTreeSet<u32>,
63 pub unkillable: BTreeSet<u32>,
64 pub gc_roots: BTreeSet<String>,
65}
66
67impl World {
68 fn tick(&mut self) {
69 let now = self.now;
70 let done: Vec<JobId> = self.jobs.iter().filter(|(_, j)| j.done_at <= now).map(|(id, _)| id.clone()).collect();
71 for id in done {
72 let job = self.jobs.remove(&id).expect("listed");
73 if let Some(g) = job.activates {
74 self.current = g;
75 }
76 }
77 }
78}
79
80#[derive(Clone, Debug)]
81pub struct FakeHost {
82 pub world: Arc<Mutex<World>>,
83 pub store: MemStore,
84}
85
86impl FakeHost {
87 pub fn new(world: Arc<Mutex<World>>, store: MemStore) -> FakeHost {
88 FakeHost { world, store }
89 }
90
91 pub fn w(&self) -> MutexGuard<'_, World> {
92 let mut w = self.world.lock().unwrap_or_else(|e| e.into_inner());
93 w.tick();
94 w
95 }
96
97 fn admit(&self, w: &mut World, op: &OpId, call: &str) -> bool {
99 let current = self.store.fence(op.txn).ok().flatten();
100 let fenced = !crate::kernel::fence_admits(current, op.epoch);
101 let pending = w.jobs.len();
102 w.calls.push(CallLog { at: w.now, op: *op, call: call.into(), fenced, pending });
103 !fenced
104 }
105}
106
107use planarian_schema::Outcome;
108
109impl GenerationBackend for FakeHost {
110 fn current(&self) -> Result<GenerationId, Outcome> {
111 Ok(self.w().current)
112 }
113
114 fn boot_entries(&self) -> Result<Vec<GenerationId>, Outcome> {
115 Ok(self.w().boot_entries.iter().copied().collect())
116 }
117
118 fn boot_default(&self) -> Result<GenerationId, Outcome> {
119 Ok(self.w().boot_default)
120 }
121
122 fn last_booted(&self) -> Result<GenerationId, Outcome> {
123 Ok(self.w().boot_default)
124 }
125
126 fn stage(&self, _owner: &PinnedSource, _overrides: &OverrideSet) -> Result<StagedSource, Outcome> {
127 Err(Outcome::Failed("fake: staging is exec's concern".into()))
128 }
129
130 fn source_hash(&self, _owner: &PinnedSource) -> Result<Hash, Outcome> {
131 Err(Outcome::Failed("fake: staging is exec's concern".into()))
132 }
133
134 fn evaluate(&self, _src: &StagedSource) -> Result<EvaluatedGeneration, Outcome> {
135 Err(Outcome::Failed("fake: evaluation is exec's concern".into()))
136 }
137
138 fn build(&self, _eval: &EvaluatedGeneration) -> Result<Built, Outcome> {
139 Err(Outcome::Failed("fake: building is exec's concern".into()))
140 }
141
142 fn verify_candidate(&self, path: &StorePath, _nar: Option<&Hash>) -> Result<bool, Outcome> {
143 Ok(self.w().generations.values().any(|p| p == path))
144 }
145
146 fn pending_jobs(&self) -> Result<Vec<JobId>, Outcome> {
147 Ok(self.w().jobs.keys().cloned().collect())
148 }
149
150 fn jobs_tagged(&self, op: &OpId) -> Result<Option<Vec<JobId>>, Outcome> {
151 let w = self.w();
152 if w.jobs_tagged_unknown {
153 return Ok(None);
154 }
155 Ok(Some(w.jobs.iter().filter(|(_, j)| j.op == *op).map(|(id, _)| id.clone()).collect()))
156 }
157
158 fn override_hash(&self) -> Result<Hash, Outcome> {
159 Ok(self.w().override_hash)
160 }
161
162 fn activate_live(&self, g: GenerationId, op: &OpId) -> Outcome {
163 let mut w = self.w();
164 if !self.admit(&mut w, op, &format!("activate_live({})", g.0)) {
165 return Outcome::fenced();
166 }
167 if w.fail_activate.contains(&g) {
168 return Outcome::Failed(format!("activation of {} failed (injected)", g.0));
169 }
170 if let Some((delay, cancellable)) = w.slow_activation {
171 w.next_job += 1;
172 let id = JobId(format!("job-{}", w.next_job));
173 let done_at = w.now.plus(delay);
174 w.jobs.insert(id, FakeJob { op: *op, done_at, cancellable, activates: Some(g) });
175 return Outcome::Unknown("job accepted; reply withheld".into());
176 }
177 w.current = g;
178 Outcome::Succeeded
179 }
180
181 fn publish_boot(&self, g: GenerationId, op: &OpId) -> Outcome {
182 let mut w = self.w();
183 if !self.admit(&mut w, op, &format!("publish_boot({})", g.0)) {
184 return Outcome::fenced();
185 }
186 if w.fail_publish {
187 return Outcome::Failed("ENOSPC on /boot (injected)".into());
188 }
189 w.boot_entries.insert(g);
190 w.boot_default = g;
191 Outcome::Succeeded
192 }
193
194 fn protect(&self, g: GenerationId, op: &OpId) -> Outcome {
195 let mut w = self.w();
196 if !self.admit(&mut w, op, &format!("protect({})", g.0)) {
197 return Outcome::fenced();
198 }
199 Outcome::Succeeded
200 }
201
202 fn cancel(&self, job: &JobId, op: &OpId) -> Outcome {
203 let mut w = self.w();
204 if !self.admit(&mut w, op, &format!("cancel({})", job.0)) {
205 return Outcome::fenced();
206 }
207 match w.jobs.get(job) {
208 Some(j) if j.cancellable => {
209 w.jobs.remove(job);
210 Outcome::Succeeded
211 }
212 Some(_) => Outcome::Failed("job cannot be cancelled".into()),
213 None => Outcome::Succeeded,
214 }
215 }
216
217 fn swap_override(&self, from: &Hash, to: &Hash, op: &OpId) -> Outcome {
218 let mut w = self.w();
219 if !self.admit(&mut w, op, "swap_override") {
220 return Outcome::fenced();
221 }
222 if w.override_hash == *to {
223 return Outcome::Succeeded;
224 }
225 if w.override_hash != *from {
226 return Outcome::Failed("override changed by a third party".into());
227 }
228 w.override_hash = *to;
229 Outcome::Succeeded
230 }
231
232 fn path_of(&self, g: GenerationId) -> Result<StorePath, Outcome> {
233 self.w().generations.get(&g).cloned().ok_or_else(|| Outcome::Failed(format!("no generation {}", g.0)))
234 }
235
236 fn evaluate_built(&self, _path: &StorePath) -> Result<EvaluatedGeneration, Outcome> {
237 Err(Outcome::Failed("fake: evaluation is exec's concern".into()))
238 }
239
240 fn read_overrides(&self) -> Result<(OverrideSet, Hash), Outcome> {
241 Ok((OverrideSet::default(), self.w().override_hash))
242 }
243
244 fn remember_override(&self, set: &OverrideSet) -> Result<Hash, Outcome> {
245 Ok(Hash::of(&serde_json::to_vec(set).expect("serializes")))
246 }
247
248 fn override_for(&self, _g: GenerationId) -> Result<Option<Hash>, Outcome> {
249 Ok(None)
250 }
251
252 fn record_override(&self, _g: GenerationId, _hash: &Hash) -> Result<(), Outcome> {
253 Ok(())
254 }
255
256 fn release_root(&self, root: &str) -> bool {
257 self.w().gc_roots.remove(root)
258 }
259
260 fn collect_garbage(&self, _protect: &[GenerationId], _emergency: bool, op: &OpId) -> Outcome {
261 let mut w = self.w();
262 if !self.admit(&mut w, op, "collect_garbage") {
263 return Outcome::fenced();
264 }
265 Outcome::Succeeded
266 }
267
268 fn gc_plan(&self, _protect: &[GenerationId], _emergency: bool) -> Result<planarian_schema::gc::GcPlan, Outcome> {
269 Ok(planarian_schema::gc::plan(&[], &[], 0, 0, |_| 0))
270 }
271
272 fn space_report(&self) -> Option<planarian_schema::gc::SpaceReport> {
273 None
274 }
275}
276
277impl InventoryBackend for FakeHost {
278 fn devices(&self) -> Result<Vec<DeviceInfo>, Outcome> {
279 Ok(self.w().devices.iter().map(|(id, d)| DeviceInfo { id: id.clone(), driver: d.clone() }).collect())
280 }
281
282 fn resources(&self) -> Result<Resources, Outcome> {
283 Ok(Resources::default())
284 }
285
286 fn bound_driver(&self, device: &UnitId) -> Result<Option<DriverRef>, Outcome> {
287 Ok(self.w().devices.get(device).cloned().flatten())
288 }
289
290 fn bind(&self, device: &UnitId, driver: &DriverRef, op: &OpId) -> Outcome {
291 let mut w = self.w();
292 if !self.admit(&mut w, op, &format!("bind({device}, {})", driver.name)) {
293 return Outcome::fenced();
294 }
295 w.devices.insert(device.clone(), Some(driver.clone()));
296 Outcome::Succeeded
297 }
298
299 fn unbind(&self, device: &UnitId, op: &OpId) -> Outcome {
300 let mut w = self.w();
301 if !self.admit(&mut w, op, &format!("unbind({device})")) {
302 return Outcome::fenced();
303 }
304 w.devices.insert(device.clone(), None);
305 Outcome::Succeeded
306 }
307}
308
309impl Host for FakeHost {
310 type Gen = FakeHost;
311 type Inv = FakeHost;
312
313 fn generations(&self) -> &FakeHost {
314 self
315 }
316
317 fn inventory(&self) -> &FakeHost {
318 self
319 }
320
321 fn execute_runtime(&self, t: &Transition, op: &OpId) -> Outcome {
322 let mut w = self.w();
323 let name = match t {
324 Transition::RestartUnit(u) => format!("restart({u})"),
325 Transition::StopUnit(u) => format!("stop({u})"),
326 other => format!("{other:?}"),
327 };
328 if !self.admit(&mut w, op, &name) {
329 return Outcome::fenced();
330 }
331 match t {
332 Transition::RestartUnit(u) => {
333 w.units.insert(u.clone(), Observed::Active);
334 }
335 Transition::StopUnit(u) => {
336 w.units.insert(u.clone(), Observed::Bound);
337 }
338 _ => {}
339 }
340 Outcome::Succeeded
341 }
342
343 fn reobserve(&self, _txn: &Transaction) -> Outcome {
344 Outcome::Succeeded
345 }
346
347 fn check(&self, _txn: &Transaction, _phase: CheckPhase) -> Verdict {
348 let w = self.w();
349 if w.env_fault {
350 return Verdict::Fail("canonical probe failed (environment)".into());
351 }
352 if w.bad.contains(&w.current) {
353 return Verdict::Fail(format!("probes fail on generation {}", w.current.0));
354 }
355 if let Some((dev, d)) =
356 w.devices.iter().find(|(_, d)| d.as_ref().is_some_and(|d| w.bad_drivers.contains(&d.name)))
357 {
358 return Verdict::Fail(format!("{dev} bound to {}", d.as_ref().expect("checked").name));
359 }
360 Verdict::Pass
361 }
362
363 fn release_gc_root(&self, path: &str) -> bool {
364 self.w().gc_roots.remove(path)
365 }
366
367 fn now(&self) -> Timestamp {
368 self.w().now
369 }
370
371 fn boot_now(&self) -> Timestamp {
372 self.w().now
373 }
374
375 fn sleep(&self, d: Millis) {
376 let mut w = self.w();
377 w.now = w.now.plus(d);
378 w.tick();
379 }
380
381 fn kill_worker(&self, pid: u32) -> bool {
382 let mut w = self.w();
383 if w.unkillable.contains(&pid) {
384 return false;
385 }
386 w.alive.remove(&pid);
387 true
388 }
389
390 fn pid_alive(&self, pid: u32) -> bool {
391 self.w().alive.contains(&pid)
392 }
393}