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;