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;