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        Time::from_nanos(atomic_time::get_steady())
333    }
334
335    /// The monotonic clock, in ONE place — issue 1334's sibling site.
336    ///
337    /// Same rule as `Self::wall_now`, one clock over: a linked port IS the
338    /// monotonic clock, and the counter is what a build with no port was
339    /// given.
340    ///
341    /// This overturns half of phase-359 W10, which moved `SystemTime` onto the
342    /// port and left this line reading "`SteadyTime` is the counter on every
343    /// flavour, deliberately: it is advanced by its owner … and the port's
344    /// monotonic export is the executor's business, not this type's". The
345    /// owner never arrived — nothing outside this file's own tests has ever
346    /// called `update_steady_time` — so on every shipped image
347    /// `Clock::steady().now()` was the constant 0, while the C surface
348    /// answered the SAME question (`RCL_STEADY_TIME`) with
349    /// `nros_platform_clock_ns`. That is issue 1334's complaint exactly, one
350    /// clock over: two surfaces, one image, different answers, and the Rust
351    /// one is not a clock. W10's own rule settles it — when a port is linked
352    /// it IS the clock — and the counter keeps its documented RTIC meaning in
353    /// the one build where it is the only source.
354    fn steady_now() -> Time {
355        if let Some(t) = platform_steady_clock() {
356            return t;
357        }
358        Time::from_nanos(atomic_time::get_steady())
359    }
360
361    /// Get the current time from this clock
362    ///
363    /// # Platform behavior
364    ///
365    /// - **A platform port linked** (`platform-clock`): the port's clocks —
366    ///   `nros_platform_time_now_ns` for the wall, `nros_platform_clock_ns`
367    ///   for the monotonic. The port is the authority, and an image must not
368    ///   hold two answers to "what time is it".
369    /// - **No port**: the internal counter, which the caller advances with
370    ///   `update_steady_time()`.
371    ///
372    /// phase-359 W10 — there is no `std::time` arm. The platform API IS the
373    /// clock: a build with a port reads it, a build without one has no clock to
374    /// read and says so with the counter it was given. `SystemTime` used to sit
375    /// here as a third answer for hosted builds, which made "what time is it"
376    /// depend on whether some crate in the graph happened to name `std`.
377    ///
378    /// `RosTime` with no override is `SystemTime`, in EVERY build shape and by
379    /// construction — both arms call `Self::wall_now`. That is rclcpp's
380    /// contract (`ClockType::ROS_TIME` with `use_sim_time` false reads the
381    /// system clock), what five doc sites across three languages already
382    /// promised, and what the C surface already did; issue 1334 is the four
383    /// phases in which the Rust arm said something else.
384    pub fn now(&self) -> Time {
385        match self.clock_type {
386            ClockType::SystemTime => Self::wall_now(),
387            ClockType::SteadyTime => Self::steady_now(),
388            ClockType::RosTime => {
389                let override_nanos = atomic_time::get_ros_override();
390                if override_nanos >= 0 {
391                    // A `/clock` source is driving simulated time.
392                    Time::from_nanos(override_nanos)
393                } else {
394                    // None is. rclcpp reads the system clock here and so do we
395                    // — the same expression `Clock::system()` evaluates, not a
396                    // copy of it (issue 1334).
397                    Self::wall_now()
398                }
399            }
400        }
401    }
402
403    /// Set a ROS time override
404    ///
405    /// When set, all `RosTime` clocks will return this time instead of
406    /// system time. This is useful for simulation.
407    ///
408    /// # Arguments
409    /// * `nanos` - Nanoseconds since epoch
410    pub fn set_ros_time_override(nanos: i64) {
411        atomic_time::set_ros_override(nanos);
412    }
413
414    /// Set a ROS time override from a Time value
415    pub fn set_ros_time_override_time(time: Time) {
416        Self::set_ros_time_override(time.to_nanos());
417    }
418
419    /// Clear the ROS time override
420    ///
421    /// After clearing, `RosTime` clocks will return system time again.
422    pub fn clear_ros_time_override() {
423        atomic_time::set_ros_override(-1);
424    }
425
426    /// Check if a ROS time override is active
427    pub fn is_ros_time_override_active() -> bool {
428        atomic_time::get_ros_override() >= 0
429    }
430
431    /// Get the current ROS time override value (if active)
432    pub fn get_ros_time_override() -> Option<Time> {
433        let nanos = atomic_time::get_ros_override();
434        if nanos >= 0 {
435            Some(Time::from_nanos(nanos))
436        } else {
437            None
438        }
439    }
440
441    /// Update the steady time counter
442    ///
443    /// For environments with NO platform port, call this periodically from
444    /// your main loop or RTIC task to advance the clocks. With a port linked
445    /// this counter is not what any clock reads — the port is (issue 1334) —
446    /// and advancing it has no observable effect.
447    ///
448    /// # Arguments
449    /// * `delta_nanos` - Nanoseconds elapsed since last call
450    pub fn update_steady_time(delta_nanos: i64) {
451        atomic_time::add_steady(delta_nanos);
452    }
453
454    /// Update the steady time counter (milliseconds version)
455    ///
456    /// Convenience method for RTIC tasks using millisecond intervals.
457    ///
458    /// # Arguments
459    /// * `delta_ms` - Milliseconds elapsed since last call
460    pub fn update_steady_time_ms(delta_ms: u64) {
461        let delta_nanos = delta_ms as i64 * 1_000_000;
462        Self::update_steady_time(delta_nanos);
463    }
464
465    /// Set the steady time counter to a specific value
466    ///
467    /// Use this to initialize the clock or synchronize with an external
468    /// time source.
469    pub fn set_steady_time(nanos: i64) {
470        atomic_time::set_steady(nanos);
471    }
472
473    /// Get the current steady time counter value
474    pub fn get_steady_time_nanos() -> i64 {
475        atomic_time::get_steady()
476    }
477}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482
483    /// Serialises every test that touches the process-global clock state —
484    /// issue 1334.
485    ///
486    /// The override, the counter and (below) the fake port are all statics, and
487    /// `cargo test` runs these in parallel threads: a test that SETS one and
488    /// reads it back is racing every sibling that writes the same static. That
489    /// was already true before this guard existed (`test_ros_time_override` and
490    /// `test_steady_time_update` write two different globals that `Clock::now`
491    /// reads together) and it becomes load-bearing here, because the new tests
492    /// move the fake port's hands while another test asserts where they point.
493    ///
494    /// A spin lock rather than `std::sync::Mutex`: this crate is `#![no_std]`
495    /// and `std` is a FEATURE, so a `Mutex` here would confine the guard — and
496    /// therefore every test using it — to the `std` flavour, which is not where
497    /// the `platform-clock` tests need to live.
498    struct ClockGuard;
499
500    static CLOCK_LOCK: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false);
501
502    impl ClockGuard {
503        fn acquire() -> Self {
504            while CLOCK_LOCK
505                .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
506                .is_err()
507            {
508                core::hint::spin_loop();
509            }
510            Self
511        }
512    }
513
514    impl Drop for ClockGuard {
515        fn drop(&mut self) {
516            // Leave the globals as a fresh process has them, so a test that
517            // does NOT take the guard still sees the documented defaults.
518            Clock::clear_ros_time_override();
519            Clock::set_steady_time(0);
520            #[cfg(feature = "platform-clock")]
521            fake_port::reset();
522            CLOCK_LOCK.store(false, Ordering::Release);
523        }
524    }
525
526    /// The stand-in platform port for the `platform-clock` flavour.
527    ///
528    /// One definition of each symbol, at module level, because a linker sees
529    /// one test binary: the wall symbol used to be defined INSIDE the single
530    /// test that needed it, which works exactly until a second test needs it
531    /// too. Both hands MOVE (they are atomics, not constants), which is what
532    /// lets a test assert that a clock ADVANCES rather than merely that it
533    /// reads a magic number.
534    #[cfg(feature = "platform-clock")]
535    mod fake_port {
536        use core::sync::atomic::{AtomicU64, Ordering};
537
538        /// 2001-09-09T01:46:40Z — a fixed instant no real clock will return.
539        pub(super) const WALL_BASE_NS: u64 = 1_000_000_000 * 1_000_000_000;
540        /// 42 s of uptime — no relation to `WALL_BASE_NS`, deliberately: the
541        /// two clocks are different quantities and a test must not be able to
542        /// pass by reading the wrong one.
543        pub(super) const STEADY_BASE_NS: u64 = 42_000_000_000;
544
545        pub(super) static WALL_NS: AtomicU64 = AtomicU64::new(WALL_BASE_NS);
546        pub(super) static STEADY_NS: AtomicU64 = AtomicU64::new(STEADY_BASE_NS);
547
548        pub(super) fn reset() {
549            WALL_NS.store(WALL_BASE_NS, Ordering::Relaxed);
550            STEADY_NS.store(STEADY_BASE_NS, Ordering::Relaxed);
551        }
552
553        #[unsafe(no_mangle)]
554        extern "C" fn nros_platform_time_now_ns() -> u64 {
555            WALL_NS.load(Ordering::Relaxed)
556        }
557
558        #[unsafe(no_mangle)]
559        extern "C" fn nros_platform_clock_ns() -> u64 {
560            STEADY_NS.load(Ordering::Relaxed)
561        }
562    }
563
564    #[test]
565    fn test_clock_type_default() {
566        let clock_type = ClockType::default();
567        assert_eq!(clock_type, ClockType::SystemTime);
568    }
569
570    #[test]
571    fn test_clock_constructors() {
572        let system = Clock::system();
573        assert_eq!(system.clock_type(), ClockType::SystemTime);
574
575        let steady = Clock::steady();
576        assert_eq!(steady.clock_type(), ClockType::SteadyTime);
577
578        let ros = Clock::ros_time();
579        assert_eq!(ros.clock_type(), ClockType::RosTime);
580
581        let custom = Clock::new(ClockType::SteadyTime);
582        assert_eq!(custom.clock_type(), ClockType::SteadyTime);
583    }
584
585    #[test]
586    fn test_clock_default() {
587        let clock = Clock::default();
588        assert_eq!(clock.clock_type(), ClockType::SystemTime);
589    }
590
591    #[test]
592    fn test_ros_time_override() {
593        let _guard = ClockGuard::acquire();
594        // Clear any existing override
595        Clock::clear_ros_time_override();
596        assert!(!Clock::is_ros_time_override_active());
597        assert!(Clock::get_ros_time_override().is_none());
598
599        // Set override
600        let override_time = Time::new(1234567890, 123456789);
601        Clock::set_ros_time_override_time(override_time);
602        assert!(Clock::is_ros_time_override_active());
603        assert_eq!(Clock::get_ros_time_override(), Some(override_time));
604
605        // ROS clock should return override time
606        let ros_clock = Clock::ros_time();
607        let now = ros_clock.now();
608        assert_eq!(now, override_time);
609
610        // Clear override
611        Clock::clear_ros_time_override();
612        assert!(!Clock::is_ros_time_override_active());
613    }
614
615    #[test]
616    fn test_steady_time_update() {
617        let _guard = ClockGuard::acquire();
618        // Reset steady time
619        Clock::set_steady_time(0);
620        assert_eq!(Clock::get_steady_time_nanos(), 0);
621
622        // Update by milliseconds
623        Clock::update_steady_time_ms(100);
624        assert_eq!(Clock::get_steady_time_nanos(), 100_000_000);
625
626        // Update by nanoseconds
627        Clock::update_steady_time(500_000_000);
628        assert_eq!(Clock::get_steady_time_nanos(), 600_000_000);
629
630        // Steady clock should return updated time -- in the build where the
631        // counter is the only clock there is. With a port linked the port
632        // outranks it (issue 1334), which is the test below.
633        #[cfg(not(feature = "platform-clock"))]
634        {
635            let steady_clock = Clock::steady();
636            let now = steady_clock.now();
637            assert_eq!(now.to_nanos(), 600_000_000);
638        }
639    }
640
641    /// With NO port linked there is no clock, and all three types say so with
642    /// the one counter they were given — issue 1334.
643    ///
644    /// This replaces `test_system_clock_returns_nonzero`, which asserted
645    /// `now.sec > 0` for `Clock::system()` in exactly this shape. That claim
646    /// was false from the day phase-359 W10 removed the `std::time` arm — a
647    /// portless build reads the counter, which is 0 — and nobody saw it,
648    /// because its `all(std, not(platform-clock))` gate put it in no lane at
649    /// all: the one nros-core feature lane is `std,platform-clock`, and the
650    /// default lane has no `std`. A test in no lane is the shape CLAUDE.md's
651    /// `required-features` rule names, one cfg over.
652    ///
653    /// What it asserts instead is the property that IS true here, and it is the
654    /// portless half of this issue's fix: `RosTime` with no override reads
655    /// whatever `SystemTime` reads, whatever that turns out to be.
656    #[test]
657    #[cfg(not(feature = "platform-clock"))]
658    fn without_a_port_every_clock_is_the_counter() {
659        let _guard = ClockGuard::acquire();
660        Clock::clear_ros_time_override();
661        Clock::set_steady_time(7_500_000_000);
662
663        assert_eq!(Clock::system().now().to_nanos(), 7_500_000_000);
664        assert_eq!(Clock::steady().now().to_nanos(), 7_500_000_000);
665        assert_eq!(
666            Clock::ros_time().now().to_nanos(),
667            7_500_000_000,
668            "with no override and no port, ROS time is what system time is"
669        );
670    }
671
672    /// phase-359 W10 follow-up — a linked port OUTRANKS `std` for the wall
673    /// clock, on a `std` build too.
674    ///
675    /// This is the one configuration where the two disagree observably, and it
676    /// is why the test image defines the port symbol itself (`fake_port`): the
677    /// value returned is nothing like a real `SystemTime`, so a
678    /// `Clock::system()` that answered with `SystemTime::now()` — which is what
679    /// this file did before, because `platform_wall_clock` was gated
680    /// `not(std)` — fails loudly rather than coincidentally passing. On POSIX
681    /// both sources are CLOCK_REALTIME, so nothing short of a port with an
682    /// opinion can tell them apart.
683    #[test]
684    #[cfg(all(feature = "std", feature = "platform-clock"))]
685    fn platform_port_outranks_std_for_the_wall_clock() {
686        let _guard = ClockGuard::acquire();
687        let now = Clock::system().now();
688        assert_eq!(
689            now.sec, 1_000_000_000,
690            "Clock::system() must read the linked port, not SystemTime"
691        );
692        assert_eq!(now.nanosec, 0);
693    }
694
695    /// ROS time with no `/clock` source is the WALL clock, and it RUNS — issue
696    /// 1334, and the negative control for the whole fix.
697    ///
698    /// Before the fix this arm read `STEADY_TIME_NANOS`, a counter whose own
699    /// doc says it is "advanced by its owner" and which nothing outside this
700    /// file's tests has ever advanced. So the assertion that fails on the
701    /// pre-fix tree is the first one (the reading was 0, not the port's wall
702    /// clock) and the one after it (a clock that does not move is not a
703    /// clock).
704    ///
705    /// `rclcpp`'s `ClockType::ROS_TIME` with `use_sim_time` false reads the
706    /// system clock; five doc sites in this tree across Rust, C and C++ said we
707    /// did too; and the C surface (`rclc_clock_get_now_time(RCL_ROS_TIME)`)
708    /// always did. This is the arm that disagreed with all of them.
709    #[test]
710    #[cfg(feature = "platform-clock")]
711    fn ros_time_with_no_override_reads_the_port_wall_clock() {
712        let _guard = ClockGuard::acquire();
713        Clock::clear_ros_time_override();
714        // A counter reading that is nothing like the wall clock, so reading the
715        // wrong source cannot pass by coincidence.
716        Clock::set_steady_time(1_234);
717
718        let ros = Clock::ros_time().now();
719        assert_eq!(
720            ros,
721            Clock::system().now(),
722            "with no override, ROS time IS system time"
723        );
724        assert_eq!(ros.sec, 1_000_000_000, "it must be the PORT's wall clock");
725
726        // ...and it advances with it. Two seconds on the port's hand.
727        fake_port::WALL_NS.fetch_add(2_000_000_000, core::sync::atomic::Ordering::Relaxed);
728        let later = Clock::ros_time().now();
729        assert_eq!(
730            later.to_nanos() - ros.to_nanos(),
731            2_000_000_000,
732            "ROS time with no source must ADVANCE with the wall clock"
733        );
734    }
735
736    /// A `/clock` override still wins over the wall clock — the sim-time path
737    /// this fix must not touch.
738    #[test]
739    #[cfg(feature = "platform-clock")]
740    fn an_override_still_outranks_the_wall_clock() {
741        let _guard = ClockGuard::acquire();
742        Clock::set_ros_time_override(5_000_000_000);
743        assert_eq!(Clock::ros_time().now().to_nanos(), 5_000_000_000);
744        assert_ne!(
745            Clock::ros_time().now(),
746            Clock::system().now(),
747            "a simulated clock must not be shadowed by the wall clock"
748        );
749        Clock::clear_ros_time_override();
750        assert_eq!(Clock::ros_time().now(), Clock::system().now());
751    }
752
753    /// The sibling site: a linked port outranks the counter for the MONOTONIC
754    /// clock too — issue 1334's sweep.
755    ///
756    /// Fails on the pre-fix tree, where `SteadyTime` read the counter on every
757    /// flavour and therefore returned 0 in every shipped image, while the C
758    /// surface answered `RCL_STEADY_TIME` with `nros_platform_clock_ns`.
759    #[test]
760    #[cfg(feature = "platform-clock")]
761    fn platform_port_outranks_the_counter_for_the_steady_clock() {
762        let _guard = ClockGuard::acquire();
763        Clock::set_steady_time(1_234);
764
765        let now = Clock::steady().now();
766        assert_eq!(
767            now.to_nanos(),
768            fake_port::STEADY_BASE_NS as i64,
769            "Clock::steady() must read the linked port, not the counter"
770        );
771        assert_ne!(
772            now.to_nanos(),
773            Clock::system().now().to_nanos(),
774            "the monotonic clock is a different quantity from the wall clock"
775        );
776
777        fake_port::STEADY_NS.fetch_add(250_000_000, core::sync::atomic::Ordering::Relaxed);
778        assert_eq!(
779            Clock::steady().now().to_nanos() - now.to_nanos(),
780            250_000_000
781        );
782    }
783}