From 53a048dcdf2db5e10c66bbe99c92d781cd9dd7e4 Mon Sep 17 00:00:00 2001 From: Target-san Date: Sat, 5 Oct 2024 15:57:37 +0300 Subject: [PATCH 1/6] Add 'handle-panics' feature --- proptest/Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/proptest/Cargo.toml b/proptest/Cargo.toml index 467abde6..3745377d 100644 --- a/proptest/Cargo.toml +++ b/proptest/Cargo.toml @@ -59,6 +59,10 @@ atomic64bit = [] bit-set = [ "dep:bit-set", "dep:bit-vec" ] +# Enables proper handling of panics +# In particular, hides all intermediate panics flowing into stderr during shrink phase +handle-panics = ["std"] + [dependencies] bitflags = "2" unarray = "0.1.4" From 639f68633b64ab1ac3cc2b71a66ecd464e3b9ebd Mon Sep 17 00:00:00 2001 From: Target-san Date: Sat, 5 Oct 2024 15:58:07 +0300 Subject: [PATCH 2/6] Add conditional scoped panic hook implementation --- proptest/src/test_runner/mod.rs | 1 + proptest/src/test_runner/scoped_panic_hook.rs | 123 ++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 proptest/src/test_runner/scoped_panic_hook.rs diff --git a/proptest/src/test_runner/mod.rs b/proptest/src/test_runner/mod.rs index d7516ab9..836d26d5 100644 --- a/proptest/src/test_runner/mod.rs +++ b/proptest/src/test_runner/mod.rs @@ -21,6 +21,7 @@ mod replay; mod result_cache; mod rng; mod runner; +mod scoped_panic_hook; pub use self::config::*; pub use self::errors::*; diff --git a/proptest/src/test_runner/scoped_panic_hook.rs b/proptest/src/test_runner/scoped_panic_hook.rs new file mode 100644 index 00000000..95793470 --- /dev/null +++ b/proptest/src/test_runner/scoped_panic_hook.rs @@ -0,0 +1,123 @@ +#[cfg(feature = "handle-panics")] +mod panicky { + //! 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 + /// + /// 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); + } + }); + } + /// 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; + } + + if let Some(hook) = unsafe { DEFAULT_HOOK.as_ref() } { + (hook)(info); + } + } + /// Executes stored closure when dropped + struct Finally(Option); + + 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(); + } + } + } + /// 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 panicky { + use std::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() + } +} + +pub use panicky::with_hook; From 343d74f18a21f15fcb38a0e5346534670a88b0d7 Mon Sep 17 00:00:00 2001 From: Target-san Date: Sat, 5 Oct 2024 16:04:10 +0300 Subject: [PATCH 3/6] Silence out panic from test case --- proptest/src/test_runner/runner.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/proptest/src/test_runner/runner.rs b/proptest/src/test_runner/runner.rs index a1af43ea..248fc994 100644 --- a/proptest/src/test_runner/runner.rs +++ b/proptest/src/test_runner/runner.rs @@ -1,5 +1,5 @@ //- -// Copyright 2017, 2018, 2019 The proptest developers +// Copyright 2017, 2018, 2019, 2024 The proptest developers // // Licensed under the Apache License, Version 2.0 or the MIT license @@ -253,7 +253,10 @@ where let time_start = time::Instant::now(); let mut result = unwrap_or!( - panic::catch_unwind(AssertUnwindSafe(|| test(case))), + 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())) From b438970a7ed60cea5018a7033ccca439c22f9e6a Mon Sep 17 00:00:00 2001 From: Target-san Date: Sat, 5 Oct 2024 16:06:50 +0300 Subject: [PATCH 4/6] Updated changelog --- proptest/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/proptest/CHANGELOG.md b/proptest/CHANGELOG.md index adc6b9da..e8e1c393 100644 --- a/proptest/CHANGELOG.md +++ b/proptest/CHANGELOG.md @@ -3,6 +3,7 @@ ### New Features - When running persisted regressions, the most recently added regression is now run first. +- Added `handle-panics` feature which enables catching panics raised in tests and turning them into failures ## 1.5.0 From 6081fe85cad7dd2e33461897065eb8263b5269d4 Mon Sep 17 00:00:00 2001 From: Target-san Date: Sat, 5 Oct 2024 16:42:44 +0300 Subject: [PATCH 5/6] Rename internal module to `internal` --- proptest/src/test_runner/scoped_panic_hook.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/proptest/src/test_runner/scoped_panic_hook.rs b/proptest/src/test_runner/scoped_panic_hook.rs index 95793470..4b84c700 100644 --- a/proptest/src/test_runner/scoped_panic_hook.rs +++ b/proptest/src/test_runner/scoped_panic_hook.rs @@ -1,5 +1,5 @@ #[cfg(feature = "handle-panics")] -mod panicky { +mod internal { //! Implementation of scoped panic hooks //! //! 1. `with_hook` serves as entry point, it executes body closure with panic hook closure @@ -108,7 +108,7 @@ mod panicky { } #[cfg(not(feature = "handle-panics"))] -mod panicky { +mod internal { use std::panic::PanicInfo; /// Simply executes `body` and returns its execution result. /// Hook parameter is ignored @@ -120,4 +120,4 @@ mod panicky { } } -pub use panicky::with_hook; +pub use internal::with_hook; From eb7fdfcec05377f444402af850ae1441f7fa8897 Mon Sep 17 00:00:00 2001 From: Target-san Date: Sat, 5 Oct 2024 16:45:48 +0300 Subject: [PATCH 6/6] Licensing header --- proptest/src/test_runner/scoped_panic_hook.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/proptest/src/test_runner/scoped_panic_hook.rs b/proptest/src/test_runner/scoped_panic_hook.rs index 4b84c700..63183fbd 100644 --- a/proptest/src/test_runner/scoped_panic_hook.rs +++ b/proptest/src/test_runner/scoped_panic_hook.rs @@ -1,3 +1,12 @@ +//- +// Copyright 2024 The proptest developers +// +// 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. + #[cfg(feature = "handle-panics")] mod internal { //! Implementation of scoped panic hooks