use crate::primitives::{Condvar, Mutex, MutexGuard};
use crate::{Clock, Signal};
#[cfg(feature = "crossbeam")]
use crossbeam_channel::{Receiver, RecvTimeoutError, Sender, TryRecvError};
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::sync::{Arc, PoisonError, Weak};
use std::time::{Duration, Instant, SystemTime};
#[cfg_attr(docsrs, doc(cfg(feature = "test-clock")))]
pub struct TestClock {
paused: Arc<Paused>,
}
impl TestClock {
pub fn new() -> Self {
let now = Instant::now();
Self {
paused: Arc::new(Paused {
start: now,
state: Mutex::new(PausedState {
now,
wall: WallAnchor {
time: SystemTime::now(),
instant: now,
},
signals: BTreeMap::new(),
blocked: 0,
deadlines: BTreeMap::new(),
#[cfg(feature = "crossbeam")]
timers: BTreeMap::new(),
#[cfg(feature = "crossbeam")]
fired: Vec::new(),
}),
changed: Condvar::new(),
#[cfg(test)]
before_park: std::sync::Mutex::new(None),
#[cfg(test)]
before_rewait: std::sync::Mutex::new(None),
#[cfg(all(test, feature = "crossbeam"))]
after_timer_receive: std::sync::Mutex::new(None),
}),
}
}
pub fn clock(&self) -> Clock {
Clock {
paused: Some(self.paused.clone()),
}
}
pub fn advance(&mut self, by: Duration) {
self.advance_with(|now| {
now.checked_add(by)
.expect("clock advance overflows Instant")
});
}
pub fn advance_to(&mut self, target: Instant) {
self.advance_with(|now| {
assert!(target >= now, "clock cannot go backwards");
target
});
}
pub fn set_system_time(&mut self, time: SystemTime) {
let mut state = self.paused.lock();
state.wall = WallAnchor {
time,
instant: state.now,
};
}
pub fn wait_blocked(&self, count: usize) {
let mut state = self.paused.lock();
while state.blocked < count {
state = self
.paused
.changed
.wait(state)
.unwrap_or_else(PoisonError::into_inner);
}
}
#[cfg(feature = "crossbeam")]
#[cfg_attr(docsrs, doc(cfg(all(feature = "test-clock", feature = "crossbeam"))))]
pub fn wait_timers(&self, count: usize) {
let mut state = self.paused.lock();
while state.timers.len() < count {
state = self
.paused
.changed
.wait(state)
.unwrap_or_else(PoisonError::into_inner);
}
}
pub fn next_deadline(&self) -> Option<Instant> {
let state = self.paused.lock();
let deadline = state.deadlines.keys().next().map(|&(deadline, _)| deadline);
#[cfg(feature = "crossbeam")]
let deadline = deadline
.into_iter()
.chain(state.timers.keys().next().map(|&(deadline, _)| deadline))
.min();
deadline
}
fn advance_with(&mut self, next: impl FnOnce(Instant) -> Instant) {
let (now, wall) = {
let state = self.paused.lock();
(state.now, state.wall)
};
let next = next(now);
if next == now {
return;
}
wall.at(next).expect("clock advance overflows SystemTime");
let mut state = self.paused.lock();
state.now = next;
let signals: Vec<_> = state
.deadlines
.range(..=(next, usize::MAX))
.map(|(&(_, key), _)| key)
.collect::<BTreeSet<_>>()
.into_iter()
.map(|key| {
state
.signals
.get(&key)
.and_then(Weak::upgrade)
.expect("parked signal is registered")
})
.collect();
#[cfg(feature = "crossbeam")]
{
while state
.timers
.first_key_value()
.is_some_and(|(&(deadline, _), _)| deadline <= next)
{
let timer = state.timers.pop_first().expect("due timer exists").1;
state.fire(&timer);
}
self.paused.changed.notify_all();
}
drop(state);
for signal in signals {
signal.wake();
}
}
}
impl Default for TestClock {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for TestClock {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let state = self.paused.lock();
let advanced = state.now - self.paused.start;
let system_time = state.system_time();
let blocked = state.blocked;
#[cfg(feature = "crossbeam")]
let timers = state.timers.len();
drop(state);
let mut debug = f.debug_struct("TestClock");
debug
.field("advanced", &advanced)
.field("system_time", &system_time)
.field("blocked", &blocked);
#[cfg(feature = "crossbeam")]
debug.field("timers", &timers);
debug.finish()
}
}
pub(crate) struct Paused {
start: Instant,
pub(crate) state: Mutex<PausedState>,
pub(crate) changed: Condvar,
#[cfg(test)]
pub(crate) before_park: std::sync::Mutex<Option<BeforePark>>,
#[cfg(test)]
pub(crate) before_rewait: std::sync::Mutex<Option<BeforePark>>,
#[cfg(all(test, feature = "crossbeam"))]
after_timer_receive: std::sync::Mutex<Option<TimerHook>>,
}
pub(crate) struct PausedState {
now: Instant,
wall: WallAnchor,
pub(crate) signals: BTreeMap<usize, Weak<Signal>>,
pub(crate) blocked: usize,
deadlines: BTreeMap<(Instant, usize), usize>,
#[cfg(feature = "crossbeam")]
timers: BTreeMap<(Instant, usize), Arc<Timer>>,
#[cfg(feature = "crossbeam")]
fired: Vec<Sender<Instant>>,
}
impl PausedState {
fn system_time(&self) -> SystemTime {
self.wall
.at(self.now)
.expect("wall time fits, since every change checks it first")
}
#[cfg(feature = "crossbeam")]
fn arm_timer(&mut self, deadline: Instant, retain: bool) -> (Arc<Timer>, Receiver<Instant>) {
let (sender, receiver) = crossbeam_channel::bounded(1);
let timer = Arc::new(Timer {
deadline,
sender,
retain,
});
if deadline > self.now {
self.timers.insert(timer.key(), timer.clone());
} else {
self.fire(&timer);
}
(timer, receiver)
}
#[cfg(feature = "crossbeam")]
fn fire(&mut self, timer: &Timer) {
let delivered = timer.sender.send(timer.deadline).is_ok();
if delivered && timer.retain {
self.fired.push(timer.sender.clone());
}
}
}
#[derive(Clone, Copy)]
struct WallAnchor {
time: SystemTime,
instant: Instant,
}
impl WallAnchor {
fn at(&self, instant: Instant) -> Option<SystemTime> {
self.time.checked_add(instant - self.instant)
}
}
impl Paused {
#[cfg(all(test, feature = "crossbeam", not(loom)))]
pub(crate) fn timer_counts(&self) -> (usize, usize) {
let state = self.lock();
(state.timers.len(), state.fired.len())
}
#[cfg(feature = "crossbeam")]
pub(crate) fn at(&self, deadline: Instant) -> Receiver<Instant> {
let mut state = self.lock();
let (_, receiver) = state.arm_timer(deadline, true);
self.changed.notify_all();
receiver
}
#[cfg(feature = "crossbeam")]
pub(crate) fn recv_deadline<T>(
&self,
receiver: &Receiver<T>,
deadline: Instant,
) -> Result<T, RecvTimeoutError> {
let (timer, timeout) = loop {
let seen = self.now();
match receiver.try_recv() {
Ok(value) => return Ok(value),
Err(TryRecvError::Disconnected) => return Err(RecvTimeoutError::Disconnected),
Err(TryRecvError::Empty) => {}
}
let mut state = self.lock();
if state.now != seen {
continue;
}
if seen >= deadline {
return Err(RecvTimeoutError::Timeout);
}
let timer = state.arm_timer(deadline, false);
self.changed.notify_all();
break timer;
};
let _registration = ReceiveTimer {
paused: self,
timer,
};
crossbeam_channel::select! {
recv(receiver) -> result => result.map_err(RecvTimeoutError::from),
recv(timeout) -> _ => {
#[cfg(test)]
{
let hook = self.after_timer_receive.lock().unwrap().take();
if let Some(hook) = hook {
hook();
}
}
drop(self.lock());
receiver.try_recv().map_err(|err| match err {
TryRecvError::Empty => RecvTimeoutError::Timeout,
TryRecvError::Disconnected => RecvTimeoutError::Disconnected,
})
}
}
}
pub(crate) fn now(&self) -> Instant {
self.lock().now
}
pub(crate) fn system_time(&self) -> SystemTime {
self.lock().system_time()
}
pub(crate) fn snapshot(&self) -> (Duration, SystemTime) {
let state = self.lock();
(state.now - self.start, state.system_time())
}
pub(crate) fn register(&self, signal: &Arc<Signal>) {
self.lock()
.signals
.insert(signal.key(), Arc::downgrade(signal));
}
pub(crate) fn unregister(&self, signal: &Arc<Signal>) {
self.lock().signals.remove(&signal.key());
}
pub(crate) fn block(&self, deadline: Option<Instant>, signal: &Signal) -> Option<Blocked<'_>> {
let mut state = self.lock();
if deadline.is_some_and(|deadline| deadline <= state.now) {
return None;
}
let deadline = deadline.map(|deadline| (deadline, signal.key()));
state.blocked += 1;
if let Some(deadline) = deadline {
*state.deadlines.entry(deadline).or_default() += 1;
}
self.changed.notify_all();
Some(Blocked {
paused: self,
deadline,
})
}
#[cfg(test)]
pub(crate) fn take_hook(&self, again: bool) -> Option<BeforePark> {
let hook = if again {
&self.before_rewait
} else {
&self.before_park
};
hook.lock().unwrap().take()
}
fn lock(&self) -> MutexGuard<'_, PausedState> {
self.state.lock().unwrap_or_else(PoisonError::into_inner)
}
}
#[cfg(feature = "crossbeam")]
struct Timer {
deadline: Instant,
sender: Sender<Instant>,
retain: bool,
}
#[cfg(feature = "crossbeam")]
impl Timer {
fn key(&self) -> (Instant, usize) {
(self.deadline, self as *const Self as usize)
}
}
#[cfg(feature = "crossbeam")]
struct ReceiveTimer<'a> {
paused: &'a Paused,
timer: Arc<Timer>,
}
#[cfg(feature = "crossbeam")]
impl Drop for ReceiveTimer<'_> {
fn drop(&mut self) {
self.paused.lock().timers.remove(&self.timer.key());
self.paused.changed.notify_all();
}
}
pub(crate) struct Blocked<'a> {
paused: &'a Paused,
deadline: Option<(Instant, usize)>,
}
impl Drop for Blocked<'_> {
fn drop(&mut self) {
let mut state = self.paused.lock();
state.blocked -= 1;
if let Some(deadline) = self.deadline {
let count = state
.deadlines
.get_mut(&deadline)
.expect("parked deadline exists");
*count -= 1;
if *count == 0 {
state.deadlines.remove(&deadline);
}
}
self.paused.changed.notify_all();
}
}
#[cfg(test)]
pub(crate) type BeforePark = Box<dyn FnOnce(Option<Instant>) + Send>;
#[cfg(all(test, feature = "crossbeam"))]
type TimerHook = Box<dyn FnOnce() + Send>;
#[cfg(all(test, feature = "crossbeam", not(loom)))]
#[cfg_attr(coverage_nightly, coverage(off))]
#[path = "tests/paused.rs"]
mod tests;