Skip to main content

epics_libcom_rs/runtime/
general_time.rs

1use std::sync::atomic::{AtomicU64, Ordering};
2use std::sync::{LazyLock, Mutex};
3use std::time::SystemTime;
4
5/// Closure that returns the current time, or `None` if unavailable.
6type CurrentTimeFn = Box<dyn Fn() -> Option<SystemTime> + Send + Sync>;
7
8/// Closure that returns the time for a given event number, or `None`.
9type EventTimeFn = Box<dyn Fn(i32) -> Option<SystemTime> + Send + Sync>;
10
11/// Seconds between the Unix epoch (1970-01-01) and the EPICS epoch
12/// (1990-01-01 00:00:00 UTC).
13pub const EPICS_EPOCH_UNIX_SECS: u64 = 631_152_000;
14
15/// The EPICS epoch (1990-01-01 00:00:00 UTC) expressed as a Unix
16/// `SystemTime`.
17///
18/// This is the value of an all-zero `epicsTimeStamp`, and therefore the
19/// single owner of "a time nobody has set yet" everywhere in the port —
20/// a record's `TIME` before its first `process()`, the general-time
21/// ratchet's seed. Seeding such a field with `SystemTime::UNIX_EPOCH`
22/// instead is a 20-year error that survives every conversion: C's
23/// `epicsTimeToTimespec` adds `POSIX_TIME_AT_EPICS_EPOCH`, so the wire
24/// value C derives from `{0,0}` is 631152000, not 0.
25///
26/// C parity: `epicsGeneralTime.c:66` zero-initialises `lastProvidedTime`
27/// to `epicsTimeStamp {0,0}`; `dbCommon.time` is likewise zeroed by
28/// `calloc` in `dbStaticLib` and never touched until the record first
29/// processes.
30pub fn epics_epoch() -> SystemTime {
31    SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(EPICS_EPOCH_UNIX_SECS)
32}
33
34struct CurrentTimeProvider {
35    name: String,
36    priority: i32,
37    get_time: CurrentTimeFn,
38    /// Whether this provider is safe to call from interrupt context.
39    /// C parity: `generalTimeAddIntCurrentTimeProvider` registers an
40    /// interrupt-callable variant queried by `epicsTimeGetCurrentInt`.
41    interrupt_safe: bool,
42    /// `true` for the built-in last-resort OS-clock provider. C tracks
43    /// this via the `osdTimeGetCurrent` function-pointer identity.
44    is_os_default: bool,
45}
46
47struct EventTimeProvider {
48    name: String,
49    priority: i32,
50    get_event: EventTimeFn,
51    /// Interrupt-callable variant — see [`CurrentTimeProvider::interrupt_safe`].
52    interrupt_safe: bool,
53}
54
55struct GeneralTimeInner {
56    current_providers: Vec<CurrentTimeProvider>,
57    event_providers: Vec<EventTimeProvider>,
58    /// Monotonic ratchet for current time.
59    last_provided_time: SystemTime,
60    /// Per-event ratchet for events 1..=255.
61    event_times: [SystemTime; 256],
62    /// Ratchet for event -1 (BestTime).
63    last_best_time: SystemTime,
64    /// Name of the provider that last supplied current time.
65    last_current_name: Option<String>,
66    /// Name of the provider that last supplied event time.
67    last_event_name: Option<String>,
68    /// C parity: `epicsGeneralTime.c:84` `useOsdGetCurrent`. Starts
69    /// `true`; while only the built-in OS-clock provider is registered,
70    /// `get_current` short-circuits straight to the OS clock and the
71    /// monotonic ratchet is **never consulted** — a real backward
72    /// wall-clock step is returned verbatim, exactly as a C IOC does.
73    /// Cleared by [`register_current_provider`] the moment a
74    /// non-default provider is registered.
75    use_osd_get_current: bool,
76    /// Rust-only notification channel — see [`register_clock_sync_hook`].
77    /// This is **not** a C-base API; it is an additive extension.
78    sync_hooks: Vec<Box<dyn Fn(SystemTime) + Send + Sync>>,
79}
80
81impl GeneralTimeInner {
82    fn new() -> Self {
83        let mut inner = Self {
84            current_providers: Vec::new(),
85            event_providers: Vec::new(),
86            last_provided_time: epics_epoch(),
87            event_times: [epics_epoch(); 256],
88            last_best_time: epics_epoch(),
89            last_current_name: None,
90            last_event_name: None,
91            use_osd_get_current: true,
92            sync_hooks: Vec::new(),
93        };
94        // Register the OS clock as the last-resort current time provider.
95        inner.current_providers.push(CurrentTimeProvider {
96            name: "OS Clock".to_string(),
97            priority: 999,
98            get_time: Box::new(|| Some(SystemTime::now())),
99            interrupt_safe: true,
100            is_os_default: true,
101        });
102        inner
103    }
104}
105
106static GENERAL_TIME: LazyLock<Mutex<GeneralTimeInner>> =
107    LazyLock::new(|| Mutex::new(GeneralTimeInner::new()));
108
109static ERROR_COUNTS: AtomicU64 = AtomicU64::new(0);
110
111/// Register a current-time provider at the given priority (lower = higher priority).
112pub fn register_current_provider(
113    name: impl Into<String>,
114    priority: i32,
115    get_time: impl Fn() -> Option<SystemTime> + Send + Sync + 'static,
116) {
117    register_current_provider_impl(name.into(), priority, Box::new(get_time), false);
118}
119
120/// Register an **interrupt-callable** current-time provider.
121///
122/// C parity: `epicsGeneralTime.c:445-459` `generalTimeAddIntCurrentTimeProvider`.
123/// The closure MUST be callable from interrupt context — it must not
124/// block, allocate, or take locks. Only providers registered this way
125/// are consulted by [`get_current_int`].
126pub fn register_int_current_provider(
127    name: impl Into<String>,
128    priority: i32,
129    get_time: impl Fn() -> Option<SystemTime> + Send + Sync + 'static,
130) {
131    register_current_provider_impl(name.into(), priority, Box::new(get_time), true);
132}
133
134fn register_current_provider_impl(
135    name: String,
136    priority: i32,
137    get_time: CurrentTimeFn,
138    interrupt_safe: bool,
139) {
140    let mut inner = GENERAL_TIME.lock().unwrap();
141    let provider = CurrentTimeProvider {
142        name,
143        priority,
144        get_time,
145        interrupt_safe,
146        is_os_default: false,
147    };
148    let pos = inner
149        .current_providers
150        .iter()
151        .position(|p| p.priority > priority)
152        .unwrap_or(inner.current_providers.len());
153    inner.current_providers.insert(pos, provider);
154    // C `insertProvider`: clear `useOsdGetCurrent` once the provider
155    // list holds more than just the built-in OS default. Any provider
156    // registered through this path is non-default, so clear the flag.
157    inner.use_osd_get_current = false;
158}
159
160/// Register an event-time provider at the given priority (lower = higher priority).
161pub fn register_event_provider(
162    name: impl Into<String>,
163    priority: i32,
164    get_event: impl Fn(i32) -> Option<SystemTime> + Send + Sync + 'static,
165) {
166    register_event_provider_impl(name.into(), priority, Box::new(get_event), false);
167}
168
169/// Register an **interrupt-callable** event-time provider.
170///
171/// C parity: `epicsGeneralTime.c:488-502` `generalTimeAddIntEventProvider`.
172/// Same interrupt-context constraints as [`register_int_current_provider`].
173/// Only providers registered this way are consulted by [`get_event_int`].
174pub fn register_int_event_provider(
175    name: impl Into<String>,
176    priority: i32,
177    get_event: impl Fn(i32) -> Option<SystemTime> + Send + Sync + 'static,
178) {
179    register_event_provider_impl(name.into(), priority, Box::new(get_event), true);
180}
181
182fn register_event_provider_impl(
183    name: String,
184    priority: i32,
185    get_event: EventTimeFn,
186    interrupt_safe: bool,
187) {
188    let mut inner = GENERAL_TIME.lock().unwrap();
189    let provider = EventTimeProvider {
190        name,
191        priority,
192        get_event,
193        interrupt_safe,
194    };
195    let pos = inner
196        .event_providers
197        .iter()
198        .position(|p| p.priority > priority)
199        .unwrap_or(inner.event_providers.len());
200    inner.event_providers.insert(pos, provider);
201}
202
203/// Register a callback fired whenever a time provider reports a fresh
204/// external sync (PTP master step, NTP sync window, GPS PPS).
205///
206/// **Rust-only extension — not an epics-base API.** EPICS base has no
207/// public registerable clock-sync hook: `osiClockTime.c` keeps its
208/// `ClockTimeSync` logic strictly internal. This is an additive
209/// notification channel for the Rust port's downstream consumers and is
210/// intentionally kept separate from the C-parity `get_current` /
211/// `get_event` paths.
212///
213/// Hooks are invoked from [`notify_clock_sync`] in registration order
214/// with the new (post-sync) time. They MUST be cheap — they execute
215/// inside the general-time mutex; long work should defer to a spawn.
216/// There is no de-registration API: hooks live for the process
217/// lifetime.
218pub fn register_clock_sync_hook<F>(hook: F)
219where
220    F: Fn(SystemTime) + Send + Sync + 'static,
221{
222    let mut inner = GENERAL_TIME.lock().unwrap();
223    inner.sync_hooks.push(Box::new(hook));
224}
225
226/// Time-source providers (PTP/NTP integrations, hardware-clock
227/// drivers) call this when they receive a fresh sync from their
228/// upstream master. Every registered [`register_clock_sync_hook`]
229/// callback fires with `t_synced` — the time the source reports as
230/// authoritative right now.
231///
232/// `notify_clock_sync` does NOT itself update any internal cache —
233/// `get_current` and `get_event` keep their existing ratchet semantics
234/// (a backward step is rejected). The hook is purely a notification
235/// channel for downstream consumers (records that want to log a
236/// step, archivers that want to insert a discontinuity marker).
237pub fn notify_clock_sync(t_synced: SystemTime) {
238    let inner = GENERAL_TIME.lock().unwrap();
239    for hook in &inner.sync_hooks {
240        hook(t_synced);
241    }
242}
243
244/// Get the current time from the highest-priority provider that succeeds.
245///
246/// C parity (`epicsGeneralTime.c:111-112`): while only the built-in
247/// OS-clock provider is registered (`use_osd_get_current`), this
248/// short-circuits straight to the OS clock and the monotonic ratchet is
249/// **not** consulted — a backward wall-clock step (NTP slew, manual
250/// `date` change) is returned verbatim and does **not** count an error.
251///
252/// Once any non-default provider is registered, the returned time is
253/// monotonically enforced: if a provider returns a time earlier than
254/// the last provided time, the last provided time is returned and the
255/// error counter is incremented.
256pub fn get_current() -> SystemTime {
257    let mut inner = GENERAL_TIME.lock().unwrap();
258
259    // C `useOsdGetCurrent` short-circuit: no ratchet, no error count.
260    if inner.use_osd_get_current {
261        if let Some(idx) = inner.current_providers.iter().position(|p| p.is_os_default) {
262            if let Some(t) = (inner.current_providers[idx].get_time)() {
263                let name = inner.current_providers[idx].name.clone();
264                inner.last_provided_time = t;
265                inner.last_current_name = Some(name);
266                return t;
267            }
268        }
269        // OS clock unavailable (should not happen) — fall through to
270        // the ratcheted path below as a last resort.
271    }
272
273    for i in 0..inner.current_providers.len() {
274        if let Some(t) = (inner.current_providers[i].get_time)() {
275            let name = inner.current_providers[i].name.clone();
276            if t >= inner.last_provided_time {
277                inner.last_provided_time = t;
278                inner.last_current_name = Some(name);
279                return t;
280            } else {
281                ERROR_COUNTS.fetch_add(1, Ordering::Relaxed);
282                inner.last_current_name = Some(name);
283                return inner.last_provided_time;
284            }
285        }
286    }
287    // All providers failed — return last known time.
288    ERROR_COUNTS.fetch_add(1, Ordering::Relaxed);
289    inner.last_provided_time
290}
291
292/// Get the current time, querying only providers **other than** the one
293/// at `ignore_priority`.
294///
295/// C parity: `epicsGeneralTime.c:106-151` `generalTimeGetExceptPriority`.
296/// Used by a time provider (typically an NTP/clock provider) that needs
297/// to read "the best time other than mine" to validate its own sync
298/// without recursing into itself.
299///
300/// Returns `(time, priority_used)` — the priority of the provider that
301/// answered — or `None` when no other provider succeeded. **No ratchet
302/// is applied**: this query may legitimately go backwards, exactly as
303/// the C function documents ("No ratchet, time from this routine may go
304/// backwards").
305///
306/// `ignore_priority` follows the C convention: a positive value skips
307/// the provider *at* that priority; a negative value `-n` skips every
308/// provider *except* the one at priority `n`.
309pub fn get_current_except_priority(ignore_priority: i32) -> Option<(SystemTime, i32)> {
310    let inner = GENERAL_TIME.lock().unwrap();
311    for p in &inner.current_providers {
312        if (ignore_priority > 0 && p.priority == ignore_priority)
313            || (ignore_priority < 0 && p.priority != -ignore_priority)
314        {
315            continue;
316        }
317        if let Some(t) = (p.get_time)() {
318            return Some((t, p.priority));
319        }
320    }
321    None
322}
323
324/// Interrupt-context current-time query.
325///
326/// C parity: `epicsGeneralTime.c:226-238` `epicsTimeGetCurrentInt`.
327/// Consults only providers registered via
328/// [`register_int_current_provider`] (interrupt-callable). Returns
329/// `None` when no interrupt-safe provider answers — the C function
330/// returns `S_time_noProvider` in that case. **No ratchet** — the C
331/// `*Int` path does not touch the shared ratchet state (it must be
332/// interrupt-safe).
333pub fn get_current_int() -> Option<SystemTime> {
334    let inner = GENERAL_TIME.lock().unwrap();
335    for p in &inner.current_providers {
336        if !p.interrupt_safe {
337            continue;
338        }
339        if let Some(t) = (p.get_time)() {
340            return Some(t);
341        }
342    }
343    None
344}
345
346/// Interrupt-context event-time query.
347///
348/// C parity: `epicsGeneralTime.c:351-367` `epicsTimeGetEventInt`.
349/// Consults only event providers registered via
350/// [`register_int_event_provider`]. Returns `None` when no
351/// interrupt-safe event provider answers. **No ratchet.**
352pub fn get_event_int(event: i32) -> Option<SystemTime> {
353    let inner = GENERAL_TIME.lock().unwrap();
354    for p in &inner.event_providers {
355        if !p.interrupt_safe {
356            continue;
357        }
358        if let Some(t) = (p.get_event)(event) {
359            return Some(t);
360        }
361    }
362    None
363}
364
365/// Name of the highest-priority registered current-time provider.
366///
367/// C parity: `epicsGeneralTime.c` `generalTimeHighestCurrentName` —
368/// the provider at the front of the priority-ordered list. Returns
369/// `None` only when no provider is registered (the Rust port always
370/// has the built-in OS clock, so this is effectively always `Some`).
371pub fn highest_current_name() -> Option<String> {
372    GENERAL_TIME
373        .lock()
374        .unwrap()
375        .current_providers
376        .first()
377        .map(|p| p.name.clone())
378}
379
380/// Get the time for a specific event number.
381///
382/// - `event == 0`: delegates to [`get_current()`].
383/// - `event == -1`: "BestTime" — queries current providers with its own ratchet.
384/// - `event 1..=255`: per-slot ratcheted event time from event providers.
385/// - `event >= 256`: event time from event providers, no ratchet.
386pub fn get_event(event: i32) -> SystemTime {
387    if event == 0 {
388        return get_current();
389    }
390
391    let mut inner = GENERAL_TIME.lock().unwrap();
392
393    if event == -1 {
394        // BestTime: query current providers, apply separate ratchet.
395        for i in 0..inner.current_providers.len() {
396            if let Some(t) = (inner.current_providers[i].get_time)() {
397                let name = inner.current_providers[i].name.clone();
398                if t >= inner.last_best_time {
399                    inner.last_best_time = t;
400                    inner.last_event_name = Some(name);
401                    return t;
402                } else {
403                    ERROR_COUNTS.fetch_add(1, Ordering::Relaxed);
404                    inner.last_event_name = Some(name);
405                    return inner.last_best_time;
406                }
407            }
408        }
409        ERROR_COUNTS.fetch_add(1, Ordering::Relaxed);
410        return inner.last_best_time;
411    }
412
413    // Positive event: query event providers.
414    for i in 0..inner.event_providers.len() {
415        if let Some(t) = (inner.event_providers[i].get_event)(event) {
416            let name = inner.event_providers[i].name.clone();
417            inner.last_event_name = Some(name);
418
419            if (1..=255).contains(&event) {
420                let slot = event as usize;
421                if t >= inner.event_times[slot] {
422                    inner.event_times[slot] = t;
423                    return t;
424                } else {
425                    ERROR_COUNTS.fetch_add(1, Ordering::Relaxed);
426                    return inner.event_times[slot];
427                }
428            }
429            // event >= 256: no ratchet
430            return t;
431        }
432    }
433
434    // No event provider succeeded — fall back to get_current().
435    drop(inner);
436    get_current()
437}
438
439/// Install a last-resort event provider that returns `SystemTime::now()` for any event.
440pub fn install_last_resort_event_provider() {
441    register_event_provider("OS Clock", 999, |_| Some(SystemTime::now()));
442}
443
444/// Return the cumulative count of monotonic-enforcement errors.
445pub fn error_counts() -> u64 {
446    ERROR_COUNTS.load(Ordering::Relaxed)
447}
448
449/// Reset the error counter to zero.
450pub fn reset_error_counts() {
451    ERROR_COUNTS.store(0, Ordering::Relaxed);
452}
453
454/// Return the name of the provider that last supplied current time.
455pub fn current_provider_name() -> Option<String> {
456    GENERAL_TIME.lock().unwrap().last_current_name.clone()
457}
458
459/// Return the name of the provider that last supplied event time.
460pub fn event_provider_name() -> Option<String> {
461    GENERAL_TIME.lock().unwrap().last_event_name.clone()
462}
463
464/// Format a `SystemTime` the way C `generalTimeReport` does
465/// (`epicsTimeToStrftime` with `"%Y-%m-%d %H:%M:%S.%06f"`).
466///
467/// `epicsTimeToStrftime` converts via `epicsTime_localtime` -> `localtime_r`
468/// (epicsTime.cpp:202 -> :318, osdTime.cpp:82), i.e. LOCAL wall-clock, not
469/// UTC — so this uses `chrono::Local` to match the C report output.
470fn format_time_sample(t: SystemTime) -> String {
471    let dt: chrono::DateTime<chrono::Local> = t.into();
472    dt.format("%Y-%m-%d %H:%M:%S.%6f").to_string()
473}
474
475/// Generate a report of registered providers.
476///
477/// C parity: `epicsGeneralTime.c:530-618` `generalTimeReport`.
478/// - First line: `Backwards time errors prevented N times.` followed
479///   by a blank line.
480/// - `Current Time Providers:` / `Event Time Providers:` headers.
481/// - Each provider line is indented with 4 spaces and formatted
482///   `"name", priority = N`.
483/// - At `level > 0`, each *current* provider also prints its current
484///   time sample on the next line (tab-indented), or
485///   `Current Time not available`.
486/// - When a list is empty, prints a tab-indented
487///   `No Providers registered.` line.
488///
489/// `level`: 0 = brief, 1+ = detailed.
490pub fn report(level: i32) -> String {
491    let inner = GENERAL_TIME.lock().unwrap();
492    let mut out = String::new();
493
494    // C: printf("Backwards time errors prevented %u times.\n\n", ...)
495    out.push_str(&format!(
496        "Backwards time errors prevented {} times.\n\n",
497        error_counts()
498    ));
499
500    // Current Time Providers.
501    out.push_str("Current Time Providers:\n");
502    if inner.current_providers.is_empty() {
503        out.push_str("\tNo Providers registered.\n");
504    } else {
505        for p in &inner.current_providers {
506            out.push_str(&format!("    \"{}\", priority = {}\n", p.name, p.priority));
507            if level != 0 {
508                match (p.get_time)() {
509                    Some(t) => {
510                        out.push_str(&format!("\tCurrent Time is {}.\n", format_time_sample(t)))
511                    }
512                    None => out.push_str("\tCurrent Time not available\n"),
513                }
514            }
515        }
516        // C `puts(message)` appends one newline after the provider block.
517        out.push('\n');
518    }
519
520    // Event Time Providers.
521    out.push_str("Event Time Providers:\n");
522    if inner.event_providers.is_empty() {
523        out.push_str("\tNo Providers registered.\n");
524    } else {
525        for p in &inner.event_providers {
526            out.push_str(&format!("    \"{}\", priority = {}\n", p.name, p.priority));
527        }
528        out.push('\n');
529    }
530
531    out
532}
533
534/// Reset all state for test isolation. Only available in tests.
535#[cfg(test)]
536fn _reset_for_testing() {
537    let mut inner = GENERAL_TIME.lock().unwrap();
538    *inner = GeneralTimeInner::new();
539    ERROR_COUNTS.store(0, Ordering::Relaxed);
540}
541
542#[cfg(test)]
543mod tests {
544    use super::*;
545    use std::time::Duration;
546
547    /// Serialize all tests that touch the global GENERAL_TIME state.
548    static TEST_LOCK: Mutex<()> = Mutex::new(());
549
550    #[test]
551    fn os_clock_default_returns_reasonable_time() {
552        let _g = TEST_LOCK.lock().unwrap();
553        _reset_for_testing();
554        let t = get_current();
555        let secs = t.duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
556        // Should be after 2020-01-01
557        assert!(secs > 1_577_836_800, "time should be after 2020");
558    }
559
560    #[test]
561    fn custom_provider_overrides_os_clock() {
562        let _g = TEST_LOCK.lock().unwrap();
563        _reset_for_testing();
564        let fixed = SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_000_000);
565        register_current_provider("Test Clock", 10, move || Some(fixed));
566
567        let t = get_current();
568        assert_eq!(t, fixed);
569        assert_eq!(current_provider_name().as_deref(), Some("Test Clock"));
570    }
571
572    #[test]
573    fn provider_returning_none_falls_through() {
574        let _g = TEST_LOCK.lock().unwrap();
575        _reset_for_testing();
576        let fixed = SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_000_000);
577        // High-priority provider that always fails.
578        register_current_provider("Broken", 1, || None);
579        // Lower-priority provider that succeeds.
580        register_current_provider("Fallback", 50, move || Some(fixed));
581
582        let t = get_current();
583        assert_eq!(t, fixed);
584        assert_eq!(current_provider_name().as_deref(), Some("Fallback"));
585    }
586
587    #[test]
588    fn monotonic_enforcement() {
589        let _g = TEST_LOCK.lock().unwrap();
590        _reset_for_testing();
591        let t1 = SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_000_000);
592        let t2 = SystemTime::UNIX_EPOCH + Duration::from_secs(1_999_999_000); // backwards
593
594        let call = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
595        let call_c = call.clone();
596
597        register_current_provider("Stepper", 10, move || {
598            let n = call_c.fetch_add(1, Ordering::Relaxed);
599            match n {
600                0 => Some(t1),
601                _ => Some(t2),
602            }
603        });
604
605        reset_error_counts();
606        let first = get_current();
607        assert_eq!(first, t1);
608        assert_eq!(error_counts(), 0);
609
610        let second = get_current();
611        // Should return the last provided (t1), not t2.
612        assert_eq!(second, t1);
613        assert_eq!(error_counts(), 1);
614    }
615
616    #[test]
617    fn event_zero_delegates_to_get_current() {
618        let _g = TEST_LOCK.lock().unwrap();
619        _reset_for_testing();
620        let fixed = SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_000_000);
621        register_current_provider("Fixed", 10, move || Some(fixed));
622
623        let t = get_event(0);
624        assert_eq!(t, fixed);
625    }
626
627    #[test]
628    fn event_per_slot_ratcheting() {
629        let _g = TEST_LOCK.lock().unwrap();
630        _reset_for_testing();
631        let t1 = SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_000_000);
632        let t2 = SystemTime::UNIX_EPOCH + Duration::from_secs(1_999_999_000); // backwards
633        let t3 = SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_001_000); // forward
634
635        let call = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
636        let call_c = call.clone();
637
638        register_event_provider("EventSrc", 10, move |_ev| {
639            let n = call_c.fetch_add(1, Ordering::Relaxed);
640            match n {
641                0 => Some(t1),
642                1 => Some(t2),
643                _ => Some(t3),
644            }
645        });
646
647        reset_error_counts();
648        let first = get_event(42);
649        assert_eq!(first, t1);
650
651        let second = get_event(42);
652        // Ratcheted: returns t1, not t2
653        assert_eq!(second, t1);
654        assert_eq!(error_counts(), 1);
655
656        let third = get_event(42);
657        assert_eq!(third, t3);
658    }
659
660    #[test]
661    fn event_best_time_ratcheting() {
662        let _g = TEST_LOCK.lock().unwrap();
663        _reset_for_testing();
664        let t1 = SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_000_000);
665        let t2 = SystemTime::UNIX_EPOCH + Duration::from_secs(1_999_999_000);
666
667        let call = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
668        let call_c = call.clone();
669
670        register_current_provider("BestSrc", 10, move || {
671            let n = call_c.fetch_add(1, Ordering::Relaxed);
672            match n {
673                0 => Some(t1),
674                _ => Some(t2),
675            }
676        });
677
678        reset_error_counts();
679        let first = get_event(-1);
680        assert_eq!(first, t1);
681
682        let second = get_event(-1);
683        assert_eq!(second, t1); // ratcheted
684        assert_eq!(error_counts(), 1);
685    }
686
687    #[test]
688    fn error_counts_reset() {
689        let _g = TEST_LOCK.lock().unwrap();
690        _reset_for_testing();
691        let t_back = SystemTime::UNIX_EPOCH + Duration::from_secs(1);
692        register_current_provider("AlwaysBack", 10, move || Some(t_back));
693
694        // First call sets last_provided_time, second triggers backward detection
695        // Actually: UNIX_EPOCH ratchet means first call with t > EPOCH is fine,
696        // but we need the OS clock at priority 999 to not interfere.
697        // After _reset_for_testing, OS clock is present. AlwaysBack at priority 10
698        // wins. First call: t=1s > EPOCH → ok. Then the ratchet is at 1s.
699        // Need to trigger backward. Let's use get_event(-1) to get a fresh ratchet.
700
701        reset_error_counts();
702        assert_eq!(error_counts(), 0);
703
704        // Force an error via best-time ratchet.
705        let t_high = SystemTime::UNIX_EPOCH + Duration::from_secs(3_000_000_000);
706        {
707            let mut inner = GENERAL_TIME.lock().unwrap();
708            inner.last_best_time = t_high;
709        }
710        // Now any current provider returning < t_high on event -1 path will error.
711        let _ = get_event(-1);
712        assert!(error_counts() > 0);
713
714        reset_error_counts();
715        assert_eq!(error_counts(), 0);
716    }
717
718    /// Rust-only sync-hook extension: a registered sync hook fires
719    /// when `notify_clock_sync` is invoked. Multiple hooks fire in
720    /// registration order. Hooks live for process lifetime — the test
721    /// asserts via Arc<Mutex<Vec<...>>> capture rather than
722    /// de-registering. (No C-base counterpart; see
723    /// `register_clock_sync_hook`.)
724    #[test]
725    fn sync_hooks_fire_in_registration_order() {
726        use std::sync::{Arc, Mutex};
727        let captured: Arc<Mutex<Vec<(usize, SystemTime)>>> = Arc::new(Mutex::new(Vec::new()));
728
729        let cap1 = captured.clone();
730        register_clock_sync_hook(move |t| {
731            cap1.lock().unwrap().push((1, t));
732        });
733        let cap2 = captured.clone();
734        register_clock_sync_hook(move |t| {
735            cap2.lock().unwrap().push((2, t));
736        });
737
738        let synced = SystemTime::UNIX_EPOCH + Duration::from_secs(5_000_000_000);
739        notify_clock_sync(synced);
740
741        let log = captured.lock().unwrap();
742        // Other tests may have registered hooks too — filter to ours.
743        let ours: Vec<_> = log.iter().filter(|(id, _)| *id == 1 || *id == 2).collect();
744        assert!(
745            ours.len() >= 2,
746            "both hooks must have fired at least once: {ours:?}"
747        );
748        // Find the most recent pair-firing — registration order
749        // means the (1, _) entry must precede the (2, _) entry that
750        // share our exact `synced` value.
751        let last1_idx = ours
752            .iter()
753            .rposition(|(id, t)| *id == 1 && *t == synced)
754            .expect("hook 1 fired with our synced timestamp");
755        let last2_idx = ours
756            .iter()
757            .rposition(|(id, t)| *id == 2 && *t == synced)
758            .expect("hook 2 fired with our synced timestamp");
759        assert!(
760            last1_idx < last2_idx,
761            "hook 1 must fire before hook 2 (registration order)"
762        );
763    }
764
765    /// M1 C-parity: while only the built-in OS clock is registered,
766    /// `get_current` bypasses the monotonic ratchet — a backward
767    /// wall-clock step is returned verbatim and counts no error.
768    #[test]
769    fn os_clock_only_bypasses_ratchet() {
770        let _g = TEST_LOCK.lock().unwrap();
771        _reset_for_testing();
772
773        // Replace the built-in OS clock with a controllable stepping
774        // clock that is still flagged as the OS default, so the
775        // `use_osd_get_current` short-circuit stays active.
776        let t_high = SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_000_000);
777        let t_low = SystemTime::UNIX_EPOCH + Duration::from_secs(1_999_999_000);
778        let call = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
779        let call_c = call.clone();
780        {
781            let mut inner = GENERAL_TIME.lock().unwrap();
782            inner.current_providers.clear();
783            inner.current_providers.push(CurrentTimeProvider {
784                name: "OS Clock".to_string(),
785                priority: 999,
786                get_time: Box::new(move || {
787                    let n = call_c.fetch_add(1, Ordering::Relaxed);
788                    Some(if n == 0 { t_high } else { t_low })
789                }),
790                interrupt_safe: true,
791                is_os_default: true,
792            });
793            inner.use_osd_get_current = true;
794        }
795
796        reset_error_counts();
797        let first = get_current();
798        assert_eq!(first, t_high);
799        // Backward step is returned verbatim — NO ratchet, NO error.
800        let second = get_current();
801        assert_eq!(
802            second, t_low,
803            "OS-clock-only path must follow a backward step (C useOsdGetCurrent)"
804        );
805        assert_eq!(
806            error_counts(),
807            0,
808            "OS-clock-only backward step must not count an error"
809        );
810    }
811
812    /// M1 C-parity: registering a non-default provider clears the
813    /// `use_osd_get_current` flag, so the ratchet is back in force.
814    #[test]
815    fn registering_provider_enables_ratchet() {
816        let _g = TEST_LOCK.lock().unwrap();
817        _reset_for_testing();
818        let t_high = SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_000_000);
819        let t_low = SystemTime::UNIX_EPOCH + Duration::from_secs(1_999_999_000);
820        let call = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
821        let call_c = call.clone();
822        register_current_provider("Stepper", 10, move || {
823            let n = call_c.fetch_add(1, Ordering::Relaxed);
824            Some(if n == 0 { t_high } else { t_low })
825        });
826
827        reset_error_counts();
828        assert_eq!(get_current(), t_high);
829        // With a non-default provider present, the ratchet clamps the
830        // backward step and counts an error.
831        assert_eq!(get_current(), t_high);
832        assert_eq!(error_counts(), 1);
833    }
834
835    /// M2 C-parity: `get_current_except_priority` skips the named
836    /// provider and applies no ratchet.
837    #[test]
838    fn except_priority_skips_named_provider() {
839        let _g = TEST_LOCK.lock().unwrap();
840        _reset_for_testing();
841        let t10 = SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_000_000);
842        let t20 = SystemTime::UNIX_EPOCH + Duration::from_secs(1_900_000_000);
843        register_current_provider("P10", 10, move || Some(t10));
844        register_current_provider("P20", 20, move || Some(t20));
845
846        // Ignoring priority 10 -> answered by P20 (priority 20).
847        let (t, prio) = get_current_except_priority(10).expect("P20 answers");
848        assert_eq!(t, t20);
849        assert_eq!(prio, 20);
850
851        // Negative ignore: keep only priority 10.
852        let (t, prio) = get_current_except_priority(-10).expect("P10 answers");
853        assert_eq!(t, t10);
854        assert_eq!(prio, 10);
855    }
856
857    /// M2 C-parity: interrupt-callable providers are consulted only by
858    /// the `*_int` query paths.
859    #[test]
860    fn int_providers_only_seen_by_int_queries() {
861        let _g = TEST_LOCK.lock().unwrap();
862        _reset_for_testing();
863        let t_int = SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_000_000);
864        register_int_current_provider("IntClock", 5, move || Some(t_int));
865        assert_eq!(get_current_int(), Some(t_int));
866
867        let t_evt = SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_000_500);
868        register_int_event_provider("IntEvent", 5, move |_| Some(t_evt));
869        assert_eq!(get_event_int(7), Some(t_evt));
870    }
871
872    /// M2 C-parity: `get_event_int` returns `None` when no
873    /// interrupt-safe event provider is registered.
874    #[test]
875    fn int_event_query_none_without_int_provider() {
876        let _g = TEST_LOCK.lock().unwrap();
877        _reset_for_testing();
878        // A non-int event provider must not satisfy the int query.
879        register_event_provider("Plain", 10, |_| {
880            Some(SystemTime::UNIX_EPOCH + Duration::from_secs(1))
881        });
882        assert_eq!(get_event_int(3), None);
883    }
884
885    /// M2 C-parity: `highest_current_name` returns the front-of-list
886    /// (highest-priority) current provider.
887    #[test]
888    fn highest_current_name_is_top_priority() {
889        let _g = TEST_LOCK.lock().unwrap();
890        _reset_for_testing();
891        // Only the OS clock (priority 999) -> it is the highest.
892        assert_eq!(highest_current_name().as_deref(), Some("OS Clock"));
893        register_current_provider("Primary", 1, || None);
894        assert_eq!(highest_current_name().as_deref(), Some("Primary"));
895    }
896
897    /// M3 C-parity: `report` output mirrors `generalTimeReport`.
898    #[test]
899    fn report_format_matches_general_time_report() {
900        let _g = TEST_LOCK.lock().unwrap();
901        _reset_for_testing();
902        let r = report(0);
903        // First line is the backwards-error count.
904        assert!(
905            r.starts_with("Backwards time errors prevented 0 times.\n\n"),
906            "report must lead with the backwards-error line: {r:?}"
907        );
908        assert!(r.contains("Current Time Providers:\n"));
909        // 4-space indent, `, priority = N` form.
910        assert!(
911            r.contains("    \"OS Clock\", priority = 999\n"),
912            "provider line must use C `\"name\", priority = N` form: {r:?}"
913        );
914        // No event providers -> tab-indented "No Providers registered."
915        assert!(
916            r.contains("Event Time Providers:\n\tNo Providers registered.\n"),
917            "empty event list must print the C placeholder: {r:?}"
918        );
919        // The old format strings must be gone.
920        assert!(!r.contains("\" priority "));
921        assert!(!r.contains("(none)"));
922    }
923
924    /// M3 C-parity: at `level > 0`, each current provider prints its
925    /// time sample on a tab-indented line.
926    #[test]
927    fn report_level_one_prints_time_sample() {
928        let _g = TEST_LOCK.lock().unwrap();
929        _reset_for_testing();
930        let r = report(1);
931        assert!(
932            r.contains("\tCurrent Time is "),
933            "level>0 report must print a per-provider time sample: {r:?}"
934        );
935    }
936
937    /// L2 C-parity: the ratchet seeds at the EPICS epoch (1990-01-01),
938    /// not the Unix epoch (1970-01-01) — `epicsTimeStamp {0,0}`.
939    #[test]
940    fn ratchet_seeds_at_epics_epoch() {
941        // 631_152_000 s past the Unix epoch == 1990-01-01 00:00:00 UTC.
942        let secs = epics_epoch()
943            .duration_since(SystemTime::UNIX_EPOCH)
944            .unwrap()
945            .as_secs();
946        assert_eq!(secs, EPICS_EPOCH_UNIX_SECS);
947        assert_eq!(secs, 631_152_000);
948
949        let _g = TEST_LOCK.lock().unwrap();
950        _reset_for_testing();
951        let inner = GENERAL_TIME.lock().unwrap();
952        assert_eq!(inner.last_provided_time, epics_epoch());
953        assert_eq!(inner.last_best_time, epics_epoch());
954        assert!(inner.event_times.iter().all(|t| *t == epics_epoch()));
955    }
956}