Skip to main content

lenso_test/
simulator.rs

1//! Cooperative controls for deterministic native `TestApp` scenarios.
2//!
3//! A Simulator Gate is private test infrastructure. It only pauses futures
4//! that voluntarily wait on it; it does not preempt arbitrary Plugin, network,
5//! database, or CPU work.
6
7use std::{cell::RefCell, future::Future, rc::Rc, time::Duration};
8
9use futures::{channel::oneshot, future::LocalBoxFuture, task::SpawnError};
10use lenso_kernel::{DeterministicDriver, DriverTask, RuntimeDriver};
11
12use crate::FaultInjector;
13
14/// A deterministic test-only execution environment around one Driver.
15#[derive(Clone, Debug, Default)]
16pub struct TestSimulator {
17    driver: DeterministicDriver,
18    faults: FaultInjector,
19}
20
21impl TestSimulator {
22    /// Creates a Simulator at virtual monotonic instant zero.
23    pub fn new() -> Self {
24        Self::default()
25    }
26
27    /// Creates a named cooperative checkpoint for one test scenario.
28    pub fn gate(&self, name: impl Into<String>) -> SimulatorGate {
29        SimulatorGate {
30            name: name.into(),
31            state: Rc::new(RefCell::new(GateState::default())),
32        }
33    }
34
35    /// Creates a cooperative, test-owned resource availability control.
36    ///
37    /// Unlike a [`SimulatorGate`], a resource may be frozen and thawed more
38    /// than once. Scenario code opts in by awaiting [`SimulatorResource::acquire`]
39    /// at the same private resource boundary it uses in production; the
40    /// simulator never substitutes a second business implementation.
41    pub fn resource(&self, name: impl Into<String>) -> SimulatorResource {
42        SimulatorResource {
43            name: name.into(),
44            state: Rc::new(RefCell::new(ResourceState::default())),
45        }
46    }
47
48    /// Returns this Simulator's test-owned, explicit fault injector.
49    ///
50    /// Injected failures stay inert until scenario code checks the exact
51    /// boundary. This avoids turning target behavior into an implicit global
52    /// fault policy.
53    pub fn faults(&self) -> FaultInjector {
54        self.faults.clone()
55    }
56
57    /// Runs one root future on the deterministic Driver.
58    ///
59    /// This must remain the outermost Driver run for a scenario. Calling it
60    /// from a future already running on this Simulator is recursive and is
61    /// rejected by the underlying Driver.
62    pub fn run<F: Future>(&self, future: F) -> F::Output {
63        self.driver.run(future)
64    }
65
66    /// Cooperatively drives currently runnable local work.
67    ///
68    /// A pump is a scheduling boundary, not a single-step scheduler: the
69    /// underlying local executor may poll more than one ready future before
70    /// this call returns. Call it only outside a running [`Self::run`] future.
71    pub fn pump(&self) {
72        self.run(self.driver.yield_now());
73    }
74
75    /// Spawns voluntary scenario work on the deterministic local lane.
76    pub fn spawn<F>(&self, future: F) -> Result<DriverTask, SpawnError>
77    where
78        F: Future<Output = ()> + 'static,
79    {
80        self.driver.spawn_local(Box::pin(future))
81    }
82
83    /// Advances virtual time and wakes elapsed deterministic timers.
84    pub fn advance(&self, duration: Duration) {
85        self.driver.advance(duration);
86    }
87
88    /// Returns the current virtual monotonic instant.
89    pub fn now(&self) -> Duration {
90        self.driver.now()
91    }
92
93    /// Waits until a virtual monotonic instant on this Simulator's Driver.
94    ///
95    /// Deadlines use the same time origin as [`Self::now`]. An elapsed deadline
96    /// completes immediately; a future deadline requires [`Self::advance`] and
97    /// polling the waiting work, for example with [`Self::pump`]. Sleeping never
98    /// advances time automatically. The returned future owns its wait and can
99    /// be moved into a Plugin or a spawned task without borrowing the Simulator.
100    pub fn sleep_until(&self, deadline: Duration) -> LocalBoxFuture<'static, ()> {
101        self.driver.sleep_until(deadline)
102    }
103
104    /// Returns the same Driver for generated Host lowering and facility clocks.
105    /// External processes and I/O are not virtualized by this Driver.
106    pub fn driver(&self) -> DeterministicDriver {
107        self.driver.clone()
108    }
109}
110
111#[derive(Debug, Default)]
112struct ResourceState {
113    acquisitions: usize,
114    frozen: bool,
115    thaw_notifiers: Vec<oneshot::Sender<()>>,
116}
117
118/// A named, cooperative resource owner that a test can freeze independently.
119///
120/// This represents ownership/availability only. It deliberately does not
121/// provide a global service locator, connection pool, or implicit resource
122/// selection mechanism.
123#[derive(Clone, Debug)]
124pub struct SimulatorResource {
125    name: String,
126    state: Rc<RefCell<ResourceState>>,
127}
128
129impl SimulatorResource {
130    /// Returns the stable test-only owner name.
131    pub fn name(&self) -> &str {
132        &self.name
133    }
134
135    /// Waits until this resource owner is available, then records one access.
136    pub async fn acquire(&self) {
137        loop {
138            let thaw = {
139                let mut state = self.state.borrow_mut();
140                if state.frozen {
141                    let (sender, receiver) = oneshot::channel();
142                    state.thaw_notifiers.push(sender);
143                    Some(receiver)
144                } else {
145                    state.acquisitions += 1;
146                    None
147                }
148            };
149
150            let Some(thaw) = thaw else {
151                return;
152            };
153            let _ = thaw.await;
154        }
155    }
156
157    /// Freezes future cooperative acquisitions of this specific owner.
158    ///
159    /// Returns true only for the transition from available to frozen.
160    #[must_use]
161    pub fn freeze(&self) -> bool {
162        let mut state = self.state.borrow_mut();
163        if state.frozen {
164            return false;
165        }
166        state.frozen = true;
167        true
168    }
169
170    /// Makes this owner available and releases all currently blocked accessors.
171    ///
172    /// Returns true only for the transition from frozen to available.
173    #[must_use]
174    pub fn thaw(&self) -> bool {
175        let notifiers = {
176            let mut state = self.state.borrow_mut();
177            if !state.frozen {
178                return false;
179            }
180            state.frozen = false;
181            std::mem::take(&mut state.thaw_notifiers)
182        };
183        for notifier in notifiers {
184            let _ = notifier.send(());
185        }
186        true
187    }
188
189    /// Returns the number of completed cooperative acquisitions.
190    pub fn acquisition_count(&self) -> usize {
191        self.state.borrow().acquisitions
192    }
193
194    /// Returns whether this owner is currently frozen.
195    pub fn is_frozen(&self) -> bool {
196        self.state.borrow().frozen
197    }
198}
199
200#[derive(Debug, Default)]
201struct GateState {
202    reached: usize,
203    released: bool,
204    reached_notifiers: Vec<oneshot::Sender<()>>,
205    release_notifiers: Vec<oneshot::Sender<()>>,
206}
207
208/// A named checkpoint that a test explicitly releases.
209#[derive(Clone, Debug)]
210pub struct SimulatorGate {
211    name: String,
212    state: Rc<RefCell<GateState>>,
213}
214
215impl SimulatorGate {
216    /// Returns the stable test-only checkpoint name.
217    pub fn name(&self) -> &str {
218        &self.name
219    }
220
221    /// Waits until the test releases this checkpoint.
222    pub async fn wait(&self) {
223        let release = {
224            let mut state = self.state.borrow_mut();
225            state.reached += 1;
226            for notifier in state.reached_notifiers.drain(..) {
227                let _ = notifier.send(());
228            }
229            if state.released {
230                None
231            } else {
232                let (sender, receiver) = oneshot::channel();
233                state.release_notifiers.push(sender);
234                Some(receiver)
235            }
236        };
237
238        if let Some(release) = release {
239            let _ = release.await;
240        }
241    }
242
243    /// Waits until at least one operation has reached this checkpoint.
244    pub async fn reached(&self) {
245        let notification = {
246            let mut state = self.state.borrow_mut();
247            if state.reached > 0 {
248                None
249            } else {
250                let (sender, receiver) = oneshot::channel();
251                state.reached_notifiers.push(sender);
252                Some(receiver)
253            }
254        };
255
256        if let Some(notification) = notification {
257            let _ = notification.await;
258        }
259    }
260
261    /// Releases every operation currently waiting at this checkpoint.
262    ///
263    /// Returns true only for the transition from blocked to released.
264    #[must_use]
265    pub fn release(&self) -> bool {
266        let notifiers = {
267            let mut state = self.state.borrow_mut();
268            if state.released {
269                return false;
270            }
271            state.released = true;
272            std::mem::take(&mut state.release_notifiers)
273        };
274        for notifier in notifiers {
275            let _ = notifier.send(());
276        }
277        true
278    }
279
280    /// Returns how many voluntary waits have reached this checkpoint.
281    pub fn reached_count(&self) -> usize {
282        self.state.borrow().reached
283    }
284
285    /// Returns whether the test has released this checkpoint.
286    pub fn is_released(&self) -> bool {
287        self.state.borrow().released
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use std::{cell::RefCell, rc::Rc};
294
295    use futures::FutureExt;
296    use lenso_kernel::TaskOutcome;
297
298    use super::*;
299
300    #[test]
301    fn gate_blocks_work_until_the_test_releases_it() {
302        let simulator = TestSimulator::new();
303        let gate = simulator.gate("provider.before-response");
304        let task = simulator
305            .spawn({
306                let gate = gate.clone();
307                async move {
308                    gate.wait().await;
309                }
310            })
311            .unwrap();
312
313        simulator.run(gate.reached());
314        assert_eq!(gate.name(), "provider.before-response");
315        assert_eq!(gate.reached_count(), 1);
316        assert!(!gate.is_released());
317        assert!(gate.release());
318        assert!(!gate.release());
319        assert_eq!(simulator.run(task), TaskOutcome::Completed);
320    }
321
322    #[test]
323    fn gates_allow_test_chosen_completion_order() {
324        let simulator = TestSimulator::new();
325        let first = simulator.gate("operation.first");
326        let second = simulator.gate("operation.second");
327        let completed = Rc::new(RefCell::new(Vec::new()));
328
329        let first_task = simulator
330            .spawn({
331                let first = first.clone();
332                let completed = completed.clone();
333                async move {
334                    first.wait().await;
335                    completed.borrow_mut().push("first");
336                }
337            })
338            .unwrap();
339        let second_task = simulator
340            .spawn({
341                let second = second.clone();
342                let completed = completed.clone();
343                async move {
344                    second.wait().await;
345                    completed.borrow_mut().push("second");
346                }
347            })
348            .unwrap();
349
350        simulator.run(async {
351            first.reached().await;
352            second.reached().await;
353        });
354        assert!(second.release());
355        assert_eq!(simulator.run(second_task), TaskOutcome::Completed);
356        assert_eq!(&*completed.borrow(), &["second"]);
357
358        assert!(first.release());
359        assert_eq!(simulator.run(first_task), TaskOutcome::Completed);
360        assert_eq!(&*completed.borrow(), &["second", "first"]);
361    }
362
363    #[test]
364    fn cancelling_one_blocked_operation_does_not_release_its_peer() {
365        let simulator = TestSimulator::new();
366        let cancelled = simulator.gate("operation.cancelled");
367        let peer = simulator.gate("operation.peer");
368        let cancelled_task = simulator
369            .spawn({
370                let cancelled = cancelled.clone();
371                async move {
372                    cancelled.wait().await;
373                }
374            })
375            .unwrap();
376        let peer_task = simulator
377            .spawn({
378                let peer = peer.clone();
379                async move {
380                    peer.wait().await;
381                }
382            })
383            .unwrap();
384
385        simulator.run(async {
386            cancelled.reached().await;
387            peer.reached().await;
388        });
389        cancelled_task.cancel();
390        assert_eq!(simulator.run(cancelled_task), TaskOutcome::Cancelled);
391        assert!(!peer.is_released());
392
393        assert!(peer.release());
394        assert_eq!(simulator.run(peer_task), TaskOutcome::Completed);
395    }
396
397    #[test]
398    fn advancing_virtual_time_wakes_elapsed_work_when_the_test_pumps() {
399        let simulator = TestSimulator::new();
400        let completed = Rc::new(RefCell::new(false));
401        let sleep = simulator.sleep_until(Duration::from_millis(5));
402        let task = simulator
403            .spawn({
404                let completed = completed.clone();
405                async move {
406                    sleep.await;
407                    *completed.borrow_mut() = true;
408                }
409            })
410            .unwrap();
411
412        simulator.pump();
413        assert!(!*completed.borrow());
414        simulator.advance(Duration::from_millis(4));
415        simulator.pump();
416        assert!(!*completed.borrow());
417        simulator.advance(Duration::from_millis(1));
418        assert_eq!(simulator.now(), Duration::from_millis(5));
419        assert!(!*completed.borrow());
420        simulator.pump();
421        assert!(*completed.borrow());
422        assert_eq!(simulator.run(task), TaskOutcome::Completed);
423    }
424
425    #[test]
426    fn sleeping_at_or_before_now_completes_without_advancing_time() {
427        let simulator = TestSimulator::new();
428        simulator.advance(Duration::from_millis(5));
429
430        for deadline in [Duration::from_millis(4), simulator.now()] {
431            assert_eq!(simulator.sleep_until(deadline).now_or_never(), Some(()));
432        }
433        assert_eq!(simulator.now(), Duration::from_millis(5));
434    }
435
436    #[test]
437    fn dropping_and_cancelling_sleepers_does_not_complete_peer_work() {
438        let simulator = TestSimulator::new();
439        let completed = Rc::new(RefCell::new(Vec::new()));
440        let deadline = Duration::from_millis(5);
441        drop(simulator.sleep_until(deadline));
442
443        let cancelled = simulator
444            .spawn({
445                let sleep = simulator.sleep_until(deadline);
446                let completed = completed.clone();
447                async move {
448                    sleep.await;
449                    completed.borrow_mut().push("cancelled");
450                }
451            })
452            .unwrap();
453        let peer = simulator
454            .spawn({
455                let sleep = simulator.sleep_until(deadline);
456                let completed = completed.clone();
457                async move {
458                    sleep.await;
459                    completed.borrow_mut().push("peer");
460                }
461            })
462            .unwrap();
463
464        simulator.pump();
465        cancelled.cancel();
466        assert_eq!(simulator.run(cancelled), TaskOutcome::Cancelled);
467        simulator.advance(Duration::from_millis(4));
468        simulator.pump();
469        assert!(completed.borrow().is_empty());
470
471        simulator.advance(Duration::from_millis(1));
472        assert_eq!(simulator.run(peer), TaskOutcome::Completed);
473        assert_eq!(&*completed.borrow(), &["peer"]);
474    }
475
476    #[test]
477    fn freezing_one_resource_owner_does_not_block_another() {
478        let simulator = TestSimulator::new();
479        let auth_store = simulator.resource("auth-store");
480        let object_store = simulator.resource("object-store");
481        assert!(auth_store.freeze());
482        assert!(!auth_store.freeze());
483
484        let blocked = simulator
485            .spawn({
486                let auth_store = auth_store.clone();
487                async move {
488                    auth_store.acquire().await;
489                }
490            })
491            .unwrap();
492        let independent = simulator
493            .spawn({
494                let object_store = object_store.clone();
495                async move {
496                    object_store.acquire().await;
497                }
498            })
499            .unwrap();
500
501        simulator.pump();
502        assert_eq!(auth_store.acquisition_count(), 0);
503        assert_eq!(object_store.acquisition_count(), 1);
504        assert_eq!(simulator.run(independent), TaskOutcome::Completed);
505        assert!(auth_store.is_frozen());
506
507        assert!(auth_store.thaw());
508        assert!(!auth_store.thaw());
509        assert_eq!(simulator.run(blocked), TaskOutcome::Completed);
510        assert_eq!(auth_store.acquisition_count(), 1);
511    }
512}