Skip to main content

nros_core/
clock.rs

1//! Clock API for nros
2//!
3//! This module provides clock abstraction for different time sources:
4//! - **SystemTime**: Wall clock time (affected by system time changes)
5//! - **SteadyTime**: Monotonic time (not affected by system time changes)
6//! - **RosTime**: Simulation time (can be paused/scaled)
7//!
8//! # Example
9//!
10//! ```text
11//! use nros::clock::{Clock, ClockType};
12//!
13//! // Create a system clock
14//! let clock = Clock::system();
15//! let now = clock.now();
16//! println!("Current time: {} sec", now.sec);
17//!
18//! // Create a steady clock for measuring durations
19//! let clock = Clock::steady();
20//! let start = clock.now();
21//! // ... do work ...
22//! let elapsed = clock.now() - start;
23//! ```
24//!
25//! # Where the time comes from
26//!
27//! With a platform port linked (the `platform-clock` feature), every clock
28//! reads the PORT: `nros_platform_time_now_ns` for the wall, and
29//! `nros_platform_clock_ns` for the monotonic. Without one there is no clock
30//! to read, and all three types fall back to an internal counter the caller
31//! advances with `update_steady_time()` — suitable for a bare-metal RTIC or
32//! polling loop, which is the only build where that counter has an owner.
33
34use crate::time::Time;
35
36// AtomicI64 is not available on all platforms (e.g., thumbv7em-none-eabihf)
37// Use AtomicI64 when available, otherwise use a simpler approach
38#[cfg(target_has_atomic = "64")]
39use core::sync::atomic::{AtomicI64, Ordering};
40
41// For platforms without 64-bit atomics, use two 32-bit values
42#[cfg(not(target_has_atomic = "64"))]
43use core::sync::atomic::{AtomicI32, Ordering};
44
45/// Type of clock to use for time queries
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
47pub enum ClockType {
48    /// System time (wall clock)
49    ///
50    /// This clock reflects the system's real-time clock and may be
51    /// affected by NTP adjustments, user changes, or daylight saving time.
52    /// Use for timestamps that need to correlate with real-world time.
53    #[default]
54    SystemTime,
55
56    /// Steady/monotonic time
57    ///
58    /// This clock is guaranteed to be monotonically increasing and is
59    /// not affected by system time changes. Use for measuring durations
60    /// and timeouts. Reads the port's `nros_platform_clock_ns` where one is
61    /// linked, and the caller-advanced counter where none is.
62    SteadyTime,
63
64    /// ROS time (simulation time)
65    ///
66    /// This clock can be overridden for simulation purposes. When a
67    /// ROS time override is active — a simulator's or a bag player's
68    /// `/clock` — `now()` returns the overridden time. Otherwise it IS
69    /// [`SystemTime`](Self::SystemTime), which is what `rclcpp`'s
70    /// `ClockType::ROS_TIME` reads with `use_sim_time` false, so a node
71    /// written for simulation still runs standalone.
72    RosTime,
73}
74
75// On platforms with 64-bit atomics, use AtomicI64 directly
76#[cfg(target_has_atomic = "64")]
77mod atomic_time {
78    use super::*;
79
80    /// Global ROS time override (nanoseconds since epoch)
81    /// When set to a non-negative value, `Clock::now()` for `RosTime` clocks
82    /// will return this value instead of system time.
83    pub(super) static ROS_TIME_OVERRIDE_NANOS: AtomicI64 = AtomicI64::new(-1);
84
85    /// Global steady time counter (nanoseconds)
86    /// For `no_std` environments, this counter must be updated manually.
87    pub(super) static STEADY_TIME_NANOS: AtomicI64 = AtomicI64::new(0);
88
89    pub(super) fn get_ros_override() -> i64 {
90        ROS_TIME_OVERRIDE_NANOS.load(Ordering::Relaxed)
91    }
92
93    pub(super) fn set_ros_override(nanos: i64) {
94        ROS_TIME_OVERRIDE_NANOS.store(nanos, Ordering::Relaxed);
95    }
96
97    pub(super) fn get_steady() -> i64 {
98        STEADY_TIME_NANOS.load(Ordering::Relaxed)
99    }
100
101    pub(super) fn set_steady(nanos: i64) {
102        STEADY_TIME_NANOS.store(nanos, Ordering::Relaxed);
103    }
104
105    pub(super) fn add_steady(delta: i64) {
106        STEADY_TIME_NANOS.fetch_add(delta, Ordering::Relaxed);
107    }
108}
109
110// On platforms without 64-bit atomics, use split 32-bit values
111// Note: This is not fully atomic but works for single-threaded embedded contexts
112#[cfg(not(target_has_atomic = "64"))]
113mod atomic_time {
114    use super::*;
115
116    // Split into high and low 32-bit parts
117    static ROS_TIME_OVERRIDE_LOW: AtomicI32 = AtomicI32::new(-1);
118    static ROS_TIME_OVERRIDE_HIGH: AtomicI32 = AtomicI32::new(-1);
119    static STEADY_TIME_LOW: AtomicI32 = AtomicI32::new(0);
120    static STEADY_TIME_HIGH: AtomicI32 = AtomicI32::new(0);
121
122    pub(super) fn get_ros_override() -> i64 {
123        let high = ROS_TIME_OVERRIDE_HIGH.load(Ordering::Relaxed);
124        let low = ROS_TIME_OVERRIDE_LOW.load(Ordering::Relaxed);
125        if high < 0 {
126            -1
127        } else {
128            ((high as i64) << 32) | (low as u32 as i64)
129        }
130    }
131
132    pub(super) fn set_ros_override(nanos: i64) {
133        if nanos < 0 {
134            ROS_TIME_OVERRIDE_HIGH.store(-1, Ordering::Relaxed);
135            ROS_TIME_OVERRIDE_LOW.store(-1, Ordering::Relaxed);
136        } else {
137            ROS_TIME_OVERRIDE_HIGH.store((nanos >> 32) as i32, Ordering::Relaxed);
138            ROS_TIME_OVERRIDE_LOW.store(nanos as i32, Ordering::Relaxed);
139        }
140    }
141
142    pub(super) fn get_steady() -> i64 {
143        let high = STEADY_TIME_HIGH.load(Ordering::Relaxed);
144        let low = STEADY_TIME_LOW.load(Ordering::Relaxed);
145        ((high as i64) << 32) | (low as u32 as i64)
146    }
147
148    pub(super) fn set_steady(nanos: i64) {
149        STEADY_TIME_HIGH.store((nanos >> 32) as i32, Ordering::Relaxed);
150        STEADY_TIME_LOW.store(nanos as i32, Ordering::Relaxed);
151    }
152
153    pub(super) fn add_steady(delta: i64) {
154        let current = get_steady();
155        set_steady(current.saturating_add(delta));
156    }
157}
158
159/// A clock for querying time
160///
161/// Clocks provide access to different time sources. Each node typically
162/// has an associated clock, but you can also create standalone clocks.
163#[derive(Debug, Clone, Copy)]
164pub struct Clock {
165    clock_type: ClockType,
166}
167
168impl Default for Clock {
169    fn default() -> Self {
170        Self::system()
171    }
172}
173
174/// The platform's wall clock, or `None` when this build has no port to ask.
175///
176/// phase-359 W10 (backend tier). `nros-core` sits BELOW `nros-platform`, so it
177/// cannot depend on it — it declares the two ABI symbols directly, exactly as
178/// `nros-node` already does for `nros_platform_clock_ns` ("every platform port
179/// exports it through the same linkage contract"). The feature is what promises
180/// a port is linked; without it this is `None` and the caller keeps the counter
181/// it had.
182///
183/// ONE symbol since issue 0532 item 5 collapsed the wall clock; this function
184/// was named there as the one place that would change, and it was.
185// phase-359 W10 follow-up — NOT `not(std)`. This used to be gated away on a
186// `std` build, so an image with a port linked read `SystemTime` from
187// `Clock::system()` and `nros_platform_time_now_ns` from the executor's epoch
188// source: two wall clocks, one image. They agree on POSIX by coincidence (both
189// are CLOCK_REALTIME) and stop agreeing the moment a port has an opinion — an
190// RTC-backed or simulated one — because only the port is authoritative and only
191// one of the two readers asked it. The rule W10 set in `nros-node` applies
192// here: when a port is linked it IS the clock, and `std` is what a build
193// without one falls back to.
194#[cfg(feature = "platform-clock")]
195fn platform_wall_clock() -> Option<Time> {
196    unsafe extern "C" {
197        fn nros_platform_time_now_ns() -> u64;
198    }
199    // SAFETY: a bare wall-clock read, no pointer arguments, guaranteed by
200    // whichever port linked the binary — the same contract `nros-node` relies
201    // on for `nros_platform_clock_ns`.
202    //
203    // ONE symbol, so one sample: issue 0532 collapsed the former
204    // `time_since_epoch_{secs,nanos}` pair, which this was written against and
205    // which needed a bounded re-read to survive a second boundary landing
206    // between the two calls. That loop is what the collapse deletes.
207    let ns = unsafe { nros_platform_time_now_ns() };
208    // A port with no RTC returns 0. Reporting the Unix epoch as "now" would be
209    // a wrong answer stated confidently, so say nothing and let the caller's
210    // counter fallback stand.
211    if ns == 0 {
212        return None;
213    }
214    Some(Time::new(
215        (ns / 1_000_000_000) as i32,
216        (ns % 1_000_000_000) as u32,
217    ))
218}
219
220#[cfg(not(feature = "platform-clock"))]
221fn platform_wall_clock() -> Option<Time> {
222    None
223}
224
225/// The platform's MONOTONIC clock, or `None` when this build has no port to ask.
226///
227/// issue 1334 — the sibling of [`platform_wall_clock`], declared the same way
228/// and under the same feature, because it is the same promise: `platform-clock`
229/// means a port is linked, and every port exports both halves
230/// (`nros/platform.h` "Wall clock" and RFC-0073's monotonic export are
231/// MANDATORY, not optional). `nros-node`, `nros-log`, `nros-c` and the `nros`
232/// umbrella already hand-declare this symbol under exactly this contract.
233///
234/// Unlike the wall clock there is no "port with no RTC" case to detect: a
235/// monotonic counter reading 0 is a legitimate answer at boot, so a zero is
236/// passed through rather than read as "no clock".
237#[cfg(feature = "platform-clock")]
238fn platform_steady_clock() -> Option<Time> {
239    unsafe extern "C" {
240        fn nros_platform_clock_ns() -> u64;
241    }
242    // SAFETY: a bare counter read, no pointer arguments, guaranteed by
243    // whichever port linked the binary — the same contract `platform_wall_clock`
244    // above relies on for `nros_platform_time_now_ns`.
245    let ns = unsafe { nros_platform_clock_ns() };
246    Some(Time::new(
247        (ns / 1_000_000_000) as i32,
248        (ns % 1_000_000_000) as u32,
249    ))
250}
251
252#[cfg(not(feature = "platform-clock"))]
253fn platform_steady_clock() -> Option<Time> {
254    None
255}
256
257impl Clock {
258    /// Create a new clock of the specified type
259    ///
260    /// ADOPT-BOUNDED: rclrs's `Clock::new` returns `(Self, Option<ClockSource>)`,
261    /// handing back the source that drives a ROS-time clock. This returns the clock
262    /// alone. Simulated time is ONE process-wide override, set with the associated
263    /// [`Clock::set_ros_time_override`], not a per-clock source: a per-clock source
264    /// is a shared owner (`Arc`) the `core` targets do not have, and `const fn` is
265    /// what lets a `Clock` be a `static`.
266    pub const fn new(clock_type: ClockType) -> Self {
267        Self { clock_type }
268    }
269
270    /// Create a system time clock
271    ///
272    /// System time reflects the real-world wall clock time.
273    pub const fn system() -> Self {
274        Self {
275            clock_type: ClockType::SystemTime,
276        }
277    }
278
279    /// Create a steady (monotonic) time clock
280    ///
281    /// Steady time is guaranteed to only increase and is not affected
282    /// by system time changes.
283    pub const fn steady() -> Self {
284        Self {
285            clock_type: ClockType::SteadyTime,
286        }
287    }
288
289    /// Create a ROS time clock
290    ///
291    /// ROS time can be overridden for simulation. When no override is
292    /// active, it returns system time — the identical expression
293    /// [`Clock::system`] evaluates, so the two cannot drift (issue 1334).
294    pub const fn ros_time() -> Self {
295        Self {
296            clock_type: ClockType::RosTime,
297        }
298    }
299
300    /// Get the clock type
301    pub const fn clock_type(&self) -> ClockType {
302        self.clock_type
303    }
304
305    /// The wall clock, in ONE place — issue 1334.
306    ///
307    /// Two clock types answer "what is the wall time", and they must answer it
308    /// identically: `SystemTime` always, and `RosTime` whenever no `/clock`
309    /// override is installed. Before this they were two expressions, and only
310    /// the first was ever moved onto the port (phase-359 W10), so `RosTime`'s
311    /// fallback was left reading the counter — a value nothing in the tree
312    /// advances. One expression is what keeps the next move from splitting
313    /// them again.
314    ///
315    /// - **A platform port linked** (`platform-clock`): the port's wall clock.
316    ///   It is the authority, and an image must not hold two answers to "what
317    ///   time is it".
318    /// - **No port**: the internal counter, which the caller advances with
319    ///   `update_steady_time()`. A build with no port has no clock to read, so
320    ///   this is the honest answer rather than a fallback with an opinion.
321    fn wall_now() -> Time {
322        // phase-359 W10 (backend tier) — a wall clock that does not need the
323        // `std` FEATURE. Before this, `SystemTime` fell back to the STEADY
324        // counter unconditionally: the same value a monotonic clock returns,
325        // presented as time since the Unix epoch. That is not a degraded wall
326        // clock, it is a different quantity, and the only thing standing
327        // between a build and it was whether some crate in the graph happened
328        // to name `std`.
329        if let Some(t) = platform_wall_clock() {
330            return t;
331        }
332        // phase-518 (issue 2071) -- a port with no RTC (FreeRTOS, ThreadX)
333        // answers 0, and the counter below is the constant 0 on every image
334        // with a port linked, because nothing advances it. So a `RosTime`
335        // timer with no `/clock` source never fired on those ports: the
336        // stock `rclcpp::create_timer(this, get_clock(), ...)` of every
337        // Autoware node was dead. Boot-relative time is what issue 0758
338        // already says those images stamp with, and it ADVANCES, which is the
339        // property a timer needs. A port that can state an epoch still wins.
340        if let Some(t) = platform_steady_clock() {
341            return t;
342        }
343        Time::from_nanos(atomic_time::get_steady())
344    }
345
346    /// The monotonic clock, in ONE place — issue 1334's sibling site.
347    ///
348    /// Same rule as `Self::wall_now`, one clock over: a linked port IS the
349    /// monotonic clock, and the counter is what a build with no port was
350    /// given.
351    ///
352    /// This overturns half of phase-359 W10, which moved `SystemTime` onto the
353    /// port and left this line reading "`SteadyTime` is the counter on every
354    /// flavour, deliberately: it is advanced by its owner … and the port's
355    /// monotonic export is the executor's business, not this type's". The
356    /// owner never arrived — nothing outside this file's own tests has ever
357    /// called `update_steady_time` — so on every shipped image
358    /// `Clock::steady().now()` was the constant 0, while the C surface
359    /// answered the SAME question (`RCL_STEADY_TIME`) with
360    /// `nros_platform_clock_ns`. That is issue 1334's complaint exactly, one
361    /// clock over: two surfaces, one image, different answers, and the Rust
362    /// one is not a clock. W10's own rule settles it — when a port is linked
363    /// it IS the clock — and the counter keeps its documented RTIC meaning in
364    /// the one build where it is the only source.
365    fn steady_now() -> Time {
366        if let Some(t) = platform_steady_clock() {
367            return t;
368        }
369        Time::from_nanos(atomic_time::get_steady())
370    }
371
372    /// Get the current time from this clock
373    ///
374    /// # Platform behavior
375    ///
376    /// - **A platform port linked** (`platform-clock`): the port's clocks —
377    ///   `nros_platform_time_now_ns` for the wall, `nros_platform_clock_ns`
378    ///   for the monotonic. The port is the authority, and an image must not
379    ///   hold two answers to "what time is it".
380    /// - **No port**: the internal counter, which the caller advances with
381    ///   `update_steady_time()`.
382    ///
383    /// phase-359 W10 — there is no `std::time` arm. The platform API IS the
384    /// clock: a build with a port reads it, a build without one has no clock to
385    /// read and says so with the counter it was given. `SystemTime` used to sit
386    /// here as a third answer for hosted builds, which made "what time is it"
387    /// depend on whether some crate in the graph happened to name `std`.
388    ///
389    /// `RosTime` with no override is `SystemTime`, in EVERY build shape and by
390    /// construction — both arms call `Self::wall_now`. That is rclcpp's
391    /// contract (`ClockType::ROS_TIME` with `use_sim_time` false reads the
392    /// system clock), what five doc sites across three languages already
393    /// promised, and what the C surface already did; issue 1334 is the four
394    /// phases in which the Rust arm said something else.
395    pub fn now(&self) -> Time {
396        match self.clock_type {
397            ClockType::SystemTime => Self::wall_now(),
398            ClockType::SteadyTime => Self::steady_now(),
399            ClockType::RosTime => {
400                let override_nanos = atomic_time::get_ros_override();
401                if override_nanos >= 0 {
402                    // A `/clock` source is driving simulated time.
403                    Time::from_nanos(override_nanos)
404                } else {
405                    // None is. rclcpp reads the system clock here and so do we
406                    // — the same expression `Clock::system()` evaluates, not a
407                    // copy of it (issue 1334).
408                    Self::wall_now()
409                }
410            }
411        }
412    }
413
414    /// Set a ROS time override
415    ///
416    /// When set, all `RosTime` clocks will return this time instead of
417    /// system time. This is useful for simulation.
418    ///
419    /// # Arguments
420    /// * `nanos` - Nanoseconds since epoch
421    pub fn set_ros_time_override(nanos: i64) {
422        atomic_time::set_ros_override(nanos);
423    }
424
425    /// Set a ROS time override from a Time value
426    pub fn set_ros_time_override_time(time: Time) {
427        Self::set_ros_time_override(time.to_nanos());
428    }
429
430    /// Clear the ROS time override
431    ///
432    /// After clearing, `RosTime` clocks will return system time again.
433    pub fn clear_ros_time_override() {
434        atomic_time::set_ros_override(-1);
435    }
436
437    /// Check if a ROS time override is active
438    pub fn is_ros_time_override_active() -> bool {
439        atomic_time::get_ros_override() >= 0
440    }
441
442    /// Get the current ROS time override value (if active)
443    pub fn get_ros_time_override() -> Option<Time> {
444        let nanos = atomic_time::get_ros_override();
445        if nanos >= 0 {
446            Some(Time::from_nanos(nanos))
447        } else {
448            None
449        }
450    }
451
452    /// Update the steady time counter
453    ///
454    /// For environments with NO platform port, call this periodically from
455    /// your main loop or RTIC task to advance the clocks. With a port linked
456    /// this counter is not what any clock reads — the port is (issue 1334) —
457    /// and advancing it has no observable effect.
458    ///
459    /// # Arguments
460    /// * `delta_nanos` - Nanoseconds elapsed since last call
461    pub fn update_steady_time(delta_nanos: i64) {
462        atomic_time::add_steady(delta_nanos);
463    }
464
465    /// Update the steady time counter (milliseconds version)
466    ///
467    /// Convenience method for RTIC tasks using millisecond intervals.
468    ///
469    /// # Arguments
470    /// * `delta_ms` - Milliseconds elapsed since last call
471    pub fn update_steady_time_ms(delta_ms: u64) {
472        let delta_nanos = delta_ms as i64 * 1_000_000;
473        Self::update_steady_time(delta_nanos);
474    }
475
476    /// Set the steady time counter to a specific value
477    ///
478    /// Use this to initialize the clock or synchronize with an external
479    /// time source.
480    pub fn set_steady_time(nanos: i64) {
481        atomic_time::set_steady(nanos);
482    }
483
484    /// Get the current steady time counter value
485    pub fn get_steady_time_nanos() -> i64 {
486        atomic_time::get_steady()
487    }
488}
489
490#[cfg(test)]
491mod tests {
492    use super::*;
493
494    /// Serialises every test that touches the process-global clock state —
495    /// issue 1334.
496    ///
497    /// The override, the counter and (below) the fake port are all statics, and
498    /// `cargo test` runs these in parallel threads: a test that SETS one and
499    /// reads it back is racing every sibling that writes the same static. That
500    /// was already true before this guard existed (`test_ros_time_override` and
501    /// `test_steady_time_update` write two different globals that `Clock::now`
502    /// reads together) and it becomes load-bearing here, because the new tests
503    /// move the fake port's hands while another test asserts where they point.
504    ///
505    /// A spin lock rather than `std::sync::Mutex`: this crate is `#![no_std]`
506    /// and `std` is a FEATURE, so a `Mutex` here would confine the guard — and
507    /// therefore every test using it — to the `std` flavour, which is not where
508    /// the `platform-clock` tests need to live.
509    struct ClockGuard;
510
511    static CLOCK_LOCK: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false);
512
513    impl ClockGuard {
514        fn acquire() -> Self {
515            while CLOCK_LOCK
516                .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
517                .is_err()
518            {
519                core::hint::spin_loop();
520            }
521            Self
522        }
523    }
524
525    impl Drop for ClockGuard {
526        fn drop(&mut self) {
527            // Leave the globals as a fresh process has them, so a test that
528            // does NOT take the guard still sees the documented defaults.
529            Clock::clear_ros_time_override();
530            Clock::set_steady_time(0);
531            #[cfg(feature = "platform-clock")]
532            fake_port::reset();
533            CLOCK_LOCK.store(false, Ordering::Release);
534        }
535    }
536
537    /// The stand-in platform port for the `platform-clock` flavour.
538    ///
539    /// One definition of each symbol, at module level, because a linker sees
540    /// one test binary: the wall symbol used to be defined INSIDE the single
541    /// test that needed it, which works exactly until a second test needs it
542    /// too. Both hands MOVE (they are atomics, not constants), which is what
543    /// lets a test assert that a clock ADVANCES rather than merely that it
544    /// reads a magic number.
545    #[cfg(feature = "platform-clock")]
546    mod fake_port {
547        use core::sync::atomic::{AtomicU64, Ordering};
548
549        /// 2001-09-09T01:46:40Z — a fixed instant no real clock will return.
550        pub(super) const WALL_BASE_NS: u64 = 1_000_000_000 * 1_000_000_000;
551        /// 42 s of uptime — no relation to `WALL_BASE_NS`, deliberately: the
552        /// two clocks are different quantities and a test must not be able to
553        /// pass by reading the wrong one.
554        pub(super) const STEADY_BASE_NS: u64 = 42_000_000_000;
555
556        pub(super) static WALL_NS: AtomicU64 = AtomicU64::new(WALL_BASE_NS);
557        pub(super) static STEADY_NS: AtomicU64 = AtomicU64::new(STEADY_BASE_NS);
558
559        pub(super) fn reset() {
560            WALL_NS.store(WALL_BASE_NS, Ordering::Relaxed);
561            STEADY_NS.store(STEADY_BASE_NS, Ordering::Relaxed);
562        }
563
564        #[unsafe(no_mangle)]
565        extern "C" fn nros_platform_time_now_ns() -> u64 {
566            WALL_NS.load(Ordering::Relaxed)
567        }
568
569        #[unsafe(no_mangle)]
570        extern "C" fn nros_platform_clock_ns() -> u64 {
571            STEADY_NS.load(Ordering::Relaxed)
572        }
573    }
574
575    #[test]
576    fn test_clock_type_default() {
577        let clock_type = ClockType::default();
578        assert_eq!(clock_type, ClockType::SystemTime);
579    }
580
581    #[test]
582    fn test_clock_constructors() {
583        let system = Clock::system();
584        assert_eq!(system.clock_type(), ClockType::SystemTime);
585
586        let steady = Clock::steady();
587        assert_eq!(steady.clock_type(), ClockType::SteadyTime);
588
589        let ros = Clock::ros_time();
590        assert_eq!(ros.clock_type(), ClockType::RosTime);
591
592        let custom = Clock::new(ClockType::SteadyTime);
593        assert_eq!(custom.clock_type(), ClockType::SteadyTime);
594    }
595
596    #[test]
597    fn test_clock_default() {
598        let clock = Clock::default();
599        assert_eq!(clock.clock_type(), ClockType::SystemTime);
600    }
601
602    #[test]
603    fn test_ros_time_override() {
604        let _guard = ClockGuard::acquire();
605        // Clear any existing override
606        Clock::clear_ros_time_override();
607        assert!(!Clock::is_ros_time_override_active());
608        assert!(Clock::get_ros_time_override().is_none());
609
610        // Set override
611        let override_time = Time::new(1234567890, 123456789);
612        Clock::set_ros_time_override_time(override_time);
613        assert!(Clock::is_ros_time_override_active());
614        assert_eq!(Clock::get_ros_time_override(), Some(override_time));
615
616        // ROS clock should return override time
617        let ros_clock = Clock::ros_time();
618        let now = ros_clock.now();
619        assert_eq!(now, override_time);
620
621        // Clear override
622        Clock::clear_ros_time_override();
623        assert!(!Clock::is_ros_time_override_active());
624    }
625
626    #[test]
627    fn test_steady_time_update() {
628        let _guard = ClockGuard::acquire();
629        // Reset steady time
630        Clock::set_steady_time(0);
631        assert_eq!(Clock::get_steady_time_nanos(), 0);
632
633        // Update by milliseconds
634        Clock::update_steady_time_ms(100);
635        assert_eq!(Clock::get_steady_time_nanos(), 100_000_000);
636
637        // Update by nanoseconds
638        Clock::update_steady_time(500_000_000);
639        assert_eq!(Clock::get_steady_time_nanos(), 600_000_000);
640
641        // Steady clock should return updated time -- in the build where the
642        // counter is the only clock there is. With a port linked the port
643        // outranks it (issue 1334), which is the test below.
644        #[cfg(not(feature = "platform-clock"))]
645        {
646            let steady_clock = Clock::steady();
647            let now = steady_clock.now();
648            assert_eq!(now.to_nanos(), 600_000_000);
649        }
650    }
651
652    /// With NO port linked there is no clock, and all three types say so with
653    /// the one counter they were given — issue 1334.
654    ///
655    /// This replaces ~~`test_system_clock_returns_nonzero`~~, which asserted
656    /// `now.sec > 0` for `Clock::system()` in exactly this shape. That claim
657    /// was false from the day phase-359 W10 removed the `std::time` arm — a
658    /// portless build reads the counter, which is 0 — and nobody saw it,
659    /// because its `all(std, not(platform-clock))` gate put it in no lane at
660    /// all: the one nros-core feature lane is `std,platform-clock`, and the
661    /// default lane has no `std`. A test in no lane is the shape CLAUDE.md's
662    /// `required-features` rule names, one cfg over.
663    ///
664    /// What it asserts instead is the property that IS true here, and it is the
665    /// portless half of this issue's fix: `RosTime` with no override reads
666    /// whatever `SystemTime` reads, whatever that turns out to be.
667    #[test]
668    #[cfg(not(feature = "platform-clock"))]
669    fn without_a_port_every_clock_is_the_counter() {
670        let _guard = ClockGuard::acquire();
671        Clock::clear_ros_time_override();
672        Clock::set_steady_time(7_500_000_000);
673
674        assert_eq!(Clock::system().now().to_nanos(), 7_500_000_000);
675        assert_eq!(Clock::steady().now().to_nanos(), 7_500_000_000);
676        assert_eq!(
677            Clock::ros_time().now().to_nanos(),
678            7_500_000_000,
679            "with no override and no port, ROS time is what system time is"
680        );
681    }
682
683    /// phase-359 W10 follow-up — a linked port OUTRANKS `std` for the wall
684    /// clock, on a `std` build too.
685    ///
686    /// This is the one configuration where the two disagree observably, and it
687    /// is why the test image defines the port symbol itself (`fake_port`): the
688    /// value returned is nothing like a real `SystemTime`, so a
689    /// `Clock::system()` that answered with `SystemTime::now()` — which is what
690    /// this file did before, because `platform_wall_clock` was gated
691    /// `not(std)` — fails loudly rather than coincidentally passing. On POSIX
692    /// both sources are CLOCK_REALTIME, so nothing short of a port with an
693    /// opinion can tell them apart.
694    #[test]
695    #[cfg(feature = "platform-clock")]
696    fn platform_port_outranks_std_for_the_wall_clock() {
697        let _guard = ClockGuard::acquire();
698        let now = Clock::system().now();
699        assert_eq!(
700            now.sec, 1_000_000_000,
701            "Clock::system() must read the linked port, not SystemTime"
702        );
703        assert_eq!(now.nanosec, 0);
704    }
705
706    /// ROS time with no `/clock` source is the WALL clock, and it RUNS — issue
707    /// 1334, and the negative control for the whole fix.
708    ///
709    /// Before the fix this arm read `STEADY_TIME_NANOS`, a counter whose own
710    /// doc says it is "advanced by its owner" and which nothing outside this
711    /// file's tests has ever advanced. So the assertion that fails on the
712    /// pre-fix tree is the first one (the reading was 0, not the port's wall
713    /// clock) and the one after it (a clock that does not move is not a
714    /// clock).
715    ///
716    /// `rclcpp`'s `ClockType::ROS_TIME` with `use_sim_time` false reads the
717    /// system clock; five doc sites in this tree across Rust, C and C++ said we
718    /// did too; and the C surface (`rclc_clock_get_now_time(RCL_ROS_TIME)`)
719    /// always did. This is the arm that disagreed with all of them.
720    #[test]
721    #[cfg(feature = "platform-clock")]
722    fn ros_time_with_no_override_reads_the_port_wall_clock() {
723        let _guard = ClockGuard::acquire();
724        Clock::clear_ros_time_override();
725        // A counter reading that is nothing like the wall clock, so reading the
726        // wrong source cannot pass by coincidence.
727        Clock::set_steady_time(1_234);
728
729        let ros = Clock::ros_time().now();
730        assert_eq!(
731            ros,
732            Clock::system().now(),
733            "with no override, ROS time IS system time"
734        );
735        assert_eq!(ros.sec, 1_000_000_000, "it must be the PORT's wall clock");
736
737        // ...and it advances with it. Two seconds on the port's hand.
738        fake_port::WALL_NS.fetch_add(2_000_000_000, core::sync::atomic::Ordering::Relaxed);
739        let later = Clock::ros_time().now();
740        assert_eq!(
741            later.to_nanos() - ros.to_nanos(),
742            2_000_000_000,
743            "ROS time with no source must ADVANCE with the wall clock"
744        );
745    }
746
747    /// A `/clock` override still wins over the wall clock — the sim-time path
748    /// this fix must not touch.
749    #[test]
750    #[cfg(feature = "platform-clock")]
751    fn an_override_still_outranks_the_wall_clock() {
752        let _guard = ClockGuard::acquire();
753        Clock::set_ros_time_override(5_000_000_000);
754        assert_eq!(Clock::ros_time().now().to_nanos(), 5_000_000_000);
755        assert_ne!(
756            Clock::ros_time().now(),
757            Clock::system().now(),
758            "a simulated clock must not be shadowed by the wall clock"
759        );
760        Clock::clear_ros_time_override();
761        assert_eq!(Clock::ros_time().now(), Clock::system().now());
762    }
763
764    /// The sibling site: a linked port outranks the counter for the MONOTONIC
765    /// clock too — issue 1334's sweep.
766    ///
767    /// Fails on the pre-fix tree, where `SteadyTime` read the counter on every
768    /// flavour and therefore returned 0 in every shipped image, while the C
769    /// surface answered `RCL_STEADY_TIME` with `nros_platform_clock_ns`.
770    #[test]
771    #[cfg(feature = "platform-clock")]
772    fn platform_port_outranks_the_counter_for_the_steady_clock() {
773        let _guard = ClockGuard::acquire();
774        Clock::set_steady_time(1_234);
775
776        let now = Clock::steady().now();
777        assert_eq!(
778            now.to_nanos(),
779            fake_port::STEADY_BASE_NS as i64,
780            "Clock::steady() must read the linked port, not the counter"
781        );
782        assert_ne!(
783            now.to_nanos(),
784            Clock::system().now().to_nanos(),
785            "the monotonic clock is a different quantity from the wall clock"
786        );
787
788        fake_port::STEADY_NS.fetch_add(250_000_000, core::sync::atomic::Ordering::Relaxed);
789        assert_eq!(
790            Clock::steady().now().to_nanos() - now.to_nanos(),
791            250_000_000
792        );
793    }
794}