fickle 0.3.0

Tools for handling fickle (flaky) tests in rust.
Documentation
#![doc=include_str!("../README.md")]

use std::any::Any;
use std::fmt::{Debug, Display};
use std::panic::catch_unwind;

use error::{FickleError, TooManyFailures};
pub use fickle_macros::fickle;
use outcome::Outcome;
use term::Pretty;

mod error;
mod outcome;
mod term;

const DEFAULT_RETRIES: usize = 1;
const DEFAULT_PASSES: usize = 1;

/// Retry a fickle test if it fails initially.
///
/// ```
/// use fickle::Fickle;
///
/// // There is a 30% chance this test fails any 1 run. But since 3 retries are allowed (4 total
/// // runs), this is just a 0.3^4=0.81% chance that they *all* fail. So `Fickle` transforms this
/// // from a test that is problematically flaky to one that should rarely be an issue.
/// fn flaky() {
///     let fickle = Fickle::new(3, 1).unwrap();
///     let flakycode: fn() -> () = || {
///         if 0.7 < rand::random() {
///             panic!("number was too big!")
///         }
///     };
///     fickle.run(flakycode).unwrap()
/// }
/// ```
#[derive(Debug, Clone, Copy)]
pub struct Fickle {
    retries: usize,
    passes: usize,
}

impl Fickle {
    /// Specify the number of retries & number of required passes.
    pub fn new(retries: usize, passes: usize) -> Result<Self, FickleError> {
        if passes > retries + 1 {
            Err(FickleError::InvalidParams(format!(
                "Requested a minimum of {} passes with a maximum of {} runs",
                passes,
                retries + 1
            )))
        } else {
            Ok(Self { retries, passes })
        }
    }

    /// Specify the number of retries, while using the default number of passes (1).
    pub fn new_retries(retries: usize) -> Result<Self, FickleError> {
        Self::new(retries, DEFAULT_PASSES)
    }

    /// Specify the number of passes, while using the default number of retries (1).
    pub fn new_passes(passes: usize) -> Result<Self, FickleError> {
        Self::new(DEFAULT_RETRIES, passes)
    }

    fn total_runs(&self) -> usize {
        self.retries + 1
    }

    pub fn run<T, E: Display, P, Q: Debug>(
        &self,
        test: impl Test<T, E, P, Q>,
    ) -> Result<T, TooManyFailures<E, Q>> {
        let mut n_passes = 0;
        let mut failures = vec![];
        for i in 0..self.total_runs() {
            let outcome = test.outcome();
            if outcome.is_success() {
                self.show_outcome(&outcome, i + 1, Pretty::Success)
            } else {
                self.show_outcome(&outcome, i + 1, Pretty::Warning)
            }
            match outcome {
                Outcome::Success(success_res) => {
                    n_passes += 1;
                    if n_passes >= self.passes {
                        Pretty::Success.print(format!(
                            "Fickle test passed {n_passes}/{} attempts (allowed {} total)",
                            i + 1,
                            self.total_runs()
                        ));
                        return Ok(success_res);
                    }
                }
                Outcome::Failure(failure) => failures.push(failure),
            }
        }
        Pretty::Error.print(format!(
            "Fickle test passed {n_passes}/{} attempts, but needed {}",
            self.total_runs(),
            self.passes,
        ));
        Err(failures.into())
    }

    fn show_outcome<T, E: Display, P: Debug>(
        &self,
        outcome: &Outcome<T, E, P>,
        run: usize,
        pretty: Pretty,
    ) {
        let hline = "-".repeat(30);
        let msg = format!(
            "Fickle: run {run} of {}\n{hline}\n{outcome}\n{hline}\n",
            self.total_runs()
        );
        pretty.print(msg)
    }
}

/// By default, just 1 retry is attempted and 1 passing test run is required.
impl Default for Fickle {
    fn default() -> Self {
        Self {
            retries: DEFAULT_RETRIES,
            passes: DEFAULT_PASSES,
        }
    }
}

/// The types that can be passed to [`Fickle::run`].
pub trait Test<T, E, P, Q> {
    fn outcome(&self) -> Outcome<T, E, Q>;
}

/// panic-style tests
///
/// The `E` type parameter is never actually constructed, so just pick something that implements
/// `Display`.
impl Test<(), String, Box<dyn Any + Send>, String> for fn() -> () {
    fn outcome(&self) -> Outcome<(), String, String> {
        catch_unwind(self).into()
    }
}

/// result-style tests
impl<T, E> Test<T, E, Box<dyn Any + Send>, String> for fn() -> Result<T, E> {
    fn outcome(&self) -> Outcome<T, E, String> {
        catch_unwind(self).into()
    }
}

#[cfg(test)]
mod tests {
    use core::panic;
    use std::panic::UnwindSafe;
    use std::sync::{Arc, Mutex};

    use outcome::Failure;

    use super::*;

    struct BehaviorChange<T> {
        lt: fn() -> T,
        threshold: usize,
        geq: fn() -> T,
        count: usize,
    }

    impl<T> BehaviorChange<T> {
        fn new(lt: fn() -> T, threshold: usize, geq: fn() -> T) -> Self {
            Self {
                lt,
                threshold,
                geq,
                count: 0,
            }
        }

