rvtest 0.3.2

A Next Level Testing Library for Rust — BDD specs, property-based testing, parametrized tests, rich reporting, and code coverage. Just a library, not a framework.
Documentation
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::Arc;
use std::time::Duration;

use rvtest::spec::describe;

#[test]
fn execution_passes_when_all_pass() {
    describe("Math")
        .it("adds", || assert_eq!(2 + 2, 4))
        .it("subtracts", || assert_eq!(5 - 3, 2))
        .run()
        .assert_all_pass();
}

#[test]
fn execution_reports_failures() {
    let result = catch_unwind(AssertUnwindSafe(|| {
        describe("Failing")
            .it("fails", || panic!("intentional failure"))
            .run()
            .assert_all_pass();
    }));
    assert!(result.is_err(), "assert_all_pass should panic on failure");
}

#[test]
fn execution_supports_tags_and_timeout() {
    describe("Tagged")
        .it("passing", || {})
        .tag("smoke")
        .timeout(Duration::from_secs(1))
        .run()
        .assert_all_pass();
}

#[test]
fn execution_retries_flaky_tests() {
    let counter = AtomicU32::new(0);
    describe("Flaky")
        .it("succeeds on retry", move || {
            let prev = counter.fetch_add(1, Ordering::SeqCst);
            if prev == 0 {
                panic!("first attempt fails");
            }
        })
        .retries(2)
        .run()
        .assert_all_pass();
}

#[test]
fn execution_runs_before_all_hook() {
    let ran = Arc::new(AtomicBool::new(false));
    let setup = Arc::clone(&ran);
    describe("Setup")
        .before_all(move || {
            setup.store(true, Ordering::SeqCst);
        })
        .it("hook executed", move || {
            assert!(ran.load(Ordering::SeqCst), "before_all should have run");
        })
        .run()
        .assert_all_pass();
}

#[test]
fn child_hooks_before_all_on_child() {
    let ran = Arc::new(AtomicBool::new(false));
    let setup = Arc::clone(&ran);
    let check = Arc::clone(&ran);
    describe("Parent")
        .describe("Child")
            .before_all(move || {
                setup.store(true, Ordering::SeqCst);
            })
            .it("child test", move || {
                assert!(check.load(Ordering::SeqCst), "before_all should have run");
            })
        .run()
        .assert_all_pass();
}

#[test]
fn child_hooks_after_all_on_child() {
    let ran = Arc::new(AtomicBool::new(false));
    let cleanup = Arc::clone(&ran);
    let verify = Arc::clone(&ran);
    describe("Parent")
        .describe("Child")
            .after_all(move || {
                cleanup.store(true, Ordering::SeqCst);
            })
            .it("child test", move || {})
        .run()
        .assert_all_pass();
    assert!(verify.load(Ordering::SeqCst), "after_all should have run");
}

#[test]
fn child_hooks_multiple_nesting_levels() {
    let order = Arc::new(std::sync::Mutex::new(Vec::new()));
    let o1 = Arc::clone(&order);
    let o2 = Arc::clone(&order);
    let o3 = Arc::clone(&order);
    let o_test = Arc::clone(&order);
    describe("Outer")
        .before_all(move || o1.lock().unwrap().push("outer_before"))
        .after_all(move || o2.lock().unwrap().push("outer_after"))
        .describe("Inner")
            .before_all(move || o3.lock().unwrap().push("inner_before"))
            .it("test", move || {
                o_test.lock().unwrap().push("test");
            })
        .run()
        .assert_all_pass();
    let ord = order.lock().unwrap();
    assert_eq!(ord[0], "outer_before", "outer before_all should run first");
    assert_eq!(ord[1], "inner_before", "inner before_all should run second");
    assert_eq!(ord[2], "test", "test should run after hooks");
    assert_eq!(ord[3], "outer_after", "outer after_all should run last");
}

