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}