1use pocket_ic::PocketIc;
2use std::{fmt, task::Poll, time::Duration};
3
4#[derive(Clone, Debug, Eq, PartialEq)]
6pub enum TickUntilError<E> {
7 ProgressLimit { rounds: u32 },
9 Failed(E),
11}
12
13impl<E: fmt::Display> fmt::Display for TickUntilError<E> {
14 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
15 match self {
16 Self::ProgressLimit { rounds } => {
17 write!(
18 formatter,
19 "readiness remained pending after {rounds} rounds"
20 )
21 }
22 Self::Failed(error) => write!(formatter, "readiness check failed: {error}"),
23 }
24 }
25}
26
27impl<E: std::error::Error + 'static> std::error::Error for TickUntilError<E> {
28 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
29 match self {
30 Self::ProgressLimit { .. } => None,
31 Self::Failed(error) => Some(error),
32 }
33 }
34}
35
36pub fn tick_until<T, E>(
48 pic: &PocketIc,
49 max_rounds: u32,
50 advance: Duration,
51 mut poll: impl FnMut(&PocketIc) -> Poll<Result<T, E>>,
52) -> Result<T, TickUntilError<E>> {
53 poll_with_rounds(
54 max_rounds,
55 || {
56 pic.advance_time(advance);
57 pic.tick();
58 },
59 || poll(pic),
60 )
61}
62
63fn poll_with_rounds<T, E>(
64 max_rounds: u32,
65 mut round: impl FnMut(),
66 mut poll: impl FnMut() -> Poll<Result<T, E>>,
67) -> Result<T, TickUntilError<E>> {
68 let mut rounds = 0;
69 loop {
70 match poll() {
71 Poll::Ready(result) => return result.map_err(TickUntilError::Failed),
72 Poll::Pending if rounds == max_rounds => {
73 return Err(TickUntilError::ProgressLimit { rounds });
74 }
75 Poll::Pending => {
76 round();
77 rounds += 1;
78 }
79 }
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86 use std::cell::Cell;
87
88 #[test]
89 fn initial_completion_and_failure_do_not_advance() {
90 for budget in [0, 3] {
91 for result in [Ok(42), Err("predicate failure")] {
92 assert_eq!(
93 poll_with_rounds(
94 budget,
95 || panic!("unexpected round"),
96 || Poll::Ready(result)
97 ),
98 result.map_err(TickUntilError::Failed)
99 );
100 }
101 }
102 }
103
104 #[test]
105 fn completion_on_last_round_preserves_poll_order() {
106 let rounds = Cell::new(0);
107 let polls = Cell::new(0);
108 let result = poll_with_rounds(
109 3,
110 || {
111 assert_eq!(polls.get(), rounds.get() + 1);
112 rounds.set(rounds.get() + 1);
113 },
114 || {
115 assert_eq!(polls.get(), rounds.get());
116 polls.set(polls.get() + 1);
117 if rounds.get() == 3 {
118 Poll::Ready(Ok::<_, ()>(42))
119 } else {
120 Poll::Pending
121 }
122 },
123 );
124 assert_eq!(result, Ok(42));
125 assert_eq!((rounds.get(), polls.get()), (3, 4));
126 }
127
128 #[test]
129 fn exhaustion_performs_exactly_the_allowed_rounds() {
130 for budget in [0, 1, 3] {
131 let rounds = Cell::new(0);
132 let polls = Cell::new(0);
133 let result = poll_with_rounds(
134 budget,
135 || rounds.set(rounds.get() + 1),
136 || {
137 polls.set(polls.get() + 1);
138 Poll::<Result<(), ()>>::Pending
139 },
140 );
141 assert_eq!(
142 result,
143 Err(TickUntilError::ProgressLimit { rounds: budget })
144 );
145 assert_eq!((rounds.get(), polls.get()), (budget, budget + 1));
146 }
147 }
148
149 #[test]
150 fn failure_after_progress_preserves_the_original_cause() {
151 let rounds = Cell::new(0);
152 let result = poll_with_rounds(
153 3,
154 || rounds.set(rounds.get() + 1),
155 || {
156 if rounds.get() == 1 {
157 Poll::Ready(Err::<(), _>(std::io::Error::from(
158 std::io::ErrorKind::PermissionDenied,
159 )))
160 } else {
161 Poll::Pending
162 }
163 },
164 );
165 let error = result.unwrap_err();
166 let source = std::error::Error::source(&error).unwrap();
167 assert_eq!(
168 source.downcast_ref::<std::io::Error>().unwrap().kind(),
169 std::io::ErrorKind::PermissionDenied
170 );
171 assert_eq!(rounds.get(), 1);
172 }
173}