darkbio_clock/timers.rs
1// clock-rs: virtual clock for testing blocking code
2// Copyright 2026 Dark Bio AG. All rights reserved.
3//
4// Use of this source code is governed by a BSD-style
5// license that can be found in the LICENSE file.
6
7//! Crossbeam timers and receive deadlines that follow a clock.
8
9use crate::Clock;
10use crossbeam_channel::{Receiver, RecvTimeoutError};
11use std::time::{Duration, Instant};
12
13impl Clock {
14 /// Returns a receiver that gets the instant `duration` from now once this
15 /// clock reaches it.
16 ///
17 /// It mirrors `crossbeam_channel::after`, which it is on the real clock. On
18 /// a test clock, an advance to the deadline sends it, and the channel stays
19 /// connected while the clock lives. A duration past the end of `Instant`'s
20 /// range returns a receiver that never fires.
21 ///
22 /// Wait for a job or a timeout, driven from a test through `wait_timers`,
23 /// since `wait_blocked` cannot see a thread blocked in `select!`:
24 ///
25 /// ```
26 /// # #[cfg(feature = "test-clock")] {
27 /// use darkbio_clock::TestClock;
28 /// use darkbio_clock::crossbeam_channel::{select, unbounded};
29 /// use std::thread;
30 /// use std::time::Duration;
31 ///
32 /// let mut tester = TestClock::new();
33 /// let clock = tester.clock();
34 /// let (_jobs, queue) = unbounded::<u32>();
35 ///
36 /// let worker = thread::spawn(move || {
37 /// select! {
38 /// recv(queue) -> job => job.ok(),
39 /// recv(clock.after(Duration::from_secs(5))) -> _ => None,
40 /// }
41 /// });
42 /// tester.wait_timers(1);
43 /// tester.advance(Duration::from_secs(5));
44 /// assert_eq!(worker.join().unwrap(), None);
45 /// # }
46 /// ```
47 #[cfg_attr(docsrs, doc(cfg(feature = "crossbeam")))]
48 pub fn after(&self, duration: Duration) -> Receiver<Instant> {
49 #[cfg(any(test, feature = "test-clock"))]
50 if let Some(paused) = &self.paused {
51 return match paused.now().checked_add(duration) {
52 Some(deadline) => paused.at(deadline),
53 None => crossbeam_channel::never(),
54 };
55 }
56 crossbeam_channel::after(duration)
57 }
58
59 /// Returns a receiver that gets `deadline` once this clock reaches it, at
60 /// once if it already has.
61 ///
62 /// It mirrors `crossbeam_channel::at`, which it is on the real clock. On a
63 /// test clock, an advance to the deadline sends it, even one that overshoots,
64 /// and the channel stays connected while the clock lives.
65 #[cfg_attr(docsrs, doc(cfg(feature = "crossbeam")))]
66 pub fn at(&self, deadline: Instant) -> Receiver<Instant> {
67 #[cfg(any(test, feature = "test-clock"))]
68 if let Some(paused) = &self.paused {
69 return paused.at(deadline);
70 }
71 crossbeam_channel::at(deadline)
72 }
73
74 /// Receives a message, waiting up to `timeout` on this clock.
75 ///
76 /// It mirrors crossbeam's `Receiver::recv_timeout`, which it is on the real
77 /// clock. A buffered message or a disconnection wins over the timeout. A
78 /// timeout past the end of `Instant`'s range waits like `Receiver::recv`.
79 #[cfg_attr(docsrs, doc(cfg(feature = "crossbeam")))]
80 pub fn recv_timeout<T>(
81 &self,
82 receiver: &Receiver<T>,
83 timeout: Duration,
84 ) -> Result<T, RecvTimeoutError> {
85 #[cfg(any(test, feature = "test-clock"))]
86 if let Some(paused) = &self.paused {
87 return match paused.now().checked_add(timeout) {
88 Some(deadline) => paused.recv_deadline(receiver, deadline),
89 None => receiver.recv().map_err(RecvTimeoutError::from),
90 };
91 }
92 receiver.recv_timeout(timeout)
93 }
94
95 /// Receives a message, waiting until this clock reaches `deadline`.
96 ///
97 /// It mirrors crossbeam's `Receiver::recv_deadline`, which it is on the real
98 /// clock. A buffered message or a disconnection wins over the deadline. On a
99 /// test clock, a waiting receive arms a timer, which `wait_timers` counts
100 /// until it fires or the receive returns.
101 #[cfg_attr(docsrs, doc(cfg(feature = "crossbeam")))]
102 pub fn recv_deadline<T>(
103 &self,
104 receiver: &Receiver<T>,
105 deadline: Instant,
106 ) -> Result<T, RecvTimeoutError> {
107 #[cfg(any(test, feature = "test-clock"))]
108 if let Some(paused) = &self.paused {
109 return paused.recv_deadline(receiver, deadline);
110 }
111 receiver.recv_deadline(deadline)
112 }
113}
114
115// The tests live in src/tests, loaded from here so that they keep this
116// module's private items in reach
117#[cfg(all(test, not(loom)))]
118#[cfg_attr(coverage_nightly, coverage(off))]
119#[path = "tests/timers.rs"]
120mod tests;