proptest/test_runner/scoped_panic_hook.rs
1//-
2// Copyright 2024 The proptest developers
3//
4// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
5// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
7// option. This file may not be copied, modified, or distributed
8// except according to those terms.
9
10#[cfg(feature = "handle-panics")]
11mod internal {
12 //! Implementation of scoped panic hooks
13 //!
14 //! 1. `with_hook` serves as entry point, it executes body closure with panic hook closure
15 //! installed as scoped panic hook
16 //! 2. Upon first execution, current panic hook is replaced with `scoped_hook_dispatcher`
17 //! in a thread-safe manner, and original hook is stored for later use
18 //! 3. When panic occurs, `scoped_hook_dispatcher` either delegates execution to scoped
19 //! panic hook, if one is installed, or back to original hook stored earlier.
20 //! This preserves original behavior when scoped hook isn't used
21 //! 4. When `with_hook` is used, it replaces stored scoped hook pointer with pointer to
22 //! hook closure passed as parameter. Old hook pointer is set to be restored unconditionally
23 //! via drop guard. Then, normal body closure is executed.
24 use std::boxed::Box;
25 use std::cell::Cell;
26 use std::panic::{set_hook, take_hook, PanicInfo};
27 use std::sync::Once;
28 use std::{mem, ptr};
29
30 thread_local! {
31 /// Pointer to currently installed scoped panic hook, if any
32 ///
33 /// NB: pointers to arbitrary fn's are fat, and Rust doesn't allow crafting null pointers
34 /// to fat objects. So we just store const pointer to tuple with whatever data we need
35 static SCOPED_HOOK_PTR: Cell<*const (*mut dyn FnMut(&PanicInfo<'_>),)> = Cell::new(ptr::null());
36 }
37
38 static INIT_ONCE: Once = Once::new();
39 /// Default panic hook, the one which was present before installing scoped one
40 ///
41 /// NB: no need for external sync, value is mutated only once, when init is performed
42 static mut DEFAULT_HOOK: Option<Box<dyn Fn(&PanicInfo<'_>) + Send + Sync>> =
43 None;
44 /// Replaces currently installed panic hook with `scoped_hook_dispatcher` once,
45 /// in a thread-safe manner
46 fn init() {
47 INIT_ONCE.call_once(|| {
48 let old_handler = take_hook();
49 set_hook(Box::new(scoped_hook_dispatcher));
50 unsafe {
51 DEFAULT_HOOK = Some(old_handler);
52 }
53 });
54 }
55 /// Panic hook which delegates execution to scoped hook,
56 /// if one installed, or to default hook
57 fn scoped_hook_dispatcher(info: &PanicInfo<'_>) {
58 let handler = SCOPED_HOOK_PTR.get();
59 if !handler.is_null() {
60 // It's assumed that if container's ptr is not null, ptr to `FnMut` is non-null too.
61 // Correctness **must** be ensured by hook switch code in `with_hook`
62 let hook = unsafe { &mut *(*handler).0 };
63 (hook)(info);
64 return;
65 }
66
67 #[allow(static_mut_refs)]
68 if let Some(hook) = unsafe { DEFAULT_HOOK.as_ref() } {
69 (hook)(info);
70 }
71 }
72 /// Executes stored closure when dropped
73 struct Finally<F: FnOnce()>(Option<F>);
74
75 impl<F: FnOnce()> Finally<F> {
76 fn new(body: F) -> Self {
77 Self(Some(body))
78 }
79 }
80
81 impl<F: FnOnce()> Drop for Finally<F> {
82 fn drop(&mut self) {
83 if let Some(body) = self.0.take() {
84 body();
85 }
86 }
87 }
88 /// Executes main closure `body` while installing `guard` as scoped panic hook,
89 /// for execution duration.
90 ///
91 /// Any panics which happen during execution of `body` are passed to `guard` hook
92 /// to collect any info necessary, although unwind process is **NOT** interrupted.
93 /// See module documentation for details
94 ///
95 /// # Parameters
96 /// * `panic_hook` - scoped panic hook, functions for the duration of `body` execution
97 /// * `body` - actual logic covered by `panic_hook`
98 ///
99 /// # Returns
100 /// `body`'s return value
101 pub fn with_hook<R>(
102 mut panic_hook: impl FnMut(&PanicInfo<'_>),
103 body: impl FnOnce() -> R,
104 ) -> R {
105 init();
106 // Construct scoped hook pointer
107 let guard_tuple = (unsafe {
108 // `mem::transmute` is needed due to borrow checker restrictions to erase all lifetimes
109 mem::transmute(&mut panic_hook as *mut dyn FnMut(&PanicInfo<'_>))
110 },);
111 let old_tuple = SCOPED_HOOK_PTR.replace(&guard_tuple);
112 // Old scoped hook **must** be restored before leaving function scope to keep it sound
113 let _undo = Finally::new(|| {
114 SCOPED_HOOK_PTR.set(old_tuple);
115 });
116 body()
117 }
118}
119
120#[cfg(not(feature = "handle-panics"))]
121mod internal {
122 use core::panic::PanicInfo;
123
124 /// Simply executes `body` and returns its execution result.
125 /// Hook parameter is ignored
126 pub fn with_hook<R>(
127 _: impl FnMut(&PanicInfo<'_>),
128 body: impl FnOnce() -> R,
129 ) -> R {
130 body()
131 }
132}
133
134pub use internal::with_hook;