#![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;
#[derive(Debug, Clone, Copy)]
pub struct Fickle {
retries: usize,
passes: usize,
}
impl Fickle {
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 })
}
}
pub fn new_retries(retries: usize) -> Result<Self, FickleError> {
Self::new(retries, DEFAULT_PASSES)
}
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)
}
}
impl Default for Fickle {
fn default() -> Self {
Self {
retries: DEFAULT_RETRIES,
passes: DEFAULT_PASSES,
}
}
}
pub trait Test<T, E, P, Q> {
fn outcome(&self) -> Outcome<T, E, Q>;
}
impl Test<(), String, Box<dyn Any + Send>, String> for fn() -> () {
fn outcome(&self) -> Outcome<(), String, String> {
catch_unwind(self).into()
}
}
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_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"
);
}
}
}
}