Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

System time implementation #4

Merged
merged 5 commits into from
Apr 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mock_instant"
version = "0.2.1"
version = "0.3.0"
authors = ["museun <museun@outlook.com>"]
edition = "2018"
license = "0BSD"
Expand All @@ -15,4 +15,4 @@ default = []
sync = ["once_cell"]

[dependencies]
once_cell = { version = "1.4", optional = true }
once_cell = { version = "1.17", optional = true }
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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
Expand All @@ -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
247 changes: 241 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,28 @@ 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;

#[cfg(not(test))]
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;

Expand All @@ -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;
Expand All @@ -40,6 +62,7 @@ mod reference {
use std::{sync::Mutex, time::Duration};

pub static TIME: OnceCell<Mutex<Duration>> = OnceCell::new();
pub static SYSTEM_TIME: OnceCell<Mutex<Duration>> = OnceCell::new();

pub fn with_time(d: impl Fn(&mut Duration)) {
let t = TIME.get_or_init(Mutex::default);
Expand All @@ -50,6 +73,16 @@ mod reference {
pub fn get_time() -> Duration {
*TIME.get_or_init(Mutex::default).lock().unwrap()
}

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);
}

pub fn get_system_time() -> Duration {
*SYSTEM_TIME.get_or_init(Mutex::default).lock().unwrap()
}
}

#[cfg(not(feature = "sync"))]
Expand All @@ -59,15 +92,24 @@ mod reference {

thread_local! {
pub static TIME: RefCell<Duration> = RefCell::new(Duration::default());
pub static SYSTEM_TIME: RefCell<Duration> = 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
Expand All @@ -80,25 +122,137 @@ 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);

pub const UNIX_EPOCH: SystemTime = SystemTime(Duration::from_secs(0));

impl SystemTime {
pub const UNIX_EPOCH: SystemTime = UNIX_EPOCH;

pub fn now() -> Self {
Self(MockClock::system_time())
}

pub fn duration_since(&self, earlier: SystemTime) -> Result<Duration, SystemTimeError> {
self.0
.checked_sub(earlier.0)
.ok_or_else(|| SystemTimeError(earlier.0 - self.0))
}

pub fn elapsed(&self) -> Result<Duration, SystemTimeError> {
Self::now().duration_since(*self)
}

pub fn checked_add(&self, duration: Duration) -> Option<SystemTime> {
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<SystemTime> {
self.0
.as_millis()
.checked_sub(duration.as_millis())
.map(|c| Duration::from_millis(c as _))
.map(Self)
}
}

impl std::ops::Add<Duration> 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<Duration> for SystemTime {
fn add_assign(&mut self, rhs: Duration) {
*self = *self + rhs
}
}

impl std::ops::Sub<Duration> 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<Duration> for SystemTime {
fn sub_assign(&mut self, rhs: Duration) {
*self = *self - rhs
}
}

/// A simple deterministic Instant wrapped around a modifiable Duration
Expand Down Expand Up @@ -188,6 +342,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));
Expand Down