#[test]
fn source_location_captures_caller() {
    let suite = describe("Loc")
        .it("inner", || {})
        .run();
    assert_eq!(suite.tests.len(), 1);
    let loc = suite.tests[0].location.as_ref().expect("should have location");
    assert!(loc.file.ends_with("tests/spec.rs"), "should be spec.rs, got {}", loc.file);
    assert!(loc.line > 0, "line should be positive");
}

#[test]
fn before_each_runs_before_each_test() {
    let counter = Arc::new(AtomicU32::new(0));
    let c_before = Arc::clone(&counter);
    let c_first = Arc::clone(&counter);
    let c_second = Arc::clone(&counter);
    let c_verify = Arc::clone(&counter);
    describe("Hooks")
        .before_each(move || { c_before.fetch_add(1, Ordering::SeqCst); })
        .it("first", move || {
            assert_eq!(counter.load(Ordering::SeqCst), 1, "before_each should have run");
        })
        .it("second", move || {
            assert_eq!(c_first.load(Ordering::SeqCst), 2, "before_each should have run again");
        })
        .run()
        .assert_all_pass();
    assert_eq!(c_second.load(Ordering::SeqCst), 2, "before_each ran exactly twice");
    let _ = c_verify;
}

#[test]
fn after_each_runs_after_each_test() {
    let ran = Arc::new(AtomicBool::new(false));
    let check = Arc::clone(&ran);
    let verify = Arc::clone(&ran);
    describe("Hooks")
        .after_each(move || {
            check.store(true, Ordering::SeqCst);
        })
        .it("test", move || {
            assert!(!verify.load(Ordering::SeqCst), "after_each should NOT have run yet");
        })
        .run()
        .assert_all_pass();
    assert!(ran.load(Ordering::SeqCst), "after_each should have run");
}

#[test]
fn after_each_runs_even_if_test_panics() {
    let ran = Arc::new(AtomicBool::new(false));
    let check = Arc::clone(&ran);
    let verify = Arc::clone(&ran);
    let result = catch_unwind(AssertUnwindSafe(|| {
        describe("Flaky")
            .after_each(move || {
                check.store(true, Ordering::SeqCst);
            })
            .it("will fail", || {
                panic!("intentional failure");
            })
            .run()
            .assert_all_pass();
    }));
    assert!(result.is_err(), "should have failed");
    assert!(verify.load(Ordering::SeqCst), "after_each should run even after panic");
}

#[test]
fn before_each_inherits_from_parent() {
    let order = Arc::new(std::sync::Mutex::new(Vec::new()));
    let o1 = Arc::clone(&order);
    let o_test = Arc::clone(&order);
    describe("Parent")
        .before_each(move || o1.lock().unwrap().push("parent"))
        .describe("Child")
            .it("test", move || {
                o_test.lock().unwrap().push("test");
            })
        .run()
        .assert_all_pass();
    assert_eq!(order.lock().unwrap().len(), 2);
    assert_eq!(order.lock().unwrap()[0], "parent");
}

#[test]
fn hook_ordering_outermost_before_each_innermost_after_each() {
    let order = Arc::new(std::sync::Mutex::new(Vec::new()));
    let o1 = Arc::clone(&order);
    let o2 = Arc::clone(&order);
    let o3 = Arc::clone(&order);
    let o4 = Arc::clone(&order);
    let o_test = Arc::clone(&order);
    describe("Outer")
        .before_each(move || o1.lock().unwrap().push("outer_before"))
        .after_each(move || o3.lock().unwrap().push("outer_after"))
        .describe("Inner")
            .before_each(move || o2.lock().unwrap().push("inner_before"))
            .after_each(move || o4.lock().unwrap().push("inner_after"))
            .it("test", move || {
                o_test.lock().unwrap().push("test");
            })
        .run()
        .assert_all_pass();
    let ord = order.lock().unwrap();
    assert_eq!(ord[0], "outer_before", "outer before_each first");
    assert_eq!(ord[1], "inner_before", "inner before_each second");
    assert_eq!(ord[2], "test", "test runs after all before_each");
    assert_eq!(ord[3], "inner_after", "inner after_each first (innermost)");
    assert_eq!(ord[4], "outer_after", "outer after_each last (outermost)");
}