diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 2106d4f6..73400938 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -48,6 +48,12 @@ jobs: run: cd proptest && cargo test --verbose - name: Run macro tests run: cd proptest-macro && cargo test --verbose + - name: Run tests handle-panics + run: cd proptest && cargo test --verbose --features handle-panics + - name: Run tests backtrace + run: cd proptest && cargo test --verbose --features backtrace + - name: Run tests handle-panics+backtrace + run: cd proptest && cargo test --verbose --features "handle-panics backtrace" - name: Build coverage no-default-features if: ${{ matrix.build == 'stable' }} env: diff --git a/proptest-macro/Cargo.toml b/proptest-macro/Cargo.toml index dc47a12a..121326a2 100644 --- a/proptest-macro/Cargo.toml +++ b/proptest-macro/Cargo.toml @@ -23,3 +23,7 @@ convert_case = "0.6" [dev-dependencies] insta = "1" prettyplease = "0.2" +# Transitive dependency of `insta`, v0.15.10 requires MSRV 1.66 +console = "=0.15.8" +# Transitive dependency of `insta`, v0.1.14+ requires some features unstable on Rust 1.65 +unicode-width = "=0.1.13" diff --git a/proptest/CHANGELOG.md b/proptest/CHANGELOG.md index 96f24d7b..aeb9bafc 100644 --- a/proptest/CHANGELOG.md +++ b/proptest/CHANGELOG.md @@ -1,5 +1,8 @@ ## Unreleased +- Added `backtrace` feature which enables capturing backtraces for both test failures and panics, + if `handle-panics` feature is enabled + ## 1.6.0 ### New Features diff --git a/proptest/Cargo.toml b/proptest/Cargo.toml index 911e13ad..f507b2a4 100644 --- a/proptest/Cargo.toml +++ b/proptest/Cargo.toml @@ -62,6 +62,11 @@ bit-set = ["dep:bit-set", "dep:bit-vec"] # In particular, hides all intermediate panics flowing into stderr during shrink phase handle-panics = ["std"] +# Enables gathering of failure backtraces +# * when test failure is reported via `prop_assert_*` macro +# * when normal assertion fails or panic fires, if `handle-panics` feature is enabled too +backtrace = ["std"] + [dependencies] bitflags = "2" unarray = "0.1.4" @@ -123,5 +128,6 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [dev-dependencies] +assert_matches = "1.5.0" regex = "1.0" trybuild = "=1.0.0" diff --git a/proptest/src/arbitrary/_alloc/collections.rs b/proptest/src/arbitrary/_alloc/collections.rs index 3d0c50bc..c6f3333c 100644 --- a/proptest/src/arbitrary/_alloc/collections.rs +++ b/proptest/src/arbitrary/_alloc/collections.rs @@ -19,6 +19,7 @@ use crate::std_facade::{ binary_heap, btree_map, btree_set, fmt, linked_list, vec, vec_deque, Arc, BTreeMap, BTreeSet, BinaryHeap, Box, LinkedList, Rc, Vec, VecDeque, }; +#[cfg(feature = "std")] use core::hash::Hash; use core::ops::{Bound, RangeInclusive}; diff --git a/proptest/src/sugar.rs b/proptest/src/sugar.rs index 7d80d07b..b09ed1d3 100644 --- a/proptest/src/sugar.rs +++ b/proptest/src/sugar.rs @@ -751,10 +751,11 @@ macro_rules! prop_assert { ($cond:expr, $($fmt:tt)*) => { if !$cond { - let message = format!($($fmt)*); - let message = format!("{} at {}:{}", message, file!(), line!()); return ::core::result::Result::Err( - $crate::test_runner::TestCaseError::fail(message)); + $crate::test_runner::TestCaseError::fail( + $crate::test_runner::Reason::with_location_and_backtrace(format!($($fmt)*)) + ) + ); } }; } diff --git a/proptest/src/test_runner/backtrace.rs b/proptest/src/test_runner/backtrace.rs new file mode 100644 index 00000000..d5a77b2e --- /dev/null +++ b/proptest/src/test_runner/backtrace.rs @@ -0,0 +1,135 @@ +//- +// Copyright 2024 +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use core::fmt; +/// Holds test failure backtrace, if captured +/// +/// If feature `backtrace` is disabled, it's a zero-sized struct with no logic +/// +/// If `backtrace` is enabled, attempts to capture backtrace using `std::backtrace::Backtrace` - +/// if requested +#[derive(Clone, Default)] +pub struct Backtrace(internal::Backtrace); + +impl Backtrace { + /// Creates empty backtrace object + /// + /// Used when client code doesn't care + pub fn empty() -> Self { + Self(internal::Backtrace::empty()) + } + /// Tells whether there's backtrace captured + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + /// Attempts to capture backtrace - but only if `backtrace` feature is enabled + #[inline(always)] + pub fn capture() -> Self { + Self(internal::Backtrace::capture()) + } +} + +impl fmt::Debug for Backtrace { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.0, f) + } +} + +impl fmt::Display for Backtrace { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +#[cfg(feature = "backtrace")] +mod internal { + use core::fmt; + use std::backtrace as bt; + use std::sync::Arc; + + // `std::backtrace::Backtrace` isn't `Clone`, so we have + // to use `Arc` to also maintain `Send + Sync` + #[derive(Clone, Default)] + pub struct Backtrace(Option>); + + impl Backtrace { + pub fn empty() -> Self { + Self(None) + } + + pub fn is_empty(&self) -> bool { + self.0.is_none() + } + + #[inline(always)] + pub fn capture() -> Self { + let bt = bt::Backtrace::capture(); + // Store only if we have backtrace + if bt.status() == bt::BacktraceStatus::Captured { + Self(Some(Arc::new(bt))) + } else { + Self(None) + } + } + } + + impl fmt::Debug for Backtrace { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(ref arc) = self.0 { + fmt::Debug::fmt(arc.as_ref(), f) + } else { + Ok(()) + } + } + } + + impl fmt::Display for Backtrace { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(ref arc) = self.0 { + fmt::Display::fmt(arc.as_ref(), f) + } else { + Ok(()) + } + } + } +} + +#[cfg(not(feature = "backtrace"))] +mod internal { + use core::fmt; + + #[derive(Clone, Default)] + pub struct Backtrace; + + impl Backtrace { + pub fn empty() -> Self { + Self + } + + pub fn is_empty(&self) -> bool { + true + } + + pub fn capture() -> Self { + Self + } + } + + impl fmt::Debug for Backtrace { + fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result { + Ok(()) + } + } + + impl fmt::Display for Backtrace { + fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result { + Ok(()) + } + } +} diff --git a/proptest/src/test_runner/errors.rs b/proptest/src/test_runner/errors.rs index bf17f201..ef3c455b 100644 --- a/proptest/src/test_runner/errors.rs +++ b/proptest/src/test_runner/errors.rs @@ -86,7 +86,9 @@ impl fmt::Display for TestCaseError { TestCaseError::Reject(ref whence) => { write!(f, "Input rejected at {}", whence) } - TestCaseError::Fail(ref why) => write!(f, "Case failed: {}", why), + TestCaseError::Fail(ref why) => { + write!(f, "Case failed: {}", why.display_detailed()) + } } } } @@ -115,7 +117,7 @@ impl fmt::Display for TestError { match *self { TestError::Abort(ref why) => write!(f, "Test aborted: {}", why), TestError::Fail(ref why, ref what) => { - writeln!(f, "Test failed: {}.", why)?; + writeln!(f, "Test failed: {}", why.display_detailed())?; write!(f, "minimal failing input: {:#?}", what) } } diff --git a/proptest/src/test_runner/mod.rs b/proptest/src/test_runner/mod.rs index 836d26d5..d694d417 100644 --- a/proptest/src/test_runner/mod.rs +++ b/proptest/src/test_runner/mod.rs @@ -12,6 +12,7 @@ //! You do not normally need to access things in this module directly except //! when implementing new low-level strategies. +mod backtrace; mod config; mod errors; mod failure_persistence; @@ -21,6 +22,7 @@ mod replay; mod result_cache; mod rng; mod runner; +#[cfg(feature = "handle-panics")] mod scoped_panic_hook; pub use self::config::*; diff --git a/proptest/src/test_runner/reason.rs b/proptest/src/test_runner/reason.rs index 38cc7e32..5db338ce 100644 --- a/proptest/src/test_runner/reason.rs +++ b/proptest/src/test_runner/reason.rs @@ -7,19 +7,65 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. +use super::backtrace::Backtrace; use crate::std_facade::{fmt, Box, Cow, String}; /// The reason for why something, such as a generated value, was rejected. /// -/// Currently this is merely a wrapper around a message, but more properties -/// may be added in the future. +/// Contains message which describes reason and optionally backtrace +/// (depending on several factors like features `backtrace` and +/// `handle-panics`, and actual spot where reason was created). /// /// This is constructed via `.into()` on a `String`, `&'static str`, or /// `Box`. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Reason(Cow<'static, str>); +#[derive(Clone)] +pub struct Reason(Cow<'static, str>, Backtrace); impl Reason { + /// Creates reason from provided message + /// + /// # Parameters + /// * `message` - anything convertible to shared or owned string + /// + /// # Returns + /// Reason object + pub fn new(message: impl Into>) -> Self { + Self(message.into(), Backtrace::empty()) + } + /// Creates reason from provided message, adding location info as its part + /// + /// # Parameters + /// * `message` - anything convertible to shared or owned string + /// + /// # Returns + /// Reason object + #[track_caller] + pub fn with_location(message: impl Into>) -> Self { + let message: Cow<'static, str> = message.into(); + let loc = core::panic::Location::caller(); + Self( + append_location(message.into_owned(), *loc).into(), + Backtrace::empty(), + ) + } + /// Creates reason from provided message, adding location info as its part, + /// and captures backtrace at callsite + /// + /// NOTE: Backtrace is actually captured only if `backtrace` feature is enabled, + /// otherwise it'll be empty + /// + /// # Parameters + /// * `message` - anything convertible to shared or owned string + /// + /// # Returns + /// Reason object with provided message, augmented with location info, and captured backtrace + #[inline(always)] + #[track_caller] + pub fn with_location_and_backtrace( + message: impl Into>, + ) -> Self { + Self(Self::with_location(message).0, Backtrace::capture()) + } /// Return the message for this `Reason`. /// /// The message is intended for human consumption, and is not guaranteed to @@ -27,28 +73,135 @@ impl Reason { pub fn message(&self) -> &str { &*self.0 } + /// Produces displayable value which displays all data stored in Reason, + /// unlike normal `Display` implementation which shows only message + pub fn display_detailed(&self) -> impl fmt::Display + '_ { + DisplayReason(self) + } +} + +impl core::cmp::PartialEq for Reason { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl core::cmp::Eq for Reason {} + +impl core::cmp::PartialOrd for Reason { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl core::cmp::Ord for Reason { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.0.cmp(&other.0) + } +} + +impl core::hash::Hash for Reason { + fn hash(&self, state: &mut H) { + self.0.hash(state); + } +} + +impl From<(Cow<'static, str>, Backtrace)> for Reason { + fn from((msg, bt): (Cow<'static, str>, Backtrace)) -> Self { + Self(msg, bt) + } } impl From<&'static str> for Reason { fn from(s: &'static str) -> Self { - Reason(s.into()) + Self(s.into(), Backtrace::empty()) } } impl From for Reason { fn from(s: String) -> Self { - Reason(s.into()) + Self(s.into(), Backtrace::empty()) } } impl From> for Reason { fn from(s: Box) -> Self { - Reason(String::from(s).into()) + Self(String::from(s).into(), Backtrace::empty()) + } +} + +#[cfg(feature = "std")] +impl<'a> From<&'a (dyn std::any::Any + Send)> for Reason { + #[inline(always)] + fn from(value: &'a (dyn std::any::Any + Send)) -> Self { + use std::string::ToString; + + let message: String = value + .downcast_ref::<&'static str>() + .map(|s| s.to_string()) + .or_else(|| value.downcast_ref::().map(|s| s.clone())) + .or_else(|| value.downcast_ref::>().map(|s| s.to_string())) + .unwrap_or_else(|| "".to_string()); + + Self(message.into(), Backtrace::empty()) + } +} + +#[cfg(feature = "std")] +impl<'a, 'b> From<&'b std::panic::PanicInfo<'a>> for Reason { + #[inline(always)] + #[track_caller] + fn from(value: &'b std::panic::PanicInfo<'a>) -> Self { + let Self(mut message, _) = value.payload().into(); + + if let Some(loc) = value.location() { + message = append_location(message.into_owned(), *loc).into(); + } + + Self(message, Backtrace::capture()) + } +} + +impl fmt::Debug for Reason { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_tuple("Reason") + .field(&self.0) + .field(&"Backtrace(...)") + .finish() } } impl fmt::Display for Reason { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(self.message(), f) + write!(f, "{}", self.message()) + } +} + +struct DisplayReason<'a>(&'a Reason); + +impl<'a> fmt::Display for DisplayReason<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let Self(Reason(msg, bt)) = self; + if bt.is_empty() { + write!(f, "{msg}") + } else { + write!(f, "{msg}\nstack backtrace:\n{bt}") + } + } +} + +fn append_location<'a>( + message: String, + loc: core::panic::Location<'a>, +) -> String { + match message.rfind('\n') { + // Message is multiline and ends with '\n' + Some(pos) if pos == message.len() - '\n'.len_utf8() => { + format!("{message}at {loc}") + } + // Message is multiline and doesn't end with '\n' + Some(_) => format!("{message}\nat {loc}"), + // Message is not multiline + _ => format!("{message} at {loc}"), } } diff --git a/proptest/src/test_runner/runner.rs b/proptest/src/test_runner/runner.rs index 8ed64809..4accd2e6 100644 --- a/proptest/src/test_runner/runner.rs +++ b/proptest/src/test_runner/runner.rs @@ -7,12 +7,10 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -use crate::std_facade::{Arc, BTreeMap, Box, String, Vec}; +use crate::std_facade::{Arc, BTreeMap, Box, Vec}; use core::sync::atomic::AtomicUsize; use core::sync::atomic::Ordering::SeqCst; use core::{fmt, iter}; -#[cfg(feature = "std")] -use std::panic::{self, AssertUnwindSafe}; #[cfg(feature = "fork")] use rusty_fork; @@ -225,6 +223,30 @@ where F: Fn(V) -> TestCaseResult, R: Iterator, { + use std::panic::{self, AssertUnwindSafe}; + + #[cfg(feature = "handle-panics")] + fn run_case(case: impl FnOnce() -> TestCaseResult) -> TestCaseResult { + let mut reason = None; + unwrap_or!( + super::scoped_panic_hook::with_hook( + |panic_info| { reason = Some(panic_info.into()); }, + || panic::catch_unwind(AssertUnwindSafe(case)) + ), + _panic => Err(TestCaseError::Fail(reason.expect( + "Reason should have been obtained from panic hook" + ))) + ) + } + + #[cfg(not(feature = "handle-panics"))] + fn run_case(case: impl FnOnce() -> TestCaseResult) -> TestCaseResult { + unwrap_or!( + panic::catch_unwind(AssertUnwindSafe(case)), + panic => Err(TestCaseError::Fail(panic.as_ref().into())) + ) + } + #[cfg(feature = "timeout")] let timeout = runner.config.timeout(); @@ -252,16 +274,7 @@ where #[cfg(feature = "timeout")] let time_start = std::time::Instant::now(); - let mut result = unwrap_or!( - super::scoped_panic_hook::with_hook( - |_| { /* Silence out panic backtrace */ }, - || panic::catch_unwind(AssertUnwindSafe(|| test(case))) - ), - what => Err(TestCaseError::Fail( - what.downcast::<&'static str>().map(|s| (*s).into()) - .or_else(|what| what.downcast::().map(|b| (*b).into())) - .or_else(|what| what.downcast::>().map(|b| (*b).into())) - .unwrap_or_else(|_| "".into())))); + let mut result = run_case(|| test(case)); // If there is a timeout and we exceeded it, fail the test here so we get // consistent behaviour. (The parent process cannot precisely time the test @@ -773,7 +786,7 @@ impl TestRunner { INFO_LOG, "Shrinking disabled by configuration" ); - return None + return None; } #[cfg(all(feature = "std", not(target_arch = "wasm32")))] @@ -1070,6 +1083,8 @@ mod test { use std::cell::Cell; use std::fs; + use assert_matches::assert_matches; + use super::*; use crate::strategy::Strategy; use crate::test_runner::{FileFailurePersistence, RngAlgorithm, TestRng}; @@ -1127,7 +1142,10 @@ mod test { assert!(v < 5, "not less than 5"); Ok(()) }); - assert_eq!(Err(TestError::Fail("not less than 5".into(), 5)), result); + assert_matches!( + result, + Err(TestError::Fail(reason, 5)) if reason.message().starts_with("not less than 5") + ); } #[test] diff --git a/proptest/src/test_runner/scoped_panic_hook.rs b/proptest/src/test_runner/scoped_panic_hook.rs index 97baef12..dccf7190 100644 --- a/proptest/src/test_runner/scoped_panic_hook.rs +++ b/proptest/src/test_runner/scoped_panic_hook.rs @@ -7,128 +7,102 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -#[cfg(feature = "handle-panics")] -mod internal { - //! Implementation of scoped panic hooks - //! - //! 1. `with_hook` serves as entry point, it executes body closure with panic hook closure - //! installed as scoped panic hook - //! 2. Upon first execution, current panic hook is replaced with `scoped_hook_dispatcher` - //! in a thread-safe manner, and original hook is stored for later use - //! 3. When panic occurs, `scoped_hook_dispatcher` either delegates execution to scoped - //! panic hook, if one is installed, or back to original hook stored earlier. - //! This preserves original behavior when scoped hook isn't used - //! 4. When `with_hook` is used, it replaces stored scoped hook pointer with pointer to - //! hook closure passed as parameter. Old hook pointer is set to be restored unconditionally - //! via drop guard. Then, normal body closure is executed. - use std::boxed::Box; - use std::cell::Cell; - use std::panic::{set_hook, take_hook, PanicInfo}; - use std::sync::Once; - use std::{mem, ptr}; +//! Implementation of scoped panic hooks +//! +//! 1. `with_hook` serves as entry point, it executes body closure with panic hook closure +//! installed as scoped panic hook +//! 2. Upon first execution, current panic hook is replaced with `scoped_hook_dispatcher` +//! in a thread-safe manner, and original hook is stored for later use +//! 3. When panic occurs, `scoped_hook_dispatcher` either delegates execution to scoped +//! panic hook, if one is installed, or back to original hook stored earlier. +//! This preserves original behavior when scoped hook isn't used +//! 4. When `with_hook` is used, it replaces stored scoped hook pointer with pointer to +//! hook closure passed as parameter. Old hook pointer is set to be restored unconditionally +//! via drop guard. Then, normal body closure is executed. +use std::boxed::Box; +use std::cell::Cell; +use std::panic::{set_hook, take_hook, PanicInfo}; +use std::sync::Once; +use std::{mem, ptr}; - thread_local! { - /// Pointer to currently installed scoped panic hook, if any - /// - /// NB: pointers to arbitrary fn's are fat, and Rust doesn't allow crafting null pointers - /// to fat objects. So we just store const pointer to tuple with whatever data we need - static SCOPED_HOOK_PTR: Cell<*const (*mut dyn FnMut(&PanicInfo<'_>),)> = Cell::new(ptr::null()); - } - - static INIT_ONCE: Once = Once::new(); - /// Default panic hook, the one which was present before installing scoped one +thread_local! { + /// Pointer to currently installed scoped panic hook, if any /// - /// NB: no need for external sync, value is mutated only once, when init is performed - static mut DEFAULT_HOOK: Option) + Send + Sync>> = - None; - /// Replaces currently installed panic hook with `scoped_hook_dispatcher` once, - /// in a thread-safe manner - fn init() { - INIT_ONCE.call_once(|| { - let old_handler = take_hook(); - set_hook(Box::new(scoped_hook_dispatcher)); - unsafe { - DEFAULT_HOOK = Some(old_handler); + /// NB: pointers to arbitrary fn's are fat, and Rust doesn't allow crafting null pointers + /// to fat objects. So we just store const pointer to tuple with whatever data we need + static SCOPED_HOOK_PTR: Cell<*const (*mut dyn FnMut(&PanicInfo<'_>),)> = Cell::new(ptr::null()); +} + +static INIT_ONCE: Once = Once::new(); +/// Replaces currently installed panic hook with `scoped_hook_dispatcher` once, +/// in a thread-safe manner +fn init() { + INIT_ONCE.call_once(|| { + let old_handler = take_hook(); + set_hook(Box::new(move |panic_info| { + if !scoped_hook_dispatcher(panic_info) { + old_handler(panic_info) } - }); + })); + }); +} +/// Panic hook which delegates execution to scoped hook, +/// if one installed, or to default hook +fn scoped_hook_dispatcher(info: &PanicInfo<'_>) -> bool { + let handler = SCOPED_HOOK_PTR.with(Cell::get); + if handler.is_null() { + return false; } - /// Panic hook which delegates execution to scoped hook, - /// if one installed, or to default hook - fn scoped_hook_dispatcher(info: &PanicInfo<'_>) { - let handler = SCOPED_HOOK_PTR.get(); - if !handler.is_null() { - // It's assumed that if container's ptr is not null, ptr to `FnMut` is non-null too. - // Correctness **must** be ensured by hook switch code in `with_hook` - let hook = unsafe { &mut *(*handler).0 }; - (hook)(info); - return; - } + // It's assumed that if container's ptr is not null, ptr to `FnMut` is non-null too. + // Correctness **must** be ensured by hook switch code in `with_hook` + let hook = unsafe { &mut *(*handler).0 }; + (hook)(info); - #[allow(static_mut_refs)] - if let Some(hook) = unsafe { DEFAULT_HOOK.as_ref() } { - (hook)(info); - } - } - /// Executes stored closure when dropped - struct Finally(Option); + true +} +/// Executes stored closure when dropped +struct Finally(Option); - impl Finally { - fn new(body: F) -> Self { - Self(Some(body)) - } +impl Finally { + fn new(body: F) -> Self { + Self(Some(body)) } +} - impl Drop for Finally { - fn drop(&mut self) { - if let Some(body) = self.0.take() { - body(); - } +impl Drop for Finally { + fn drop(&mut self) { + if let Some(body) = self.0.take() { + body(); } } - /// Executes main closure `body` while installing `guard` as scoped panic hook, - /// for execution duration. - /// - /// Any panics which happen during execution of `body` are passed to `guard` hook - /// to collect any info necessary, although unwind process is **NOT** interrupted. - /// See module documentation for details - /// - /// # Parameters - /// * `panic_hook` - scoped panic hook, functions for the duration of `body` execution - /// * `body` - actual logic covered by `panic_hook` - /// - /// # Returns - /// `body`'s return value - pub fn with_hook( - mut panic_hook: impl FnMut(&PanicInfo<'_>), - body: impl FnOnce() -> R, - ) -> R { - init(); - // Construct scoped hook pointer - let guard_tuple = (unsafe { - // `mem::transmute` is needed due to borrow checker restrictions to erase all lifetimes - mem::transmute(&mut panic_hook as *mut dyn FnMut(&PanicInfo<'_>)) - },); - let old_tuple = SCOPED_HOOK_PTR.replace(&guard_tuple); - // Old scoped hook **must** be restored before leaving function scope to keep it sound - let _undo = Finally::new(|| { - SCOPED_HOOK_PTR.set(old_tuple); - }); - body() - } } - -#[cfg(not(feature = "handle-panics"))] -mod internal { - use core::panic::PanicInfo; - - /// Simply executes `body` and returns its execution result. - /// Hook parameter is ignored - pub fn with_hook( - _: impl FnMut(&PanicInfo<'_>), - body: impl FnOnce() -> R, - ) -> R { - body() - } +/// Executes main closure `body` while installing `guard` as scoped panic hook, +/// for execution duration. +/// +/// Any panics which happen during execution of `body` are passed to `guard` hook +/// to collect any info necessary, although unwind process is **NOT** interrupted. +/// See module documentation for details +/// +/// # Parameters +/// * `panic_hook` - scoped panic hook, functions for the duration of `body` execution +/// * `body` - actual logic covered by `panic_hook` +/// +/// # Returns +/// `body`'s return value +pub fn with_hook( + mut panic_hook: impl FnMut(&PanicInfo<'_>), + body: impl FnOnce() -> R, +) -> R { + init(); + // Construct scoped hook pointer + let guard_tuple = (unsafe { + // `mem::transmute` is needed due to borrow checker restrictions to erase all lifetimes + mem::transmute(&mut panic_hook as *mut dyn FnMut(&PanicInfo<'_>)) + },); + let old_tuple = SCOPED_HOOK_PTR.with(|c| c.replace(&guard_tuple)); + // Old scoped hook **must** be restored before leaving function scope to keep it sound + let _undo = Finally::new(|| { + SCOPED_HOOK_PTR.with(|c| c.set(old_tuple)); + }); + body() } - -pub use internal::with_hook;