panic_control/panic_control.rs
1// Copyright 2017 Mikhail Zabaluev <mikhail.zabaluev@gmail.com>
2// See the COPYRIGHT file at the top-level directory of this source tree.
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
11//! Controlled panics using dynamic type checking.
12//!
13//! Sometimes there is a need to test how Rust code behaves on occurrence
14//! of a panic. A panic can be invoked on purpose in a thread spawned by the
15//! test and the effects observed after the thread is joined.
16//! The problem with "benign" panics is that it may be cumbersome to tell them
17//! apart from panics indicating actual errors, such as assertion failures.
18//!
19//! Another issue is the behavior of the default panic hook.
20//! It is very useful for getting information about the cause of an
21//! unexpected thread panic, but for tests causing panics on purpose it
22//! produces annoying output noise. The panic hook can be overridden,
23//! but custom panic hooks affect the entire program, which in typical
24//! usage is the test runner; it is easy to misuse them causing important
25//! error information to go unreported.
26//!
27//! The simplest way, as provided by the standard library, to propagate
28//! a panic that occurred in a child thread to the thread that spawned it
29//! is to call `unwrap` on the result of `JoinHandle::join`. Unfortunately,
30//! due to [an issue](https://github.com/rust-lang/rfcs/issues/1389) with
31//! the implementation of `Any`, the resulting panic message does not relay
32//! information from the child thread's panic.
33//!
34//! This crate provides utilities and an ergonomic interface for testing
35//! panics in a controlled and output-friendly way using dynamic type checks
36//! to discern between expected and unexpected panics.
37//!
38//! # Expected Panic Type
39//!
40//! The recommended way to designate panics as expected is by using values of
41//! a custom type as the parameter for `panic!`. The type could be as simple
42//! as a token unit-like struct, or it can be equipped to carry additional
43//! information from the panic site.
44//! Any panic value type shall be `Sized`, `'static`, and `Send`.
45//! For the value to be usable in testing, it should also implement
46//! at least `Debug` and `PartialEq`.
47//!
48//! # Examples
49//!
50//! ```
51//! use panic_control::{Context, Outcome};
52//! use panic_control::{chain_hook_ignoring, spawn_quiet};
53//! use panic_control::ThreadResultExt;
54//!
55//! use std::thread;
56//!
57//! #[derive(Debug, PartialEq, Eq)]
58//! enum Expected {
59//! Token,
60//! Int(i32),
61//! String(String)
62//! }
63//!
64//! // Rust's stock test runner does not provide a way to do global
65//! // initialization and the tests are run in parallel in a random
66//! // order by default. So this is our solution, to be called at
67//! // the beginning of every test exercising a panic with an
68//! // Expected value.
69//! fn silence_expected_panics() {
70//! use std::sync::{Once, ONCE_INIT};
71//! static HOOK_ONCE: Once = ONCE_INIT;
72//! HOOK_ONCE.call_once(|| {
73//! chain_hook_ignoring::<Expected>()
74//! });
75//! }
76//!
77//! # struct TypeUnderTest;
78//! # impl TypeUnderTest {
79//! # fn new() -> TypeUnderTest { TypeUnderTest }
80//! # fn doing_fine(&self) -> bool { true }
81//! # }
82//! // ...
83//!
84//! silence_expected_panics();
85//! let thread_builder = thread::Builder::new()
86//! .name("My panicky thread".into());
87//! let ctx = Context::<Expected>::from(thread_builder);
88//! let h = ctx.spawn(|| {
89//! let unwind_me = TypeUnderTest::new();
90//! assert!(unwind_me.doing_fine());
91//! // ^-- If this fails, join() will return Err
92//! panic!(Expected::String("Rainbows and unicorns!".into()));
93//! });
94//! let outcome = h.join().unwrap_or_propagate();
95//! match outcome {
96//! Outcome::Panicked(Expected::String(s)) => {
97//! println!("thread panicked as expected: {}", s);
98//! }
99//! _ => panic!("unexpected value returned from join()")
100//! }
101//!
102//! let ctx = Context::<Expected>::new();
103//! let h = ctx.spawn_quiet(|| {
104//! let h = spawn_quiet(|| {
105//! panic!("Sup dawg, we heard you like panics \
106//! so we put a panic in your panic!");
107//! });
108//! h.join().unwrap_or_propagate();
109//! });
110//! let res = h.join();
111//! let msg = res.panic_value_as_str().unwrap();
112//! assert!(msg.contains("panic in your panic"));
113//! ```
114
115use std::any::Any;
116use std::cell::Cell;
117use std::fmt;
118use std::fmt::Debug;
119use std::marker;
120use std::panic;
121use std::panic::{PanicInfo};
122use std::thread;
123use std::sync::{Once, ONCE_INIT};
124
125
126/// Enumerates the expected outcomes from joining a panic-checked thread.
127///
128/// `Outcome` values are returned in the `Ok` result variant
129/// of the `join()` method of a `CheckedJoinHandle`.
130#[derive(Copy, Clone, Debug, PartialEq, Eq)]
131pub enum Outcome<T, P> {
132 /// Indicates that the thread closure has
133 /// returned normally, and provides the return value.
134 NoPanic(T),
135 /// Indicates that the thread has panicked with the expected type,
136 /// and provides the panic value.
137 Panicked(P)
138}
139
140impl<T, P> Outcome<T, P> {
141 /// Tests whether the value contains the variant `Panicked`.
142 pub fn has_panicked(&self) -> bool {
143 match *self {
144 Outcome::NoPanic(_) => false,
145 Outcome::Panicked(_) => true,
146 }
147 }
148}
149
150/// Wraps `std::thread::JoinHandle` for panic value discrimination.
151///
152/// A `CheckedJoinHandle` works like a standard `JoinHandle`,
153/// except that its `join()` method dynamically checks the type of
154/// the possible panic value for matching the type that is the
155/// parameter of the `Context` this handle was obtained from,
156/// and if the type matches, returns the resolved value in the
157/// "successful panic" result variant.
158///
159/// See the documentation of the `join()` method for details and
160/// an example of use.
161pub struct CheckedJoinHandle<T, P> {
162 thread_handle: thread::JoinHandle<T>,
163 phantom: marker::PhantomData<P>
164}
165
166impl<T, P> Debug for CheckedJoinHandle<T, P> {
167 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
168 f.write_str("CheckedJoinHandle { .. }")
169 }
170}
171
172impl<T, P: Any> CheckedJoinHandle<T, P> {
173
174 /// Works like `std::thread::JoinHandle::join()`, except that when
175 /// the child thread's panic value is of the expected type, it is
176 /// returned in `Ok(Outcome::Panicked(_))`. If the child thread's
177 /// closure returns normally, its return value is returned in
178 /// `Ok(Outcome::NoPanic(_))`
179 ///
180 /// # Examples
181 ///
182 /// ```
183 /// use panic_control::{Context, Outcome};
184 /// use std::thread;
185 ///
186 /// #[derive(Debug, PartialEq, Eq)]
187 /// struct Expected(pub u32);
188 ///
189 /// let ctx = Context::<Expected>::new();
190 /// let h = ctx.spawn(|| {
191 /// panic!(Expected(42));
192 /// });
193 ///
194 /// let outcome = h.join().unwrap();
195 ///
196 /// match outcome {
197 /// Outcome::Panicked(Expected(n)) => {
198 /// println!("thread panicked as expected with {}", n);
199 /// }
200 /// _ => panic!("unexpected return value from join()")
201 /// }
202 /// ```
203 pub fn join(self) -> thread::Result<Outcome<T, P>> {
204 match self.thread_handle.join() {
205 Ok(rv) => Ok(Outcome::NoPanic(rv)),
206 Err(box_any) => {
207 match box_any.downcast::<P>() {
208 Ok(p) => Ok(Outcome::Panicked(*p)),
209 Err(e) => Err(e)
210 }
211 }
212 }
213 }
214}
215
216impl<T, P> CheckedJoinHandle<T, P> {
217
218 /// Returns a reference to the underlying `JoinHandle`.
219 pub fn as_thread_join_handle(&self) -> &thread::JoinHandle<T> {
220 &self.thread_handle
221 }
222
223 /// Converts into the underlying `JoinHandle`,
224 /// giving up panic discrimination.
225 pub fn into_thread_join_handle(self) -> thread::JoinHandle<T> {
226 self.thread_handle
227 }
228}
229
230impl<T, P> AsRef<thread::JoinHandle<T>> for CheckedJoinHandle<T, P> {
231 fn as_ref(&self) -> &thread::JoinHandle<T> {
232 self.as_thread_join_handle()
233 }
234}
235
236impl<T, P> Into<thread::JoinHandle<T>> for CheckedJoinHandle<T, P> {
237 fn into(self) -> thread::JoinHandle<T> {
238 self.into_thread_join_handle()
239 }
240}
241
242/// The launch pad for a thread checked for the expected panic type.
243///
244/// The generic type `Context` serves as the type system's anchor for the
245/// expected type of the panic value, which is given as the type parameter.
246/// It can be constructed from an `std::thread::Builder` providing
247/// a customized thread configuration.
248///
249/// The method `spawn()` is used to spawn a new thread, similarly to
250/// how the function `std::thread::spawn()` works. See the documentation
251/// of the `spawn()` method for detailed description and examples.
252///
253/// # Examples
254///
255/// The example below demonstrates how to construct a `Context` from a
256/// configured `std::thread::Builder` using the implementation of
257/// the standard trait `From`.
258///
259/// ```
260/// use panic_control::Context;
261/// use std::thread;
262///
263/// #[derive(Debug, PartialEq, Eq)]
264/// struct ExpectedToken;
265///
266/// let thread_builder = thread::Builder::new()
267/// .name("My panicky thread".into())
268/// .stack_size(65 * 1024);
269/// let ctx = Context::<ExpectedToken>::from(thread_builder);
270/// let h = ctx.spawn(|| {
271/// // ...
272/// });
273/// h.join().unwrap();
274/// ```
275pub struct Context<P> {
276 thread_builder: thread::Builder,
277 phantom: marker::PhantomData<P>
278}
279
280impl<P> Debug for Context<P>
281{
282 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
283 f.debug_struct("Context")
284 .field("thread_builder", &self.thread_builder)
285 .finish()
286 }
287}
288
289impl<P: Any> Default for Context<P> {
290 fn default() -> Context<P> { Context::new() }
291}
292
293impl<P: Any> Context<P> {
294
295 /// Constructs a context with the default thread configuration.
296 ///
297 /// The type parameter can be specified explicitly:
298 ///
299 /// ```
300 /// # use panic_control::Context;
301 /// # #[derive(Debug, PartialEq)] struct Expected(pub i32);
302 /// let ctx = Context::<Expected>::new();
303 /// ```
304 pub fn new() -> Context<P> {
305 Context {
306 thread_builder: thread::Builder::new(),
307 phantom: marker::PhantomData
308 }
309 }
310
311 /// Spawns a new thread taking ownership of the `Context`, and
312 /// returns the `CheckedJoinHandle` for the thread. Other than the
313 /// return value, and panicking on an OS failure to create a thread,
314 /// this method behaves exactly like the `spawn()`
315 /// method of `std::thread::Builder` does, and if the `Context`
316 /// was constructed from a `std::thread::Builder`, its
317 /// thread configuration will be applied.
318 ///
319 /// # Panics
320 ///
321 /// Panics if the underlying call to `std::thread::Builder::spawn()`
322 /// returns an `Err` value. Any panics that function can cause apply
323 /// as well.
324 ///
325 /// # Examples
326 ///
327 /// The example below uses some kludges to work around a compiler
328 /// quirk:
329 ///
330 /// ```
331 /// # use panic_control::{Context, Outcome};
332 /// # #[derive(Debug, PartialEq)] struct Expected(pub i32);
333 /// let ctx = Context::<Expected>::new();
334 ///
335 /// #[allow(unreachable_code)]
336 /// let h = ctx.spawn(|| {
337 /// panic!(Expected(42));
338 /// ()
339 /// });
340 ///
341 /// let outcome = h.join().unwrap();
342 /// assert_eq!(outcome, Outcome::Panicked(Expected(42)));
343 /// ```
344 ///
345 /// Note that unless the unreachable return expression is present,
346 /// the compiler would have no information to infer the unspecified
347 /// first type parameter of `Outcome`, so it would settle on a default type
348 /// that is going to be changed to `!` in Rust 1.26. In previous versions
349 /// of the compiler, code that invokes this behavior is
350 /// [denied by a lint](https://github.com/rust-lang/rust/issues/39216),
351 /// so the following example does not compile:
352 ///
353 /// ```ignore
354 /// # use panic_control::{Context, Outcome};
355 /// # #[derive(Debug, PartialEq)] struct Expected(pub i32);
356 /// let ctx = Context::<Expected>::new();
357 /// let h = ctx.spawn(|| {
358 /// panic!(Expected(42));
359 /// });
360 /// let outcome = h.join().unwrap();
361 /// assert_eq!(outcome, Outcome::Panicked(Expected(42)));
362 /// ```
363 ///
364 /// A way to avoid the future incompatibility without resorting
365 /// to warning overrides is to match the `Outcome` value without
366 /// touching the sensitive parts:
367 ///
368 /// ```
369 /// # use panic_control::{Context, Outcome};
370 /// # #[derive(Debug, PartialEq)] struct Expected(pub i32);
371 /// let ctx = Context::<Expected>::new();
372 /// let h = ctx.spawn(|| {
373 /// panic!(Expected(42));
374 /// });
375 /// let outcome = h.join().unwrap();
376 /// match outcome {
377 /// Outcome::Panicked(Expected(n)) => assert_eq!(n, 42),
378 /// _ => panic!("join() returned an unexpected Outcome value")
379 /// }
380 /// ```
381 pub fn spawn<T, F>(self, f: F) -> CheckedJoinHandle<T, P>
382 where F: FnOnce() -> T,
383 F: Send + 'static,
384 T: Send + 'static
385 {
386 let thread_handle = self.thread_builder.spawn(f).unwrap();
387 CheckedJoinHandle {
388 thread_handle: thread_handle,
389 phantom: self.phantom
390 }
391 }
392
393 /// Like `spawn()`, but disables the panic hook
394 /// if the spawned thread panics.
395 ///
396 /// The process-global panic hook, either installed with
397 /// `std::panic::set_hook()` or the standard library default,
398 /// gets augmented with a filter that disables invocation of the
399 /// hook closure if the spawned thread panics.
400 ///
401 /// This function can be used in any order together with other
402 /// functions and methods of this crate that modify the panic hook.
403 ///
404 /// # Caveats
405 ///
406 /// Note that the suppression can apply to the default panic hook
407 /// that is normally used to report assertion failures and other
408 /// unexpected panics on the standard error stream.
409 /// The only remaining way to observe the panic is by checking
410 /// the result of `join()` for the spawned thread.
411 ///
412 /// Other code within the program that modifies the panic hook,
413 /// concurrently to, or after, a call to this function, may cause
414 /// the suppression not to work as intended. See the documentation
415 /// on the function `disable_hook_in_current_thread()` for possible
416 /// pitfalls.
417 ///
418 /// # Examples
419 ///
420 /// ```
421 /// # use panic_control::{Context, Outcome};
422 /// # #[derive(Debug, PartialEq)] struct Expected(pub i32);
423 /// let ctx = Context::<Expected>::new();
424 /// let h = ctx.spawn_quiet(|| {
425 /// assert!(false, "I'm panicking, \
426 /// but you can only learn about it through join()");
427 /// });
428 /// let res = h.join();
429 /// assert!(res.is_err());
430 /// ```
431 pub fn spawn_quiet<T, F>(self, f: F) -> CheckedJoinHandle<T, P>
432 where F: FnOnce() -> T,
433 F: Send + 'static,
434 T: Send + 'static
435 {
436 self.spawn(|| {
437 disable_hook_in_current_thread();
438 f()
439 })
440 }
441}
442
443impl<P: Any> From<thread::Builder> for Context<P> {
444 fn from(builder: thread::Builder) -> Context<P> {
445 Context {
446 thread_builder: builder,
447 phantom: marker::PhantomData
448 }
449 }
450}
451
452/// Helpful extension methods for `std::thread::Result`.
453///
454/// The `Result` type defined in `std::thread` is a
455/// specialization of the standard `Result` with a `Box<Any>`
456/// in the `Err` variant, which receives the payload
457/// value of a panic.
458/// As such, `Result` does not provide convenient ways
459/// to examine the content of the panic value. Furthermore,
460/// the generic implementations of `unwrap()` and related methods
461/// use the `Debug` implementation of the content of `Err` to format
462/// the panic message, which is
463/// [not very useful](https://github.com/rust-lang/rfcs/issues/1389)
464/// in case of `Any`.
465///
466/// When this trait is used in a lexical scope, it augments
467/// any `Result` value that matches the specialization of
468/// `std::thread::Result` with methods that facilitate
469/// examination and reporting of the possible string value
470/// which is most often found in the dynamically typed
471/// `Err` variant. The methods are meant to be used on the
472/// result of `std::thread::JoinHandle::join()` or
473/// `CheckedJoinHandle::join()`.
474///
475/// # Examples
476///
477/// ```
478/// use panic_control::ThreadResultExt;
479///
480/// use panic_control::Context;
481/// use std::thread;
482///
483/// let h = thread::spawn(|| {
484/// 42
485/// });
486/// let n = h.join().unwrap_or_propagate();
487/// assert_eq!(n, 42);
488///
489/// #[derive(Debug, PartialEq, Eq)]
490/// struct Expected;
491///
492/// let ctx = Context::<Expected>::new();
493/// let h = ctx.spawn_quiet(|| {
494/// panic!("No coffee beans left in the bag!");
495/// });
496/// let res = h.join();
497/// let msg = res.panic_value_as_str().unwrap();
498/// println!("{}", msg);
499/// ```
500///
501pub trait ThreadResultExt<T> : sealed::ThreadResultExtSealed {
502
503 /// Unwraps a result, yielding the content of an `Ok`.
504 ///
505 /// # Panics
506 ///
507 /// Panics if the value is an `Err`, with a panic message appended with
508 /// the `Err`'s string value if that is found to be such, or a generic
509 /// message otherwise. The message is meant to relay information from
510 /// a panic in a child thread that is observed through this result value,
511 /// as returned by `std::thread::JoinHandle::join()` or
512 /// `CheckedJoinHandle::join()`.
513 fn unwrap_or_propagate(self) -> T;
514
515 /// If the value is an `Err` and its content is a string, returns the
516 /// content as a string slice. Otherwise returns `None`.
517 fn panic_value_as_str(&self) -> Option<&str>;
518}
519
520fn str_from_any(something: &Any) -> Option<&str> {
521 if let Some(s) = something.downcast_ref::<&'static str>() {
522 Some(*s)
523 } else if let Some(s) = something.downcast_ref::<String>() {
524 Some(&s[..])
525 } else {
526 None
527 }
528}
529
530fn propagate_panic(box_any: Box<Any + Send>) -> ! {
531 match str_from_any(box_any.as_ref()) {
532 Some(s) => panic!("observed an unexpected thread panic: {}", s),
533 None => panic!("observed an unexpected thread panic \
534 with undetermined value")
535 }
536}
537
538mod sealed {
539 use std::thread;
540
541 // trait ThreadResultExt is sealed by this crate: it should only
542 // make sense for thread::Result.
543
544 pub trait ThreadResultExtSealed { }
545 impl<T> ThreadResultExtSealed for thread::Result<T> { }
546}
547
548impl<T> ThreadResultExt<T> for thread::Result<T> {
549
550 fn unwrap_or_propagate(self) -> T {
551 match self {
552 Ok(rv) => rv,
553 Err(e) => propagate_panic(e)
554 }
555 }
556
557 fn panic_value_as_str(&self) -> Option<&str> {
558 match *self {
559 Ok(_) => None,
560 Err(ref ref_box_any) => str_from_any(&**ref_box_any)
561 }
562 }
563}
564
565/// Augments the panic hook, filtering out panics of a particular type.
566///
567/// The current panic hook, either installed with `std::panic::set_hook()`
568/// or the standard library default, gets chained, again using
569/// `std::panic::set_hook()`, behind
570/// a dynamic type check for the panic payload. If it is found to be
571/// of the same type as the type parameter of this generic function,
572/// the chained hook is not called.
573///
574/// # Caveats
575///
576/// Every call to this function allocates state data and increases the
577/// filtering chain for the process-global panic hook, so it should not be
578/// called repeatedly unless necessary.
579/// Other code within the program that modifies the panic hook, concurrently
580/// to, or after, the call to this function, may cause the hook chain to stop
581/// working as intended.
582/// This function interoperates in a predictable way only with the other
583/// functions and methods of this crate that modify the panic hook,
584/// and only when used in strictly serialized order with those functions,
585/// unless noted otherwise in those functions' documentation.
586/// This function is only intended to be used in tests or initialization
587/// code of a program;
588/// libraries other than those designed for test purposes should avoid
589/// using it.
590///
591/// # Examples
592///
593/// ```
594/// use panic_control::chain_hook_ignoring;
595/// use std::sync::{Once, ONCE_INIT};
596///
597/// #[derive(Debug, PartialEq, Eq)]
598/// struct PanicToken;
599///
600/// #[derive(Debug, PartialEq, Eq)]
601/// struct PanicMessage(pub String);
602///
603/// static HOOK_ONCE: Once = ONCE_INIT;
604/// HOOK_ONCE.call_once(|| {
605/// chain_hook_ignoring::<PanicToken>();
606/// chain_hook_ignoring::<PanicMessage>();
607/// });
608/// ```
609pub fn chain_hook_ignoring<P: 'static>() {
610 chain_hook_ignoring_if(|_: &P| { true })
611}
612
613/// Augments the panic hook, filtering out panics with a
614/// statically typed closure.
615///
616/// The current panic hook, either installed with `std::panic::set_hook()`
617/// or the standard library default, gets chained, again using
618/// `std::panic::set_hook()`, behind
619/// the boolean predicate closure passed as the parameter, testing a value
620/// of a particular type.
621/// If the panic payload is found to be of the same type and the predicate
622/// returns true, the chained hook is not called.
623///
624/// # Caveats
625///
626/// Every call to this function allocates state data and increases the
627/// filtering chain for the process-global panic hook, so it should not be
628/// called repeatedly unless necessary.
629/// Other code within the program that modifies the panic hook, concurrently
630/// to, or after, the call to this function, may cause the hook chain to stop
631/// working as intended.
632/// This function interoperates in a predictable way only with the other
633/// functions and methods of this crate that modify the panic hook,
634/// and only when used in strictly serialized order with those functions,
635/// unless noted otherwise in those functions' documentation.
636/// This function is only intended to be used in tests or initialization
637/// code of a program;
638/// libraries other than those designed for test purposes should avoid
639/// using it.
640///
641/// # Examples
642///
643/// The value types most commonly used in panics are `&'string str` or
644/// `String`, depending on whether the `panic!` macro was used in the single
645/// parameter form or the formatting form, respectively. The example below
646/// filters out either kind if the string message contains a particular
647/// substring.
648///
649/// ```
650/// use panic_control::chain_hook_ignoring_if;
651/// use std::sync::{Once, ONCE_INIT};
652///
653/// const MAGIC: &str = "Move along, nothing to see here";
654///
655/// static HOOK_ONCE: Once = ONCE_INIT;
656/// HOOK_ONCE.call_once(|| {
657/// chain_hook_ignoring_if(|s: &&'static str| {
658/// s.contains(MAGIC)
659/// });
660/// chain_hook_ignoring_if(|s: &String| {
661/// s.contains(MAGIC)
662/// });
663/// });
664/// ```
665pub fn chain_hook_ignoring_if<P, F>(predicate: F)
666 where F: Fn(&P) -> bool,
667 F: Send,
668 F: Sync,
669 F: 'static,
670 P: 'static
671{
672 chain_hook_ignoring_full(move |info| {
673 match info.payload().downcast_ref::<P>() {
674 Some(p) => predicate(p),
675 None => false
676 }
677 })
678}
679
680/// Augments the panic hook, filtering out panics with a free-form check.
681///
682/// The current panic hook, either installed with `std::panic::set_hook()`
683/// or the standard library default, gets chained, again using
684/// `std::panic::set_hook()`, behind
685/// the boolean predicate closure passed as the parameter, testing the
686/// `std::panic::PanicInfo` structure passed to the panic hook.
687/// If the predicate returns true, the chained hook is not called.
688///
689/// # Caveats
690///
691/// Every call to this function allocates state data and increases the
692/// filtering chain for the process-global panic hook, so it should not be
693/// called repeatedly unless necessary.
694/// Other code within the program that modifies the panic hook, concurrently
695/// to, or after, the call to this function, may cause the hook chain to stop
696/// working as intended.
697/// This function interoperates in a predictable way only with the other
698/// functions and methods of this crate that modify the panic hook,
699/// and only when used in strictly serialized order with those functions,
700/// unless noted otherwise in those functions' documentation.
701/// This function is only intended to be used in tests or initialization
702/// code of a program;
703/// libraries other than those designed for test purposes should avoid
704/// using it.
705///
706/// # Examples
707///
708/// This example filters out any non-string panics:
709///
710/// ```
711/// use panic_control::chain_hook_ignoring_full;
712/// use std::sync::{Once, ONCE_INIT};
713///
714/// static HOOK_ONCE: Once = ONCE_INIT;
715/// HOOK_ONCE.call_once(|| {
716/// chain_hook_ignoring_full(|info| {
717/// let payload = info.payload();
718/// !(payload.is::<&'static str>() ||
719/// payload.is::<String>())
720/// });
721/// });
722/// ```
723pub fn chain_hook_ignoring_full<F>(predicate: F)
724 where F: Fn(&PanicInfo) -> bool,
725 F: Send,
726 F: Sync,
727 F: 'static
728{
729 // Make sure the thread filter hook is set up,
730 // in case some other thread is calling
731 // {disable,enable}_hook_in_current_thread()
732 init_thread_filter_hook();
733 chain_hook_waive_init_thread_filter(predicate)
734}
735
736fn chain_hook_waive_init_thread_filter<F>(predicate: F)
737 where F: Fn(&PanicInfo) -> bool,
738 F: Send,
739 F: Sync,
740 F: 'static
741{
742 let next_hook = panic::take_hook();
743 panic::set_hook(Box::new(move |info| {
744 if !predicate(info) {
745 next_hook(info);
746 }
747 }));
748}
749
750thread_local!(static IGNORE_HOOK: Cell<bool> = Cell::new(false));
751
752fn init_thread_filter_hook() {
753 static HOOK_ONCE: Once = ONCE_INIT;
754 HOOK_ONCE.call_once(|| {
755 // Avoid recursion and deadlocking on HOOK_ONCE here
756 chain_hook_waive_init_thread_filter(|_| {
757 IGNORE_HOOK.with(|cell| { cell.get() })
758 });
759 });
760}
761
762/// Disables the panic hook for the current thread.
763///
764/// The process-global panic hook, either installed with
765/// `std::panic::set_hook()` or the standard library default,
766/// gets augmented with a filter that disables invocation of the
767/// hook closure if the thread that is calling this function panics.
768///
769/// This function does not allocate resources when called repeatedly in
770/// the same thread, and it can be used in any order together with other
771/// functions and methods of this crate that modify the panic hook.
772///
773/// # Caveats
774///
775/// Note that the suppression can apply to the default panic hook
776/// that is normally used to report assertion failures and other
777/// unexpected panics on the standard error stream.
778///
779/// Other code within the program that modifies the panic hook, concurrently
780/// to, or after, a call to this function, may cause the hook chain to stop
781/// working as intended.
782/// This function interoperates in a predictable way only with the other
783/// functions and methods of this crate that modify the panic hook.
784/// Libraries other than those designed for test purposes should avoid
785/// using this function.
786pub fn disable_hook_in_current_thread() {
787 init_thread_filter_hook();
788 IGNORE_HOOK.with(|cell| {
789 cell.set(true);
790 });
791}
792
793/// Enables the panic hook for the current thread.
794///
795/// If the panic hook has been disabled for the current thread with
796/// `disable_hook_in_current_thread()`, calling this function enables it
797/// back.
798///
799/// This function does not allocate resources when called repeatedly in
800/// the same thread, and it can be used in any order together with other
801/// functions and methods of this crate that modify the panic hook.
802///
803/// # Caveats
804///
805/// Other code within the program that modifies the panic hook, concurrently
806/// to, or after, a call to this function, may cause the hook chain to stop
807/// working as intended.
808/// This function interoperates only with the other
809/// functions and methods of this crate that modify the panic hook.
810/// Libraries other than those designed for test purposes should avoid
811/// using this function.
812pub fn enable_hook_in_current_thread() {
813 init_thread_filter_hook();
814 IGNORE_HOOK.with(|cell| {
815 cell.set(false);
816 });
817}
818
819/// Like `std::thread::spawn()`, but disables the panic hook
820/// if the spawned thread panics.
821///
822/// The process-global panic hook, either installed with
823/// `std::panic::set_hook()` or the standard library default,
824/// gets augmented with a filter that disables invocation of the
825/// hook closure if the spawned thread panics.
826///
827/// This function can be used in any order together with other
828/// functions and methods of this crate that modify the panic hook.
829///
830/// # Caveats
831///
832/// Note that the suppression can apply to the default panic hook
833/// that is normally used to report assertion failures and other
834/// unexpected panics on the standard error stream.
835/// The only remaining way to observe the panic is by checking
836/// the result of `join()` for the spawned thread.
837///
838/// Other code within the program that modifies the panic hook,
839/// concurrently to, or after, a call to this function, may cause
840/// the suppression not to work as intended. See the documentation
841/// on the function `disable_hook_in_current_thread()` for possible
842/// pitfalls.
843///
844/// # Examples
845///
846/// ```
847/// use panic_control::spawn_quiet;
848///
849/// let h = spawn_quiet(|| {
850/// assert!(false, "I'm panicking, \
851/// but you can only learn about it through join()");
852/// });
853/// let res = h.join();
854/// assert!(res.is_err());
855/// ```
856pub fn spawn_quiet<T, F>(f: F) -> thread::JoinHandle<T>
857 where F: FnOnce() -> T,
858 F: Send + 'static,
859 T: Send + 'static
860{
861 thread::spawn(|| {
862 disable_hook_in_current_thread();
863 f()
864 })
865}
866
867
868#[cfg(test)]
869mod tests {
870 use super::{Context, Outcome};
871 use super::{ThreadResultExt};
872 use super::chain_hook_ignoring;
873 use std::sync::{Once, ONCE_INIT};
874 use std::thread;
875
876 #[derive(Debug, PartialEq, Eq)]
877 struct Expected(pub u32);
878
879 fn silence_expected_panics() {
880 static GUARD: Once = ONCE_INIT;
881 GUARD.call_once(|| {
882 chain_hook_ignoring::<Expected>()
883 });
884 }
885
886 #[test]
887 fn no_panic() {
888 let ctx = Context::<Expected>::new();
889 let h = ctx.spawn(|| {
890 42
891 });
892 let outcome = h.join().unwrap();
893 assert_eq!(outcome, Outcome::NoPanic(42));
894 }
895
896 #[test]
897 fn context_default() {
898 let ctx = Context::<Expected>::default();
899 // Also use spawn_quiet and exercise the normal return path there
900 let h = ctx.spawn_quiet(|| {
901 42
902 });
903 let outcome = h.join().unwrap();
904 assert_eq!(outcome, Outcome::NoPanic(42));
905 }
906
907 #[test]
908 fn expected_panic() {
909 silence_expected_panics();
910 let ctx = Context::<Expected>::new();
911 let h = ctx.spawn(|| {
912 panic!(Expected(42));
913 });
914 let outcome = h.join().unwrap();
915 match outcome {
916 Outcome::Panicked(Expected(n)) => assert_eq!(n, 42),
917 _ => panic!("unexpected Outcome value returned from join()")
918 }
919 }
920
921 #[test]
922 fn int_literal_gotcha() {
923 let h = Context::<u32>::new().spawn_quiet(|| {
924 panic!(42);
925 });
926 // This wouldn't work:
927 // let outcome = h.join().unwrap();
928 // assert_eq!(outcome, Outcome::Panicked(42));
929 let res = h.join();
930 assert!(res.is_err());
931 }
932
933 #[test]
934 fn from_thread_builder() {
935 silence_expected_panics();
936 const THREAD_NAME: &str = "a panicky thread";
937 let thread_builder = thread::Builder::new()
938 .name(THREAD_NAME.into());
939 let ctx = Context::<Expected>::from(thread_builder);
940 let h = ctx.spawn(|| {
941 let thread = thread::current();
942 let name = thread.name();
943 assert!(name.is_some());
944 assert_eq!(name.unwrap(), THREAD_NAME);
945 42
946 });
947 h.join().unwrap_or_propagate();
948 }
949
950 #[test]
951 fn checked_join_handle_inherent_as_ref() {
952 const THREAD_NAME: &str = "a non-panicky thread";
953 let thread_builder = thread::Builder::new()
954 .name(THREAD_NAME.into());
955 let ctx = Context::<Expected>::from(thread_builder);
956 let h = ctx.spawn(|| {});
957 {
958 let h = h.as_thread_join_handle();
959 let name = h.thread().name().unwrap();
960 assert_eq!(name, THREAD_NAME);
961 }
962 h.join().unwrap_or_propagate();
963 }
964
965 #[test]
966 fn checked_join_handle_trait_as_ref() {
967 const THREAD_NAME: &str = "a non-panicky thread";
968 let thread_builder = thread::Builder::new()
969 .name(THREAD_NAME.into());
970 let ctx = Context::<Expected>::from(thread_builder);
971 let h = ctx.spawn(|| {});
972 {
973 let h: &thread::JoinHandle<()> = h.as_ref();
974 let name = h.thread().name().unwrap();
975 assert_eq!(name, THREAD_NAME);
976 }
977 h.join().unwrap_or_propagate();
978 }
979
980 #[test]
981 fn checked_join_handle_inherent_into() {
982 silence_expected_panics();
983 let ctx = Context::<Expected>::new();
984 let h = ctx.spawn(|| {
985 panic!(Expected(42));
986 });
987 let h = h.into_thread_join_handle();
988 let res = h.join();
989 assert!(res.is_err());
990 }
991
992 #[test]
993 fn checked_join_handle_trait_into() {
994 silence_expected_panics();
995 let ctx = Context::<Expected>::new();
996 #[allow(unreachable_code)]
997 let h = ctx.spawn(|| {
998 panic!(Expected(42));
999 ()
1000 });
1001 let h: thread::JoinHandle<()> = h.into();
1002 let res = h.join();
1003 assert!(res.is_err());
1004 }
1005
1006 #[test]
1007 fn debug_impls_omit_phantom() {
1008 let ctx = Context::<Expected>::new();
1009 let repr = format!("{:?}", ctx);
1010 assert!(repr.starts_with("Context"));
1011 assert!(!repr.contains("phantom"));
1012 assert!(!repr.contains("PhantomData"));
1013 let h = ctx.spawn(|| { });
1014 let repr = format!("{:?}", h);
1015 assert_eq!(repr, "CheckedJoinHandle { .. }");
1016 h.join().unwrap();
1017 }
1018}