Skip to content

Commit 35d5302

Browse files
committed
feat: Add functions assert_panic and assert_panic_contains
1 parent c541887 commit 35d5302

2 files changed

Lines changed: 138 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, assert_panic_contains};
7678

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

crates/libtest2/src/panic.rs

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

0 commit comments

Comments
 (0)