use pricelevel::TimestampMs;
use std::fmt;
use std::sync::atomic::{AtomicU64, Ordering};
pub trait Clock: Send + Sync + fmt::Debug {
fn now_millis(&self) -> TimestampMs;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct MonotonicClock;
impl Clock for MonotonicClock {
#[inline]
fn now_millis(&self) -> TimestampMs {
TimestampMs::new(crate::utils::current_time_millis())
}
}
#[derive(Debug)]
pub struct StubClock {
counter: AtomicU64,
step: u64,
}
impl StubClock {
#[must_use]
pub fn new() -> Self {
Self {
counter: AtomicU64::new(0),
step: 1,
}
}
#[must_use]
pub fn starting_at(start: u64) -> Self {
Self {
counter: AtomicU64::new(start),
step: 1,
}
}
#[must_use]
pub fn with_step(start: u64, step: u64) -> Self {
Self {
counter: AtomicU64::new(start),
step: step.max(1),
}
}
#[must_use]
pub fn peek(&self) -> u64 {
self.counter.load(Ordering::Relaxed)
}
}
impl Default for StubClock {
fn default() -> Self {
Self::new()
}
}
impl Clock for StubClock {
#[inline]
fn now_millis(&self) -> TimestampMs {
let v = self.counter.fetch_add(self.step, Ordering::Relaxed);
TimestampMs::new(v)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
use std::sync::Arc;
use std::thread;
#[test]
fn test_monotonic_clock_returns_nonzero_ms() {
let clock = MonotonicClock;
let ts = clock.now_millis();
assert!(ts.as_u64() > 0, "wall-clock must be after the epoch");
}
#[test]
fn test_stub_clock_starts_at_zero_and_advances_by_one() {
let clock = StubClock::new();
assert_eq!(clock.now_millis().as_u64(), 0);
assert_eq!(clock.now_millis().as_u64(), 1);
assert_eq!(clock.now_millis().as_u64(), 2);
}
#[test]
fn test_stub_clock_with_step() {
let clock = StubClock::with_step(100, 5);
assert_eq!(clock.now_millis().as_u64(), 100);
assert_eq!(clock.now_millis().as_u64(), 105);
assert_eq!(clock.now_millis().as_u64(), 110);
}
#[test]
fn test_stub_clock_peek_does_not_advance() {
let clock = StubClock::starting_at(42);
assert_eq!(clock.peek(), 42);
assert_eq!(clock.peek(), 42);
let first = clock.now_millis();
assert_eq!(first.as_u64(), 42);
assert_eq!(clock.peek(), 43);
}
#[test]
fn test_stub_clock_concurrent_advance_is_monotonic() {
let start: u64 = 0;
let step: u64 = 1;
let threads = 4usize;
let per_thread = 1000usize;
let total = threads.checked_mul(per_thread).expect("overflow");
let clock = Arc::new(StubClock::with_step(start, step));
let mut handles = Vec::with_capacity(threads);
for _ in 0..threads {
let c = Arc::clone(&clock);
handles.push(thread::spawn(move || {
let mut local = Vec::with_capacity(per_thread);
for _ in 0..per_thread {
local.push(c.now_millis().as_u64());
}
local
}));
}
let mut all: Vec<u64> = Vec::with_capacity(total);
for h in handles {
let part = h.join().expect("thread panicked");
all.extend(part);
}
assert_eq!(all.len(), total);
let set: HashSet<u64> = all.iter().copied().collect();
assert_eq!(
set.len(),
total,
"expected every observed tick to be unique"
);
let expected_max = start
.checked_add(
(total as u64)
.checked_mul(step)
.and_then(|v| v.checked_sub(1))
.expect("overflow"),
)
.expect("overflow");
let observed_max = all.iter().copied().max().expect("non-empty");
assert_eq!(observed_max, expected_max);
}
#[test]
fn test_stub_clock_with_step_zero_clamps_to_one() {
let clock = StubClock::with_step(10, 0);
assert_eq!(clock.now_millis().as_u64(), 10);
assert_eq!(clock.now_millis().as_u64(), 11);
assert_eq!(clock.now_millis().as_u64(), 12);
}
}