fluent_asserter/
panic_asserter.rs

1use super::*;
2use std::panic;
3use std::sync::{Arc, Mutex};
4/*
5TODO: this is non-deterministic and results in failing test due set_hook set the panic handling globally!!!
6Idea how to solve: One option could be to make a panic hook that delegates to some thread-local state. Have all of your tests install that hook and then setup the thread local hook to what they want.
7*/
8
9lazy_static! {
10    static ref LOCK_FOR_PANIC_ASSERTER: std::sync::Mutex<()> = Mutex::new(());
11}
12
13pub struct PanicAssertions {
14    actual_panic_message: String,
15}
16
17impl PanicAssertions {
18    pub fn new(actual_panic_message: String) -> PanicAssertions {
19        PanicAssertions {
20            actual_panic_message,
21        }
22    }
23
24    pub fn with_message(self, expected_panic_message: &str) {
25        if self.actual_panic_message != expected_panic_message {
26            panic!(
27                "Expected a panic message '{}', but found '{}'",
28                expected_panic_message, self.actual_panic_message
29            )
30        }
31    }
32
33    pub fn with_having_message(self, expected_panic_message: &str) {
34        if !self.actual_panic_message.contains(expected_panic_message) {
35            panic!(
36                "The text '{}' is not present in the panic message '{}'",
37                expected_panic_message, self.actual_panic_message
38            )
39        }
40    }
41}
42
43impl<F, R> PanicAsserter<F, R>
44where
45    F: FnOnce() -> R + panic::UnwindSafe,
46{
47    pub fn new(f: F) -> Self {
48        PanicAsserter { value: f }
49    }
50
51    pub fn panics(self) -> PanicAssertions {
52        //TODO: try to use a thread local storage with RefCell or so
53        let _guard = LOCK_FOR_PANIC_ASSERTER.lock();
54        let old_hook = panic::take_hook();
55        let global_buffer = Arc::new(Mutex::new(String::new()));
56
57        register_panic_hook_to_capture_output(&global_buffer);
58        let result = panic::catch_unwind(self.value);
59        panic::set_hook(old_hook);
60
61        PanicAssertions {
62            actual_panic_message: get_panic_message_if_present(result, global_buffer),
63        }
64    }
65
66    pub fn does_not_panic(self) {
67        let result = self.catch_unwind_silent();
68
69        if result.is_err() {
70            panic!("Expected code to panic, but it does not panic."); //TODO: "did" instead of "does"
71        }
72    }
73
74    fn catch_unwind_silent(self) -> std::thread::Result<R> {
75        let old_hook = panic::take_hook();
76        panic::set_hook(Box::new(|_| {}));
77        let result = panic::catch_unwind(self.value);
78        panic::set_hook(old_hook);
79        result
80    }
81}
82
83fn register_panic_hook_to_capture_output(global_buffer: &Arc<Mutex<String>>) {
84    panic::set_hook({
85        let global_buffer = global_buffer.clone();
86        Box::new(move |info| {
87            let mut global_buffer = global_buffer.lock().unwrap();
88
89            //Capture for string literal like panic("some string")
90            if let Some(s) = info.payload().downcast_ref::<&str>() {
91                global_buffer.push_str(s);
92            }
93
94            //Check for dynamically created String like panic("some {}", "string")
95            if let Some(s) = info.payload().downcast_ref::<String>() {
96                global_buffer.push_str(s);
97            }
98        })
99    });
100}
101
102fn get_panic_message_if_present<R>(
103    result: Result<R, Box<dyn std::any::Any + Send>>,
104    global_buffer: Arc<Mutex<String>>,
105) -> String {
106    match result {
107        Ok(_res) => {
108            panic!("There was no panic, but it was expected.")
109        }
110        Err(_) => {
111            return global_buffer.lock().unwrap().to_string();
112        }
113    }
114}