Skip to content

Commit 94e0434

Browse files
committed
feat: Add functions assert_panic and assert_panic_contains
1 parent 7f83351 commit 94e0434

File tree

2 files changed

+170
-0
lines changed

2 files changed

+170
-0
lines changed

crates/libtest2/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
mod case;
5252
mod macros;
5353

54+
pub mod panic;
55+
5456
#[doc(hidden)]
5557
pub mod _private {
5658
pub use distributed_list::push;

crates/libtest2/src/panic.rs

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
//! This module contains functionality related to handling panics
2+
3+
use std::borrow::Cow;
4+
5+
const DID_NOT_PANIC: &str = "test did not panic as expected";
6+
7+
/// Error returned by [`assert_panic`] and [`assert_panic_contains`]
8+
#[derive(Debug)]
9+
pub struct AssertPanicError(Cow<'static, str>);
10+
11+
impl std::fmt::Display for AssertPanicError {
12+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
13+
std::fmt::Display::fmt(&self.0, f)
14+
}
15+
}
16+
17+
impl std::error::Error for AssertPanicError {}
18+
19+
/// Assert that a piece of code is intended to panic
20+
///
21+
/// This will wrap the provided closure and check the result for a panic. If the function fails to panic
22+
/// an error value is returned, otherwise `Ok(())` is returned.
23+
///
24+
/// ```rust
25+
/// # use libtest2::panic::assert_panic;
26+
/// fn panicky_test() {
27+
/// panic!("intentionally fails");
28+
/// }
29+
///
30+
/// let result = assert_panic(panicky_test);
31+
/// assert!(result.is_ok());
32+
/// ```
33+
///
34+
/// If you also want to check that the panic contains a specific message see [`assert_panic_contains`].
35+
///
36+
/// # Notes
37+
/// This function will wrap the provided closure with a call to [`catch_unwind`](`std::panic::catch_unwind`),
38+
/// and will therefore inherit the caveats of this function, most notably that it will be unable to catch
39+
/// panics if they are not implemented via unwinding.
40+
pub fn assert_panic<T, F: FnOnce() -> T>(f: F) -> Result<(), AssertPanicError> {
41+
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
42+
// The test should have panicked, but didn't.
43+
Ok(_) => Err(AssertPanicError(Cow::Borrowed(DID_NOT_PANIC))),
44+
45+
// The test panicked, as expected.
46+
Err(_) => Ok(()),
47+
}
48+
}
49+
50+
/// Assert that a piece of code is intended to panic with a specific message
51+
///
52+
/// This will wrap the provided closure and check the result for a panic. If the function fails to panic with
53+
/// a message that contains the expected string an error value is returned, otherwise `Ok(())` is returned.
54+
///
55+
/// ```rust
56+
/// # use libtest2::panic::assert_panic_contains;
57+
/// fn panicky_test() {
58+
/// panic!("intentionally fails");
59+
/// }
60+
///
61+
/// let result = assert_panic_contains(panicky_test, "fail");
62+
/// assert!(result.is_ok());
63+
///
64+
/// let result = assert_panic_contains(panicky_test, "can't find this");
65+
/// assert!(result.is_err());
66+
/// ```
67+
///
68+
/// If you don't want to check that the panic contains a specific message see [`assert_panic`].
69+
///
70+
/// # Notes
71+
/// This function will wrap the provided closure with a call to [`catch_unwind`](`std::panic::catch_unwind`),
72+
/// and will therefore inherit the caveats of this function, most notably that it will be unable to catch
73+
/// panics if they are not implemented via unwinding.
74+
pub fn assert_panic_contains<T, F: FnOnce() -> T>(
75+
f: F,
76+
expected: &str,
77+
) -> Result<(), AssertPanicError> {
78+
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
79+
// The test should have panicked, but didn't.
80+
Ok(_) => Err(AssertPanicError(Cow::Borrowed(DID_NOT_PANIC))),
81+
82+
// The test panicked, as expected, but we need to check the panic message
83+
Err(payload) => check_panic_message(&*payload, expected),
84+
}
85+
}
86+
87+
#[cold]
88+
fn check_panic_message(
89+
payload: &dyn std::any::Any,
90+
expected: &str,
91+
) -> Result<(), AssertPanicError> {
92+
// The `panic` information is just an `Any` object representing the
93+
// value the panic was invoked with. For most panics (which use
94+
// `panic!` like `println!`), this is either `&str` or `String`.
95+
let maybe_panic_str = payload
96+
.downcast_ref::<String>()
97+
.map(|s| s.as_str())
98+
.or_else(|| payload.downcast_ref::<&str>().copied());
99+
100+
// Check the panic message against the expected message.
101+
match maybe_panic_str {
102+
Some(panic_str) if panic_str.contains(expected) => Ok(()),
103+
104+
Some(panic_str) => {
105+
let error_msg = ::std::format!(
106+
r#"panic did not contain expected string
107+
panic message: {panic_str:?}
108+
expected substring: {expected:?}"#
109+
);
110+
111+
Err(AssertPanicError(Cow::Owned(error_msg)))
112+
}
113+
114+
None => {
115+
let type_id = (*payload).type_id();
116+
let error_msg = ::std::format!(
117+
r#"expected panic with string value,
118+
found non-string value: `{type_id:?}`
119+
expected substring: {expected:?}"#,
120+
);
121+
122+
Err(AssertPanicError(Cow::Owned(error_msg)))
123+
}
124+
}
125+
}
126+
127+
#[cfg(test)]
128+
mod tests {
129+
use super::*;
130+
131+
#[test]
132+
fn assert_panic_with_panic() {
133+
let result = assert_panic(|| panic!("some message"));
134+
result.unwrap();
135+
}
136+
137+
#[test]
138+
fn assert_panic_no_panic() {
139+
let result = assert_panic(|| { /* do absolutely nothing */ });
140+
let error = result.unwrap_err();
141+
assert_eq!(error.to_string(), DID_NOT_PANIC);
142+
}
143+
144+
#[test]
145+
fn assert_panic_contains_correct_panic_message() {
146+
let result = assert_panic_contains(|| panic!("some message"), "mess");
147+
result.unwrap();
148+
}
149+
150+
#[test]
151+
fn assert_panic_contains_no_panic() {
152+
let result = assert_panic_contains(|| { /* do absolutely nothing */ }, "fail");
153+
let error = result.unwrap_err();
154+
assert_eq!(error.to_string(), DID_NOT_PANIC);
155+
}
156+
157+
#[test]
158+
fn assert_panic_contains_wrong_panic_message() {
159+
let result = assert_panic_contains(|| panic!("some message"), "fail");
160+
let error = result.unwrap_err();
161+
assert_eq!(
162+
error.0,
163+
r#"panic did not contain expected string
164+
panic message: "some message"
165+
expected substring: "fail""#
166+
);
167+
}
168+
}

0 commit comments

Comments
 (0)