suitecase 0.0.1

The structured test toolkit. A sync Rust crate for named cases, optional suite and per-case hooks, and macros so each case appears in cargo test—without a custom harness or DSL.
Documentation
use suitecase::{
    Case, HookFns, RunConfig, cargo_case_tests, cargo_case_tests_with_hooks, cases_fn, run,
    suite_methods,
};

#[derive(Default)]
struct Recorder {
    log: Vec<&'static str>,
}

impl Recorder {
    fn push(&mut self, e: &'static str) {
        self.log.push(e);
    }
}

impl Recorder {
    fn test_a(&mut self) {
        self.push("a");
    }

    fn test_b(&mut self) {
        self.push("b");
    }

    fn test_only(&mut self) {
        self.push("only");
    }

    fn test_bad(&mut self) {
        let _ = self;
        panic!("boom");
    }
}

fn rec_setup(s: &mut Recorder) {
    s.push("setup_suite");
}
fn rec_teardown(s: &mut Recorder) {
    s.push("teardown_suite");
}
fn rec_before(s: &mut Recorder) {
    s.push("before_each");
}
fn rec_after(s: &mut Recorder) {
    s.push("after_each");
}

static RECORDER_HOOKS: HookFns<Recorder> = HookFns {
    setup_suite: Some(rec_setup),
    teardown_suite: Some(rec_teardown),
    before_each: Some(rec_before),
    after_each: Some(rec_after),
};

static RECORDER_CASES: &[Case<Recorder>] = suite_methods![Recorder, s => test_a, test_b];

#[derive(Default)]
struct DefaultsOnly {
    n: i32,
}

impl DefaultsOnly {
    fn test_one(&mut self) {
        self.n = 7;
    }
}

static DEFAULTS_CASES: &[Case<DefaultsOnly>] = suite_methods![DefaultsOnly, s => test_one];

cargo_case_tests!(DefaultsOnly::default(), DEFAULTS_CASES, [test_one]);

cargo_case_tests_with_hooks!(
    Recorder::default(),
    RECORDER_CASES,
    RECORDER_HOOKS,
    [test_a, test_b]
);

fn case_fn_a(s: &mut Recorder) {
    s.push("a");
}
fn case_fn_b(s: &mut Recorder) {
    s.push("b");
}

static RECORDER_FN_CASES: &[Case<Recorder>] = cases_fn![
    Recorder =>
    suite_cf_a => case_fn_a,
    suite_cf_b => case_fn_b
];

cargo_case_tests_with_hooks!(
    Recorder::default(),
    RECORDER_FN_CASES,
    RECORDER_HOOKS,
    [suite_cf_a, suite_cf_b]
);

#[test]
fn default_hooks_run_cases_only() {
    let mut suite = DefaultsOnly::default();
    run(
        &mut suite,
        DEFAULTS_CASES,
        RunConfig::all(),
        &HookFns::default(),
    );
    assert_eq!(suite.n, 7);
}

#[test]
fn hook_order_all_cases() {
    let mut suite = Recorder::default();
    run(
        &mut suite,
        suite_methods![Recorder, s => test_a, test_b],
        RunConfig::all(),
        &RECORDER_HOOKS,
    );

    assert_eq!(
        suite.log,
        vec![
            "setup_suite",
            "before_each",
            "a",
            "after_each",
            "before_each",
            "b",
            "after_each",
            "teardown_suite",
        ]
    );
}

#[test]
fn singular_filtered_case_runs_setup_and_teardown() {
    let mut suite = Recorder::default();
    run(
        &mut suite,
        suite_methods![Recorder, s => test_a, test_b],
        RunConfig::filter("test_b"),
        &RECORDER_HOOKS,
    );

    assert_eq!(
        suite.log,
        vec![
            "setup_suite",
            "before_each",
            "b",
            "after_each",
            "teardown_suite",
        ]
    );
}

#[test]
fn cases_fn_macro_works() {
    let mut suite = Recorder::default();
    run(
        &mut suite,
        RECORDER_FN_CASES,
        RunConfig::all(),
        &RECORDER_HOOKS,
    );
    assert_eq!(
        suite.log,
        vec![
            "setup_suite",
            "before_each",
            "a",
            "after_each",
            "before_each",
            "b",
            "after_each",
            "teardown_suite",
        ]
    );
}

#[test]
fn no_cases_no_setup() {
    let mut suite = Recorder::default();
    run(&mut suite, &[], RunConfig::all(), &RECORDER_HOOKS);
    assert!(suite.log.is_empty());
}

#[test]
#[should_panic(expected = "matched no cases")]
fn invalid_filter_panics() {
    let mut suite = Recorder::default();
    run(
        &mut suite,
        suite_methods![Recorder, s => test_only],
        RunConfig::filter("nope"),
        &RECORDER_HOOKS,
    );
}

#[test]
fn after_each_runs_when_case_panics() {
    let mut suite = Recorder::default();
    let err = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        run(
            &mut suite,
            suite_methods![Recorder, s => test_bad],
            RunConfig::all(),
            &RECORDER_HOOKS,
        );
    }));
    assert!(err.is_err());
    assert_eq!(
        suite.log,
        vec!["setup_suite", "before_each", "after_each", "teardown_suite",]
    );
}

#[test]
fn partial_hooks_only_run_some() {
    let mut suite = Recorder::default();
    run(
        &mut suite,
        suite_methods![Recorder, s => test_a],
        RunConfig::all(),
        &HookFns {
            setup_suite: Some(rec_setup),
            teardown_suite: None,
            before_each: Some(rec_before),
            after_each: None,
        },
    );
    assert_eq!(suite.log, vec!["setup_suite", "before_each", "a"]);
}