Skip to main content

ic_testkit/pic/
time.rs

1use crate::pocket_ic::ClientError;
2use crate::pocket_ic::PocketIc;
3use std::{task::Poll, time::Duration};
4
5/// A caller-owned readiness check failed or exhausted its simulated round budget.
6#[derive(Debug, thiserror::Error)]
7pub enum TickUntilError<E> {
8    /// A simulated progression operation failed; no remaining ticks are dispatched.
9    #[error("PocketIC progression failed: {0}")]
10    Progress(#[source] ClientError),
11    /// The predicate was still pending after this many advance/tick rounds.
12    #[error("readiness remained pending after {rounds} rounds")]
13    ProgressLimit {
14        /// Completed simulated advance/tick rounds.
15        rounds: u32,
16    },
17    /// The predicate returned its own failure.
18    #[error("readiness check failed: {0}")]
19    Failed(#[source] E),
20}
21
22/// Advance simulated time once, then deliver exactly the caller-selected ticks.
23///
24/// Zero duration performs no time read or write; zero ticks permits advancement
25/// without delivering a round. This function does not poll, retry, start a server
26/// or decide application readiness. Callers own completion and cadence budgets.
27///
28/// # Errors
29/// Returns the first client failure without dispatching remaining ticks.
30pub fn advance_and_tick(pic: &PocketIc, advance: Duration, ticks: u32) -> Result<(), ClientError> {
31    if !advance.is_zero() {
32        pic.advance_time(advance)?;
33    }
34    for _ in 0..ticks {
35        pic.tick()?;
36    }
37    Ok(())
38}
39
40/// Check a caller-owned predicate, advancing simulated time and ticking while pending.
41///
42/// The predicate runs once before any mutation. Each pending result permits one
43/// round of `advance_time(advance)`, then `tick()`, then another predicate check,
44/// up to `max_rounds`. A zero budget still performs the initial check. A zero
45/// duration permits ticks without reading or writing simulated time. Completion
46/// on the last allowed round succeeds; predicate failures return immediately without another round.
47///
48/// This bounds simulated progression, not wall-clock time spent inside `PocketIC`
49/// calls or the predicate. Client failures return immediately as `Progress`.
50/// Applications own readiness meaning and predicate error types.
51///
52/// # Errors
53/// Returns the predicate error or `ProgressLimit` when all permitted rounds remain pending.
54pub fn tick_until<T, E>(
55    pic: &PocketIc,
56    max_rounds: u32,
57    advance: Duration,
58    mut poll: impl FnMut(&PocketIc) -> Poll<Result<T, E>>,
59) -> Result<T, TickUntilError<E>> {
60    poll_with_rounds(
61        max_rounds,
62        || advance_and_tick(pic, advance, 1),
63        || poll(pic),
64    )
65}
66
67fn poll_with_rounds<T, E>(
68    max_rounds: u32,
69    mut round: impl FnMut() -> Result<(), ClientError>,
70    mut poll: impl FnMut() -> Poll<Result<T, E>>,
71) -> Result<T, TickUntilError<E>> {
72    let mut rounds = 0;
73    loop {
74        match poll() {
75            Poll::Ready(result) => return result.map_err(TickUntilError::Failed),
76            Poll::Pending if rounds == max_rounds => {
77                return Err(TickUntilError::ProgressLimit { rounds });
78            }
79            Poll::Pending => {
80                round().map_err(TickUntilError::Progress)?;
81                rounds += 1;
82            }
83        }
84    }
85}
86
87#[cfg(test)]
88mod tests;