From de3286ed3a5f1577052a28829a06a64920e99386 Mon Sep 17 00:00:00 2001 From: Yuki Kishimoto Date: Tue, 24 Feb 2026 09:58:28 +0100 Subject: [PATCH] Refactor to use compile-time time provider verification Changed the library to fail at link-time instead of runtime when no time provider is available on no_std platforms. This ensures you can't accidentally deploy code that panics when calling time functions. Key changes: - Use std::time automatically on supported platforms (zero overhead) - Require define_time_provider! macro on no_std/WASM-unknown - Link errors (not runtime panics) if provider missing - Disabled global module on std platforms - Moved tests from tests/ directory into inline modules Signed-off-by: Yuki Kishimoto --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 8 ++ Cargo.toml | 6 +- README.md | 91 +++++++++++++---- examples/custom.rs | 49 ---------- examples/standard.rs | 11 --- src/global.rs | 205 ++++++++++----------------------------- src/instant.rs | 129 ++++++++++++++++++++++-- src/lib.rs | 29 +++++- src/system.rs | 58 +++++++++-- tests/time_api.rs | 195 ------------------------------------- 11 files changed, 332 insertions(+), 451 deletions(-) delete mode 100644 examples/custom.rs delete mode 100644 examples/standard.rs delete mode 100644 tests/time_api.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52d39ae..2d90a71 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: strategy: matrix: args: - - --default-features + - --features std - --no-default-features - --target wasm32-unknown-unknown steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 1193c25..7c219df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,14 @@ --> +## Unreleased + +### Breaking changes + +- Remove `set_global_time_context` function (https://github.com/shadowylab/universal-time/pull/1) +- Link errors if the time provider is missing (https://github.com/shadowylab/universal-time/pull/1) +- Require `define_time_provider!` macro on `no_std`/`WASM-unknown` (https://github.com/shadowylab/universal-time/pull/1) + ## v0.1.0 - 2026/02/23 First release. diff --git a/Cargo.toml b/Cargo.toml index c20492f..2f13090 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,13 +2,17 @@ name = "universal-time" version = "0.1.0" edition = "2021" -description = "Cross-platform time primitives" +description = "Cross-platform time primitives with compile-time guarantees — no runtime panics!" authors = ["Yuki Kishimoto "] homepage = "https://github.com/shadowylab/universal-time" repository = "https://github.com/shadowylab/universal-time.git" license = "MIT" rust-version = "1.70.0" +[package.metadata.docs.rs] +no-default-features = true +rustdoc-args = ["--cfg", "docsrs"] + [features] default = ["std"] std = [] diff --git a/README.md b/README.md index 162891c..16d58e9 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,94 @@ # universal-time -Cross-platform time primitives for Rust that can run in any envuironment. +Cross-platform time primitives with **compile-time guarantees** — no runtime panics! -## Why +This library provides `Instant` (monotonic time) and `SystemTime` (wall-clock time) that work consistently across all platforms with zero runtime overhead. -`universal-time` gives you a single API for: +## Why? -- `Instant` for monotonic elapsed-time measurements -- `SystemTime` for wall-clock timestamps -- Trait-based clock injection for platforms without built-in time access +This library **fails at link time** if you try to build without a time provider on platforms that need one. This means: + +- ✅ **Zero runtime panics** from missing time sources +- ✅ **Compile-time verification** that time is available +- ✅ **Single consistent API** across all platforms +- ✅ **No overhead** – compiles away to platform calls ## Quick Start +### With `std` (default) + +Works automatically with `std::time`: + ```rust,no_run use universal_time::{Instant, SystemTime, UNIX_EPOCH}; fn main() { let start = Instant::now(); - let now = SystemTime::now(); let elapsed = start.elapsed(); - let since_epoch = now.duration_since(UNIX_EPOCH).unwrap_or_default(); - println!("elapsed = {:?}", elapsed); - println!("since epoch = {:?}", since_epoch); + let now = SystemTime::now(); + let since_epoch = now.duration_since(UNIX_EPOCH).unwrap(); + + println!("Elapsed: {:?}", elapsed); + println!("Since Unix epoch: {:?}", since_epoch); +} +``` + +### Without `std` (embedded, WASM unknown) + +Define a custom time provider using the `define_time_provider!` macro: + +```rust,ignore +# use core::time::Duration; +# use universal_time::{define_time_provider, Instant, SystemTime, WallClock, MonotonicClock}; +struct MyTimeProvider; + +impl WallClock for MyTimeProvider { + fn system_time(&self) -> SystemTime { + // Your platform-specific implementation + # SystemTime::from_unix_duration(Duration::from_secs(0)) + } } + +impl MonotonicClock for MyTimeProvider { + fn instant(&self) -> Instant { + // Your platform-specific implementation + # Instant::from_ticks(Duration::from_secs(0)) + } +} + +define_time_provider!(MyTimeProvider); +# fn main() { +# // Now Instant::now() and SystemTime::now() work! +# let _now = Instant::now(); +# } +``` + +## How It Works + +The library uses **extern symbols** to enforce time provider availability at **link time**: + +| Platform | Behavior | +|--------------------------------------|----------------------------------------| +| Linux/macOS/Windows with `std` | Uses `std::time` automatically | +| `no_std` and `wasm*-unknown-unknown` | Requires `define_time_provider!` macro | +| Other WASM targets with `std` | Uses `std::time` automatically | + +**Without a provider on no_std**, you get a clear link error: +```text +error: undefined reference to '__universal_time_provider' ``` -For more examples, check out the [examples](examples) directory. +**Duplicate provider?** Link error: "duplicate symbol" – catches configuration mistakes at compile time! -## Panic Behavior +## Features -In `no_std` mode, and in `std` mode on `wasm32-unknown-unknown`, both -`Instant::now()` and `SystemTime::now()` panic when: +- `std` (enabled by default) - Uses `std::time` on supported platforms -- no global context has been installed, or -- installed context returns `None` for that clock type +## Changelog -This is intentional so missing time sources fail fast instead of silently returning fake timestamps. +All notable changes to this library are documented in the [CHANGELOG.md](CHANGELOG.md). -## Concurrency Notes +## License -- `std`: global context uses `OnceLock` -- `no_std` with atomics: global context uses lock-free once initialization -- `no_std` without atomics: fallback expects single-threaded startup initialization +This project is distributed under the MIT software license – see the [LICENSE](./LICENSE) file for details diff --git a/examples/custom.rs b/examples/custom.rs deleted file mode 100644 index c51dbad..0000000 --- a/examples/custom.rs +++ /dev/null @@ -1,49 +0,0 @@ -use core::time::Duration; - -use universal_time::{ - set_global_time_context, Instant, MonotonicClock, SystemTime, WallClock, UNIX_EPOCH, -}; - -struct BoardClock; - -impl WallClock for BoardClock { - fn system_time(&self) -> Option { - // Replace with your RTC / SNTP source. - let unix_secs: u64 = rtc_unix_seconds(); - Some(SystemTime::from_unix_duration(Duration::from_secs( - unix_secs, - ))) - } -} - -impl MonotonicClock for BoardClock { - fn instant(&self) -> Option { - // Replace with your monotonic timer ticks. - let millis: u64 = board_millis_since_boot(); - Some(Instant::from_ticks(Duration::from_millis(millis))) - } -} - -fn rtc_unix_seconds() -> u64 { - // platform-specific implementation - 0 -} - -fn board_millis_since_boot() -> u64 { - // platform-specific implementation - 0 -} - -static CLOCK: BoardClock = BoardClock; - -fn main() { - set_global_time_context(&CLOCK).unwrap(); - - let start = Instant::now(); - let now = SystemTime::now(); - let elapsed = start.elapsed(); - let since_epoch = now.duration_since(UNIX_EPOCH).unwrap_or_default(); - - println!("elapsed = {:?}", elapsed); - println!("since epoch = {:?}", since_epoch); -} diff --git a/examples/standard.rs b/examples/standard.rs deleted file mode 100644 index 97976b1..0000000 --- a/examples/standard.rs +++ /dev/null @@ -1,11 +0,0 @@ -use universal_time::{Instant, SystemTime, UNIX_EPOCH}; - -fn main() { - let start = Instant::now(); - let now = SystemTime::now(); - let elapsed = start.elapsed(); - let since_epoch = now.duration_since(UNIX_EPOCH).unwrap_or_default(); - - println!("elapsed = {:?}", elapsed); - println!("since epoch = {:?}", since_epoch); -} diff --git a/src/global.rs b/src/global.rs index 6e3972e..fd60ada 100644 --- a/src/global.rs +++ b/src/global.rs @@ -1,171 +1,72 @@ -#[cfg(all(not(feature = "std"), target_has_atomic = "8"))] -use core::cell::UnsafeCell; -#[cfg(all(not(feature = "std"), target_has_atomic = "8"))] -use core::sync::atomic::{AtomicU8, Ordering}; - use crate::{Instant, SystemTime}; /// Source of wall-clock timestamps. pub trait WallClock { - /// Returns the current wall-clock time, or `None` if not available. - fn system_time(&self) -> Option; + /// Returns the current wall-clock time. + fn system_time(&self) -> SystemTime; } /// Source of monotonic instants. pub trait MonotonicClock { - /// Returns the current monotonic instant, or `None` if not available. - fn instant(&self) -> Option; + /// Returns the current monotonic instant. + fn instant(&self) -> Instant; } /// A full time context that can provide wall-clock and monotonic time. -pub trait TimeContext: WallClock + MonotonicClock + Send + Sync {} - -impl TimeContext for T where T: WallClock + MonotonicClock + Send + Sync {} +pub trait TimeProvider: WallClock + MonotonicClock + Sync {} -/// Error returned when attempting to set the global time context more than once. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct GlobalTimeContextAlreadySet; +impl TimeProvider for T where T: WallClock + MonotonicClock + Sync {} -#[cfg(feature = "std")] -static GLOBAL_TIME_CONTEXT: std::sync::OnceLock<&'static dyn TimeContext> = - std::sync::OnceLock::new(); - -#[cfg(all(not(feature = "std"), target_has_atomic = "8"))] -struct NoStdGlobalTimeContext { - state: AtomicU8, - value: UnsafeCell>, +// On platforms without std, users must provide the time provider symbol via define_time_provider! macro +extern "Rust" { + #[link_name = "__universal_time_provider"] + static TIME_PROVIDER: &'static dyn TimeProvider; } -#[cfg(all(not(feature = "std"), target_has_atomic = "8"))] -unsafe impl Sync for NoStdGlobalTimeContext {} - -#[cfg(all(not(feature = "std"), target_has_atomic = "8"))] -impl NoStdGlobalTimeContext { - const UNINITIALIZED: u8 = 0; - const INITIALIZING: u8 = 1; - const READY: u8 = 2; - - const fn new() -> Self { - Self { - state: AtomicU8::new(Self::UNINITIALIZED), - value: UnsafeCell::new(None), - } - } - - fn set(&self, context: &'static dyn TimeContext) -> Result<(), GlobalTimeContextAlreadySet> { - match self.state.compare_exchange( - Self::UNINITIALIZED, - Self::INITIALIZING, - Ordering::AcqRel, - Ordering::Acquire, - ) { - Ok(_) => { - unsafe { - *self.value.get() = Some(context); - } - self.state.store(Self::READY, Ordering::Release); - Ok(()) - } - Err(_) => { - while self.state.load(Ordering::Acquire) == Self::INITIALIZING { - core::hint::spin_loop(); - } - Err(GlobalTimeContextAlreadySet) - } - } - } - - fn get(&self) -> Option<&'static dyn TimeContext> { - let mut state = self.state.load(Ordering::Acquire); - while state == Self::INITIALIZING { - core::hint::spin_loop(); - state = self.state.load(Ordering::Acquire); - } - - if state == Self::READY { - unsafe { *self.value.get() } - } else { - None - } - } +#[inline(always)] +pub(crate) fn get_time_provider() -> &'static dyn TimeProvider { + unsafe { TIME_PROVIDER } } -#[cfg(all(not(feature = "std"), target_has_atomic = "8"))] -static GLOBAL_TIME_CONTEXT: NoStdGlobalTimeContext = NoStdGlobalTimeContext::new(); - -#[cfg(all(not(feature = "std"), not(target_has_atomic = "8")))] -static mut GLOBAL_TIME_CONTEXT: Option<&'static dyn TimeContext> = None; - -/// Installs the global time context. +/// Macro to define a custom time provider for no_std platforms. /// -/// This can be called only once for the process lifetime. -pub fn set_global_time_context( - context: &'static dyn TimeContext, -) -> Result<(), GlobalTimeContextAlreadySet> { - #[cfg(feature = "std")] - { - GLOBAL_TIME_CONTEXT - .set(context) - .map_err(|_| GlobalTimeContextAlreadySet) - } - - #[cfg(all(not(feature = "std"), target_has_atomic = "8"))] - { - GLOBAL_TIME_CONTEXT.set(context) - } - - #[cfg(all(not(feature = "std"), not(target_has_atomic = "8")))] - { - // Fallback for targets without atomics. Call during single-threaded startup - // before concurrency begins. - unsafe { - let current = core::ptr::read(core::ptr::addr_of!(GLOBAL_TIME_CONTEXT)); - if current.is_some() { - Err(GlobalTimeContextAlreadySet) - } else { - core::ptr::write(core::ptr::addr_of_mut!(GLOBAL_TIME_CONTEXT), Some(context)); - Ok(()) - } - } - } -} - -/// Returns the globally configured time context if one was installed. -pub fn global_time_context() -> Option<&'static dyn TimeContext> { - #[cfg(feature = "std")] - { - GLOBAL_TIME_CONTEXT.get().copied() - } - - #[cfg(all(not(feature = "std"), target_has_atomic = "8"))] - { - GLOBAL_TIME_CONTEXT.get() - } - - #[cfg(all(not(feature = "std"), not(target_has_atomic = "8")))] - { - // Fallback for targets without atomics. See synchronization note in - // `set_global_time_context`. - unsafe { core::ptr::read(core::ptr::addr_of!(GLOBAL_TIME_CONTEXT)) } - } -} - -#[cfg(any( - not(feature = "std"), - all(feature = "std", target_family = "wasm", target_os = "unknown") -))] -pub(crate) fn panic_missing_system_time() -> ! { - panic!( - "no wall-clock time source is available; install one with universal_time::set_global_time_context()" - ) -} - -#[cfg(any( - not(feature = "std"), - all(feature = "std", target_family = "wasm", target_os = "unknown") -))] -pub(crate) fn panic_missing_instant() -> ! { - panic!( - "no monotonic clock is available; install one with universal_time::set_global_time_context()" - ) +/// This macro must be called in your binary crate when using this library on no_std +/// or WASM unknown targets. Pass a static instance of your time provider. +/// +/// # Example +/// +/// ``` +/// use core::time::Duration; +/// +/// use universal_time::{define_time_provider, Instant, MonotonicClock, SystemTime, WallClock}; +/// +/// struct MyTimeProvider; +/// +/// impl WallClock for MyTimeProvider { +/// fn system_time(&self) -> SystemTime { +/// SystemTime::from_unix_duration(Duration::from_secs(0)) +/// } +/// } +/// +/// impl MonotonicClock for MyTimeProvider { +/// fn instant(&self) -> Instant { +/// Instant::from_ticks(Duration::from_secs(0)) +/// } +/// } +/// +/// define_time_provider!(MyTimeProvider); +/// ``` +#[cfg_attr( + docsrs, + doc(cfg(any( + not(feature = "std"), + all(feature = "std", target_family = "wasm", target_os = "unknown") + ))) +)] +#[macro_export] +macro_rules! define_time_provider { + ($provider_instance:expr) => { + #[export_name = "__universal_time_provider"] + static __UNIVERSAL_TIME_PROVIDER: &dyn $crate::TimeProvider = &$provider_instance; + }; } diff --git a/src/instant.rs b/src/instant.rs index 34d4c24..5157e3d 100644 --- a/src/instant.rs +++ b/src/instant.rs @@ -21,14 +21,15 @@ impl Instant { } /// Returns an instant corresponding to "now". + /// + /// # Platform behavior + /// + /// - With `std` feature on supported platforms: uses `std::time::Instant` + /// - Without `std` or on WASM unknown: uses the provider defined via `define_time_provider!` macro + /// + /// If no provider is defined on platforms without std, you'll get a **link error** at compile time. #[inline] pub fn now() -> Self { - if let Some(context) = crate::global::global_time_context() { - if let Some(now) = context.instant() { - return now; - } - } - #[cfg(all( feature = "std", not(all(target_family = "wasm", target_os = "unknown")) @@ -39,10 +40,10 @@ impl Instant { #[cfg(any( not(feature = "std"), - all(feature = "std", all(target_family = "wasm", target_os = "unknown")) + all(feature = "std", target_family = "wasm", target_os = "unknown") ))] { - crate::global::panic_missing_instant() + crate::global::get_time_provider().instant() } } @@ -114,3 +115,115 @@ impl Sub for Instant { self.duration_since(other) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn roundtrip_ticks() { + let ticks = Duration::from_millis(42); + let instant = Instant::from_ticks(ticks); + assert_eq!(instant.to_ticks(), ticks); + } + + #[test] + fn duration_since_saturates_at_zero() { + let earlier = Instant::from_ticks(Duration::from_secs(10)); + let later = Instant::from_ticks(Duration::from_secs(3)); + assert_eq!(later.duration_since(earlier), Duration::ZERO); + } + + #[test] + fn checked_duration_since_some() { + let earlier = Instant::from_ticks(Duration::from_secs(3)); + let later = Instant::from_ticks(Duration::from_secs(10)); + assert_eq!( + later.checked_duration_since(earlier), + Some(Duration::from_secs(7)) + ); + } + + #[test] + fn checked_duration_since_none() { + let earlier = Instant::from_ticks(Duration::from_secs(10)); + let later = Instant::from_ticks(Duration::from_secs(3)); + assert_eq!(later.checked_duration_since(earlier), None); + } + + #[test] + fn checked_add_and_sub_roundtrip() { + let start = Instant::from_ticks(Duration::from_secs(5)); + let delta = Duration::from_secs(2); + let end = start.checked_add(delta).expect("must not overflow"); + assert_eq!(end.to_ticks(), Duration::from_secs(7)); + assert_eq!(end.checked_sub(delta), Some(start)); + } + + #[test] + fn checked_add_overflow_returns_none() { + let start = Instant::from_ticks(Duration::MAX); + assert_eq!(start.checked_add(Duration::from_nanos(1)), None); + } + + #[test] + fn checked_sub_underflow_returns_none() { + let start = Instant::from_ticks(Duration::ZERO); + assert_eq!(start.checked_sub(Duration::from_nanos(1)), None); + } + + #[test] + fn add_operator_works() { + let start = Instant::from_ticks(Duration::from_secs(5)); + let end = start + Duration::from_secs(2); + assert_eq!(end.to_ticks(), Duration::from_secs(7)); + } + + #[test] + #[should_panic(expected = "overflow while adding Duration to Instant")] + fn add_operator_panics_on_overflow() { + let _ = Instant::from_ticks(Duration::MAX) + Duration::from_nanos(1); + } + + #[test] + fn sub_operator_works() { + let end = Instant::from_ticks(Duration::from_secs(7)); + let start = end - Duration::from_secs(2); + assert_eq!(start.to_ticks(), Duration::from_secs(5)); + } + + #[test] + #[should_panic(expected = "underflow while subtracting Duration from Instant")] + fn sub_operator_panics_on_underflow() { + let _ = Instant::from_ticks(Duration::ZERO) - Duration::from_nanos(1); + } + + #[test] + fn sub_instant_operator_is_saturating() { + let a = Instant::from_ticks(Duration::from_secs(9)); + let b = Instant::from_ticks(Duration::from_secs(4)); + assert_eq!(a - b, Duration::from_secs(5)); + assert_eq!(b - a, Duration::ZERO); + } + + #[test] + #[cfg(all( + feature = "std", + not(all(target_family = "wasm", target_os = "unknown")) + ))] + fn now_is_monotonic() { + let first = Instant::now(); + let second = Instant::now(); + assert!(second >= first); + } + + #[test] + #[cfg(all( + feature = "std", + not(all(target_family = "wasm", target_os = "unknown")) + ))] + fn elapsed_is_non_negative() { + let start = Instant::now(); + assert!(start.elapsed() >= Duration::ZERO); + } +} diff --git a/src/lib.rs b/src/lib.rs index cb42c14..2aaf3d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,17 +1,36 @@ -//! Cross-platform time primitives. -//! -//! Provides `Instant` and `SystemTime` plus traits for injecting custom -//! platform clocks through a global time context. - +#![deny(missing_docs)] #![cfg_attr(not(feature = "std"), no_std)] +#![cfg_attr(docsrs, feature(doc_cfg))] #![doc = include_str!("../README.md")] pub use core::time::Duration; +#[cfg(any( + not(feature = "std"), + all(feature = "std", target_family = "wasm", target_os = "unknown") +))] +#[cfg_attr( + docsrs, + doc(cfg(any( + not(feature = "std"), + all(feature = "std", target_family = "wasm", target_os = "unknown") + ))) +)] mod global; mod instant; mod system; +#[cfg(any( + not(feature = "std"), + all(feature = "std", target_family = "wasm", target_os = "unknown") +))] +#[cfg_attr( + docsrs, + doc(cfg(any( + not(feature = "std"), + all(feature = "std", target_family = "wasm", target_os = "unknown") + ))) +)] pub use self::global::*; pub use self::instant::*; pub use self::system::*; diff --git a/src/system.rs b/src/system.rs index f0a91e9..95b1f21 100644 --- a/src/system.rs +++ b/src/system.rs @@ -23,14 +23,15 @@ impl SystemTime { } /// Returns the current system time. + /// + /// # Platform behavior + /// + /// - With `std` feature on supported platforms: uses `std::time::SystemTime` + /// - Without `std` or on WASM unknown: uses the provider defined via `define_time_provider!` macro + /// + /// If no provider is defined on platforms without std, you'll get a **link error** at compile time. #[inline] pub fn now() -> Self { - if let Some(context) = crate::global::global_time_context() { - if let Some(now) = context.system_time() { - return now; - } - } - #[cfg(all( feature = "std", not(all(target_family = "wasm", target_os = "unknown")) @@ -45,10 +46,10 @@ impl SystemTime { #[cfg(any( not(feature = "std"), - all(feature = "std", all(target_family = "wasm", target_os = "unknown")) + all(feature = "std", target_family = "wasm", target_os = "unknown") ))] { - crate::global::panic_missing_system_time() + crate::global::get_time_provider().system_time() } } @@ -61,3 +62,44 @@ impl SystemTime { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn unix_epoch_is_zero() { + assert_eq!(UNIX_EPOCH.as_unix_duration(), Duration::ZERO); + } + + #[test] + fn roundtrip_unix_duration() { + let duration = Duration::from_secs(123) + Duration::from_nanos(456); + let time = SystemTime::from_unix_duration(duration); + assert_eq!(time.as_unix_duration(), duration); + } + + #[test] + fn duration_since_forward() { + let earlier = SystemTime::from_unix_duration(Duration::from_secs(10)); + let later = SystemTime::from_unix_duration(Duration::from_secs(12)); + assert_eq!(later.duration_since(earlier), Ok(Duration::from_secs(2))); + } + + #[test] + fn duration_since_backward() { + let earlier = SystemTime::from_unix_duration(Duration::from_secs(10)); + let later = SystemTime::from_unix_duration(Duration::from_secs(12)); + assert_eq!(earlier.duration_since(later), Err(Duration::from_secs(2))); + } + + #[test] + #[cfg(all( + feature = "std", + not(all(target_family = "wasm", target_os = "unknown")) + ))] + fn now_is_after_unix_epoch() { + let now = SystemTime::now(); + assert!(now.duration_since(UNIX_EPOCH).is_ok()); + } +} diff --git a/tests/time_api.rs b/tests/time_api.rs deleted file mode 100644 index f650bfb..0000000 --- a/tests/time_api.rs +++ /dev/null @@ -1,195 +0,0 @@ -use std::sync::OnceLock; -use std::time::Duration; - -use universal_time::{ - global_time_context, set_global_time_context, GlobalTimeContextAlreadySet, Instant, - MonotonicClock, SystemTime, TimeContext, WallClock, UNIX_EPOCH, -}; - -struct TestContext; - -static TEST_CONTEXT: TestContext = TestContext; -static TEST_START: OnceLock = OnceLock::new(); -static INSTALL_RESULT: OnceLock> = OnceLock::new(); - -impl WallClock for TestContext { - fn system_time(&self) -> Option { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .ok()?; - Some(SystemTime::from_unix_duration(now)) - } -} - -impl MonotonicClock for TestContext { - fn instant(&self) -> Option { - let ticks = TEST_START.get_or_init(std::time::Instant::now).elapsed(); - Some(Instant::from_ticks(ticks)) - } -} - -fn install_test_context() { - let _ = INSTALL_RESULT.get_or_init(|| set_global_time_context(&TEST_CONTEXT)); -} - -fn assert_send_sync() {} - -#[test] -fn test_context_is_send_sync() { - assert_send_sync::(); -} - -#[test] -fn time_context_requires_send_sync() { - fn assert_time_context() {} - assert_time_context::(); -} - -#[test] -fn unix_epoch_is_zero() { - assert_eq!(UNIX_EPOCH.as_unix_duration(), Duration::ZERO); -} - -#[test] -fn system_time_roundtrip_unix_duration() { - let duration = Duration::from_secs(123) + Duration::from_nanos(456); - let time = SystemTime::from_unix_duration(duration); - assert_eq!(time.as_unix_duration(), duration); -} - -#[test] -fn system_time_duration_since_forward() { - let earlier = SystemTime::from_unix_duration(Duration::from_secs(10)); - let later = SystemTime::from_unix_duration(Duration::from_secs(12)); - assert_eq!(later.duration_since(earlier), Ok(Duration::from_secs(2))); -} - -#[test] -fn system_time_duration_since_backward() { - let earlier = SystemTime::from_unix_duration(Duration::from_secs(10)); - let later = SystemTime::from_unix_duration(Duration::from_secs(12)); - assert_eq!(earlier.duration_since(later), Err(Duration::from_secs(2))); -} - -#[test] -fn instant_roundtrip_ticks() { - let ticks = Duration::from_millis(42); - let instant = Instant::from_ticks(ticks); - assert_eq!(instant.to_ticks(), ticks); -} - -#[test] -fn instant_duration_since_saturates_at_zero() { - let earlier = Instant::from_ticks(Duration::from_secs(10)); - let later = Instant::from_ticks(Duration::from_secs(3)); - assert_eq!(later.duration_since(earlier), Duration::ZERO); -} - -#[test] -fn instant_checked_duration_since_some() { - let earlier = Instant::from_ticks(Duration::from_secs(3)); - let later = Instant::from_ticks(Duration::from_secs(10)); - assert_eq!( - later.checked_duration_since(earlier), - Some(Duration::from_secs(7)) - ); -} - -#[test] -fn instant_checked_duration_since_none() { - let earlier = Instant::from_ticks(Duration::from_secs(10)); - let later = Instant::from_ticks(Duration::from_secs(3)); - assert_eq!(later.checked_duration_since(earlier), None); -} - -#[test] -fn instant_checked_add_and_sub_roundtrip() { - let start = Instant::from_ticks(Duration::from_secs(5)); - let delta = Duration::from_secs(2); - let end = start.checked_add(delta).expect("must not overflow"); - assert_eq!(end.to_ticks(), Duration::from_secs(7)); - assert_eq!(end.checked_sub(delta), Some(start)); -} - -#[test] -fn instant_checked_add_overflow_returns_none() { - let start = Instant::from_ticks(Duration::MAX); - assert_eq!(start.checked_add(Duration::from_nanos(1)), None); -} - -#[test] -fn instant_checked_sub_underflow_returns_none() { - let start = Instant::from_ticks(Duration::ZERO); - assert_eq!(start.checked_sub(Duration::from_nanos(1)), None); -} - -#[test] -fn instant_add_operator_works() { - let start = Instant::from_ticks(Duration::from_secs(5)); - let end = start + Duration::from_secs(2); - assert_eq!(end.to_ticks(), Duration::from_secs(7)); -} - -#[test] -#[should_panic(expected = "overflow while adding Duration to Instant")] -fn instant_add_operator_panics_on_overflow() { - let _ = Instant::from_ticks(Duration::MAX) + Duration::from_nanos(1); -} - -#[test] -fn instant_sub_operator_works() { - let end = Instant::from_ticks(Duration::from_secs(7)); - let start = end - Duration::from_secs(2); - assert_eq!(start.to_ticks(), Duration::from_secs(5)); -} - -#[test] -#[should_panic(expected = "underflow while subtracting Duration from Instant")] -fn instant_sub_operator_panics_on_underflow() { - let _ = Instant::from_ticks(Duration::ZERO) - Duration::from_nanos(1); -} - -#[test] -fn instant_sub_instant_operator_is_saturating() { - let a = Instant::from_ticks(Duration::from_secs(9)); - let b = Instant::from_ticks(Duration::from_secs(4)); - assert_eq!(a - b, Duration::from_secs(5)); - assert_eq!(b - a, Duration::ZERO); -} - -#[test] -fn global_context_is_available_after_install() { - install_test_context(); - assert!(global_time_context().is_some()); -} - -#[test] -fn global_context_can_only_be_set_once() { - install_test_context(); - assert_eq!( - set_global_time_context(&TEST_CONTEXT), - Err(GlobalTimeContextAlreadySet) - ); -} - -#[test] -fn instant_now_is_monotonic() { - install_test_context(); - let first = Instant::now(); - let second = Instant::now(); - assert!(second >= first); -} - -#[test] -fn elapsed_is_non_negative() { - install_test_context(); - let start = Instant::now(); - assert!(start.elapsed() >= Duration::ZERO); -} - -#[test] -fn system_time_now_is_after_unix_epoch() { - install_test_context(); - let now = SystemTime::now(); - assert!(now.duration_since(UNIX_EPOCH).is_ok()); -}