use std::panic::{catch_unwind, AssertUnwindSafe};
use std::path::Path;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::Arc;
use std::time::Duration;
use rvtest::core::{RunnerConfig, TestRun};
use rvtest::param::{parametrize, parametrize_named};
use rvtest::property::{any, check};
use rvtest::report::{CompactReporter, GithubReporter, JsonReporter, PrettyReporter, TapReporter, TestReporter};
use rvtest::runner::TestRunner;
use rvtest::spec::describe;
#[test]
fn rvtest_spec() {
describe("Spec")
.describe("execution")
.it("passes when all tests pass", || {
describe("Math")
.it("adds", || assert_eq!(2 + 2, 4))
.it("subtracts", || assert_eq!(5 - 3, 2))
.run()
.assert_all_pass();
})
.it("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");
})
.it("supports tags and timeout", || {
describe("Tagged")
.it("passing", || {})
.tag("smoke")
.timeout(Duration::from_secs(1))
.run()
.assert_all_pass();
})
.it("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();
})
.it("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();
})
.tag("spec")
.run()
.assert_all_pass();
}
#[test]
fn rvtest_parametrized() {
describe("Parametrized")
.it("runs all cases", || {
let results = parametrize("add", [(1, 1, 2), (0, 0, 0), (-1, 1, 0)], |(a, b, exp)| {
assert_eq!(a + b, *exp);
});
assert!(results.iter().all(|c| c.status.is_passed()));
assert_eq!(results.len(), 3);
})
.it("supports named cases", || {
let results = parametrize_named(
"parse",
[("empty", ""), ("valid", "42")],
|input| {
if !input.is_empty() {
assert!(input.parse::<i32>().is_ok());
}
},
);
assert!(results.iter().all(|c| c.status.is_passed()));
})
.tag("param")
.run()
.assert_all_pass();
}
#[test]
fn rvtest_property() {
describe("Property")
.it("passes for valid properties", || {
check(
"identity with zero",
any::<i32>(),
|a: &i32| a + 0 == *a,
);
})
.it("detects falsified properties", || {
let result = catch_unwind(AssertUnwindSafe(|| {
check(
"intentionally false",
any::<i32>(),
|_: &i32| false,
);
}));
assert!(result.is_err(), "check should panic on falsified property");
})
.tag("property")
.run()
.assert_all_pass();
}
#[test]
fn rvtest_runner() {
describe("Runner")
.it("executes specs with custom config", || {
let config = RunnerConfig {
parallel: false,
verbose: true,
..RunnerConfig::default()
};
let run = TestRunner::new(config)
.add_spec(describe("Runner test").it("works", || {}))
.run();
assert!(run.success());
assert_eq!(run.total(), 1);
})
.tag("runner")
.run()
.assert_all_pass();
}
#[test]
fn rvtest_architecture() {
use rvtest::arch::arch_check;
let src_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
arch_check()
.src_dir(src_dir)
.module("core").may_not_depend_on(&["report", "coverage", "runner"])
.module("report").may_depend_on(&["core"])
.module("report").may_not_depend_on(&["coverage", "runner"])
.module("runner").may_depend_on(&["core", "report"])
.all_modules().must_not_have_cycles()
.assert_all_pass();
}
#[test]
fn rvtest_snapshot_create_and_match() {
use rvtest::snapshot::{assert_snapshot, set_snapshot_dir, set_update_all};
set_update_all(false);
let tmp = std::env::temp_dir().join("rvtest_snap_test");
let _ = std::fs::remove_dir_all(&tmp);
set_snapshot_dir(&tmp);
set_update_all(true);
assert_snapshot("hello", &"Hello, world!");
set_update_all(false);
assert_snapshot("hello", &"Hello, world!");
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn rvtest_snapshot_mismatch_detected() {
use rvtest::snapshot::{assert_snapshot, set_snapshot_dir, set_update_all};
set_update_all(false);
let tmp = std::env::temp_dir().join("rvtest_snap_mismatch");
let _ = std::fs::remove_dir_all(&tmp);
set_snapshot_dir(&tmp);
set_update_all(true);
assert_snapshot("mismatch_test", &"original value");
set_update_all(false);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
assert_snapshot("mismatch_test", &"different value");
}));
assert!(result.is_err(), "snapshot mismatch should panic");
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn rvtest_reporters() {
describe("Reporters")
.it("pretty reporter shows summary", || {
let report = PrettyReporter::new().colour(false).report(&TestRun::new());
assert!(report.contains("Tests"), "should have Tests line");
assert!(report.contains("0 passed"), "should show pass count");
assert!(report.contains("Time"), "should have Time line");
})
.it("tap reporter outputs correct header", || {
let report = TapReporter.report(&TestRun::new());
assert!(report.starts_with("1..0"));
})
.it("compact reporter shows counts", || {
let report = CompactReporter.report(&TestRun::new());
assert!(report.contains("Results:"), "should have results line: {report:?}");
assert!(report.contains("0/0"), "should show zero counts");
})
.it("json reporter is valid", || {
let report = JsonReporter.report(&TestRun::new());
assert!(report.contains(r#""success":true"#));
assert!(report.contains(r#""suites":["#));
})
.it("github reporter shows zero failures", || {
let report = GithubReporter.report(&TestRun::new());
assert!(report.contains("0/0 passed"));
})
.tag("report")
.run()
.assert_all_pass();
}
#[test]
fn rvtest_child_hooks() {
describe("Child hooks")
.it("executes before_all on child spec", || {
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();
})
.it("executes after_all on child spec", || {
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");
})
.it("runs hooks at 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");
})
.tag("hooks")
.run()
.assert_all_pass();
}
#[test]
fn rvtest_source_location() {
describe("Source location")
.it("captures caller location", || {
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/integration.rs"), "should be integration.rs, got {}", loc.file);
assert!(loc.line > 0, "line should be positive");
})
.tag("spec")
.run()
.assert_all_pass();
}
#[test]
fn rvtest_before_each_after_each() {
describe("before_each / after_each")
.it("runs before_each 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;
})
.it("runs after_each 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");
})
.it("runs after_each 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");
})
.it("inherits before_each from parent spec", || {
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");
})
.it("chains before_each outermost-first and after_each innermost-first", || {
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)");
})
.tag("hooks")
.run()
.assert_all_pass();
}
#[test]
fn rvtest_assert_macros() {
describe("Assert macros")
.it("assert_eq passes on equal values", || {
rvtest::assert_eq!(42, 42);
rvtest::assert_eq!("hello", "hello");
rvtest::assert_eq!(vec![1, 2, 3], vec![1, 2, 3]);
})
.it("assert_eq panics on mismatch", || {
let result = catch_unwind(AssertUnwindSafe(|| {
rvtest::assert_eq!(1, 2);
}));
assert!(result.is_err(), "assert_eq should panic on mismatch");
})
.it("assert_eq with custom message", || {
let result = catch_unwind(AssertUnwindSafe(|| {
rvtest::assert_eq!(1, 2, "custom: expected 1 == 2");
}));
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err().downcast_ref::<String>().unwrap());
assert!(msg.contains("custom:"), "message should contain custom text: {msg}");
})
.it("assert_ok returns inner value", || {
let v = rvtest::assert_ok!(Ok::<_, &str>(42));
assert_eq!(v, 42);
})
.it("assert_ok panics on Err", || {
let result = catch_unwind(AssertUnwindSafe(|| {
let val: Result<i32, &str> = Err("fail");
let _v = rvtest::assert_ok!(val);
}));
assert!(result.is_err(), "assert_ok should panic on Err");
})
.it("assert_ok with custom message", || {
let val: Result<i32, &str> = Err("fail");
let result = catch_unwind(AssertUnwindSafe(|| {
let _v = rvtest::assert_ok!(val, "expected Ok");
}));
assert!(result.is_err());
})
.it("assert_err returns error value", || {
let e = rvtest::assert_err!(Err::<i32, _>("error msg"));
assert_eq!(e, "error msg");
})
.it("assert_err panics on Ok", || {
let result = catch_unwind(AssertUnwindSafe(|| {
let val: Result<i32, &str> = Ok(42);
let _e = rvtest::assert_err!(val);
}));
assert!(result.is_err(), "assert_err should panic on Ok");
})
.it("assert_matches passes on match", || {
rvtest::assert_matches!(Some(42), Some(_));
rvtest::assert_matches!(Ok::<_, ()>(1), Ok(_));
})
.it("assert_matches panics on mismatch", || {
let result = catch_unwind(AssertUnwindSafe(|| {
rvtest::assert_matches!(None::<i32>, Some(_));
}));
assert!(result.is_err(), "assert_matches should panic on mismatch");
})
.it("assert_delta passes within epsilon", || {
rvtest::assert_delta!(1.0_f64, 1.001_f64, 0.01_f64);
rvtest::assert_delta!(100.0_f64, 100.0001_f64, 0.001_f64);
})
.it("assert_delta panics outside epsilon", || {
let result = catch_unwind(AssertUnwindSafe(|| {
rvtest::assert_delta!(1.0_f64, 2.0_f64, 0.1_f64);
}));
assert!(result.is_err(), "assert_delta should panic outside epsilon");
})
.tag("assert")
.run()
.assert_all_pass();
}
#[test]
fn rvtest_coverage_edge_cases() {
describe("Coverage edge cases")
.it("assert_eq multiline diff message", || {
let r = catch_unwind(AssertUnwindSafe(|| {
rvtest::assert_eq!(
vec![vec![1, 2], vec![3, 4]],
vec![vec![1, 2], vec![9, 9]]
);
}));
assert!(r.is_err(), "should fail on multiline mismatch");
})
.it("assert_eq with custom message on multiline types", || {
let r = catch_unwind(AssertUnwindSafe(|| {
rvtest::assert_eq!(
vec![1, 2],
vec![1, 99],
"lists don't match"
);
}));
assert!(r.is_err());
let msg = format!("{}", r.unwrap_err().downcast_ref::<String>().unwrap());
assert!(msg.contains("lists don't match"));
})
.it("assert_ok with complex error type", || {
let v = rvtest::assert_ok!(Ok::<_, Box<dyn std::error::Error>>(42));
assert_eq!(v, 42);
})
.it("assert_err with complex ok type", || {
let r = catch_unwind(AssertUnwindSafe(|| {
let _e = rvtest::assert_err!(Ok::<i32, &str>(42));
}));
assert!(r.is_err());
})
.it("assert_matches on Option::None", || {
rvtest::assert_matches!(None::<i32>, None);
})
.it("assert_delta with negative values", || {
rvtest::assert_delta!(-10.0_f64, -10.5_f64, 1.0_f64);
})
.it("assert_delta with message on excess", || {
let r = catch_unwind(AssertUnwindSafe(|| {
rvtest::assert_delta!(1.0_f64, 100.0_f64, 1.0_f64, "too far apart");
}));
assert!(r.is_err());
})
.it("timeout test passes quickly", || {
describe("Quick")
.it("fast", || {})
.timeout(Duration::from_secs(1))
.run()
.assert_all_pass();
})
.it("tag exclude filters correctly", || {
let suite = describe("Filtered")
.tag("slow")
.it("should be excluded", || {})
.run_with_config(&RunnerConfig {
exclude_tags: vec!["slow".into()],
..RunnerConfig::default()
});
assert_eq!(suite.tests.len(), 0, "slow test should be excluded");
})
.it("tag include filters correctly", || {
let suite = describe("Filtered")
.tag("smoke")
.it("should be included", || {})
.run_with_config(&RunnerConfig {
include_tags: vec!["smoke".into()],
..RunnerConfig::default()
});
assert_eq!(suite.tests.len(), 1, "smoke test should be included");
})
.it("name filter excludes correctly", || {
let suite = describe("Name filter")
.it("keep_me", || {})
.it("exclude_me", || {})
.run_with_config(&RunnerConfig {
filter: Some("keep".into()),
..RunnerConfig::default()
});
assert_eq!(suite.tests.len(), 1);
assert_eq!(suite.tests[0].name, "Name filter :: keep_me");
})
.tag("coverage")
.run()
.assert_all_pass();
}
#[test]
fn rvtest_mocking() {
describe("Mocking")
.it("spy records calls", || {
let spy = rvtest::mock::Spy::new(|x: i32| x * 2);
rvtest::assert_eq!(spy.call(5), 10);
rvtest::assert_eq!(spy.call_count(), 1);
spy.assert_called_with(&[5]);
})
.it("spy records multiple calls in order", || {
let spy = rvtest::mock::Spy::new(|x: i32| x + 1);
spy.call(1);
spy.call(2);
spy.call(3);
spy.assert_called_with(&[1, 2, 3]);
})
.it("spy can be reset", || {
let spy = rvtest::mock::Spy::new(|x: i32| x);
spy.call(1);
spy.call(2);
rvtest::assert_eq!(spy.call_count(), 2);
spy.reset();
rvtest::assert_eq!(spy.call_count(), 0);
})
.it("spy assert_called works when called", || {
let spy = rvtest::mock::Spy::new(|x: i32| x);
spy.call(1);
spy.assert_called();
})
.it("spy assert_called panics when never called", || {
let spy = rvtest::mock::Spy::new(|x: i32| x);
let r = catch_unwind(AssertUnwindSafe(|| {
spy.assert_called();
}));
assert!(r.is_err());
})
.it("spy assert_called_with panics on mismatch", || {
let spy = rvtest::mock::Spy::new(|x: i32| x);
spy.call(1);
let r = catch_unwind(AssertUnwindSafe(|| {
spy.assert_called_with(&[99]);
}));
assert!(r.is_err());
})
.it("stub returns fixed value", || {
let stub = rvtest::mock::Stub::new(42);
rvtest::assert_eq!(stub.call("anything"), 42);
})
.tag("mock")
.run()
.assert_all_pass();
}
#[test]
fn rvtest_capture_toggle_integration() {
use rvtest::capture::{is_capture_enabled, set_capture_enabled};
set_capture_enabled(true);
assert!(is_capture_enabled());
set_capture_enabled(false);
assert!(!is_capture_enabled());
}
#[test]
fn rvtest_capture_with_spec() {
use rvtest::capture::set_capture_enabled;
use rvtest::spec::describe;
set_capture_enabled(true);
let suite = describe("Capture")
.it("prints something", || {
print!("hello from test");
})
.run_with_config(&rvtest::core::RunnerConfig {
output_capture: true,
..rvtest::core::RunnerConfig::default()
});
set_capture_enabled(false);
assert_eq!(suite.tests.len(), 1);
assert!(suite.tests[0].status.is_passed());
}