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 async fn advance_and_tick(
31    pic: &PocketIc,
32    advance: Duration,
33    ticks: u32,
34) -> Result<(), ClientError> {
35    if !advance.is_zero() {
36        pic.advance_time(advance).await?;
37    }
38    for _ in 0..ticks {
39        pic.tick().await?;
40    }
41    Ok(())
42}
43
44/// Check a caller-owned predicate, advancing simulated time and ticking while pending.
45///
46/// The predicate runs once before any mutation. Each pending result permits one
47/// round of `advance_time(advance)`, then `tick()`, then another predicate check,
48/// up to `max_rounds`. A zero budget still performs the initial check. A zero
49/// duration permits ticks without reading or writing simulated time. Completion
50/// on the last allowed round succeeds; predicate failures return immediately without another round.
51///
52/// This bounds simulated progression, not wall-clock time spent inside `PocketIC`
53/// calls or the predicate. Client failures return immediately as `Progress`.
54/// Applications own readiness meaning and predicate error types.
55///
56/// # Errors
57/// Returns the predicate error or `ProgressLimit` when all permitted rounds remain pending.
58pub async fn tick_until<T, E>(
59    pic: &PocketIc,
60    max_rounds: u32,
61    advance: Duration,
62    mut poll: impl AsyncFnMut(&PocketIc) -> Poll<Result<T, E>>,
63) -> Result<T, TickUntilError<E>> {
64    poll_with_rounds(
65        max_rounds,
66        async || advance_and_tick(pic, advance, 1).await,
67        async || poll(pic).await,
68    )
69    .await
70}
71
72async fn poll_with_rounds<T, E>(
73    max_rounds: u32,
74    mut round: impl AsyncFnMut() -> Result<(), ClientError>,
75    mut poll: impl AsyncFnMut() -> Poll<Result<T, E>>,
76) -> Result<T, TickUntilError<E>> {
77    let mut rounds = 0;
78    loop {
79        match poll().await {
80            Poll::Ready(result) => return result.map_err(TickUntilError::Failed),
81            Poll::Pending if rounds == max_rounds => {
82                return Err(TickUntilError::ProgressLimit { rounds });
83            }
84            Poll::Pending => {
85                round().await.map_err(TickUntilError::Progress)?;
86                rounds += 1;
87            }
88        }
89    }
90}
91
92#[cfg(test)]
93mod tests;