Skip to main content

nexus_rt/
testing.rs

1//! Testing utilities for nexus-rt handlers and timer drivers.
2//!
3//! Two tiers of testing infrastructure:
4//!
5//! - [`TestHarness`] — dispatch events through handlers directly,
6//!   auto-advancing sequence numbers, with world access for assertions.
7//! - `TestTimerDriver` (requires `timer` feature) — wraps `TimerPoller`
8//!   with virtual time control for deterministic timer testing.
9//!
10//! Always available (no feature gate).
11
12use crate::Handler;
13use crate::world::{Registry, World, WorldBuilder};
14
15// =============================================================================
16// TestHarness
17// =============================================================================
18
19/// Minimal test harness for handler dispatch.
20///
21/// Owns a [`World`] and auto-advances the sequence number before each
22/// dispatch. Designed for unit testing handlers without wiring up real
23/// drivers.
24///
25/// # Examples
26///
27/// ```
28/// use nexus_rt::{WorldBuilder, ResMut, IntoHandler, Resource};
29/// use nexus_rt::testing::TestHarness;
30///
31/// #[derive(Resource)]
32/// struct Counter(u64);
33///
34/// fn accumulate(mut counter: ResMut<Counter>, event: u64) {
35///     counter.0 += event;
36/// }
37///
38/// let mut builder = WorldBuilder::new();
39/// builder.register(Counter(0));
40/// let mut harness = TestHarness::new(builder);
41///
42/// let mut handler = accumulate.into_handler(harness.registry());
43/// harness.dispatch(&mut handler, 10u64);
44/// harness.dispatch(&mut handler, 5u64);
45///
46/// assert_eq!(harness.world().resource::<Counter>().0, 15);
47/// ```
48pub struct TestHarness {
49    world: World,
50}
51
52impl TestHarness {
53    /// Build a test harness from a [`WorldBuilder`].
54    pub fn new(builder: WorldBuilder) -> Self {
55        Self {
56            world: builder.build(),
57        }
58    }
59
60    /// Registry access for creating handlers after build.
61    pub fn registry(&self) -> &Registry {
62        self.world.registry()
63    }
64
65    /// Advance sequence and dispatch one event through a handler.
66    pub fn dispatch<E>(&mut self, handler: &mut impl Handler<E>, event: E) {
67        self.world.next_sequence();
68        handler.run(&mut self.world, event);
69    }
70
71    /// Dispatch multiple events sequentially, advancing sequence per event.
72    pub fn dispatch_many<E>(
73        &mut self,
74        handler: &mut impl Handler<E>,
75        events: impl IntoIterator<Item = E>,
76    ) {
77        for event in events {
78            self.dispatch(handler, event);
79        }
80    }
81
82    /// Read-only world access for assertions.
83    pub fn world(&self) -> &World {
84        &self.world
85    }
86
87    /// Mutable world access (e.g. to stamp resources manually).
88    pub fn world_mut(&mut self) -> &mut World {
89        &mut self.world
90    }
91}
92
93// =============================================================================
94// TestTimerDriver (behind `timer` feature)
95// =============================================================================
96
97#[cfg(feature = "timer")]
98mod timer_driver {
99    use std::ops::DerefMut;
100    use std::time::{Duration, Instant};
101
102    use crate::Handler;
103    use crate::timer::TimerPoller;
104    use crate::world::World;
105
106    /// Virtual-time wrapper around [`TimerPoller`] for deterministic timer
107    /// testing.
108    ///
109    /// Captures `Instant::now()` at construction as the starting time.
110    /// [`advance`](Self::advance) and [`set_now`](Self::set_now) control
111    /// the virtual clock. [`poll`](Self::poll) delegates to the inner
112    /// poller using the virtual time.
113    ///
114    /// # Examples
115    ///
116    /// ```ignore
117    /// use std::time::{Duration, Instant};
118    /// use nexus_rt::{WorldBuilder, ResMut, IntoHandler};
119    /// use nexus_rt::timer::{TimerInstaller, TimerWheel, Wheel};
120    /// use nexus_rt::testing::TestTimerDriver;
121    ///
122    /// let mut builder = WorldBuilder::new();
123    /// builder.register::<bool>(false);
124    /// let wheel = Wheel::unbounded(64, Instant::now());
125    /// let poller = builder.install_driver(TimerInstaller::new(wheel));
126    /// let mut timer = TestTimerDriver::new(poller);
127    /// let mut world = builder.build();
128    ///
129    /// fn on_fire(mut flag: ResMut<bool>, _now: std::time::Instant) {
130    ///     *flag = true;
131    /// }
132    ///
133    /// let handler = on_fire.into_handler(world.registry());
134    /// let deadline = timer.now() + Duration::from_millis(100);
135    /// world.resource_mut::<TimerWheel>()
136    ///     .schedule_forget(deadline, Box::new(handler));
137    ///
138    /// timer.advance(Duration::from_millis(150));
139    /// let fired = timer.poll(&mut world);
140    /// assert_eq!(fired, 1);
141    /// assert!(*world.resource::<bool>());
142    /// ```
143    pub struct TestTimerDriver<S: 'static = Box<dyn Handler<Instant>>> {
144        poller: TimerPoller<S>,
145        now: Instant,
146    }
147
148    impl<S: DerefMut + Send + 'static> TestTimerDriver<S>
149    where
150        S::Target: Handler<Instant>,
151    {
152        /// Wrap an installed [`TimerPoller`]. Captures `Instant::now()` as
153        /// the starting time.
154        pub fn new(poller: TimerPoller<S>) -> Self {
155            Self {
156                poller,
157                now: Instant::now(),
158            }
159        }
160
161        /// Current virtual time.
162        pub fn now(&self) -> Instant {
163            self.now
164        }
165
166        /// Advance virtual time by a duration.
167        pub fn advance(&mut self, duration: Duration) {
168            self.now += duration;
169        }
170
171        /// Set virtual time to a specific instant.
172        pub fn set_now(&mut self, now: Instant) {
173            self.now = now;
174        }
175
176        /// Poll expired timers at the current virtual time.
177        ///
178        /// Delegates to [`TimerPoller::poll`] with the virtual time.
179        /// Returns the number of timers fired.
180        pub fn poll(&mut self, world: &mut World) -> usize {
181            self.poller.poll(world, self.now)
182        }
183
184        /// Earliest deadline in the wheel.
185        pub fn next_deadline(&self, world: &World) -> Option<Instant> {
186            self.poller.next_deadline(world)
187        }
188
189        /// Number of active timers.
190        pub fn len(&self, world: &World) -> usize {
191            self.poller.len(world)
192        }
193
194        /// Whether the wheel is empty.
195        pub fn is_empty(&self, world: &World) -> bool {
196            self.poller.is_empty(world)
197        }
198    }
199}
200
201#[cfg(feature = "timer")]
202pub use timer_driver::TestTimerDriver;
203
204// =============================================================================
205// Tests
206// =============================================================================
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use crate::{IntoHandler, ResMut};
212
213    // -- TestHarness tests ------------------------------------------------
214
215    fn accumulate(mut counter: ResMut<u64>, event: u64) {
216        *counter += event;
217    }
218
219    #[test]
220    fn dispatch_advances_sequence() {
221        let mut builder = WorldBuilder::new();
222        builder.register::<u64>(0);
223        let mut harness = TestHarness::new(builder);
224
225        let seq_before = harness.world().current_sequence();
226        let mut handler = accumulate.into_handler(harness.registry());
227        harness.dispatch(&mut handler, 1u64);
228        assert_eq!(harness.world().current_sequence().0, seq_before.0 + 1);
229    }
230
231    #[test]
232    fn dispatch_runs_handler() {
233        let mut builder = WorldBuilder::new();
234        builder.register::<u64>(0);
235        let mut harness = TestHarness::new(builder);
236
237        let mut handler = accumulate.into_handler(harness.registry());
238        harness.dispatch(&mut handler, 10u64);
239        assert_eq!(*harness.world().resource::<u64>(), 10);
240    }
241
242    #[test]
243    fn dispatch_many_sequential() {
244        let mut builder = WorldBuilder::new();
245        builder.register::<u64>(0);
246        let mut harness = TestHarness::new(builder);
247
248        let seq_before = harness.world().current_sequence();
249        let mut handler = accumulate.into_handler(harness.registry());
250        harness.dispatch_many(&mut handler, [10u64, 5, 3]);
251        assert_eq!(*harness.world().resource::<u64>(), 18);
252        assert_eq!(harness.world().current_sequence().0, seq_before.0 + 3);
253    }
254
255    #[test]
256    fn world_access() {
257        let mut builder = WorldBuilder::new();
258        builder.register::<u64>(42);
259        let mut harness = TestHarness::new(builder);
260
261        assert_eq!(*harness.world().resource::<u64>(), 42);
262
263        *harness.world_mut().resource_mut::<u64>() = 99;
264        assert_eq!(*harness.world().resource::<u64>(), 99);
265    }
266
267    // -- TestTimerDriver tests --------------------------------------------
268
269    #[cfg(feature = "timer")]
270    mod timer_tests {
271        use crate::testing::TestTimerDriver;
272        use crate::timer::{TimerInstaller, TimerPoller, TimerWheel, Wheel};
273        use crate::{IntoHandler, ResMut, WorldBuilder};
274        use std::time::{Duration, Instant};
275
276        fn set_flag(mut flag: ResMut<bool>, _now: Instant) {
277            *flag = true;
278        }
279
280        #[test]
281        fn advance_moves_time() {
282            let mut builder = WorldBuilder::new();
283            let poller: TimerPoller =
284                builder.install_driver(TimerInstaller::new(Wheel::unbounded(64, Instant::now())));
285            let mut timer = TestTimerDriver::new(poller);
286
287            let start = timer.now();
288            timer.advance(Duration::from_millis(100));
289            assert_eq!(timer.now(), start + Duration::from_millis(100));
290        }
291
292        #[test]
293        fn poll_fires_expired() {
294            let mut builder = WorldBuilder::new();
295            builder.register::<bool>(false);
296            let poller: TimerPoller =
297                builder.install_driver(TimerInstaller::new(Wheel::unbounded(64, Instant::now())));
298            let mut timer = TestTimerDriver::new(poller);
299            let mut world = builder.build();
300
301            let deadline = timer.now() + Duration::from_millis(100);
302            let handler = set_flag.into_handler(world.registry());
303            world
304                .resource_mut::<TimerWheel>()
305                .schedule_forget(deadline, Box::new(handler));
306
307            timer.advance(Duration::from_millis(150));
308            let fired = timer.poll(&mut world);
309            assert_eq!(fired, 1);
310            assert!(*world.resource::<bool>());
311        }
312
313        #[test]
314        fn poll_skips_future() {
315            let mut builder = WorldBuilder::new();
316            builder.register::<bool>(false);
317            let poller: TimerPoller =
318                builder.install_driver(TimerInstaller::new(Wheel::unbounded(64, Instant::now())));
319            let mut timer = TestTimerDriver::new(poller);
320            let mut world = builder.build();
321
322            let deadline = timer.now() + Duration::from_secs(60);
323            let handler = set_flag.into_handler(world.registry());
324            world
325                .resource_mut::<TimerWheel>()
326                .schedule_forget(deadline, Box::new(handler));
327
328            let fired = timer.poll(&mut world);
329            assert_eq!(fired, 0);
330            assert!(!*world.resource::<bool>());
331        }
332
333        #[test]
334        fn set_now_overrides() {
335            let mut builder = WorldBuilder::new();
336            let poller: TimerPoller =
337                builder.install_driver(TimerInstaller::new(Wheel::unbounded(64, Instant::now())));
338            let mut timer = TestTimerDriver::new(poller);
339
340            let target = timer.now() + Duration::from_secs(999);
341            timer.set_now(target);
342            assert_eq!(timer.now(), target);
343        }
344    }
345}