Skip to content

Commit eacbd66

Browse files
committed
feat: Add assert_panic function
1 parent c541887 commit eacbd66

2 files changed

Lines changed: 87 additions & 0 deletions

File tree

crates/libtest2/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050

5151
mod case;
5252
mod macros;
53+
mod panic;
5354

5455
#[doc(hidden)]
5556
pub mod _private {
@@ -73,6 +74,7 @@ pub use libtest2_harness::RunResult;
7374
pub use libtest2_harness::TestContext;
7475
pub use libtest2_proc_macro::main;
7576
pub use libtest2_proc_macro::test;
77+
pub use panic::assert_panic;
7678

7779
#[doc = include_str!("../README.md")]
7880
#[cfg(doctest)]

crates/libtest2/src/panic.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
use libtest2_harness::{RunError, RunResult};
2+
3+
/// Assert that a piece of code is intended to panic
4+
///
5+
/// This will wrap the provided closure with a call to [`catch_unwind`](`std::panic::catch_unwind`),
6+
/// and check the result for a panic. If the function fails to panic an error value is returned,
7+
/// otherwise `Ok(())` is returned.
8+
///
9+
/// # Examples
10+
/// Asserting that a test panics
11+
///
12+
/// ```rust
13+
/// fn panicky_test() {
14+
/// panic!("intentionally fails");
15+
/// }
16+
///
17+
/// let result = libtest2::assert_panic(panicky_test, None);
18+
/// assert!(result.is_ok());
19+
/// ```
20+
///
21+
/// Asserting that a test panics with a specific message, in which case the panic message must
22+
/// contain the provided substring.
23+
///
24+
/// ```rust
25+
/// fn panicky_test() {
26+
/// panic!("intentionally fails");
27+
/// }
28+
///
29+
/// let result = libtest2::assert_panic(panicky_test, Some("fail"));
30+
/// assert!(result.is_ok());
31+
///
32+
/// let result = libtest2::assert_panic(panicky_test, Some("can't find this"));
33+
/// assert!(result.is_err());
34+
/// ```
35+
pub fn assert_panic<T, F: FnOnce() -> T>(f: F, panic_message: Option<&str>) -> RunResult {
36+
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
37+
38+
match (result, panic_message) {
39+
// The test should have panicked, but didn't.
40+
(Ok(_), _) => {
41+
// TODO: Rust includes the source file location here, consider doing the same?
42+
Err(RunError::fail("test did not panic as expected"))
43+
}
44+
45+
// The test panicked, as expected.
46+
(Err(_), None) => Ok(()),
47+
48+
// The test panicked, as expected. We need to check the panic message
49+
(Err(payload), Some(expected)) => {
50+
// The `panic` information is just an `Any` object representing the
51+
// value the panic was invoked with. For most panics (which use
52+
// `panic!` like `println!`), this is either `&str` or `String`.
53+
let maybe_panic_str = payload
54+
.downcast_ref::<String>()
55+
.map(|s| s.as_str())
56+
.or_else(|| payload.downcast_ref::<&str>().copied());
57+
58+
// Check the panic message against the expected message.
59+
match maybe_panic_str {
60+
Some(panic_str) if panic_str.contains(expected) => Ok(()),
61+
62+
Some(panic_str) => {
63+
let error_msg = ::std::format!(
64+
r#"panic did not contain expected string
65+
panic message: {panic_str:?}
66+
expected substring: {expected:?}"#
67+
);
68+
69+
Err(RunError::fail(error_msg))
70+
}
71+
72+
None => {
73+
let type_id = (*payload).type_id();
74+
let error_msg = ::std::format!(
75+
r#"expected panic with string value,
76+
found non-string value: `{type_id:?}`
77+
expected substring: {expected:?}"#,
78+
);
79+
80+
Err(RunError::fail(error_msg))
81+
}
82+
}
83+
}
84+
}
85+
}

0 commit comments

Comments
 (0)