1use std::fmt;
34use std::future::{Future, poll_fn};
35use std::panic::Location;
36use std::pin::{Pin, pin};
37use std::task::Poll;
38use std::time::{Duration, Instant};
39
40use test_better_core::{ErrorKind, TestError, TestResult};
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub struct Elapsed {
48 pub limit: Duration,
50}
51
52impl fmt::Display for Elapsed {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 write!(f, "future did not complete within {:?}", self.limit)
55 }
56}
57
58impl std::error::Error for Elapsed {}
59
60#[diagnostic::on_unimplemented(
68 message = "this async timing assertion needs a runtime, but no runtime feature is enabled",
69 note = "enable exactly one runtime feature on `test-better`: `tokio`, `async-std`, or `smol`",
70 note = "or, for `eventually`, use the runtime-free `eventually_blocking`"
71)]
72pub trait RuntimeAvailable {}
73
74#[cfg(any(feature = "tokio", feature = "async-std", feature = "smol"))]
75impl<T: ?Sized> RuntimeAvailable for T {}
76
77#[cfg(feature = "tokio")]
84fn selected_sleep(duration: Duration) -> Pin<Box<dyn Future<Output = ()> + Send>> {
85 Box::pin(async move { tokio::time::sleep(duration).await })
86}
87
88#[cfg(all(feature = "async-std", not(feature = "tokio")))]
89fn selected_sleep(duration: Duration) -> Pin<Box<dyn Future<Output = ()> + Send>> {
90 Box::pin(async move { async_std::task::sleep(duration).await })
91}
92
93#[cfg(all(feature = "smol", not(any(feature = "tokio", feature = "async-std"))))]
94fn selected_sleep(duration: Duration) -> Pin<Box<dyn Future<Output = ()> + Send>> {
95 Box::pin(async move {
96 smol::Timer::after(duration).await;
97 })
98}
99
100#[cfg(not(any(feature = "tokio", feature = "async-std", feature = "smol")))]
101fn selected_sleep(_duration: Duration) -> Pin<Box<dyn Future<Output = ()> + Send>> {
102 Box::pin(std::future::pending())
103}
104
105async fn race<F, S>(fut: F, timer: S) -> Result<F::Output, ()>
113where
114 F: Future,
115 S: Future<Output = ()>,
116{
117 let mut fut = pin!(fut);
118 let mut timer = pin!(timer);
119 poll_fn(move |cx| {
120 if let Poll::Ready(output) = fut.as_mut().poll(cx) {
121 return Poll::Ready(Ok(output));
122 }
123 if timer.as_mut().poll(cx).is_ready() {
124 return Poll::Ready(Err(()));
125 }
126 Poll::Pending
127 })
128 .await
129}
130
131pub async fn run_within<F>(limit: Duration, fut: F) -> Result<F::Output, Elapsed>
138where
139 F: Future + RuntimeAvailable,
140{
141 match race(fut, selected_sleep(limit)).await {
142 Ok(output) => Ok(output),
143 Err(()) => Err(Elapsed { limit }),
144 }
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub struct Backoff {
156 pub initial: Duration,
158 pub ceiling: Duration,
160 pub factor: u32,
163}
164
165impl Default for Backoff {
166 fn default() -> Self {
170 Self {
171 initial: Duration::from_millis(1),
172 ceiling: Duration::from_millis(100),
173 factor: 2,
174 }
175 }
176}
177
178impl Backoff {
179 fn next_nap(&self, previous: Duration) -> Duration {
182 previous.saturating_mul(self.factor).min(self.ceiling)
183 }
184}
185
186fn eventually_error(
190 timeout: Duration,
191 elapsed: Duration,
192 probes: u32,
193 location: &'static Location<'static>,
194) -> TestError {
195 let plural = if probes == 1 { "" } else { "s" };
196 TestError::new(ErrorKind::Timeout)
197 .with_message(format!(
198 "condition was not met within {timeout:?}: gave up after {probes} probe{plural} \
199 over {elapsed:?}"
200 ))
201 .with_location(location)
202}
203
204async fn eventually_impl<F, Fut>(
207 timeout: Duration,
208 backoff: Backoff,
209 mut probe: F,
210 location: &'static Location<'static>,
211) -> TestResult
212where
213 F: FnMut() -> Fut,
214 Fut: Future<Output = bool>,
215{
216 let start = Instant::now();
217 let mut nap = backoff.initial;
218 let mut probes: u32 = 0;
219 loop {
220 probes = probes.saturating_add(1);
221 if probe().await {
222 return Ok(());
223 }
224 let elapsed = start.elapsed();
225 if elapsed >= timeout {
226 return Err(eventually_error(timeout, elapsed, probes, location));
227 }
228 selected_sleep(nap.min(timeout - elapsed)).await;
229 nap = backoff.next_nap(nap);
230 }
231}
232
233fn eventually_blocking_impl<F>(
235 timeout: Duration,
236 backoff: Backoff,
237 mut probe: F,
238 location: &'static Location<'static>,
239) -> TestResult
240where
241 F: FnMut() -> bool,
242{
243 let start = Instant::now();
244 let mut nap = backoff.initial;
245 let mut probes: u32 = 0;
246 loop {
247 probes = probes.saturating_add(1);
248 if probe() {
249 return Ok(());
250 }
251 let elapsed = start.elapsed();
252 if elapsed >= timeout {
253 return Err(eventually_error(timeout, elapsed, probes, location));
254 }
255 std::thread::sleep(nap.min(timeout - elapsed));
256 nap = backoff.next_nap(nap);
257 }
258}
259
260#[track_caller]
288pub fn eventually<F, Fut>(timeout: Duration, probe: F) -> impl Future<Output = TestResult>
289where
290 F: FnMut() -> Fut + RuntimeAvailable,
291 Fut: Future<Output = bool>,
292{
293 eventually_impl(timeout, Backoff::default(), probe, Location::caller())
294}
295
296#[track_caller]
298pub fn eventually_with<F, Fut>(
299 timeout: Duration,
300 backoff: Backoff,
301 probe: F,
302) -> impl Future<Output = TestResult>
303where
304 F: FnMut() -> Fut + RuntimeAvailable,
305 Fut: Future<Output = bool>,
306{
307 eventually_impl(timeout, backoff, probe, Location::caller())
308}
309
310#[track_caller]
332pub fn eventually_blocking<F>(timeout: Duration, probe: F) -> TestResult
333where
334 F: FnMut() -> bool,
335{
336 eventually_blocking_impl(timeout, Backoff::default(), probe, Location::caller())
337}
338
339#[track_caller]
342pub fn eventually_blocking_with<F>(timeout: Duration, backoff: Backoff, probe: F) -> TestResult
343where
344 F: FnMut() -> bool,
345{
346 eventually_blocking_impl(timeout, backoff, probe, Location::caller())
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352 use std::cell::Cell;
353 use std::future::{pending, ready};
354
355 use test_better_matchers::{check, eq, ge, is_true};
356
357 #[test]
358 fn race_returns_the_future_output_when_it_is_ready_first() -> TestResult {
359 let outcome = pollster::block_on(race(ready(7), pending::<()>()));
362 check!(outcome).satisfies(eq(Ok(7)))
363 }
364
365 #[test]
366 fn race_reports_the_timer_when_the_future_is_not_ready() -> TestResult {
367 let outcome = pollster::block_on(race(pending::<i32>(), ready(())));
368 check!(outcome).satisfies(eq(Err(())))
369 }
370
371 #[test]
372 fn race_prefers_the_future_when_both_are_ready() -> TestResult {
373 let outcome = pollster::block_on(race(ready("done"), ready(())));
375 check!(outcome).satisfies(eq(Ok("done")))
376 }
377
378 #[test]
379 fn backoff_grows_by_factor_and_stops_at_the_ceiling() -> TestResult {
380 let backoff = Backoff {
381 initial: Duration::from_millis(10),
382 ceiling: Duration::from_millis(25),
383 factor: 2,
384 };
385 check!(backoff.next_nap(Duration::from_millis(10)))
386 .satisfies(eq(Duration::from_millis(20)))?;
387 check!(backoff.next_nap(Duration::from_millis(20))).satisfies(eq(Duration::from_millis(25)))
389 }
390
391 #[test]
392 fn eventually_blocking_stops_as_soon_as_the_probe_passes() -> TestResult {
393 let calls = Cell::new(0u32);
394 eventually_blocking(Duration::from_secs(5), || {
395 calls.set(calls.get() + 1);
396 calls.get() >= 3
397 })?;
398 check!(calls.get()).satisfies(eq(3))
400 }
401
402 #[test]
403 fn eventually_blocking_passes_immediately_when_the_probe_is_already_true() -> TestResult {
404 let calls = Cell::new(0u32);
405 eventually_blocking(Duration::from_secs(5), || {
406 calls.set(calls.get() + 1);
407 true
408 })?;
409 check!(calls.get()).satisfies(eq(1))
410 }
411
412 #[test]
413 fn eventually_blocking_reports_elapsed_and_probe_count_on_timeout() -> TestResult {
414 let calls = Cell::new(0u32);
415 let error = eventually_blocking_with(
416 Duration::from_millis(40),
417 Backoff {
418 initial: Duration::from_millis(5),
419 ceiling: Duration::from_millis(5),
420 factor: 2,
421 },
422 || {
423 calls.set(calls.get() + 1);
424 false
425 },
426 )
427 .expect_err("a probe that is never true must time out");
428 let rendered = error.to_string();
429 check!(rendered.contains("was not met within")).satisfies(is_true())?;
430 check!(calls.get()).satisfies(ge(2))?;
433 check!(rendered.contains(&format!("{} probe", calls.get()))).satisfies(is_true())
434 }
435
436 #[test]
437 fn eventually_blocking_failure_kind_is_timeout() -> TestResult {
438 let error = eventually_blocking(Duration::from_millis(1), || false)
439 .expect_err("an always-false probe times out");
440 check!(error.kind).satisfies(eq(ErrorKind::Timeout))
441 }
442}