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/// C parity: `epicsGeneralTime.c:342-349` `epicsTimeGetEvent` plus
383/// `generalTimeGetEventPriority` (`:241-338`). `None` is C's non-zero
384/// status — the caller must treat it as C treats it, leaving the
385/// destination stamp untouched:
386///
387/// - `event < -1`: `S_time_badEvent` (`:254-255`, and
388///   `epicsTimeEventBestTime == -1` in `epicsTime.h:103`).
389/// - `event == 0`: delegates to [`get_current()`].
390/// - `event == -1`: "BestTime" — queries current providers with its own ratchet.
391/// - `event 1..=255`: per-slot ratcheted event time from event providers.
392/// - `event >= 256`: event time from event providers, no ratchet.
393/// - no event provider answers: `S_time_noProvider` (`:243`, the initial
394///   `status`). C installs no event provider by default —
395///   `installLastResortEventProvider` (`:521-525`) is an iocsh command —
396///   so an IOC with none holds the old stamp instead of stamping now.
397pub fn get_event(event: i32) -> Option<SystemTime> {
398    if event < -1 {
399        return None;
400    }
401    if event == 0 {
402        return Some(get_current());
403    }
404
405    let mut inner = GENERAL_TIME.lock().unwrap();
406
407    if event == -1 {
408        // BestTime: query current providers, apply separate ratchet.
409        for i in 0..inner.current_providers.len() {
410            if let Some(t) = (inner.current_providers[i].get_time)() {
411                let name = inner.current_providers[i].name.clone();
412                if t >= inner.last_best_time {
413                    inner.last_best_time = t;
414                    inner.last_event_name = Some(name);
415                    return Some(t);
416                } else {
417                    ERROR_COUNTS.fetch_add(1, Ordering::Relaxed);
418                    inner.last_event_name = Some(name);
419                    return Some(inner.last_best_time);
420                }
421            }
422        }
423        ERROR_COUNTS.fetch_add(1, Ordering::Relaxed);
424        return Some(inner.last_best_time);
425    }
426
427    // Positive event: query event providers.
428    for i in 0..inner.event_providers.len() {
429        if let Some(t) = (inner.event_providers[i].get_event)(event) {
430            let name = inner.event_providers[i].name.clone();
431            inner.last_event_name = Some(name);
432
433            if (1..=255).contains(&event) {
434                let slot = event as usize;
435                if t >= inner.event_times[slot] {
436                    inner.event_times[slot] = t;
437                    return Some(t);
438                } else {
439                    ERROR_COUNTS.fetch_add(1, Ordering::Relaxed);
440                    return Some(inner.event_times[slot]);
441                }
442            }
443            // event >= 256: no ratchet
444            return Some(t);
445        }
446    }
447
448    // No event provider answered: C's `status` is still `S_time_noProvider`.
449    None
450}
451
452/// C `installLastResortEventProvider` (`epicsGeneralTime.c:521-525`) —
453/// register `lastResortGetEvent`, which answers every event number with
454/// the current time, at `LAST_RESORT_PRIORITY` (999).
455///
456/// This is the ONLY thing that makes an event stamp fall back to the wall
457/// clock. C never calls it during init: `libComRegister.c` registers it as
458/// the `installLastResortEventProvider` iocsh command, so it is an
459/// operator opt-in and an IOC without it leaves `prec->time` alone for a
460/// `TSE` no provider serves.
461///
462/// The provider's name is C's `"Last Resort Event"`, which is what
463/// `generalTimeReport` prints; `"OS Clock"` is the name of the *current
464/// time* provider (`osiClockTime.c:116`), a different table.
465pub fn install_last_resort_event_provider() {
466    register_event_provider("Last Resort Event", 999, |_| Some(SystemTime::now()));
467}
468
469/// Return the cumulative count of monotonic-enforcement errors.
470pub fn error_counts() -> u64 {
471    ERROR_COUNTS.load(Ordering::Relaxed)
472}
473
474/// Reset the error counter to zero.
475pub fn reset_error_counts() {
476    ERROR_COUNTS.store(0, Ordering::Relaxed);
477}
478
479/// Return the name of the provider that last supplied current time.
480pub fn current_provider_name() -> Option<String> {
481    GENERAL_TIME.lock().unwrap().last_current_name.clone()
482}
483
484/// Return the name of the provider that last supplied event time.
485pub fn event_provider_name() -> Option<String> {
486    GENERAL_TIME.lock().unwrap().last_event_name.clone()
487}
488
489/// Format a `SystemTime` the way C `generalTimeReport` does
490/// (`epicsTimeToStrftime` with `"%Y-%m-%d %H:%M:%S.%06f"`).
491///
492/// `epicsTimeToStrftime` converts via `epicsTime_localtime` -> `localtime_r`
493/// (epicsTime.cpp:202 -> :318, osdTime.cpp:82), i.e. LOCAL wall-clock, not
494/// UTC — so this uses `chrono::Local` to match the C report output.
495fn format_time_sample(t: SystemTime) -> String {
496    let dt: chrono::DateTime<chrono::Local> = t.into();
497    dt.format("%Y-%m-%d %H:%M:%S.%6f").to_string()
498}
499
500/// Generate a report of registered providers.
501///
502/// C parity: `epicsGeneralTime.c:530-618` `generalTimeReport`.
503/// - First line: `Backwards time errors prevented N times.` followed
504///   by a blank line.
505/// - `Current Time Providers:` / `Event Time Providers:` headers.
506/// - Each provider line is indented with 4 spaces and formatted
507///   `"name", priority = N`.
508/// - At `level > 0`, each *current* provider also prints its current
509///   time sample on the next line (tab-indented), or
510///   `Current Time not available`.
511/// - When a list is empty, prints a tab-indented
512///   `No Providers registered.` line.
513///
514/// `level`: 0 = brief, 1+ = detailed.
515pub fn report(level: i32) -> String {
516    let inner = GENERAL_TIME.lock().unwrap();
517    let mut out = String::new();
518
519    // C: printf("Backwards time errors prevented %u times.\n\n", ...)
520    out.push_str(&format!(
521        "Backwards time errors prevented {} times.\n\n",
522        error_counts()
523    ));
524
525    // Current Time Providers.
526    out.push_str("Current Time Providers:\n");
527    if inner.current_providers.is_empty() {
528        out.push_str("\tNo Providers registered.\n");
529    } else {
530        for p in &inner.current_providers {
531            out.push_str(&format!("    \"{}\", priority = {}\n", p.name, p.priority));
532            if level != 0 {
533                match (p.get_time)() {
534                    Some(t) => {
535                        out.push_str(&format!("\tCurrent Time is {}.\n", format_time_sample(t)))
536                    }
537                    None => out.push_str("\tCurrent Time not available\n"),
538                }
539            }
540        }
541        // C `puts(message)` appends one newline after the provider block.
542        out.push('\n');
543    }
544
545    // Event Time Providers.
546    out.push_str("Event Time Providers:\n");
547    if inner.event_providers.is_empty() {
548        out.push_str("\tNo Providers registered.\n");
549    } else {
550        for p in &inner.event_providers {
551            out.push_str(&format!("    \"{}\", priority = {}\n", p.name, p.priority));
552        }
553        out.push('\n');
554    }
555
556    out
557}
558
559/// Reset all state for test isolation. Only available in tests.
560#[cfg(test)]
561fn _reset_for_testing() {
562    let mut inner = GENERAL_TIME.lock().unwrap();
563    *inner = GeneralTimeInner::new();
564    ERROR_COUNTS.store(0, Ordering::Relaxed);
565}
566
567#[cfg(test)]
568mod tests {
569    use super::*;
570    use std::time::Duration;
571
572    /// Serialize all tests that touch the global GENERAL_TIME state.
573    static TEST_LOCK: Mutex<()> = Mutex::new(());
574
575    #[test]
576    fn os_clock_default_returns_reasonable_time() {
577        let _g = TEST_LOCK.lock().unwrap();
578        _reset_for_testing();
579        let t = get_current();
580        let secs = t.duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
581        // Should be after 2020-01-01
582        assert!(secs > 1_577_836_800, "time should be after 2020");
583    }
584
585    #[test]
586    fn custom_provider_overrides_os_clock() {
587        let _g = TEST_LOCK.lock().unwrap();
588        _reset_for_testing();
589        let fixed = SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_000_000);
590        register_current_provider("Test Clock", 10, move || Some(fixed));
591
592        let t = get_current();
593        assert_eq!(t, fixed);
594        assert_eq!(current_provider_name().as_deref(), Some("Test Clock"));
595    }
596
597    #[test]
598    fn provider_returning_none_falls_through() {
599        let _g = TEST_LOCK.lock().unwrap();
600        _reset_for_testing();
601        let fixed = SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_000_000);
602        // High-priority provider that always fails.
603        register_current_provider("Broken", 1, || None);
604        // Lower-priority provider that succeeds.
605        register_current_provider("Fallback", 50, move || Some(fixed));
606
607        let t = get_current();
608        assert_eq!(t, fixed);
609        assert_eq!(current_provider_name().as_deref(), Some("Fallback"));
610    }
611
612    #[test]
613    fn monotonic_enforcement() {
614        let _g = TEST_LOCK.lock().unwrap();
615        _reset_for_testing();
616        let t1 = SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_000_000);
617        let t2 = SystemTime::UNIX_EPOCH + Duration::from_secs(1_999_999_000); // backwards
618
619        let call = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
620        let call_c = call.clone();
621
622        register_current_provider("Stepper", 10, move || {
623            let n = call_c.fetch_add(1, Ordering::Relaxed);
624            match n {
625                0 => Some(t1),
626                _ => Some(t2),
627            }
628        });
629
630        reset_error_counts();
631        let first = get_current();
632        assert_eq!(first, t1);
633        assert_eq!(error_counts(), 0);
634
635        let second = get_current();
636        // Should return the last provided (t1), not t2.
637        assert_eq!(second, t1);
638        assert_eq!(error_counts(), 1);
639    }
640
641    #[test]
642    fn event_zero_delegates_to_get_current() {
643        let _g = TEST_LOCK.lock().unwrap();
644        _reset_for_testing();
645        let fixed = SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_000_000);
646        register_current_provider("Fixed", 10, move || Some(fixed));
647
648        let t = get_event(0);
649        assert_eq!(t, Some(fixed));
650    }
651
652    /// C `generalTimeGetEventPriority` (`epicsGeneralTime.c:254-255`):
653    /// `if (eventNumber < epicsTimeEventBestTime) return S_time_badEvent;`
654    /// `epicsTimeEventBestTime` is -1 (`epicsTime.h:103`), so every TSE a
655    /// record can hold below it — `TSE` is `epicsInt16`, hence -32768..-3 —
656    /// is a bad event, not a request for the wall clock.
657    #[test]
658    fn an_event_below_best_time_is_a_bad_event() {
659        let _g = TEST_LOCK.lock().unwrap();
660        _reset_for_testing();
661        let fixed = SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_000_000);
662        register_current_provider("Fixed", 10, move || Some(fixed));
663        register_event_provider("EventSrc", 10, move |_ev| Some(fixed));
664
665        assert_eq!(get_event(-3), None);
666        assert_eq!(get_event(i16::MIN as i32), None);
667        // -1 and 0 stay answerable: they are C's BestTime and CurrentTime.
668        assert_eq!(get_event(-1), Some(fixed));
669        assert_eq!(get_event(0), Some(fixed));
670    }
671
672    /// C `generalTimeGetEventPriority` enters with
673    /// `status = S_time_noProvider` (`epicsGeneralTime.c:243`) and only an
674    /// answering provider clears it. Nothing registers an event provider by
675    /// default — `installLastResortEventProvider` (`:521-525`) is an iocsh
676    /// command — so an event number no provider serves is an error, not the
677    /// current time.
678    #[test]
679    fn an_event_no_provider_serves_is_not_the_current_time() {
680        let _g = TEST_LOCK.lock().unwrap();
681        _reset_for_testing();
682        let fixed = SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_000_000);
683        register_current_provider("Fixed", 10, move || Some(fixed));
684
685        assert_eq!(get_event(42), None);
686        assert_eq!(get_event(300), None);
687    }
688
689    #[test]
690    fn event_per_slot_ratcheting() {
691        let _g = TEST_LOCK.lock().unwrap();
692        _reset_for_testing();
693        let t1 = SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_000_000);
694        let t2 = SystemTime::UNIX_EPOCH + Duration::from_secs(1_999_999_000); // backwards
695        let t3 = SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_001_000); // forward
696
697        let call = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
698        let call_c = call.clone();
699
700        register_event_provider("EventSrc", 10, move |_ev| {
701            let n = call_c.fetch_add(1, Ordering::Relaxed);
702            match n {
703                0 => Some(t1),
704                1 => Some(t2),
705                _ => Some(t3),
706            }
707        });
708
709        reset_error_counts();
710        let first = get_event(42);
711        assert_eq!(first, Some(t1));
712
713        let second = get_event(42);
714        // Ratcheted: returns t1, not t2
715        assert_eq!(second, Some(t1));
716        assert_eq!(error_counts(), 1);
717
718        let third = get_event(42);
719        assert_eq!(third, Some(t3));
720    }
721
722    #[test]
723    fn event_best_time_ratcheting() {
724        let _g = TEST_LOCK.lock().unwrap();
725        _reset_for_testing();
726        let t1 = SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_000_000);
727        let t2 = SystemTime::UNIX_EPOCH + Duration::from_secs(1_999_999_000);
728
729        let call = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
730        let call_c = call.clone();
731
732        register_current_provider("BestSrc", 10, move || {
733            let n = call_c.fetch_add(1, Ordering::Relaxed);
734            match n {
735                0 => Some(t1),
736                _ => Some(t2),
737            }
738        });
739
740        reset_error_counts();
741        let first = get_event(-1);
742        assert_eq!(first, Some(t1));
743
744        let second = get_event(-1);
745        assert_eq!(second, Some(t1)); // ratcheted
746        assert_eq!(error_counts(), 1);
747    }
748
749    #[test]
750    fn error_counts_reset() {
751        let _g = TEST_LOCK.lock().unwrap();
752        _reset_for_testing();
753        let t_back = SystemTime::UNIX_EPOCH + Duration::from_secs(1);
754        register_current_provider("AlwaysBack", 10, move || Some(t_back));
755
756        // First call sets last_provided_time, second triggers backward detection
757        // Actually: UNIX_EPOCH ratchet means first call with t > EPOCH is fine,
758        // but we need the OS clock at priority 999 to not interfere.
759        // After _reset_for_testing, OS clock is present. AlwaysBack at priority 10
760        // wins. First call: t=1s > EPOCH → ok. Then the ratchet is at 1s.
761        // Need to trigger backward. Let's use get_event(-1) to get a fresh ratchet.
762
763        reset_error_counts();
764        assert_eq!(error_counts(), 0);
765
766        // Force an error via best-time ratchet.
767        let t_high = SystemTime::UNIX_EPOCH + Duration::from_secs(3_000_000_000);
768        {
769            let mut inner = GENERAL_TIME.lock().unwrap();
770            inner.last_best_time = t_high;
771        }
772        // Now any current provider returning < t_high on event -1 path will error.
773        let _ = get_event(-1);
774        assert!(error_counts() > 0);
775
776        reset_error_counts();
777        assert_eq!(error_counts(), 0);
778    }
779
780    /// Rust-only sync-hook extension: a registered sync hook fires
781    /// when `notify_clock_sync` is invoked. Multiple hooks fire in
782    /// registration order. Hooks live for process lifetime — the test
783    /// asserts via Arc<Mutex<Vec<...>>> capture rather than
784    /// de-registering. (No C-base counterpart; see
785    /// `register_clock_sync_hook`.)
786    #[test]
787    fn sync_hooks_fire_in_registration_order() {
788        use std::sync::{Arc, Mutex};
789        let captured: Arc<Mutex<Vec<(usize, SystemTime)>>> = Arc::new(Mutex::new(Vec::new()));
790
791        let cap1 = captured.clone();
792        register_clock_sync_hook(move |t| {
793            cap1.lock().unwrap().push((1, t));
794        });
795        let cap2 = captured.clone();
796        register_clock_sync_hook(move |t| {
797            cap2.lock().unwrap().push((2, t));
798        });
799
800        let synced = SystemTime::UNIX_EPOCH + Duration::from_secs(5_000_000_000);
801        notify_clock_sync(synced);
802
803        let log = captured.lock().unwrap();
804        // Other tests may have registered hooks too — filter to ours.
805        let ours: Vec<_> = log.iter().filter(|(id, _)| *id == 1 || *id == 2).collect();
806        assert!(
807            ours.len() >= 2,
808            "both hooks must have fired at least once: {ours:?}"
809        );
810        // Find the most recent pair-firing — registration order
811        // means the (1, _) entry must precede the (2, _) entry that
812        // share our exact `synced` value.
813        let last1_idx = ours
814            .iter()
815            .rposition(|(id, t)| *id == 1 && *t == synced)
816            .expect("hook 1 fired with our synced timestamp");
817        let last2_idx = ours
818            .iter()
819            .rposition(|(id, t)| *id == 2 && *t == synced)
820            .expect("hook 2 fired with our synced timestamp");
821        assert!(
822            last1_idx < last2_idx,
823            "hook 1 must fire before hook 2 (registration order)"
824        );
825    }
826
827    /// M1 C-parity: while only the built-in OS clock is registered,
828    /// `get_current` bypasses the monotonic ratchet — a backward
829    /// wall-clock step is returned verbatim and counts no error.
830    #[test]
831    fn os_clock_only_bypasses_ratchet() {
832        let _g = TEST_LOCK.lock().unwrap();
833        _reset_for_testing();
834
835        // Replace the built-in OS clock with a controllable stepping
836        // clock that is still flagged as the OS default, so the
837        // `use_osd_get_current` short-circuit stays active.
838        let t_high = SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_000_000);
839        let t_low = SystemTime::UNIX_EPOCH + Duration::from_secs(1_999_999_000);
840        let call = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
841        let call_c = call.clone();
842        {
843            let mut inner = GENERAL_TIME.lock().unwrap();
844            inner.current_providers.clear();
845            inner.current_providers.push(CurrentTimeProvider {
846                name: "OS Clock".to_string(),
847                priority: 999,
848                get_time: Box::new(move || {
849                    let n = call_c.fetch_add(1, Ordering::Relaxed);
850                    Some(if n == 0 { t_high } else { t_low })
851                }),
852                interrupt_safe: true,
853                is_os_default: true,
854            });
855            inner.use_osd_get_current = true;
856        }
857
858        reset_error_counts();
859        let first = get_current();
860        assert_eq!(first, t_high);
861        // Backward step is returned verbatim — NO ratchet, NO error.
862        let second = get_current();
863        assert_eq!(
864            second, t_low,
865            "OS-clock-only path must follow a backward step (C useOsdGetCurrent)"
866        );
867        assert_eq!(
868            error_counts(),
869            0,
870            "OS-clock-only backward step must not count an error"
871        );
872    }
873
874    /// M1 C-parity: registering a non-default provider clears the
875    /// `use_osd_get_current` flag, so the ratchet is back in force.
876    #[test]
877    fn registering_provider_enables_ratchet() {
878        let _g = TEST_LOCK.lock().unwrap();
879        _reset_for_testing();
880        let t_high = SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_000_000);
881        let t_low = SystemTime::UNIX_EPOCH + Duration::from_secs(1_999_999_000);
882        let call = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
883        let call_c = call.clone();
884        register_current_provider("Stepper", 10, move || {
885            let n = call_c.fetch_add(1, Ordering::Relaxed);
886            Some(if n == 0 { t_high } else { t_low })
887        });
888
889        reset_error_counts();
890        assert_eq!(get_current(), t_high);
891        // With a non-default provider present, the ratchet clamps the
892        // backward step and counts an error.
893        assert_eq!(get_current(), t_high);
894        assert_eq!(error_counts(), 1);
895    }
896
897    /// M2 C-parity: `get_current_except_priority` skips the named
898    /// provider and applies no ratchet.
899    #[test]
900    fn except_priority_skips_named_provider() {
901        let _g = TEST_LOCK.lock().unwrap();
902        _reset_for_testing();
903        let t10 = SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_000_000);
904        let t20 = SystemTime::UNIX_EPOCH + Duration::from_secs(1_900_000_000);
905        register_current_provider("P10", 10, move || Some(t10));
906        register_current_provider("P20", 20, move || Some(t20));
907
908        // Ignoring priority 10 -> answered by P20 (priority 20).
909        let (t, prio) = get_current_except_priority(10).expect("P20 answers");
910        assert_eq!(t, t20);
911        assert_eq!(prio, 20);
912
913        // Negative ignore: keep only priority 10.
914        let (t, prio) = get_current_except_priority(-10).expect("P10 answers");
915        assert_eq!(t, t10);
916        assert_eq!(prio, 10);
917    }
918
919    /// M2 C-parity: interrupt-callable providers are consulted only by
920    /// the `*_int` query paths.
921    #[test]
922    fn int_providers_only_seen_by_int_queries() {
923        let _g = TEST_LOCK.lock().unwrap();
924        _reset_for_testing();
925        let t_int = SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_000_000);
926        register_int_current_provider("IntClock", 5, move || Some(t_int));
927        assert_eq!(get_current_int(), Some(t_int));
928
929        let t_evt = SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_000_500);
930        register_int_event_provider("IntEvent", 5, move |_| Some(t_evt));
931        assert_eq!(get_event_int(7), Some(t_evt));
932    }
933
934    /// M2 C-parity: `get_event_int` returns `None` when no
935    /// interrupt-safe event provider is registered.
936    #[test]
937    fn int_event_query_none_without_int_provider() {
938        let _g = TEST_LOCK.lock().unwrap();
939        _reset_for_testing();
940        // A non-int event provider must not satisfy the int query.
941        register_event_provider("Plain", 10, |_| {
942            Some(SystemTime::UNIX_EPOCH + Duration::from_secs(1))
943        });
944        assert_eq!(get_event_int(3), None);
945    }
946
947    /// M2 C-parity: `highest_current_name` returns the front-of-list
948    /// (highest-priority) current provider.
949    #[test]
950    fn highest_current_name_is_top_priority() {
951        let _g = TEST_LOCK.lock().unwrap();
952        _reset_for_testing();
953        // Only the OS clock (priority 999) -> it is the highest.
954        assert_eq!(highest_current_name().as_deref(), Some("OS Clock"));
955        register_current_provider("Primary", 1, || None);
956        assert_eq!(highest_current_name().as_deref(), Some("Primary"));
957    }
958
959    /// M3 C-parity: `report` output mirrors `generalTimeReport`.
960    #[test]
961    fn report_format_matches_general_time_report() {
962        let _g = TEST_LOCK.lock().unwrap();
963        _reset_for_testing();
964        let r = report(0);
965        // First line is the backwards-error count.
966        assert!(
967            r.starts_with("Backwards time errors prevented 0 times.\n\n"),
968            "report must lead with the backwards-error line: {r:?}"
969        );
970        assert!(r.contains("Current Time Providers:\n"));
971        // 4-space indent, `, priority = N` form.
972        assert!(
973            r.contains("    \"OS Clock\", priority = 999\n"),
974            "provider line must use C `\"name\", priority = N` form: {r:?}"
975        );
976        // No event providers -> tab-indented "No Providers registered."
977        assert!(
978            r.contains("Event Time Providers:\n\tNo Providers registered.\n"),
979            "empty event list must print the C placeholder: {r:?}"
980        );
981        // The old format strings must be gone.
982        assert!(!r.contains("\" priority "));
983        assert!(!r.contains("(none)"));
984    }
985
986    /// M3 C-parity: at `level > 0`, each current provider prints its
987    /// time sample on a tab-indented line.
988    #[test]
989    fn report_level_one_prints_time_sample() {
990        let _g = TEST_LOCK.lock().unwrap();
991        _reset_for_testing();
992        let r = report(1);
993        assert!(
994            r.contains("\tCurrent Time is "),
995            "level>0 report must print a per-provider time sample: {r:?}"
996        );
997    }
998
999    /// L2 C-parity: the ratchet seeds at the EPICS epoch (1990-01-01),
1000    /// not the Unix epoch (1970-01-01) — `epicsTimeStamp {0,0}`.
1001    #[test]
1002    fn ratchet_seeds_at_epics_epoch() {
1003        // 631_152_000 s past the Unix epoch == 1990-01-01 00:00:00 UTC.
1004        let secs = epics_epoch()
1005            .duration_since(SystemTime::UNIX_EPOCH)
1006            .unwrap()
1007            .as_secs();
1008        assert_eq!(secs, EPICS_EPOCH_UNIX_SECS);
1009        assert_eq!(secs, 631_152_000);
1010
1011        let _g = TEST_LOCK.lock().unwrap();
1012        _reset_for_testing();
1013        let inner = GENERAL_TIME.lock().unwrap();
1014        assert_eq!(inner.last_provided_time, epics_epoch());
1015        assert_eq!(inner.last_best_time, epics_epoch());
1016        assert!(inner.event_times.iter().all(|t| *t == epics_epoch()));
1017    }
1018}