cool_asserts/assert_panics.rs
1use std::{any::Any, ops::Deref};
2
3/// Assert that an expression panics.
4///
5/// This macro asserts that a given expression panics. If the expression
6/// doesn't panic, this macro will panic, with a description including the
7/// expression itself, as well as the return value of the expression.
8///
9/// This macro is intended to replace `#[should_panic]` in rust tests:
10///
11/// - It allows for checking that a particular expression or statement panics,
12/// rather than an entire test function.
13/// - It allows checking for the presence or absence of multiple substrings.
14/// `should_panic` can only test for a single substring (with `expected`),
15/// and can't test for absence at all.
16/// - It allows checking for multiple panicking expressions in the same test
17/// case
18///
19/// ## Examples
20///
21/// ```
22/// // This example passes
23/// use cool_asserts::assert_panics;
24///
25/// assert_panics!({
26/// let _x = 1 + 2;
27/// panic!("Panic Message");
28/// });
29/// ```
30/// ```should_panic
31/// // This example panics
32/// use cool_asserts::assert_panics;
33///
34/// assert_panics!(1 + 2);
35/// ```
36///
37/// # Substring checking
38///
39/// Optionally, provide a list of conditions of the form `includes(pattern)`
40/// or `excludes(pattern)`. If given, the assertion will check that the
41/// panic message contains or does not contain each of the given patterns.
42/// It uses [`str::contains`], which means that anything
43/// implementing [`Pattern`][::std::str::Pattern] can be used as a matcher.
44///
45/// ## Examples
46///
47/// ```
48/// use cool_asserts::assert_panics;
49///
50/// assert_panics!(
51/// panic!("Custom message: {}", "message"),
52/// includes("message: message"),
53/// includes("Custom"),
54/// excludes("Foo"),
55/// );
56/// ```
57///
58/// ```should_panic
59/// // THIS EXAMPLE PANICS
60///
61/// use cool_asserts::assert_panics;
62///
63/// // The expression panics, which passes the assertion, but it fails
64/// // the message test, which means the overall assertion fails.
65/// assert_panics!(
66/// panic!("Message"),
67/// excludes("Message")
68/// );
69/// ```
70///
71/// # Generic message testing
72///
73/// If the `includes(..)` and `excludes(..)` are not powerful enough,
74/// `assert_panics` can also accept a closure as an argument. If the expression
75/// panics, the closure is called with the panic message as an `&str`. This
76/// closure can contain any additional assertions you wish to perform on the
77/// panic message.
78///
79/// ## Examples
80///
81/// ```
82/// use cool_asserts::assert_panics;
83///
84/// assert_panics!(panic!("{}, {}!", "Hello", "World"), |msg| {
85/// assert_eq!(msg, "Hello, World!")
86/// });
87/// ```
88/// ```
89/// use cool_asserts::assert_panics;
90///
91/// assert_panics!(
92/// assert_panics!(
93/// panic!("Message"),
94/// |msg| panic!("Custom Message")
95/// ),
96/// includes("Custom Message"),
97/// excludes("assertion failed")
98/// )
99/// ```
100///
101/// # Generic panic values
102///
103/// If you need to test panics that aren't event messages– that is, that aren't
104/// [`String`] or `&str` values provided by most panics (including most
105/// assertions)– you can provide a reference type in the closure. The macro
106/// will attempt to cast the panic value to that type. It will fail the
107/// assertion if the type doesn't match; otherwise the closure will be called.
108///
109/// ```
110/// use cool_asserts::assert_panics;
111///
112/// assert_panics!(std::panic::panic_any(10i64), |value: &i64| {
113/// assert_eq!(value, &10);
114/// })
115/// ```
116///
117/// ```
118/// use cool_asserts::assert_panics;
119///
120/// assert_panics!(
121/// assert_panics!(
122/// std::panic::panic_any(10i64),
123/// |value: &i32| {
124/// panic!("CUSTOM PANIC");
125/// }
126/// ),
127/// includes("expression panic type mismatch"),
128/// includes("i32"),
129/// // Currently, type_name of Any returns &dyn Any, rather than the actual type
130/// // includes("i64"),
131/// excludes("expression didn't panic"),
132/// excludes("CUSTOM PANIC")
133/// );
134/// ```
135#[macro_export]
136macro_rules! assert_panics {
137 ($expression:expr, |$panic:ident: Box<$(dyn)? Any $(+ Send)? $(+ 'static)?>| $body:expr) => (
138 match ::std::panic::catch_unwind(|| {$expression}) {
139 Ok(result) => $crate::assertion_failure!(
140 "expression didn't panic",
141 expression: stringify!($expression),
142 returned debug: result,
143 ),
144 Err($panic) => $body,
145 }
146 );
147
148 ($expression:expr, |$msg:ident $(: &str)?| $body:expr) => (
149 $crate::assert_panics!($expression, |panic: Box<dyn Any>|
150 match $crate::get_panic_message(&panic) {
151 Some($msg) => $body,
152 None => $crate::assertion_failure!(
153 "expression panic type wasn't String or &str",
154 expression: stringify!($expression),
155 ),
156 }
157 )
158 );
159
160 ($expression:expr, |$value:ident: &$type:ty| $body:expr) => (
161 $crate::assert_panics!($expression, |panic: Box<dyn Any>|
162 match panic.downcast_ref::<$type>() {
163 Some($value) => $body,
164 None => $crate::assertion_failure!(
165 "expression panic type mismatch",
166 expression: stringify!($expression),
167 expected: stringify!($type),
168 )
169 }
170 )
171 );
172
173 ($expression:expr $(,)?) => (
174 $crate::assert_panics!($expression, |_panic: Box<dyn Any>| {})
175 );
176
177
178 ($expression:expr $(, $rule:ident($arg:expr))+ $(,)?) => {
179 $crate::assert_panics!($expression, |msg| {
180 $($crate::check_rule!($expression, msg, $rule($arg));)+
181 })
182 }
183}
184
185#[macro_export]
186#[doc(hidden)]
187macro_rules! check_rule {
188 ($expression:expr, $msg:ident, includes($incl:expr)) => (
189 if !$msg.contains($incl) {
190 $crate::assertion_failure!(
191 "panic message didn't include expected string",
192 expression: stringify!($expression),
193 message debug: $msg,
194 includes debug: $incl,
195 );
196 }
197 );
198
199 ($expression:expr, $msg:ident, excludes($excl:expr)) => (
200 if $msg.contains($excl) {
201 $crate::assertion_failure!(
202 "panic message included disallowed string",
203 expression: stringify!($expression),
204 message debug: $msg,
205 excludes debug: $excl,
206 );
207 }
208 );
209}
210
211#[cfg(test)]
212mod test_assert_panics {
213 use std::panic::panic_any;
214
215 use crate::assert_panics;
216 mod bootstrap_tests {
217 use crate::assert_panics;
218
219 // We use a few should_panic tests to bootstrap– ensure that assert_panics
220 // basically works– then we do the more complex testing with itself.
221 // If all these tests pass, assert_panics has at least enough working
222 // that it can test itself more thoroughly.
223 #[test]
224 fn passes_with_panic() {
225 assert_panics!(panic!());
226 }
227
228 #[test]
229 #[should_panic(expected = "expression didn't panic")]
230 fn fails_without_panic() {
231 assert_panics!(1 + 1);
232 }
233
234 #[test]
235 #[should_panic(expected = "panic message didn't include expected string")]
236 fn fails_without_substring() {
237 assert_panics!(panic!("{} {}", "This", "Message"), includes("That Message"))
238 }
239
240 #[test]
241 #[should_panic(expected = "panic message included disallowed string")]
242 fn fails_with_substring() {
243 assert_panics!(panic!("{} {}", "This", "Message"), excludes("Message"))
244 }
245 }
246
247 #[test]
248 fn fails_without_panic() {
249 assert_panics!(
250 assert_panics!(1 + 1),
251 includes("assertion failed at"),
252 includes("expression didn't panic"),
253 includes("expression: 1 + 1"),
254 includes("returned: 2")
255 );
256 }
257
258 #[test]
259 fn fails_without_substring() {
260 assert_panics!(
261 assert_panics!(panic!("{} {}", "This", "Message"), includes("That Message")),
262 includes("assertion failed at"),
263 excludes("expression didn't panic"),
264 includes("panic message didn't include expected string"),
265 includes("expression: panic!(\"{} {}\", \"This\", \"Message\")"),
266 includes(" message: \"This Message\""),
267 includes("includes: \"That Message\""),
268 );
269 }
270
271 #[test]
272 fn fails_with_substring() {
273 assert_panics!(
274 assert_panics!(panic!("{} {}", "This", "Message"), excludes("Message")),
275 includes("assertion failed at"),
276 excludes("expression didn't panic"),
277 includes("panic message included disallowed string"),
278 includes("expression: panic!(\"{} {}\", \"This\", \"Message\")"),
279 includes(" message: \"This Message\""),
280 includes("excludes: \"Message\""),
281 );
282 }
283
284 #[test]
285 fn string_closure() {
286 assert_panics!(panic!("{}, {}!", "Hello", "World"), |msg| {
287 assert_eq!(msg, "Hello, World!");
288 })
289 }
290
291 #[test]
292 fn str_closure() {
293 assert_panics!(panic!("Hello, World!"), |msg| {
294 assert_eq!(msg, "Hello, World!");
295 })
296 }
297
298 #[test]
299 fn str_closure_mismatch() {
300 assert_panics!(
301 assert_panics!(panic_any(32), |_msg| {
302 panic!("CUSTOM PANIC");
303 }),
304 includes("assertion failed at"),
305 includes("expression panic type wasn't String or &str"),
306 excludes("CUSTOM PANIC")
307 )
308 }
309
310 #[test]
311 fn str_closure_panics() {
312 assert_panics!(
313 assert_panics!(panic!("Hello, World!"), |_msg| {
314 panic!("NOPE");
315 }),
316 includes("NOPE"),
317 excludes("Hello, World!"),
318 );
319 }
320
321 #[test]
322 fn typed_closure() {
323 assert_panics!(panic_any(244i32), |value: &i32| {
324 assert_eq!(value, &244);
325 })
326 }
327
328 #[test]
329 fn typed_closure_mismatch() {
330 assert_panics!(
331 assert_panics!(panic_any(244i32), |_value: &i64| { panic!("CUSTOM PANIC") }),
332 excludes("CUSTOM PANIC"),
333 includes("expression panic type mismatch"),
334 includes("expression: panic_any(244i32)"),
335 includes("expected: i64"),
336 );
337 }
338}
339
340/// Get the panic message as a `&str`, if available
341///
342/// While a panic value can be any type, *usually* it is either a `String`
343/// or a `str`, hidden inside a `Box<dyn Any + Send + Sync>` (see
344/// [`std::thread::Result`] for more info). This function gets the panic
345/// message as an &str (if available) from a panic value.
346///
347/// ```
348/// use std::panic::catch_unwind;
349/// use cool_asserts::get_panic_message;
350///
351/// let result = catch_unwind(|| panic!("{}, {}!", "Hello", "World"));
352/// let panic = result.unwrap_err();
353/// let message = get_panic_message(&panic).unwrap();
354///
355/// assert_eq!(message, "Hello, World!");
356#[inline]
357pub fn get_panic_message(panic: &Box<dyn Any + Send>) -> Option<&str> {
358 panic
359 .downcast_ref::<String>()
360 .map(String::as_str)
361 .or_else(|| panic.downcast_ref::<&'static str>().map(Deref::deref))
362}
363
364#[cfg(test)]
365mod test_get_panic_message {
366 use super::get_panic_message;
367 use std::panic::{catch_unwind, panic_any};
368
369 #[test]
370 fn str_message() {
371 let result =
372 catch_unwind(|| panic!("Hello, World!")).expect_err("Function didn't panic????");
373 assert_eq!(get_panic_message(&result), Some("Hello, World!"));
374 }
375
376 #[test]
377 fn string_message() {
378 let result = catch_unwind(|| panic_any("Hello, World!".to_owned()))
379 .expect_err("Function didn't panic????");
380
381 assert_eq!(get_panic_message(&result), Some("Hello, World!"));
382 }
383
384 #[test]
385 fn formatted_message() {
386 let result = catch_unwind(|| panic!("{}, {}!", "Hello", "World"))
387 .expect_err("Function didn't panic????");
388 assert_eq!(get_panic_message(&result), Some("Hello, World!"));
389 }
390
391 #[test]
392 fn other_message() {
393 let result = catch_unwind(|| panic_any(25)).expect_err("Function didn't panic????");
394 assert_eq!(get_panic_message(&result), None);
395 }
396}