Skip to main content

nros_node/
timer.rs

1//! Timer API for nros
2//!
3//! This module provides timer support matching rclrs patterns while maintaining
4//! `no_std` compatibility for embedded systems.
5//!
6//! # Overview
7//!
8//! Timers allow scheduling periodic or one-shot callbacks. In embedded environments
9//! without background threads, timers must be processed manually via `process_timers()`.
10//!
11//! # Timer Modes
12//!
13//! - **Repeating**: Fires at regular intervals until canceled
14//! - **OneShot**: Fires once after a delay, then becomes inert
15//! - **Inert**: Never fires, useful as a placeholder
16//!
17//! # Example (with std)
18//!
19//! ```ignore
20//! use nros::prelude::*;
21//! use nros::timer::Duration;
22//!
23//! let mut node = ConnectedNode::connect(config, locator)?;
24//!
25//! // Create a repeating timer
26//! let timer = node.create_timer_repeating(
27//!     Duration::from_millis(100),
28//!     || println!("Timer fired!"),
29//! )?;
30//!
31//! // Process timers periodically
32//! loop {
33//!     node.process_timers(10); // 10ms elapsed
34//!     std::thread::sleep(core::time::Duration::from_millis(10));
35//! }
36//! ```
37//!
38//! # Example (RTIC)
39//!
40//! ```ignore
41//! // In RTIC, use a periodic task to process timers
42//! #[task(priority = 2, shared = [node])]
43//! async fn timer_process(mut cx: timer_process::Context) {
44//!     loop {
45//!         cx.shared.node.lock(|node| {
46//!             node.process_timers(TIMER_PROCESS_INTERVAL_MS as u64);
47//!         });
48//!         Systick::delay(TIMER_PROCESS_INTERVAL_MS.millis()).await;
49//!     }
50//! }
51//! ```
52
53use core::marker::PhantomData;
54
55/// Duration type for timer periods
56///
57/// This is a simple millisecond-based duration for `no_std` compatibility.
58/// It can be converted to/from the ROS Duration type.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
60pub struct TimerDuration {
61    /// Duration in microseconds.
62    ///
63    /// Issue #505: this was milliseconds, so `from_micros` silently
64    /// truncated — `from_micros(500)` produced a ZERO period (which the
65    /// dispatcher treats as "every spin") and `from_micros(1_500)` a
66    /// 1 ms one, a 50% rate error with no diagnostic. The executor's
67    /// timer accounting is microsecond-based, so store microseconds and
68    /// keep the millisecond constructors/accessors as exact wrappers.
69    micros: u64,
70}
71
72impl TimerDuration {
73    /// Create a new duration from milliseconds
74    pub const fn from_millis(millis: u64) -> Self {
75        Self {
76            micros: millis.saturating_mul(1000),
77        }
78    }
79
80    /// Create a new duration from seconds
81    pub const fn from_secs(secs: u64) -> Self {
82        Self {
83            micros: secs.saturating_mul(1_000_000),
84        }
85    }
86
87    /// Create a new duration from microseconds
88    pub const fn from_micros(micros: u64) -> Self {
89        Self { micros }
90    }
91
92    /// Create a zero duration
93    pub const fn zero() -> Self {
94        Self { micros: 0 }
95    }
96
97    /// Get duration as milliseconds (truncated; prefer
98    /// [`Self::as_micros`] for sub-millisecond periods)
99    pub const fn as_millis(&self) -> u64 {
100        self.micros / 1000
101    }
102
103    /// Get duration as microseconds
104    pub const fn as_micros(&self) -> u64 {
105        self.micros
106    }
107
108    /// Get duration as seconds (truncated)
109    pub const fn as_secs(&self) -> u64 {
110        self.micros / 1_000_000
111    }
112
113    /// Check if duration is zero
114    pub const fn is_zero(&self) -> bool {
115        self.micros == 0
116    }
117
118    /// Saturating subtraction
119    pub const fn saturating_sub(self, rhs: Self) -> Self {
120        Self {
121            micros: self.micros.saturating_sub(rhs.micros),
122        }
123    }
124}
125
126impl From<nros_core::Duration> for TimerDuration {
127    fn from(d: nros_core::Duration) -> Self {
128        let micros = (d.sec as i64 * 1_000_000 + d.nanosec as i64 / 1_000) as u64;
129        Self { micros }
130    }
131}
132
133impl From<TimerDuration> for nros_core::Duration {
134    fn from(d: TimerDuration) -> Self {
135        let sec = (d.micros / 1_000_000) as i32;
136        let nanosec = ((d.micros % 1_000_000) * 1_000) as u32;
137        nros_core::Duration { sec, nanosec }
138    }
139}
140
141// phase-430 W4 — this enum lives HERE, in the ungated `timer` module, rather
142// than in `executor::arena` where phase-425 W4 first wrote it. `mod arena` is
143// `#[cfg(any(has_rmw, test))]`, so the type could not be named by the
144// DECLARATIVE layer (`nros::node_metadata::EntityMetadata`, which a
145// metadata-mode build compiles with no transport) — and a metadata field that
146// cannot name the enum is how a second spelling of it gets invented one layer
147// up, which is the 0135/0160 drift. One enum, one spelling, reachable from
148// both halves; `executor` re-exports it so every existing path still resolves.
149/// Which clock advances a timer (phase-425 W4, RFC-0075-adjacent: this is the
150/// distinction rclcpp draws between `create_wall_timer` and `create_timer`).
151///
152/// The default is [`Steady`](Self::Steady) and it is the only source that costs
153/// nothing: it consumes the spin delta the executor already measured. The other
154/// two READ a clock on every poll of the timer, which is one relaxed atomic load
155/// plus whatever the platform's time call costs.
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
157#[repr(u8)]
158pub enum TimerClockSource {
159    /// The executor's monotonic spin delta — a WALL timer in rclcpp's sense.
160    /// Unaffected by `/clock`: a paused simulator does not pause it, which is
161    /// exactly what a watchdog or a transport keep-alive wants.
162    #[default]
163    Steady = 0,
164    /// `ClockType::RosTime`: simulated time when a `/clock` source is active,
165    /// and system time when none is (the same fallback `rclcpp::Clock` has, so
166    /// a node built for simulation still runs standalone).
167    Ros = 1,
168    /// `ClockType::SystemTime`: the wall clock, NTP steps and all. Present
169    /// because rclrs offers it (`TimerClock::SystemTime`); a timer that must
170    /// not jump wants `Steady`.
171    System = 2,
172}
173
174// The helpers are the EXECUTOR's half: only `timer_try_process` and the timer
175// registrar read a clock, and both live behind `mod arena` / `mod spin`, which
176// are `#[cfg(any(has_rmw, test))]`. The TYPE stays ungated — the declarative
177// metadata layer names it with no transport linked — so without a backend these
178// two would be dead code under `-D warnings`.
179#[cfg(any(has_rmw, test))]
180impl TimerClockSource {
181    /// The `nros_core` clock this source reads, or `None` for [`Steady`](Self::Steady),
182    /// which reads no clock at all.
183    pub(crate) fn clock(self) -> Option<nros_core::clock::Clock> {
184        match self {
185            TimerClockSource::Steady => None,
186            TimerClockSource::Ros => Some(nros_core::clock::Clock::ros_time()),
187            TimerClockSource::System => Some(nros_core::clock::Clock::system()),
188        }
189    }
190
191    /// The clock's current reading in nanoseconds, or 0 for [`Steady`](Self::Steady).
192    pub(crate) fn now_ns(self) -> i64 {
193        self.clock().map(|c| c.now().to_nanos()).unwrap_or(0)
194    }
195
196    /// Whether the time left on a timer with this source is a quantity of WALL
197    /// microseconds — issue 1321.
198    ///
199    /// The executor asks this in two places, and both are asking the same
200    /// thing: `period_us - elapsed_us` is offered to
201    /// `next_wake_bound_attributed_us` as a park bound, and `period_us` is
202    /// compared against the declared spin period by
203    /// `audit_spin_quantization`. Both quantities on the other side of those
204    /// comparisons are wall microseconds — what the port's park primitive
205    /// waits and what the tier loop paces — so a timer whose `elapsed_us`
206    /// advances on some OTHER clock must take part in neither.
207    ///
208    /// * [`Steady`](Self::Steady) — **yes.** It consumes the executor's own
209    ///   spin delta, which is wall time by construction. This is phase-436 W1
210    ///   / issue 1192, and it is unchanged.
211    /// * [`System`](Self::System) — **yes**, and it is decided separately
212    ///   rather than by "not `Steady`". Its clock IS wall time (the port's
213    ///   `nros_platform_time_now_ns`), so there is no rate indirection and
214    ///   nothing can hold it still. It can STEP — NTP, `settimeofday` — and a
215    ///   step makes the bound an estimate rather than a fact, but a bounded
216    ///   one in both directions: the bound only ever shortens a park (it is
217    ///   one member of a `min` whose first member is the caller's budget) and
218    ///   `timer_try_process` re-reads the clock before dispatching, so a step
219    ///   costs at worst one extra wake and can never fire a callback early.
220    /// * [`Ros`](Self::Ros) — **no.** Its `elapsed_us` advances by the
221    ///   `/clock` step, so the remainder is SIMULATED microseconds, related to
222    ///   wall time by the bag's replay rate — which the executor does not know
223    ///   and by design cannot. On a paused `/clock` the remainder freezes and
224    ///   the image would wake once per timer period forever. Contributing
225    ///   nothing restores the pre-436 bound for these timers, which is the
226    ///   caller's budget — never unbounded, because
227    ///   `next_wake_bound_attributed_us` seeds the `min` with it — and that
228    ///   bound IS property 1 of phase-430: a ROS-time timer's wake source is a
229    ///   `/clock` MESSAGE, and a message wakes `drive_io` like any sample, so
230    ///   the timer's own deadline tells the executor nothing it does not
231    ///   already have.
232    ///
233    ///   The answer does not become "yes" when no `/clock` source is attached,
234    ///   and issue 1334 changed WHY. When this rule was written, the fallback
235    ///   was measured to read `nros_core`'s in-image steady counter — a value
236    ///   nothing in the tree advances — so the remainder was a constant in
237    ///   that state too and the rule was true twice over. 1334 fixed that
238    ///   arm: `ClockType::RosTime` with no override now reads the wall clock,
239    ///   exactly as its own doc, rclcpp and the C surface always said, so the
240    ///   remainder there IS wall microseconds and the first reason is gone.
241    ///   The decision stands on the two that remain, and both are about the
242    ///   RULE rather than about one image's state:
243    ///
244    ///   * property 1 of phase-430 — a ROS-time timer's wake source is a
245    ///     `/clock` MESSAGE. A rule that flipped with whether a publisher
246    ///     happened to be running would make the park bound change under a
247    ///     live image for a reason no declaration named, and it would flip at
248    ///     the worst moment: the first `/clock` sample, the instant the
249    ///     remainder stops being wall microseconds. Deciding it per-timer at
250    ///     REGISTRATION is not available either — the source can attach later
251    ///     (`use_sim_time` is settable at runtime, phase-430 W2).
252    ///   * what it costs is LATENCY and nothing else, bounded by the caller.
253    ///     A `Ros` timer standing alone is paced by `spin_once`'s own budget
254    ///     instead of by its period, so a 10 ms timer under a 100 ms budget
255    ///     fires late — the pre-436 behaviour of every timer in the tree, and
256    ///     never unbounded, because `next_wake_bound_attributed_us` seeds its
257    ///     `min` with that budget. An image that wants its standalone ROS
258    ///     timer paced to the millisecond spins to the millisecond, which is
259    ///     what `bins/sim-clock-listener` already does at 5 ms.
260    pub(crate) fn remaining_is_wall_time(self) -> bool {
261        match self {
262            TimerClockSource::Steady | TimerClockSource::System => true,
263            TimerClockSource::Ros => false,
264        }
265    }
266}
267
268/// Timer mode (repeating, one-shot, or inert)
269#[derive(Debug, Clone, Copy, PartialEq, Eq)]
270pub enum TimerMode {
271    /// Timer fires repeatedly at the specified period
272    Repeating,
273    /// Timer fires once then becomes inert
274    OneShot,
275    /// Timer never fires (placeholder)
276    Inert,
277}
278
279/// Timer callback as a bare function pointer (`no_std`, no heap required).
280pub type TimerCallbackFn = fn();
281
282/// Internal timer state
283///
284/// Stored in the node's timer collection.
285///
286/// ADOPT-BOUNDED against rclrs's `Timer` (`Arc<TimerState>`), which is created per
287/// use and dropped when done. Here a timer is an entry in the executor's STATIC
288/// table: declared once (in `register`) and re-armed in place with
289/// [`set_repeating`](Self::set_repeating) / [`set_oneshot`](Self::set_oneshot) /
290/// [`set_inert`](Self::set_inert), because a static table cannot grow a timer at
291/// runtime. The mode vocabulary is rclrs's; the lifetime is not.
292pub struct TimerState {
293    /// Timer period in milliseconds
294    period_ms: u64,
295    /// Time elapsed since last fire in milliseconds
296    elapsed_ms: u64,
297    /// Timer mode
298    mode: TimerMode,
299    /// Whether the timer is canceled
300    canceled: bool,
301    /// Callback function pointer (no heap)
302    callback_fn: Option<TimerCallbackFn>,
303}
304
305impl core::fmt::Debug for TimerState {
306    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
307        f.debug_struct("TimerState")
308            .field("period_ms", &self.period_ms)
309            .field("elapsed_ms", &self.elapsed_ms)
310            .field("mode", &self.mode)
311            .field("canceled", &self.canceled)
312            .field("has_callback_fn", &self.callback_fn.is_some())
313            .finish()
314    }
315}
316
317impl TimerState {
318    /// Create a new timer state with function pointer callback
319    pub fn new_with_fn(period: TimerDuration, mode: TimerMode, callback: TimerCallbackFn) -> Self {
320        Self {
321            period_ms: period.as_millis(),
322            elapsed_ms: 0,
323            mode,
324            canceled: false,
325            callback_fn: Some(callback),
326        }
327    }
328
329    /// Create an inert timer state
330    pub fn new_inert(period: TimerDuration) -> Self {
331        Self {
332            period_ms: period.as_millis(),
333            elapsed_ms: 0,
334            mode: TimerMode::Inert,
335            canceled: false,
336            callback_fn: None,
337        }
338    }
339
340    /// Get the timer period
341    pub fn period(&self) -> TimerDuration {
342        TimerDuration::from_millis(self.period_ms)
343    }
344
345    /// Get the timer mode
346    pub fn mode(&self) -> TimerMode {
347        self.mode
348    }
349
350    /// Check if the timer is canceled
351    pub fn is_canceled(&self) -> bool {
352        self.canceled
353    }
354
355    /// Cancel the timer
356    pub fn cancel(&mut self) {
357        self.canceled = true;
358    }
359
360    /// Reset the timer (uncancels and resets elapsed time)
361    pub fn reset(&mut self) {
362        self.canceled = false;
363        self.elapsed_ms = 0;
364    }
365
366    /// Check if the timer is ready to fire
367    pub fn is_ready(&self) -> bool {
368        !self.canceled && self.mode != TimerMode::Inert && self.elapsed_ms >= self.period_ms
369    }
370
371    /// Get time until next call (0 if ready)
372    pub fn time_until_next_call(&self) -> TimerDuration {
373        if self.canceled || self.mode == TimerMode::Inert {
374            return TimerDuration::from_millis(u64::MAX);
375        }
376        if self.elapsed_ms >= self.period_ms {
377            TimerDuration::zero()
378        } else {
379            TimerDuration::from_millis(self.period_ms - self.elapsed_ms)
380        }
381    }
382
383    /// Get time since last call
384    pub fn time_since_last_call(&self) -> TimerDuration {
385        TimerDuration::from_millis(self.elapsed_ms)
386    }
387
388    /// Set callback to function pointer
389    pub fn set_callback_fn(&mut self, callback: TimerCallbackFn) {
390        self.callback_fn = Some(callback);
391    }
392
393    /// Set timer to repeating mode
394    pub fn set_repeating(&mut self) {
395        self.mode = TimerMode::Repeating;
396    }
397
398    /// Set timer to one-shot mode
399    pub fn set_oneshot(&mut self) {
400        self.mode = TimerMode::OneShot;
401    }
402
403    /// Set timer to inert mode
404    pub fn set_inert(&mut self) {
405        self.mode = TimerMode::Inert;
406    }
407
408    /// Update elapsed time and return true if timer should fire
409    #[allow(dead_code)] // Used by ConnectedNode when zenoh feature is enabled
410    pub(crate) fn update(&mut self, delta_ms: u64) -> bool {
411        if self.canceled || self.mode == TimerMode::Inert {
412            return false;
413        }
414
415        self.elapsed_ms = self.elapsed_ms.saturating_add(delta_ms);
416
417        self.elapsed_ms >= self.period_ms
418    }
419
420    /// Fire the timer callback and handle mode-specific behavior
421    #[allow(dead_code)] // Used by ConnectedNode when zenoh feature is enabled
422    pub(crate) fn fire(&mut self) {
423        // Execute callback
424        if let Some(ref callback) = self.callback_fn {
425            callback();
426        }
427
428        // Handle mode-specific behavior
429        match self.mode {
430            TimerMode::Repeating => {
431                // Reset elapsed time for next period
432                self.elapsed_ms = self.elapsed_ms.saturating_sub(self.period_ms);
433            }
434            TimerMode::OneShot => {
435                // Become inert after firing
436                self.mode = TimerMode::Inert;
437                self.elapsed_ms = 0;
438            }
439            TimerMode::Inert => {
440                // Should not reach here
441            }
442        }
443    }
444}
445
446/// A handle to a timer stored in a node
447///
448/// This is a lightweight handle that references a timer by index.
449/// The actual timer state is stored in the `ConnectedNode`.
450///
451/// # Type Parameters
452///
453/// - `C`: Callback type marker (function pointer or boxed)
454#[derive(Debug, Clone, Copy)]
455pub struct TimerHandle<C = TimerCallbackFn> {
456    /// Timer index in the node's timer collection
457    index: usize,
458    /// Phantom data for callback type
459    _marker: PhantomData<C>,
460}
461
462impl<C> TimerHandle<C> {
463    /// Create a new timer handle
464    #[allow(dead_code)] // Used by ConnectedNode when zenoh feature is enabled
465    pub(crate) fn new(index: usize) -> Self {
466        Self {
467            index,
468            _marker: PhantomData,
469        }
470    }
471
472    /// Get the timer index
473    pub fn index(&self) -> usize {
474        self.index
475    }
476}
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481
482    #[test]
483    fn test_timer_duration() {
484        let d = TimerDuration::from_millis(1500);
485        assert_eq!(d.as_millis(), 1500);
486        assert_eq!(d.as_secs(), 1);
487
488        let d2 = TimerDuration::from_secs(2);
489        assert_eq!(d2.as_millis(), 2000);
490
491        let d3 = TimerDuration::from_micros(5500);
492        assert_eq!(d3.as_millis(), 5);
493    }
494
495    #[test]
496    fn test_timer_duration_conversion() {
497        let ros_dur = nros_core::Duration::from_millis(1500);
498        let timer_dur: TimerDuration = ros_dur.into();
499        assert_eq!(timer_dur.as_millis(), 1500);
500
501        let back: nros_core::Duration = timer_dur.into();
502        assert_eq!(back.sec, 1);
503        assert_eq!(back.nanosec, 500_000_000);
504    }
505
506    fn test_callback() {
507        // Empty callback for testing
508    }
509
510    #[test]
511    fn test_timer_state_repeating() {
512        let mut state = TimerState::new_with_fn(
513            TimerDuration::from_millis(100),
514            TimerMode::Repeating,
515            test_callback,
516        );
517
518        assert_eq!(state.period().as_millis(), 100);
519        assert_eq!(state.mode(), TimerMode::Repeating);
520        assert!(!state.is_canceled());
521        assert!(!state.is_ready());
522
523        // Advance time
524        assert!(!state.update(50));
525        assert!(!state.is_ready());
526        assert_eq!(state.time_until_next_call().as_millis(), 50);
527        assert_eq!(state.time_since_last_call().as_millis(), 50);
528
529        // Advance to ready
530        assert!(state.update(50));
531        assert!(state.is_ready());
532        assert_eq!(state.time_until_next_call().as_millis(), 0);
533
534        // Fire and check it repeats
535        state.fire();
536        assert_eq!(state.mode(), TimerMode::Repeating);
537        assert!(!state.is_ready());
538    }
539
540    #[test]
541    fn test_timer_state_oneshot() {
542        let mut state = TimerState::new_with_fn(
543            TimerDuration::from_millis(100),
544            TimerMode::OneShot,
545            test_callback,
546        );
547
548        assert_eq!(state.mode(), TimerMode::OneShot);
549
550        // Advance to ready
551        state.update(100);
552        assert!(state.is_ready());
553
554        // Fire and check it becomes inert
555        state.fire();
556        assert_eq!(state.mode(), TimerMode::Inert);
557        assert!(!state.is_ready());
558    }
559
560    #[test]
561    fn test_timer_state_inert() {
562        let state = TimerState::new_inert(TimerDuration::from_millis(100));
563
564        assert_eq!(state.mode(), TimerMode::Inert);
565        assert!(!state.is_ready());
566    }
567
568    #[test]
569    fn test_timer_cancel_reset() {
570        let mut state = TimerState::new_with_fn(
571            TimerDuration::from_millis(100),
572            TimerMode::Repeating,
573            test_callback,
574        );
575
576        state.update(50);
577        state.cancel();
578        assert!(state.is_canceled());
579        assert!(!state.is_ready());
580
581        state.update(100);
582        assert!(!state.is_ready()); // Still canceled
583
584        state.reset();
585        assert!(!state.is_canceled());
586        assert_eq!(state.time_since_last_call().as_millis(), 0);
587    }
588
589    #[test]
590    fn test_timer_mode_changes() {
591        let mut state = TimerState::new_with_fn(
592            TimerDuration::from_millis(100),
593            TimerMode::Repeating,
594            test_callback,
595        );
596
597        state.set_oneshot();
598        assert_eq!(state.mode(), TimerMode::OneShot);
599
600        state.set_inert();
601        assert_eq!(state.mode(), TimerMode::Inert);
602
603        state.set_repeating();
604        assert_eq!(state.mode(), TimerMode::Repeating);
605    }
606
607    #[test]
608    fn test_timer_handle() {
609        let handle: TimerHandle = TimerHandle::new(5);
610        assert_eq!(handle.index(), 5);
611    }
612}
613
614// =============================================================================
615// Ghost model validation
616// =============================================================================
617
618#[cfg(test)]
619mod ghost_checks {
620    use super::*;
621    use nros_ghost_types::{TimerGhost, TimerModeGhost};
622
623    /// Structural check: map TimerMode to TimerModeGhost.
624    /// If a variant is added or removed, this fails to compile.
625    fn ghost_mode(m: &TimerMode) -> TimerModeGhost {
626        match m {
627            TimerMode::Repeating => TimerModeGhost::Repeating,
628            TimerMode::OneShot => TimerModeGhost::OneShot,
629            TimerMode::Inert => TimerModeGhost::Inert,
630        }
631    }
632
633    /// Structural check: construct TimerGhost from TimerState private fields.
634    /// If a field is renamed or retyped, this fails to compile.
635    fn ghost_from_timer(t: &TimerState) -> TimerGhost {
636        TimerGhost {
637            period_ms: t.period_ms,
638            elapsed_ms: t.elapsed_ms,
639            mode: ghost_mode(&t.mode),
640            canceled: t.canceled,
641        }
642    }
643
644    fn test_callback() {}
645
646    #[test]
647    fn ghost_new_state() {
648        let state = TimerState::new_with_fn(
649            TimerDuration::from_millis(100),
650            TimerMode::Repeating,
651            test_callback,
652        );
653        let ghost = ghost_from_timer(&state);
654        assert_eq!(ghost.period_ms, 100);
655        assert_eq!(ghost.elapsed_ms, 0);
656        assert_eq!(ghost.mode, TimerModeGhost::Repeating);
657        assert!(!ghost.canceled);
658    }
659
660    #[test]
661    fn ghost_update_accumulates() {
662        let mut state = TimerState::new_with_fn(
663            TimerDuration::from_millis(100),
664            TimerMode::Repeating,
665            test_callback,
666        );
667        state.update(30);
668        let ghost = ghost_from_timer(&state);
669        assert_eq!(ghost.elapsed_ms, 30);
670
671        state.update(25);
672        let ghost2 = ghost_from_timer(&state);
673        assert_eq!(ghost2.elapsed_ms, 55);
674    }
675
676    #[test]
677    fn ghost_canceled_no_fire() {
678        let mut state = TimerState::new_with_fn(
679            TimerDuration::from_millis(100),
680            TimerMode::Repeating,
681            test_callback,
682        );
683        state.cancel();
684        let fired = state.update(200);
685        assert!(!fired);
686        let ghost = ghost_from_timer(&state);
687        assert!(ghost.canceled);
688    }
689
690    #[test]
691    fn ghost_inert_no_fire() {
692        let mut state = TimerState::new_inert(TimerDuration::from_millis(100));
693        let fired = state.update(200);
694        assert!(!fired);
695        let ghost = ghost_from_timer(&state);
696        assert_eq!(ghost.mode, TimerModeGhost::Inert);
697    }
698
699    #[test]
700    fn ghost_oneshot_becomes_inert() {
701        let mut state = TimerState::new_with_fn(
702            TimerDuration::from_millis(100),
703            TimerMode::OneShot,
704            test_callback,
705        );
706        state.update(100);
707        state.fire();
708        let ghost = ghost_from_timer(&state);
709        assert_eq!(ghost.mode, TimerModeGhost::Inert);
710    }
711}