test-casing 0.1.3

Parameterized test cases and test decorators
Documentation
//! Integration tests for the `decorate` macro.

use async_std::task;

use std::{
    error::Error,
    sync::atomic::{AtomicBool, AtomicU32, Ordering},
    thread,
    time::Duration,
};

use test_casing::{decorate, decorators::*, test_casing};

#[test]
#[decorate(Timeout(Duration::from_secs(5)))]
fn with_inlined_timeout() {
    thread::sleep(Duration::from_millis(10));
}

const TIMEOUT: Timeout = Timeout::secs(3);

#[test]
#[decorate(TIMEOUT)]
fn with_timeout_constant() {
    thread::sleep(Duration::from_millis(10));
}

#[test]
#[decorate(TIMEOUT, Retry::times(2))]
fn with_mixed_decorators() {
    thread::sleep(Duration::from_millis(10));
}

#[test]
#[decorate(Retry::times(1))]
fn with_retries() {
    static COUNTER: AtomicU32 = AtomicU32::new(0);

    assert!(
        COUNTER.fetch_add(1, Ordering::Relaxed) != 0,
        "Sometimes we all fail"
    );
}

#[test]
#[decorate(Retry::times(1))]
fn with_retries_and_error() -> Result<(), Box<dyn Error>> {
    static COUNTER: AtomicU32 = AtomicU32::new(0);

    if COUNTER.fetch_add(1, Ordering::Relaxed) == 0 {
        Err("Sometimes we all fail".into())
    } else {
        Ok(())
    }
}

const RETRY_ERRORS: RetryErrors<Box<dyn Error>> =
    Retry::times(1).on_error(|err| err.to_string().contains("retry"));

#[test]
#[decorate(RETRY_ERRORS)]
fn with_error_retries() -> Result<(), Box<dyn Error>> {
    static COUNTER: AtomicU32 = AtomicU32::new(0);

    if COUNTER.fetch_add(1, Ordering::Relaxed) == 0 {
        Err("please retry me".into())
    } else {
        Ok(())
    }
}

#[derive(Debug, Clone, Copy)]
struct ShouldError(&'static str);

impl<E: ToString> DecorateTest<Result<(), E>> for ShouldError {
    fn decorate_and_test<F: TestFn<Result<(), E>>>(&'static self, test_fn: F) -> Result<(), E> {
        let Err(err) = test_fn() else {
            panic!("Expected test to error, but it completed successfully");
        };
        let err = err.to_string();
        if err.contains(self.0) {
            Ok(())
        } else {
            panic!(
                "Expected error message to contain `{}`, but it was: {err}",
                self.0
            );
        }
    }
}

#[test]
#[decorate(RETRY_ERRORS, ShouldError("oops"))] // also tests custom decorators
fn mismatched_error_with_error_retries() -> Result<(), Box<dyn Error>> {
    Err("oops".into())
}

#[test]
#[decorate(ShouldError("oops"), Retry::times(2))]
fn with_custom_decorator_and_retries() -> Result<(), &'static str> {
    static COUNTER: AtomicU32 = AtomicU32::new(0);

    match COUNTER.fetch_add(1, Ordering::Relaxed) {
        1 => Err("nothing to see here"),
        2 => Err("oops"),
        _ => Ok(()),
    }
}

#[test]
#[decorate(ShouldError("oops"))]
#[decorate(Retry::times(2))]
fn with_several_decorator_macros() -> Result<(), &'static str> {
    static COUNTER: AtomicU32 = AtomicU32::new(0);

    match COUNTER.fetch_add(1, Ordering::Relaxed) {
        1 => Err("nothing to see here"),
        2 => Err("oops"),
        _ => Ok(()),
    }
}

#[async_std::test]
#[decorate(Timeout::millis(100), Retry::times(1))]
async fn async_test_with_timeout() {
    static COUNTER: AtomicU32 = AtomicU32::new(0);

    if COUNTER.fetch_add(1, Ordering::Relaxed) == 0 {
        task::sleep(Duration::from_millis(500)).await;
        // ^ will cause the test failure
    }
}

static SEQUENCE: Sequence = Sequence::new().abort_on_failure();

/// Checks that test in a `Sequence` are in fact sequential.
#[derive(Debug)]
struct SequenceChecker {
    is_running: AtomicBool,
}

impl SequenceChecker {
    const fn new() -> Self {
        Self {
            is_running: AtomicBool::new(false),
        }
    }

    fn start(&self) -> SequenceCheckerGuard<'_> {
        let prev_value = self.is_running.swap(true, Ordering::SeqCst);
        assert!(!prev_value, "Sequential tests are not sequential!");
        SequenceCheckerGuard {
            is_running: &self.is_running,
        }
    }
}

#[derive(Debug)]
struct SequenceCheckerGuard<'a> {
    is_running: &'a AtomicBool,
}

impl Drop for SequenceCheckerGuard<'_> {
    fn drop(&mut self) {
        self.is_running.store(false, Ordering::SeqCst);
    }
}

static SEQUENCE_CHECKER: SequenceChecker = SequenceChecker::new();

#[test]
#[should_panic(expected = "oops")]
#[decorate(&SEQUENCE)]
fn panicking_sequential_test() {
    let _guard = SEQUENCE_CHECKER.start();
    thread::sleep(Duration::from_millis(50));
    panic!("oops");
}

#[test]
#[decorate(&SEQUENCE)]
fn other_sequential_test() {
    let _guard = SEQUENCE_CHECKER.start();
    thread::sleep(Duration::from_millis(50));
}

#[async_std::test]
#[decorate(Retry::times(1), &SEQUENCE)]
async fn async_sequential_test() -> Result<(), Box<dyn Error>> {
    static COUNTER: AtomicU32 = AtomicU32::new(0);

    let _guard = SEQUENCE_CHECKER.start();
    task::sleep(Duration::from_millis(50)).await;
    if COUNTER.fetch_add(1, Ordering::Relaxed) == 0 {
        Err("oops".into())
    } else {
        Ok(())
    }
}

#[test_casing(3, ["1", "2", "3!"])]
#[decorate(Retry::times(1))]
fn cases_with_retries(s: &str) {
    // This is sloppy (the test case ordering is non-deterministic, so we can skip starting cases),
    // but sort of OK for the purpose.
    static IGNORE_ERROR: AtomicBool = AtomicBool::new(false);

    if IGNORE_ERROR.load(Ordering::SeqCst) {
        return;
    }

    let parse_result = s.parse::<usize>();
    if parse_result.is_err() {
        IGNORE_ERROR.store(true, Ordering::SeqCst);
    }
    parse_result.unwrap();
}