use crate::primitives::{Condvar, Mutex, MutexGuard};
use crate::{Clock, Signal};
#[cfg(feature = "crossbeam")]
use crossbeam_channel::{Receiver, RecvTimeoutError, Sender, TryRecvError};
use std::collections::BTreeMap;
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,
system_time: SystemTime::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_send: 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) {
self.paused.lock().system_time = time;
}
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().copied();
#[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, system_time) = {
let state = self.paused.lock();
(state.now, state.system_time)
};
let next = next(now);
if next == now {
return;
}
let system_time = system_time
.checked_add(next - now)
.expect("clock advance overflows SystemTime");
let mut state = self.paused.lock();
let signals: Vec<_> = state.signals.values().filter_map(Weak::upgrade).collect();
state.now = next;
state.system_time = system_time;
#[cfg(feature = "crossbeam")]
let timers = {
let mut timers = Vec::new();
while state
.timers
.first_key_value()
.is_some_and(|(&(deadline, _), _)| deadline <= next)
{
timers.push(state.timers.pop_first().expect("due timer exists").1);
}
self.paused.changed.notify_all();
timers
};
drop(state);
#[cfg(feature = "crossbeam")]
for timer in timers {
self.paused.fire(&timer);
}
for signal in signals {
signal.advance();
}
}
}
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_send: std::sync::Mutex<Option<TimerHook>>,
#[cfg(all(test, feature = "crossbeam"))]
after_timer_receive: std::sync::Mutex<Option<TimerHook>>,
}
pub(crate) struct PausedState {
now: Instant,
system_time: SystemTime,
pub(crate) signals: BTreeMap<usize, Weak<Signal>>,
pub(crate) blocked: usize,
deadlines: BTreeMap<Instant, usize>,
#[cfg(feature = "crossbeam")]
timers: BTreeMap<(Instant, usize), Arc<Timer>>,
#[cfg(feature = "crossbeam")]
fired: Vec<Sender<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> {
self.arm_timer(deadline, true).1
}
#[cfg(feature = "crossbeam")]
pub(crate) fn recv_deadline<T>(
&self,
receiver: &Receiver<T>,
deadline: Instant,
) -> Result<T, RecvTimeoutError> {
match receiver.try_recv() {
Ok(value) => return Ok(value),
Err(TryRecvError::Disconnected) => return Err(RecvTimeoutError::Disconnected),
Err(TryRecvError::Empty) => {}
}
if self.now() >= deadline {
return Err(RecvTimeoutError::Timeout);
}
let (timer, timeout) = self.arm_timer(deadline, false);
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();
}
}
receiver.try_recv().map_err(|err| match err {
TryRecvError::Empty => RecvTimeoutError::Timeout,
TryRecvError::Disconnected => RecvTimeoutError::Disconnected,
})
}
}
}
#[cfg(feature = "crossbeam")]
fn arm_timer(&self, deadline: Instant, retain: bool) -> (Arc<Timer>, Receiver<Instant>) {
let (sender, receiver) = crossbeam_channel::bounded(1);
let timer = Arc::new(Timer {
deadline,
sender,
retain,
});
let mut state = self.lock();
if deadline > state.now {
state.timers.insert(timer.key(), timer.clone());
self.changed.notify_all();
drop(state);
} else {
drop(state);
self.fire(&timer);
}
(timer, receiver)
}
#[cfg(feature = "crossbeam")]
fn fire(&self, timer: &Timer) {
let delivered = timer.sender.send(timer.deadline).is_ok();
#[cfg(test)]
{
let hook = self.after_timer_send.lock().unwrap().take();
if let Some(hook) = hook {
hook();
}
}
if delivered && timer.retain {
self.lock().fired.push(timer.sender.clone());
}
}
pub(crate) fn now(&self) -> Instant {
self.lock().now
}
pub(crate) fn system_time(&self) -> SystemTime {
self.lock().system_time
}
pub(crate) fn advanced(&self) -> Duration {
self.lock().now - self.start
}
pub(crate) fn register(&self, signal: &Arc<Signal>) {
self.lock()
.signals
.insert(Arc::as_ptr(signal) as usize, Arc::downgrade(signal));
}
pub(crate) fn unregister(&self, signal: &Arc<Signal>) {
self.lock().signals.remove(&(Arc::as_ptr(signal) as usize));
}
pub(crate) fn block(&self, deadline: Option<Instant>) -> Blocked<'_> {
let mut state = self.lock();
state.blocked += 1;
if let Some(deadline) = deadline {
*state.deadlines.entry(deadline).or_default() += 1;
}
self.changed.notify_all();
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>,
}
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;