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