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}