repeated_assert/
lib.rs

1//! Run assertions multiple times
2//!
3//! **This crate currently requires the nightly version of the Rust compiler.**
4//!
5//! `repeated_assert` runs assertions until they either pass
6//! or the maximum amount of repetitions has been reached.
7//! The current thread will be blocked between tries.
8//!
9//! This is useful when waiting for events from another thread (or process).
10//! Waiting for a short time might result in a failing test, while waiting too long is a waste of time.
11//!
12//! # Crate features
13//!
14//! * **async** - Enables the `that_async` and `with_catch_async` functions. It depends on the `futures` and `tokio` crates, which is why it's disabled by default.
15//!
16//! # Examples
17//!
18//! Waiting for a file to appear (re-try up to 10 times, wait 50 ms between tries)
19//!
20//! ```rust,ignore
21//! repeated_assert::that(10, Duration::from_millis(50), || {
22//!     assert!(Path::new("should_appear_soon.txt").exists());
23//! });
24//! ```
25//!
26//! Waiting for variable `x` to equal `3`
27//!
28//! ```rust,ignore
29//! repeated_assert::that(10, Duration::from_millis(50), || {
30//!     assert_eq!(x, 3);
31//! });
32//! ```
33//!
34//! Temporary variables
35//!
36//! ```rust,ignore
37//! repeated_assert::that(10, Duration::from_millis(50), || {
38//!     let checksum = crc("should_appear_soon.txt");
39//!     assert_eq!(checksum, 1234);
40//! });
41//! ```
42//!
43//! Return result
44//!
45//! ```rust,ignore
46//! repeated_assert::that(10, Duration::from_millis(50), || -> Result<_, Box<dyn std::error::Error>> {
47//!     let checksum = crc("should_appear_soon.txt")?;
48//!     assert_eq!(checksum, 1234);
49//! })?;
50//! ```
51//!
52//! Async
53//!
54//! ```rust,ignore
55//! repeated_assert::that_async(10, Duration::from_millis(50), || async {
56//!     let status = query_db().await;
57//!     assert_eq!(status, "success");
58//! }).await;
59//! ```
60//!
61//! # Catch failing tests
62//!
63//! It's also possible to "catch" failing tests by executing some code if the expressions couldn't be asserted in order to trigger an alternate strategy.
64//! This can be useful if the tested program relies on an unreliable service.
65//! This counters the idea of a test to some degree, so use it only if the unreliable service is not critical for your program.
66//!
67//! Poke unreliable service after 5 unsuccessful assertion attempts
68//!
69//! ```rust,ignore
70//! repeated_assert::with_catch(10, Duration::from_millis(50), 5,
71//!     || {
72//!         // poke unreliable service
73//!     },
74//!     || {
75//!         assert!(Path::new("should_appear_soon.txt").exists());
76//!     }
77//! );
78//! ```
79use std::{
80    collections::HashSet,
81    panic,
82    sync::{Mutex, OnceLock},
83    thread,
84    time::Duration,
85};
86
87mod macros;
88
89fn ignore_threads() -> &'static Mutex<HashSet<String>> {
90    static INSTANCE: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
91    INSTANCE.get_or_init(|| {
92        // get original panic hook
93        let panic_hook = panic::take_hook();
94        // set custom panic hook
95        panic::set_hook(Box::new(move |panic_info| {
96            let ignore_threads = ignore_threads().lock().expect("lock ignore threads");
97            if let Some(thread_name) = thread::current().name() {
98                if !ignore_threads.contains(thread_name) {
99                    // call original panic hook
100                    panic_hook(panic_info);
101                }
102            } else {
103                // call original panic hook
104                panic_hook(panic_info);
105            }
106        }));
107        Mutex::new(HashSet::new())
108    })
109}
110
111struct IgnoreGuard;
112
113impl IgnoreGuard {
114    fn new() -> IgnoreGuard {
115        if let Some(thread_name) = thread::current().name() {
116            ignore_threads()
117                .lock()
118                .expect("lock ignore threads")
119                .insert(thread_name.to_string());
120        }
121        IgnoreGuard
122    }
123}
124
125impl Drop for IgnoreGuard {
126    fn drop(&mut self) {
127        if let Some(thread_name) = thread::current().name() {
128            ignore_threads()
129                .lock()
130                .expect("lock ignore threads")
131                .remove(thread_name);
132        }
133    }
134}
135
136/// Run the provided function `assert` up to `repetitions` times with a `delay` in between tries.
137///
138/// Panics (including failed assertions) will be caught and ignored until the last try is executed.
139///
140/// # Examples
141///
142/// Waiting for a file to appear (re-try up to 10 times, wait 50 ms between tries)
143///
144/// ```rust,ignore
145/// repeated_assert::that(10, Duration::from_millis(50), || {
146///     assert!(Path::new("should_appear_soon.txt").exists());
147/// });
148/// ```
149///
150/// # Info
151///
152/// Behind the scene `std::panic::set_hook` is used to set a custom panic handler.
153/// For every iteration but the last, panics are ignored and re-tried after a delay.
154/// Only when the last iteration is reached, panics are handled by the panic handler that was registered prior to calling `repeated_assert`.
155///
156/// The panic handler can only be registerd for the entire process, and it is done on demand the first time `repeated_assert` is used.
157/// `repeated_assert` works with multiple threads. Each thread is identified by its name, which is automatically set for tests.
158pub fn that<A, R>(repetitions: usize, delay: Duration, assert: A) -> R
159where
160    A: Fn() -> R,
161{
162    // add current thread to ignore list
163    let ignore_guard = IgnoreGuard::new();
164
165    for _ in 0..(repetitions - 1) {
166        // run assertions, catching panics
167        let result = panic::catch_unwind(panic::AssertUnwindSafe(&assert));
168        // return if assertions succeeded
169        if let Ok(value) = result {
170            return value;
171        }
172        // or sleep until the next try
173        thread::sleep(delay);
174    }
175
176    // remove current thread from ignore list
177    drop(ignore_guard);
178
179    // run assertions without catching panics
180    assert()
181}
182
183#[cfg(feature = "async")]
184// #[doc(cfg(feature = "async"))]
185pub async fn that_async<A, F, R>(repetitions: usize, delay: Duration, assert: A) -> R
186where
187    A: Fn() -> F,
188    F: std::future::Future<Output = R>,
189{
190    use futures::future::FutureExt;
191
192    // add current thread to ignore list
193    let ignore_guard = IgnoreGuard::new();
194
195    for _ in 0..(repetitions - 1) {
196        // run assertions, catching panics
197        let result = panic::AssertUnwindSafe(assert()).catch_unwind().await;
198        // return if assertions succeeded
199        if let Ok(value) = result {
200            return value;
201        }
202        // or sleep until the next try
203        tokio::time::sleep(delay).await;
204    }
205
206    // remove current thread from ignore list
207    drop(ignore_guard);
208
209    // run assertions without catching panics
210    assert().await
211}
212
213/// Run the provided function `assert` up to `repetitions` times with a `delay` in between tries.
214/// Execute the provided function `catch` after `repetitions_catch` failed tries in order to trigger an alternate strategy.
215///
216/// Panics (including failed assertions) will be caught and ignored until the last try is executed.
217///
218/// # Examples
219///
220/// ```rust,ignore
221/// repeated_assert::with_catch(10, Duration::from_millis(50), 5,
222///     || {
223///         // poke unreliable service
224///     },
225///     || {
226///         assert!(Path::new("should_appear_soon.txt").exists());
227///     }
228/// );
229/// ```
230///
231/// # Info
232///
233/// See [`that`].
234pub fn with_catch<A, C, R>(
235    repetitions: usize,
236    delay: Duration,
237    repetitions_catch: usize,
238    catch: C,
239    assert: A,
240) -> R
241where
242    A: Fn() -> R,
243    C: FnOnce(),
244{
245    let ignore_guard = IgnoreGuard::new();
246
247    for _ in 0..repetitions_catch {
248        // run assertions, catching panics
249        let result = panic::catch_unwind(panic::AssertUnwindSafe(&assert));
250        // return if assertions succeeded
251        if let Ok(value) = result {
252            return value;
253        }
254        // or sleep until the next try
255        thread::sleep(delay);
256    }
257
258    let thread_name = thread::current()
259        .name()
260        .unwrap_or("<unnamed thread>")
261        .to_string();
262    println!("{}: executing repeated-assert catch block", thread_name);
263    catch();
264
265    for _ in repetitions_catch..(repetitions - 1) {
266        // run assertions, catching panics
267        let result = panic::catch_unwind(panic::AssertUnwindSafe(&assert));
268        // return if assertions succeeded
269        if let Ok(value) = result {
270            return value;
271        }
272        // or sleep until the next try
273        thread::sleep(delay);
274    }
275
276    // remove current thread from ignore list
277    drop(ignore_guard);
278
279    // run assertions without catching panics
280    assert()
281}
282
283#[cfg(feature = "async")]
284// #[doc(cfg(feature = "async"))]
285pub async fn with_catch_async<A, F, C, G, R>(
286    repetitions: usize,
287    delay: Duration,
288    repetitions_catch: usize,
289    catch: C,
290    assert: A,
291) -> R
292where
293    A: Fn() -> F,
294    F: std::future::Future<Output = R>,
295    C: FnOnce() -> G,
296    G: std::future::Future<Output = ()>,
297{
298    use futures::future::FutureExt;
299
300    let ignore_guard = IgnoreGuard::new();
301
302    for _ in 0..repetitions_catch {
303        // run assertions, catching panics
304        let result = panic::AssertUnwindSafe(assert()).catch_unwind().await;
305        // return if assertions succeeded
306        if let Ok(value) = result {
307            return value;
308        }
309        // or sleep until the next try
310        tokio::time::sleep(delay).await;
311    }
312
313    let thread_name = thread::current()
314        .name()
315        .unwrap_or("<unnamed thread>")
316        .to_string();
317    println!("{}: executing repeated-assert catch block", thread_name);
318    catch().await;
319
320    for _ in repetitions_catch..(repetitions - 1) {
321        // run assertions, catching panics
322        let result = panic::AssertUnwindSafe(assert()).catch_unwind().await;
323        // return if assertions succeeded
324        if let Ok(value) = result {
325            return value;
326        }
327        // or sleep until the next try
328        tokio::time::sleep(delay).await;
329    }
330
331    // remove current thread from ignore list
332    drop(ignore_guard);
333
334    // run assertions without catching panics
335    assert().await
336}
337
338#[cfg(test)]
339mod tests {
340    use crate as repeated_assert;
341    use std::sync::{Arc, Mutex};
342    use std::thread;
343    use std::time::Duration;
344
345    static STEP_MS: u64 = 100;
346
347    fn spawn_thread(x: Arc<Mutex<i32>>) {
348        thread::spawn(move || loop {
349            thread::sleep(Duration::from_millis(10 * STEP_MS));
350            if let Ok(mut x) = x.lock() {
351                *x += 1;
352            }
353        });
354    }
355
356    // #[test]
357    // fn slow() {
358    //     let x = Arc::new(Mutex::new(0));
359
360    //     spawn_thread(x.clone());
361
362    //     repeated_assert::that(10, Duration::from_millis(10 * STEP_MS), || {
363    //         assert!(*x.lock().unwrap() > 5);
364    //     });
365    // }
366
367    // #[test]
368    // fn panic() {
369    //     let x = Arc::new(Mutex::new(0));
370
371    //     spawn_thread(x.clone());
372
373    //     repeated_assert::that(3, Duration::from_millis(10 * STEP_MS), || {
374    //         assert!(*x.lock().unwrap() > 5);
375    //     });
376    // }
377
378    #[test]
379    fn single_success() {
380        let x = Arc::new(Mutex::new(0));
381
382        spawn_thread(x.clone());
383
384        repeated_assert::that(5, Duration::from_millis(5 * STEP_MS), || {
385            assert!(*x.lock().unwrap() > 0);
386        });
387    }
388
389    #[cfg(feature = "async")]
390    #[tokio::test]
391    async fn single_success_async() {
392        let x = Arc::new(Mutex::new(0));
393
394        spawn_thread(x.clone());
395
396        repeated_assert::that_async(5, Duration::from_millis(5 * STEP_MS), || async {
397            assert!(*x.lock().unwrap() > 0);
398        })
399        .await;
400    }
401
402    #[test]
403    #[should_panic(expected = "assertion failed: *x.lock().unwrap() > 0")]
404    fn single_failure() {
405        let x = Arc::new(Mutex::new(0));
406
407        spawn_thread(x.clone());
408
409        repeated_assert::that(3, Duration::from_millis(STEP_MS), || {
410            assert!(*x.lock().unwrap() > 0);
411        });
412    }
413
414    #[cfg(feature = "async")]
415    #[tokio::test]
416    #[should_panic(expected = "assertion failed: *x.lock().unwrap() > 0")]
417    async fn single_failure_async() {
418        let x = Arc::new(Mutex::new(0));
419
420        spawn_thread(x.clone());
421
422        repeated_assert::that_async(3, Duration::from_millis(STEP_MS), || async {
423            assert!(*x.lock().unwrap() > 0);
424        })
425        .await;
426    }
427
428    #[test]
429    fn multiple_success() {
430        let x = Arc::new(Mutex::new(0));
431        let a = 11;
432        let b = 11;
433
434        spawn_thread(x.clone());
435
436        repeated_assert::that(5, Duration::from_millis(5 * STEP_MS), || {
437            assert!(*x.lock().unwrap() > 0);
438            assert_eq!(a, b);
439        });
440    }
441
442    #[cfg(feature = "async")]
443    #[tokio::test]
444    async fn multiple_success_async() {
445        let x = Arc::new(Mutex::new(0));
446        let a = 11;
447        let b = 11;
448
449        spawn_thread(x.clone());
450
451        repeated_assert::that_async(5, Duration::from_millis(5 * STEP_MS), || async {
452            assert!(*x.lock().unwrap() > 0);
453            assert_eq!(a, b);
454        })
455        .await;
456    }
457
458    #[test]
459    #[should_panic(expected = "assertion failed: *x.lock().unwrap() > 0")]
460    fn multiple_failure_1() {
461        let x = Arc::new(Mutex::new(0));
462        let a = 11;
463        let b = 11;
464
465        spawn_thread(x.clone());
466
467        repeated_assert::that(3, Duration::from_millis(STEP_MS), || {
468            assert!(*x.lock().unwrap() > 0);
469            assert_eq!(a, b);
470        });
471    }
472
473    #[cfg(feature = "async")]
474    #[tokio::test]
475    #[should_panic(expected = "assertion failed: *x.lock().unwrap() > 0")]
476    async fn multiple_failure_1_async() {
477        let x = Arc::new(Mutex::new(0));
478        let a = 11;
479        let b = 11;
480
481        spawn_thread(x.clone());
482
483        repeated_assert::that_async(3, Duration::from_millis(STEP_MS), || async {
484            assert!(*x.lock().unwrap() > 0);
485            assert_eq!(a, b);
486        })
487        .await;
488    }
489
490    #[test]
491    #[should_panic(expected = "assertion `left == right` failed")]
492    fn multiple_failure_2() {
493        let x = Arc::new(Mutex::new(0));
494        let a = 11;
495        let b = 12;
496
497        spawn_thread(x.clone());
498
499        repeated_assert::that(5, Duration::from_millis(5 * STEP_MS), || {
500            assert!(*x.lock().unwrap() > 0);
501            assert_eq!(a, b);
502        });
503    }
504
505    #[cfg(feature = "async")]
506    #[tokio::test]
507    #[should_panic(expected = "assertion `left == right` failed")]
508    async fn multiple_failure_2_async() {
509        let x = Arc::new(Mutex::new(0));
510        let a = 11;
511        let b = 12;
512
513        spawn_thread(x.clone());
514
515        repeated_assert::that_async(5, Duration::from_millis(5 * STEP_MS), || async {
516            assert!(*x.lock().unwrap() > 0);
517            assert_eq!(a, b);
518        })
519        .await;
520    }
521
522    #[test]
523    fn catch() {
524        let x = Arc::new(Mutex::new(-1_000));
525
526        spawn_thread(x.clone());
527
528        repeated_assert::with_catch(
529            10,
530            Duration::from_millis(5 * STEP_MS),
531            5,
532            || {
533                *x.lock().unwrap() = 0;
534            },
535            || {
536                assert!(*x.lock().unwrap() > 0);
537            },
538        );
539    }
540
541    #[cfg(feature = "async")]
542    #[tokio::test]
543    async fn catch_async() {
544        let x = Arc::new(Mutex::new(-1_000));
545
546        spawn_thread(x.clone());
547
548        repeated_assert::with_catch_async(
549            10,
550            Duration::from_millis(5 * STEP_MS),
551            5,
552            || async {
553                *x.lock().unwrap() = 0;
554            },
555            || async {
556                assert!(*x.lock().unwrap() > 0);
557            },
558        )
559        .await;
560    }
561}