unwind_context/
context_with_io.rs

1use core::fmt::Debug;
2use core::panic::Location;
3use std::io::Write;
4
5use crate::{AnsiColorScheme, AnsiColored, DebugAnsiColored, PanicDetector};
6
7/// A structure representing a scoped guard with unwind context with
8/// [`core::fmt::Write`] writer.
9///
10/// If dropped during unwind it will write a message to a given writer
11/// containing given function or scope context.
12///
13/// When this structure is dropped (falls out of scope) and the current thread
14/// is not unwinding, the unwind context will be forgotten.
15///
16/// # Examples
17///
18/// ```rust
19/// use unwind_context::{unwind_context, UnwindContextWithIo};
20///
21/// fn func(foo: u32, bar: &str, secret: &str) {
22///     let _ctx: UnwindContextWithIo<_, _, _> = unwind_context!(fn(foo, bar, ...));
23///     // ...
24/// }
25/// ```
26///
27/// [`unwind_context`]: crate::unwind_context
28#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
29pub struct UnwindContextWithIo<W: Write, T: Debug + DebugAnsiColored, P: PanicDetector> {
30    data: T,
31    writer: W,
32    panic_detector: P,
33    color_scheme: Option<&'static AnsiColorScheme>,
34    location: &'static Location<'static>,
35}
36
37impl<W: Write, T: Debug + DebugAnsiColored, P: PanicDetector> Drop
38    for UnwindContextWithIo<W, T, P>
39{
40    #[inline]
41    fn drop(&mut self) {
42        if self.panic_detector.is_panicking() {
43            self.print();
44        }
45    }
46}
47
48impl<W: Write, T: Debug + DebugAnsiColored, P: PanicDetector> UnwindContextWithIo<W, T, P> {
49    /// Create a new `UnwindContextWithFmt` with the provided
50    /// [`core::fmt::Write`] writer, context scope data, and color scheme.
51    ///
52    /// This function is not intended to be used directly. Consider using macros
53    /// like [`unwind_context`] or [`unwind_context_with_io`] instead.
54    ///
55    /// [`unwind_context`]: crate::unwind_context
56    /// [`unwind_context_with_io`]: crate::unwind_context_with_io
57    #[inline]
58    #[must_use = "\
59        if unused, the `UnwindContextWithIo` will immediately drop,
60        consider binding the `UnwindContextWithIo` like `let _ctx = ...`.
61    "]
62    #[track_caller]
63    pub fn new(
64        data: T,
65        writer: W,
66        panic_detector: P,
67        color_scheme: Option<&'static AnsiColorScheme>,
68    ) -> Self {
69        Self {
70            data,
71            writer,
72            panic_detector,
73            color_scheme,
74            location: Location::caller(),
75        }
76    }
77
78    /// Print context to a writer specified in the `UnwindContextWithIo`
79    /// constructor.
80    ///
81    /// This method is called when a panic detected.
82    #[cold]
83    #[inline(never)]
84    pub fn print(&mut self) {
85        if let Some(color_scheme) = self.color_scheme {
86            let _ = writeln!(
87                self.writer,
88                "{:?}\n    at {}{}:{}:{}{}",
89                AnsiColored::new(&self.data, color_scheme),
90                color_scheme.location,
91                self.location.file(),
92                self.location.line(),
93                self.location.column(),
94                color_scheme.default,
95            );
96        } else {
97            let _ = writeln!(
98                self.writer,
99                "{:?}\n    at {}:{}:{}",
100                self.data,
101                self.location.file(),
102                self.location.line(),
103                self.location.column(),
104            );
105        }
106        let _ = self.writer.flush();
107    }
108}
109
110/// Creates [`UnwindContextWithIo`] with a given [`std::io::Write`] writer,
111/// panic detector, color scheme, and a given function or scope context.
112///
113/// If not specified it uses [`std::io::stderr`] as a default writer,
114/// [`StdPanicDetector`] as a default panic detector and
115/// [`get_default_color_scheme_if_enabled`] as a default color scheme. When
116/// using default values for all optional parameters, consider the
117/// use of [`unwind_context`] macro instead. See
118/// [equivalent macros](#equivalent-macros) section below.
119///
120/// The returned unwind context scope guard value should be kept alive as long
121/// as unwind context is needed. If unused, the [`UnwindContextWithIo`] will
122/// immediately drop.
123///
124/// Passed context arguments are lazily formatted. The created wrapper takes
125/// ownership of the given arguments, so it may be necessary to use value
126/// references, clones, or pass the pre-prepared string representation. It also
127/// supports the `...` placeholder to show that some values have been omitted.
128///
129/// For more information about context argument, see
130/// [`build_unwind_context_data`].
131///
132/// # Examples
133///
134/// ```rust
135/// use unwind_context::unwind_context_with_io;
136///
137/// fn example1(foo: u32, bar: &str, secret: &str) {
138///     let _ctx = unwind_context_with_io!((fn(foo, bar, ...)), color_scheme = None);
139///     // ...
140/// }
141/// ```
142///
143/// ```rust
144/// use unwind_context::unwind_context_with_io;
145///
146/// fn example2(foo: u32, bar: &str, secret: &str) {
147///     let _ctx = unwind_context_with_io!((fn(foo, bar, ...)), writer = ::std::io::stdout());
148///     // ...
149/// }
150/// ```
151///
152/// ```rust
153/// use unwind_context::{unwind_context_with_io, AnsiColorScheme};
154///
155/// fn example3<W: std::io::Write, P: unwind_context::PanicDetector>(
156///     foo: u32,
157///     bar: &str,
158///     custom_writer: &mut W,
159///     custom_panic_detector: P,
160///     custom_color_scheme: &'static AnsiColorScheme,
161/// ) {
162///     let _ctx = unwind_context_with_io!(
163///         (fn(foo, bar)),
164///         writer = custom_writer,
165///         panic_detector = custom_panic_detector,
166///         color_scheme = Some(custom_color_scheme),
167///     );
168///     // ...
169/// }
170/// ```
171///
172/// # Equivalent macros
173/// ```rust
174/// use unwind_context::{unwind_context, unwind_context_with_io};
175///
176/// fn func(foo: u32, bar: &str) {
177///     let _ctx = unwind_context!(fn(foo, bar));
178///     let _ctx = unwind_context_with_io!((fn(foo, bar)));
179///     let _ctx = unwind_context_with_io!(
180///         (fn(foo, bar)),
181///         writer = ::std::io::stderr(),
182///         panic_detector = unwind_context::StdPanicDetector,
183///         color_scheme = unwind_context::get_default_color_scheme_if_enabled(),
184///     );
185/// }
186/// ```
187///
188/// [`unwind_context`]: crate::unwind_context
189/// [`StdPanicDetector`]: crate::StdPanicDetector
190/// [`get_default_color_scheme_if_enabled`]: crate::get_default_color_scheme_if_enabled
191/// [`build_unwind_context_data`]: crate::build_unwind_context_data
192#[macro_export]
193macro_rules! unwind_context_with_io {
194    (
195        ( $( $context:tt )* )
196        $(, writer = $writer:expr )?
197        $(, panic_detector = $panic_detector:expr )?
198        $(, color_scheme = $color_scheme:expr )?
199        $(,)?
200    ) => {
201        $crate::UnwindContextWithIo::new(
202            $crate::build_unwind_context_data!( $($context)* ),
203            $crate::expr_or_default_expr!(
204                $( $writer )?,
205                ::std::io::stderr()
206            ),
207            $crate::expr_or_default_expr!(
208                $( $panic_detector )?,
209                $crate::StdPanicDetector
210            ),
211            $crate::expr_or_default_expr!(
212                $( $color_scheme )?,
213                $crate::get_default_color_scheme_if_enabled()
214            ),
215        )
216    };
217}
218
219/// Creates [`UnwindContextWithIo`] with a given [`std::io::Write`] writer,
220/// panic detector, color scheme, and a given function or scope context in debug
221/// builds only.
222///
223/// If not specified it uses [`std::io::stderr`] as a default writer,
224/// [`StdPanicDetector`] as a default panic detector and
225/// [`get_default_color_scheme_if_enabled`] as a default color scheme. When
226/// using default values for all optional parameters, consider the
227/// use of [`debug_unwind_context`] macro instead. See
228/// [equivalent macros](#equivalent-macros) section below.
229///
230/// The returned unwind context scope guard value should be kept alive as long
231/// as unwind context is needed. If unused, the [`UnwindContextWithIo`] will
232/// immediately drop.
233///
234/// Passed context arguments are lazily formatted. The created wrapper takes
235/// ownership of the given arguments, so it may be necessary to use value
236/// references, clones, or pass the pre-prepared string representation. It also
237/// supports the `...` placeholder to show that some values have been omitted.
238///
239/// An optimized build will generate `()` unless `-C debug-assertions` is passed
240/// to the compiler. This makes this macro no-op with the default release
241/// profile.
242///
243/// For more information about macro arguments, see [`unwind_context_with_io`].
244/// For more information about context argument, see
245/// [`build_unwind_context_data`].
246///
247/// # Examples
248///
249/// ```rust
250/// use unwind_context::debug_unwind_context_with_io;
251///
252/// fn example1(foo: u32, bar: &str, secret: &str) {
253///     let _ctx = debug_unwind_context_with_io!((fn(foo, bar, ...)), color_scheme = None);
254///     // ...
255/// }
256/// ```
257///
258/// ```rust
259/// use unwind_context::debug_unwind_context_with_io;
260///
261/// fn example2(foo: u32, bar: &str, secret: &str) {
262///     let _ctx = debug_unwind_context_with_io!((fn(foo, bar, ...)), writer = ::std::io::stdout());
263///     // ...
264/// }
265/// ```
266///
267/// ```rust
268/// use unwind_context::{debug_unwind_context_with_io, AnsiColorScheme};
269///
270/// fn example3<W: std::io::Write, P: unwind_context::PanicDetector>(
271///     foo: u32,
272///     bar: &str,
273///     custom_writer: &mut W,
274///     custom_panic_detector: P,
275///     custom_color_scheme: &'static AnsiColorScheme,
276/// ) {
277///     let _ctx = debug_unwind_context_with_io!(
278///         (fn(foo, bar)),
279///         writer = custom_writer,
280///         panic_detector = custom_panic_detector,
281///         color_scheme = Some(custom_color_scheme),
282///     );
283///     // ...
284/// }
285/// ```
286///
287/// # Equivalent macros
288/// ```rust
289/// use unwind_context::{debug_unwind_context, debug_unwind_context_with_io};
290///
291/// fn func(foo: u32, bar: &str) {
292///     debug_unwind_context!(fn(foo, bar));
293///     debug_unwind_context_with_io!((fn(foo, bar)));
294///     debug_unwind_context_with_io!(
295///         (fn(foo, bar)),
296///         writer = ::std::io::stderr(),
297///         panic_detector = unwind_context::StdPanicDetector,
298///         color_scheme = unwind_context::get_default_color_scheme_if_enabled(),
299///     );
300/// }
301/// ```
302///
303/// [`unwind_context_with_io`]: crate::unwind_context_with_io
304/// [`debug_unwind_context`]: crate::debug_unwind_context
305/// [`StdPanicDetector`]: crate::StdPanicDetector
306/// [`get_default_color_scheme_if_enabled`]: crate::get_default_color_scheme_if_enabled
307/// [`build_unwind_context_data`]: crate::build_unwind_context_data
308#[macro_export]
309macro_rules! debug_unwind_context_with_io {
310    ( $( $tokens:tt )* ) => { $crate::debug_unwind_context_with_io_impl!( $($tokens)* ) };
311}
312
313#[doc(hidden)]
314#[cfg(debug_assertions)]
315#[macro_export]
316macro_rules! debug_unwind_context_with_io_impl {
317    ( $( $tokens:tt )* ) => { $crate::unwind_context_with_io!( $($tokens)* ) };
318}
319
320#[doc(hidden)]
321#[cfg(not(debug_assertions))]
322#[macro_export]
323macro_rules! debug_unwind_context_with_io_impl {
324    ($($tokens:tt)*) => {
325        ()
326    };
327}
328
329#[cfg(test)]
330mod tests {
331    use std::borrow::ToOwned;
332    use std::io::{Result as IoResult, Write as IoWrite};
333    use std::string::String;
334    use std::sync::mpsc;
335
336    use crate::test_common::{check_location_part, TEST_COLOR_SCHEME};
337    use crate::test_util::{collect_string_from_recv, PatternMatcher};
338    use crate::AnsiColorScheme;
339
340    #[derive(Clone)]
341    pub struct Writer(mpsc::Sender<String>);
342
343    impl IoWrite for Writer {
344        #[allow(clippy::unwrap_used)]
345        fn write(&mut self, buf: &[u8]) -> IoResult<usize> {
346            self.0
347                .send(String::from_utf8(buf.to_owned()).unwrap())
348                .unwrap();
349            Ok(buf.len())
350        }
351
352        fn flush(&mut self) -> IoResult<()> {
353            Ok(())
354        }
355    }
356
357    fn get_min_line() -> u32 {
358        line!()
359    }
360
361    #[allow(clippy::unwrap_used)]
362    fn func1<W: Clone + IoWrite>(
363        foo: usize,
364        bar: &str,
365        writer: &mut W,
366        color_scheme: Option<&'static AnsiColorScheme>,
367    ) -> usize {
368        let _ctx = unwind_context_with_io!(
369            (fn(foo, bar)),
370            writer = writer.clone(),
371            color_scheme = color_scheme
372        );
373        func2(foo.checked_mul(2).unwrap(), &bar[1..], writer, color_scheme)
374    }
375
376    #[allow(clippy::unwrap_used)]
377    fn func2<W: Clone + IoWrite>(
378        foo: usize,
379        bar: &str,
380        writer: &mut W,
381        color_scheme: Option<&'static AnsiColorScheme>,
382    ) -> usize {
383        let _ctx = unwind_context_with_io!(
384            (fn(foo, bar)),
385            writer = writer.clone(),
386            color_scheme = color_scheme
387        );
388        func3(foo.checked_mul(3).unwrap(), &bar[1..], writer, color_scheme)
389    }
390
391    #[allow(clippy::unwrap_used)]
392    fn func3<W: IoWrite>(
393        foo: usize,
394        bar: &str,
395        writer: &mut W,
396        color_scheme: Option<&'static AnsiColorScheme>,
397    ) -> usize {
398        let _ctx =
399            unwind_context_with_io!((fn(foo, bar)), writer = writer, color_scheme = color_scheme);
400        foo.checked_sub(bar.len()).unwrap()
401    }
402
403    #[allow(clippy::unwrap_used)]
404    fn func_with_debug_unwind_context<W: IoWrite>(
405        foo: usize,
406        bar: &str,
407        #[allow(unused_variables)] writer: &mut W,
408        #[allow(unused_variables)] color_scheme: Option<&'static AnsiColorScheme>,
409    ) -> usize {
410        let _ctx = debug_unwind_context_with_io!(
411            (fn(foo, bar)),
412            writer = writer,
413            color_scheme = color_scheme
414        );
415        foo.checked_sub(bar.len()).unwrap()
416    }
417
418    fn get_max_line() -> u32 {
419        line!()
420    }
421
422    #[allow(clippy::unwrap_used)]
423    #[test]
424    fn test_unwind_context_with_io_without_unwind() {
425        let (sender, recv) = mpsc::channel();
426        let mut writer = Writer(sender);
427        let result = func1(1000, "abcdef", &mut writer, None);
428        assert_eq!(result, 5996);
429        assert_eq!(collect_string_from_recv(&recv), "");
430
431        let (sender, recv) = mpsc::channel();
432        let mut writer = Writer(sender);
433        let result = func1(1000, "ab", &mut writer, None);
434        assert_eq!(result, 6000);
435        assert_eq!(collect_string_from_recv(&recv), "");
436    }
437
438    #[allow(clippy::unwrap_used)]
439    #[test]
440    fn test_unwind_context_with_io_with_unwind() {
441        let (sender, recv) = mpsc::channel();
442        let mut writer = Writer(sender);
443        let result = std::panic::catch_unwind(move || func1(1000, "a", &mut writer, None));
444        assert!(result.is_err());
445        let output = collect_string_from_recv(&recv);
446        let output = &mut output.as_str();
447        output
448            .expect_str("fn func2(foo: 2000, bar: \"\")\n")
449            .unwrap();
450        check_location_part(output, "", "", file!(), get_min_line(), get_max_line());
451        output
452            .expect_str("fn func1(foo: 1000, bar: \"a\")\n")
453            .unwrap();
454        check_location_part(output, "", "", file!(), get_min_line(), get_max_line());
455        assert_eq!(*output, "");
456
457        let (sender, recv) = mpsc::channel();
458        let mut writer = Writer(sender);
459        let result = std::panic::catch_unwind(move || func1(1000, "", &mut writer, None));
460        assert!(result.is_err());
461        let output = collect_string_from_recv(&recv);
462        let output = &mut output.as_str();
463        output
464            .expect_str("fn func1(foo: 1000, bar: \"\")\n")
465            .unwrap();
466        check_location_part(output, "", "", file!(), get_min_line(), get_max_line());
467        assert_eq!(*output, "");
468
469        let (sender, recv) = mpsc::channel();
470        let mut writer = Writer(sender);
471        let result = std::panic::catch_unwind(move || func1(0, "abcdef", &mut writer, None));
472        assert!(result.is_err());
473        let output = collect_string_from_recv(&recv);
474        let output = &mut output.as_str();
475        output
476            .expect_str("fn func3(foo: 0, bar: \"cdef\")\n")
477            .unwrap();
478        check_location_part(output, "", "", file!(), get_min_line(), get_max_line());
479        output
480            .expect_str("fn func2(foo: 0, bar: \"bcdef\")\n")
481            .unwrap();
482        check_location_part(output, "", "", file!(), get_min_line(), get_max_line());
483        output
484            .expect_str("fn func1(foo: 0, bar: \"abcdef\")\n")
485            .unwrap();
486        check_location_part(output, "", "", file!(), get_min_line(), get_max_line());
487        assert_eq!(*output, "");
488    }
489
490    #[allow(clippy::unwrap_used)]
491    #[test]
492    fn test_unwind_context_with_io_with_unwind_with_colored_fmt() {
493        let (sender, recv) = mpsc::channel();
494        let mut writer = Writer(sender);
495        let result = std::panic::catch_unwind(move || {
496            func1(1000, "a", &mut writer, Some(&TEST_COLOR_SCHEME))
497        });
498        assert!(result.is_err());
499        let output = collect_string_from_recv(&recv);
500        let output = &mut output.as_str();
501        output
502            .expect_str(
503                "{FN}fn {FN_NAME}func2{FN_BRACE}({DEF}foo: {NUM}2000{DEF}, bar: \
504                 {QUOT}\"\"{DEF}{FN_BRACE}){DEF}\n",
505            )
506            .unwrap();
507        check_location_part(
508            output,
509            "{LOC}",
510            "{DEF}",
511            file!(),
512            get_min_line(),
513            get_max_line(),
514        );
515        output
516            .expect_str(
517                "{FN}fn {FN_NAME}func1{FN_BRACE}({DEF}foo: {NUM}1000{DEF}, bar: \
518                 {QUOT}\"a\"{DEF}{FN_BRACE}){DEF}\n",
519            )
520            .unwrap();
521        check_location_part(
522            output,
523            "{LOC}",
524            "{DEF}",
525            file!(),
526            get_min_line(),
527            get_max_line(),
528        );
529        assert_eq!(*output, "");
530    }
531
532    #[allow(clippy::unwrap_used)]
533    #[test]
534    fn test_debug_unwind_context_with_io_without_unwind() {
535        let (sender, recv) = mpsc::channel();
536        let mut writer = Writer(sender);
537        let result = std::panic::catch_unwind(move || {
538            func_with_debug_unwind_context(4, "abc", &mut writer, None)
539        });
540
541        assert_eq!(result.unwrap(), 1);
542        let output = collect_string_from_recv(&recv);
543        assert_eq!(output, "");
544    }
545
546    #[test]
547    fn test_debug_unwind_context_with_io_with_unwind() {
548        let (sender, recv) = mpsc::channel();
549        let mut writer = Writer(sender);
550        let result = std::panic::catch_unwind(move || {
551            func_with_debug_unwind_context(2, "abc", &mut writer, None)
552        });
553        assert!(result.is_err());
554        let output = collect_string_from_recv(&recv);
555        let output = &mut output.as_str();
556
557        #[cfg(debug_assertions)]
558        {
559            output
560                .expect_str("fn func_with_debug_unwind_context(foo: 2, bar: \"abc\")\n")
561                .unwrap();
562            check_location_part(output, "", "", file!(), get_min_line(), get_max_line());
563            assert_eq!(*output, "");
564        }
565        assert_eq!(*output, "");
566    }
567}