        fn call(&mut self) -> T {
            let res = if self.count < self.threshold {
                (self.lt)()
            } else {
                (self.geq)()
            };
            self.count += 1;
            res
        }
    }

    impl<I> Test<(), String, Box<dyn Any + Send>, String> for (fn(&I) -> (), I)
    where
        for<'a> &'a I: UnwindSafe,
    {
        fn outcome(&self) -> Outcome<(), String, String> {
            let (func, input) = self;
            catch_unwind(|| (func)(input)).into()
        }
    }

    impl<T, E, I> Test<T, E, Box<dyn Any + Send>, String> for (fn(&I) -> Result<T, E>, I)
    where
        for<'a> &'a I: UnwindSafe,
    {
        fn outcome(&self) -> Outcome<T, E, String> {
            let (func, input) = self;
            catch_unwind(|| (func)(input)).into()
        }
    }

    #[test]
    fn passes_first_panic() {
        let bc = Arc::new(Mutex::new(BehaviorChange::new(|| 1, 2, || panic!())));
        let fickle = Fickle::default();
        let block: fn(&Arc<Mutex<BehaviorChange<u8>>>) -> () = |bc| {
            Arc::clone(bc).lock().unwrap().call();
        };
        fickle.run((block, bc.clone())).unwrap();
        let final_bc = bc.lock().unwrap();
        assert_eq!(final_bc.count, 1)
    }

    #[test]
    fn passes_first_res() {
        let bc = Arc::new(Mutex::new(BehaviorChange::new(|| Ok(1), 2, || Err(2))));
        let fickle = Fickle::default();
        let block: fn(&Arc<Mutex<BehaviorChange<Result<u8, u8>>>>) -> _ =
            |bc| Arc::clone(bc).lock().unwrap().call();
        fickle.run((block, bc.clone())).unwrap();
        let final_bc = bc.lock().unwrap();
        assert_eq!(final_bc.count, 1)
    }

    // #[test]
    // fn fails_all_panic() {
    //     let bc = Arc::new(Mutex::new(BehaviorChange::new(|| {}, 0, || panic!())));
    //     let fickle = Fickle::default();
    //     let block: fn(&Arc<Mutex<BehaviorChange<()>>>) -> () = |bc| {
    //         Arc::clone(bc).lock().unwrap().call();
    //     };
    //     let res = fickle.run((block, bc.clone()));
    //     assert!(res.is_err());
    //     let count = Arc::clone(&bc).lock().unwrap().count;
    //     assert_eq!(count, 2)
    // }

    #[test]
    fn fails_all_res() {
        let bc = Arc::new(Mutex::new(BehaviorChange::new(|| Err(1), 0, || Err(2))));
        let fickle = Fickle::default();
        let block: fn(&Arc<Mutex<BehaviorChange<Result<u8, u8>>>>) -> _ =
            |bc| Arc::clone(bc).lock().unwrap().call();
        let res = fickle.run((block, bc.clone()));
        assert!(res.is_err());
        let final_bc = bc.lock().unwrap();
        assert_eq!(final_bc.count, 2)
    }

    #[test]
    fn fails_all_res_with_panic_last() {
        let bc = Arc::new(Mutex::new(BehaviorChange::new(
            || Err("also bad".to_string()),
            1,
            || panic!("oops"),
        )));
        let fickle = Fickle::default();
        let block: fn(&Arc<Mutex<BehaviorChange<Result<(), String>>>>) -> _ =
            |bc| Arc::clone(bc).lock().unwrap().call();
        let res = fickle.run((block, bc.clone()));
        match res {
            Ok(_) => panic!("test passed for some reason"),
            Err(err) => {
                let fails = err.failures().iter().collect::<Vec<_>>();
                assert_eq!(fails.len(), 2);
                assert!(
                    matches!(fails.first().unwrap(), Failure::Err(_)),
                    "first was not an err"
                );
                assert!(
                    matches!(fails.last().unwrap(), Failure::Panic(_)),
                    "second was not a panic"
                );
            }
        }
    }

    // #[test]
    // fn fails_all_res_with_panic_first() {
    //     let bc = Arc::new(Mutex::new(BehaviorChange::new(
    //         || panic!("oops"),
    //         1,
    //         || Err("also bad".to_string()),
    //     )));
    //     let fickle = Fickle::default();
    //     let block: fn(&Arc<Mutex<BehaviorChange<Result<(), String>>>>) -> _ =
    //         |bc| Arc::clone(bc).lock().unwrap().call();
    //     let res = fickle.run((block, (&bc).clone()));
    //     assert!(res.is_err());
    //     let count = Arc::clone(&bc).lock().unwrap().count;
    //     assert_eq!(count, 2)
    // }

    // #[test]
    // fn fails_then_passes_panic() {
    //     let bc = Arc::new(Mutex::new(BehaviorChange::new(
    //         || panic!("bad"),
    //         2,
    //         || "Yay!".into(),
    //     )));
    //     let fickle = Fickle::default();
    //     let block: fn(&Arc<Mutex<BehaviorChange<String>>>) -> () = |bc| {
    //         Arc::clone(bc).lock().unwrap().call();
    //     };
    //     fickle.run((block, bc.clone())).unwrap();
    //     // let final_bc = bc.lock().unwrap();
    //     // assert_eq!(final_bc.results, vec![1])
    // }
}