From 5951aa0af82d06d53a117a56a000d977b3d20a73 Mon Sep 17 00:00:00 2001 From: museun Date: Wed, 19 Apr 2023 20:44:47 -0400 Subject: [PATCH 1/5] bump deps --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 2d5f281..e4a774b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,4 +15,4 @@ default = [] sync = ["once_cell"] [dependencies] -once_cell = { version = "1.4", optional = true } +once_cell = { version = "1.17", optional = true } From 490c618021e774f23179688fd8d01417dab4e804 Mon Sep 17 00:00:00 2001 From: museun Date: Wed, 19 Apr 2023 21:46:30 -0400 Subject: [PATCH 2/5] add SystemTime abstraction --- Cargo.toml | 2 +- README.md | 28 +++++- src/lib.rs | 245 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 266 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e4a774b..4a64423 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mock_instant" -version = "0.2.1" +version = "0.3.0" authors = ["museun "] edition = "2018" license = "0BSD" diff --git a/README.md b/README.md index 23e3abf..d48d831 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@ This crate allows you to test Instant/Duration code, deterministically **_per th If cross-thread determinism is required, enable the `sync` feature: ```toml -mock_instant = { version = "0.2", features = ["sync"] } +mock_instant = { version = "0.3", features = ["sync"] } ``` -It provides a replacement `std::time::Instant` that uses a deterministic thread-local 'clock' +It provides a replacement `std::time::Instant` and `std::time::SystemTime` that uses a deterministic thread-local 'clock' You can swap out the `std::time::Instant` with this one by doing something similar to: @@ -22,6 +22,16 @@ use mock_instant::Instant; use std::time::Instant; ``` +or for a `std::time::SystemTime` + +``` +#[cfg(test)] +use mock_instant::{SystemTime, SystemTimeError}; + +#[cfg(not(test))] +use std::time::{SystemTime, SystemTimeError}; +``` + ## Example ```rust @@ -35,4 +45,18 @@ MockClock::advance(Duration::from_secs(2)); assert_eq!(now.elapsed(), Duration::from_secs(17)); ``` +# Mocking a SystemTime + +``` +# use mock_instant::{MockClock, SystemTime}; +use std::time::Duration; + +let now = SystemTime::now(); +MockClock::advance_system_time(Duration::from_secs(15)); +MockClock::advance_system_time(Duration::from_secs(2)); + +// its been '17' seconds +assert_eq!(now.elapsed().unwrap(), Duration::from_secs(17)); +``` + License: 0BSD diff --git a/src/lib.rs b/src/lib.rs index 8650c86..a7b67c5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,10 +7,10 @@ If cross-thread determinism is required, enable the `sync` feature: mock_instant = { version = "0.2", features = ["sync"] } ``` -It provides a replacement `std::time::Instant` that uses a deterministic thread-local 'clock' +It provides a replacement `std::time::Instant` and `std::time::SystemTime` that uses a deterministic thread-local 'clock' You can swap out the `std::time::Instant` with this one by doing something similar to: -``` +```rust #[cfg(test)] use mock_instant::Instant; @@ -18,8 +18,17 @@ use mock_instant::Instant; use std::time::Instant; ``` -# Example +or for a `std::time::SystemTime` +```rust +#[cfg(test)] +use mock_instant::{SystemTime, SystemTimeError}; + +#[cfg(not(test))] +use std::time::{SystemTime, SystemTimeError}; ``` + +# Example +```rust # use mock_instant::{MockClock, Instant}; use std::time::Duration; @@ -30,6 +39,19 @@ MockClock::advance(Duration::from_secs(2)); // its been '17' seconds assert_eq!(now.elapsed(), Duration::from_secs(17)); ``` + +# Mocking a SystemTime +```rust +# use mock_instant::{MockClock, SystemTime}; +use std::time::Duration; + +let now = SystemTime::now(); +MockClock::advance_system_time(Duration::from_secs(15)); +MockClock::advance_system_time(Duration::from_secs(2)); + +// its been '17' seconds +assert_eq!(now.elapsed().unwrap(), Duration::from_secs(17)); +``` */ use std::time::Duration; @@ -40,6 +62,7 @@ mod reference { use std::{sync::Mutex, time::Duration}; pub static TIME: OnceCell> = OnceCell::new(); + pub static SYSTEM_TIME: OnceCell> = OnceCell::new(); pub fn with_time(d: impl Fn(&mut Duration)) { let t = TIME.get_or_init(Mutex::default); @@ -50,6 +73,16 @@ mod reference { pub fn get_time() -> Duration { *TIME.get_or_init(Mutex::default).lock().unwrap() } + + pub fn get_time() -> Duration { + let t = SYSTEM_TIME.get_or_init(Mutex::default); + let mut t = t.lock().unwrap(); + d(&mut t); + } + + pub fn get_system_time() -> Duration { + *SYSTEM_TIME.get_or_init(Mutex::default).lock().unwrap() + } } #[cfg(not(feature = "sync"))] @@ -59,15 +92,24 @@ mod reference { thread_local! { pub static TIME: RefCell = RefCell::new(Duration::default()); + pub static SYSTEM_TIME: RefCell = RefCell::new(Duration::default()); } pub fn with_time(d: impl Fn(&mut Duration)) { TIME.with(|t| d(&mut *t.borrow_mut())) } + pub fn with_system_time(d: impl Fn(&mut Duration)) { + SYSTEM_TIME.with(|t| d(&mut *t.borrow_mut())) + } + pub fn get_time() -> Duration { TIME.with(|t| *t.borrow()) } + + pub fn get_system_time() -> Duration { + SYSTEM_TIME.with(|t| *t.borrow()) + } } /// A Mock clock @@ -80,25 +122,135 @@ impl std::fmt::Debug for MockClock { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("MockClock") .field("time", &Self::time()) + .field("system_time", &Self::system_time()) .finish() } } impl MockClock { - /// Set the internal clock to this 'Duration' + /// Set the internal Instant clock to this 'Duration' pub fn set_time(time: Duration) { reference::with_time(|t| *t = time); } - /// Advance the internal clock by this 'Duration' + /// Advance the internal Instant clock by this 'Duration' pub fn advance(time: Duration) { reference::with_time(|t| *t += time); } - /// Get the current duration + /// Get the current Instant duration pub fn time() -> Duration { reference::get_time() } + + /// Set the internal SystemTime clock to this 'Duration' + pub fn set_system_time(time: Duration) { + reference::with_system_time(|t| *t = time); + } + + /// Advance the internal SystemTime clock by this 'Duration' + pub fn advance_system_time(time: Duration) { + reference::with_system_time(|t| *t += time); + } + + /// Get the current SystemTime duration + pub fn system_time() -> Duration { + reference::get_system_time() + } +} + +/// An error returned from the duration_since and elapsed methods on SystemTime, used to learn how far in the opposite direction a system time lies. +#[derive(Clone, Debug)] +pub struct SystemTimeError(Duration); + +impl SystemTimeError { + pub fn duration(&self) -> Duration { + self.0 + } +} + +impl std::fmt::Display for SystemTimeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "second time provided was later than self") + } +} + +impl std::error::Error for SystemTimeError { + #[allow(deprecated)] + fn description(&self) -> &str { + "other time was not earlier than self" + } +} + +/// A simple deterministic SystemTime wrapped around a modifiable Duration +/// +/// This used a thread-local state as the 'wall clock' that is configurable via +/// the `MockClock` +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +pub struct SystemTime(Duration); + +impl SystemTime { + pub const UNIX_EPOCH: SystemTime = SystemTime(Duration::from_secs(0)); + + pub fn now() -> Self { + Self(MockClock::system_time()) + } + + pub fn duration_since(&self, earlier: SystemTime) -> Result { + self.0 + .checked_sub(earlier.0) + .ok_or_else(|| SystemTimeError(earlier.0 - self.0)) + } + + pub fn elapsed(&self) -> Result { + Self::now().duration_since(*self) + } + + pub fn checked_add(&self, duration: Duration) -> Option { + duration + .as_millis() + .checked_add(self.0.as_millis()) + .map(|c| Duration::from_millis(c as _)) + .map(Self) + } + + pub fn checked_sub(&self, duration: Duration) -> Option { + self.0 + .as_millis() + .checked_sub(duration.as_millis()) + .map(|c| Duration::from_millis(c as _)) + .map(Self) + } +} + +impl std::ops::Add for SystemTime { + type Output = SystemTime; + + fn add(self, rhs: Duration) -> Self::Output { + self.checked_add(rhs) + .expect("overflow when adding duration to instant") + } +} + +impl std::ops::AddAssign for SystemTime { + fn add_assign(&mut self, rhs: Duration) { + *self = *self + rhs + } +} + +impl std::ops::Sub for SystemTime { + type Output = SystemTime; + + fn sub(self, rhs: Duration) -> Self::Output { + self.checked_sub(rhs) + .expect("overflow when subtracting duration from instant") + } +} + +impl std::ops::SubAssign for SystemTime { + fn sub_assign(&mut self, rhs: Duration) { + *self = *self - rhs + } } /// A simple deterministic Instant wrapped around a modifiable Duration @@ -188,6 +340,87 @@ mod tests { MockClock::set_time(Duration::default()) } + fn reset_system_time() { + MockClock::set_system_time(Duration::default()) + } + + #[test] + fn set_system_time() { + MockClock::set_system_time(Duration::from_secs(42)); + assert_eq!(MockClock::system_time(), Duration::from_secs(42)); + + reset_system_time(); + assert_eq!(MockClock::system_time(), Duration::default()); + } + + #[test] + fn advance_system_time() { + for i in 0..3 { + MockClock::advance_system_time(Duration::from_millis(100)); + let time = Duration::from_millis(100 * (i + 1)); + assert_eq!(MockClock::system_time(), time); + } + } + + #[test] + fn system_time() { + let now = SystemTime::now(); + for i in 0..3 { + MockClock::advance_system_time(Duration::from_millis(100)); + assert_eq!(now.elapsed().unwrap(), Duration::from_millis(100 * (i + 1))); + } + MockClock::advance_system_time(Duration::from_millis(100)); + + let next = SystemTime::now(); + assert_eq!( + next.duration_since(now).unwrap(), + Duration::from_millis(400) + ); + } + + #[test] + fn system_time_methods() { + let system_time = SystemTime::now(); + let interval = Duration::from_millis(42); + MockClock::advance_system_time(interval); + + // zero + 1 = 1 + assert_eq!( + system_time.checked_add(Duration::from_millis(1)).unwrap(), + SystemTime(Duration::from_millis(1)) + ); + + // now + 1 = diff + 1 + assert_eq!( + SystemTime::now() + .checked_add(Duration::from_millis(1)) + .unwrap(), + SystemTime(Duration::from_millis(43)) + ); + + // zero - 1 = None + assert!(system_time.checked_sub(Duration::from_millis(1)).is_none()); + + // now - 1 = diff -1 + assert_eq!( + SystemTime::now() + .checked_sub(Duration::from_millis(1)) + .unwrap(), + SystemTime(Duration::from_millis(41)) + ); + + // now - 1 = diff - 1 + assert_eq!( + SystemTime::now() - Duration::from_millis(1), + SystemTime(Duration::from_millis(41)) + ); + + // now - diff + 1 = none + assert!(SystemTime::now() + .checked_sub(Duration::from_millis(43)) + .is_none()); + } + #[test] fn set_time() { MockClock::set_time(Duration::from_secs(42)); From 84e724b096ade6dc43fc84cf1ea996d099a23770 Mon Sep 17 00:00:00 2001 From: museun Date: Thu, 20 Apr 2023 18:54:19 -0400 Subject: [PATCH 3/5] rename duplicate get_time -> with_system_time --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index a7b67c5..41f2308 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,7 +74,7 @@ mod reference { *TIME.get_or_init(Mutex::default).lock().unwrap() } - pub fn get_time() -> Duration { + pub fn with_system_time() -> Duration { let t = SYSTEM_TIME.get_or_init(Mutex::default); let mut t = t.lock().unwrap(); d(&mut t); From d9afb747e7f10de0a62f1db5c1df71712b7e01de Mon Sep 17 00:00:00 2001 From: museun Date: Thu, 20 Apr 2023 18:55:19 -0400 Subject: [PATCH 4/5] fix with_system_time arguments --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 41f2308..e3b19f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,7 +74,7 @@ mod reference { *TIME.get_or_init(Mutex::default).lock().unwrap() } - pub fn with_system_time() -> Duration { + pub fn with_system_time(d: impl Fn(&mut Duration)) -> Duration { let t = SYSTEM_TIME.get_or_init(Mutex::default); let mut t = t.lock().unwrap(); d(&mut t); From 07c1ab2172457672d5ff653625607f486628d649 Mon Sep 17 00:00:00 2001 From: museun Date: Thu, 20 Apr 2023 18:57:14 -0400 Subject: [PATCH 5/5] mimic std's UNIX_EPOCH duplication --- src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index e3b19f5..0dbd982 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -189,8 +189,10 @@ impl std::error::Error for SystemTimeError { #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] pub struct SystemTime(Duration); +pub const UNIX_EPOCH: SystemTime = SystemTime(Duration::from_secs(0)); + impl SystemTime { - pub const UNIX_EPOCH: SystemTime = SystemTime(Duration::from_secs(0)); + pub const UNIX_EPOCH: SystemTime = UNIX_EPOCH; pub fn now() -> Self { Self(MockClock::system_time())