Skip to main content

reth_e2e_test_utils/
wait.rs

1//! Helpers for waiting on conditions of test nodes.
2
3use eyre::{ensure, eyre};
4use std::{fmt::Display, future::Future, time::Duration};
5use tokio::time::Instant;
6
7/// Maximum time the wait helpers wait for a condition, e.g. for a node to sync to or commit a
8/// block.
9pub const WAIT_TIMEOUT: Duration = Duration::from_secs(60);
10
11/// Interval at which [`poll_until`] polls, [`assert_holds_for`] checks and
12/// [`NodeTestContext::advance_while`] advances the chain.
13///
14/// [`NodeTestContext::advance_while`]: crate::node::NodeTestContext::advance_while
15pub const POLL_INTERVAL: Duration = Duration::from_millis(20);
16
17/// Calls `poll` every [`POLL_INTERVAL`] until it returns a value.
18///
19/// Returns an error describing `what` was awaited if the condition is not met within
20/// [`WAIT_TIMEOUT`], or the first error returned by `poll`. This is [`poll_until_with`] with the
21/// default [`PollOpts`], see there for when `poll` is called.
22///
23/// `what` is held across awaits, so it must be `Send` for the returned future to be `Send`. This
24/// rules out `format_args!`, use `format!` instead.
25pub async fn poll_until<T, F, Fut>(what: impl Display + Send, poll: F) -> eyre::Result<T>
26where
27    F: FnMut() -> Fut,
28    Fut: Future<Output = eyre::Result<Option<T>>>,
29{
30    poll_until_with(PollOpts::default(), what, poll).await
31}
32
33/// Calls `poll` until it returns a value, with the timeout and interval of `opts`.
34///
35/// The first poll starts immediately, and every further poll `opts.interval` after the previous
36/// one returned `None`. The wait ends `opts.timeout` after the call: a poll that is still running
37/// is dropped, and there is no final poll at the deadline, so a condition that only becomes true
38/// during the last interval is reported as timed out, and an interval at least as long as the
39/// timeout polls once.
40///
41/// Returns the value of the first poll that returns `Some`, the first error returned by `poll`, or
42/// an error naming `what` and the timeout, e.g. `timed out after 30s waiting for block 1`.
43///
44/// `what` is held across awaits, so it must be `Send` for the returned future to be `Send`. This
45/// rules out `format_args!`, use `format!` instead.
46///
47/// ```ignore
48/// let receipt = poll_until_with(
49///     PollOpts { interval: Duration::from_secs(1), ..Default::default() },
50///     format!("receipt of transaction {tx_hash}"),
51///     || async { Ok(provider.get_transaction_receipt(tx_hash).await?) },
52/// )
53/// .await?;
54/// ```
55pub async fn poll_until_with<T, F, Fut>(
56    opts: PollOpts,
57    what: impl Display + Send,
58    mut poll: F,
59) -> eyre::Result<T>
60where
61    F: FnMut() -> Fut,
62    Fut: Future<Output = eyre::Result<Option<T>>>,
63{
64    let PollOpts { timeout, interval } = opts;
65    let wait = async {
66        loop {
67            if let Some(value) = poll().await? {
68                return Ok(value)
69            }
70            tokio::time::sleep(interval).await;
71        }
72    };
73    tokio::time::timeout(timeout, wait)
74        .await
75        .map_err(|_| eyre!("timed out after {timeout:?} waiting for {what}"))?
76}
77
78/// Asserts that the condition checked by `check` holds for `duration`.
79///
80/// Use this instead of a fixed sleep followed by an assertion to verify that something does not
81/// happen, e.g. that a transaction stays in the pool. `check` returns whether the condition still
82/// holds, as a future like the poll of [`poll_until`], so it can query the node over RPC and
83/// return errors with `?`.
84///
85/// The first check starts immediately, every further check [`POLL_INTERVAL`] after the previous
86/// one returned, and the last check starts at `duration` after the call, so the condition is
87/// verified at both ends of `duration`. A condition that breaks and recovers between two checks
88/// goes unnoticed. Checks are not cancelled, so a successful call takes `duration` plus the time of
89/// the last check.
90///
91/// Returns the first error returned by `check`, or an error as soon as a check returns `false`,
92/// naming `what` and the time from the call to the start of that check, e.g. `expected
93/// transaction to stay in the pool for 2s, but it did not hold after 1.24s`.
94///
95/// `what` is held across awaits, so it must be `Send` for the returned future to be `Send`. This
96/// rules out `format_args!`, use `format!` instead.
97pub async fn assert_holds_for<F, Fut>(
98    duration: Duration,
99    what: impl Display + Send,
100    mut check: F,
101) -> eyre::Result<()>
102where
103    F: FnMut() -> Fut,
104    Fut: Future<Output = eyre::Result<bool>>,
105{
106    let start = Instant::now();
107    let deadline = start + duration;
108    loop {
109        let checked_at = Instant::now();
110        ensure!(
111            check().await?,
112            "expected {what} for {duration:?}, but it did not hold after {:?}",
113            checked_at - start
114        );
115        if checked_at >= deadline {
116            return Ok(())
117        }
118        tokio::time::sleep_until(deadline.min(Instant::now() + POLL_INTERVAL)).await;
119    }
120}
121
122/// Timeout and interval of a single [`poll_until_with`] call.
123///
124/// The default is [`WAIT_TIMEOUT`] and [`POLL_INTERVAL`], which [`poll_until`] uses, so set only
125/// what differs, e.g. `PollOpts { interval: Duration::from_secs(1), ..Default::default() }`.
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub struct PollOpts {
128    /// Maximum time to wait for the condition, from the start of the wait.
129    pub timeout: Duration,
130    /// Time between a poll returning `None` and the start of the next poll.
131    pub interval: Duration,
132}
133
134impl Default for PollOpts {
135    fn default() -> Self {
136        Self { timeout: WAIT_TIMEOUT, interval: POLL_INTERVAL }
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[tokio::test]
145    async fn poll_until_with_times_out_without_final_poll() {
146        let mut polls = 0;
147        let opts = PollOpts { timeout: Duration::from_millis(50), interval: WAIT_TIMEOUT };
148        let err = poll_until_with(opts, "condition", || {
149            polls += 1;
150            async { Ok(None::<()>) }
151        })
152        .await
153        .unwrap_err();
154        assert_eq!(err.to_string(), "timed out after 50ms waiting for condition");
155        assert_eq!(polls, 1);
156    }
157
158    #[tokio::test]
159    async fn assert_holds_for_returns_after_duration() {
160        let duration = Duration::from_millis(50);
161        let start = Instant::now();
162        assert_holds_for(duration, "condition", || async { Ok(true) }).await.unwrap();
163        assert!(start.elapsed() >= duration);
164    }
165
166    #[tokio::test]
167    async fn assert_holds_for_fails_when_condition_breaks() {
168        let mut checks = 0;
169        let err = assert_holds_for(WAIT_TIMEOUT, "condition", || {
170            checks += 1;
171            let holds = checks < 2;
172            async move { Ok(holds) }
173        })
174        .await
175        .unwrap_err()
176        .to_string();
177        assert!(
178            err.starts_with("expected condition for 60s, but it did not hold after "),
179            "unexpected error: {err}"
180        );
181        assert_eq!(checks, 2);
182    }
183
184    #[tokio::test]
185    async fn assert_holds_for_checks_at_deadline() {
186        let duration = Duration::from_millis(50);
187        let deadline = Instant::now() + duration;
188        let result = assert_holds_for(duration, "condition", || {
189            let holds = Instant::now() < deadline;
190            async move { Ok(holds) }
191        })
192        .await;
193        assert!(result.is_err(), "condition broken at the deadline was not checked");
194    }
195}