unwind_context/
context_with_fmt.rs

1use core::fmt::{Debug, Write};
2use core::panic::Location;
3
4use crate::{AnsiColorScheme, AnsiColored, DebugAnsiColored, PanicDetector};
5
6/// A structure representing a scoped guard with unwind context with
7/// [`std::io::Write`] writer.
8///
9/// If dropped during unwind it will write a message to a given writer
10/// containing given function or scope context. If created with
11/// [`unwind_context`] it will write to [`std::io::Stderr`].
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_with_fmt, UnwindContextWithFmt};
20///
21/// fn func(foo: u32, bar: &str, secret: &str, custom_writer: &mut String) {
22///     let _ctx: UnwindContextWithFmt<_, _, _> = unwind_context_with_fmt!(
23///         (fn(foo, bar, ...)),
24///         writer = custom_writer,
25///         panic_detector = unwind_context::StdPanicDetector,
26///         color_scheme = None,
27///     );
28///     // ...
29/// }
30/// ```
31///
32/// [`unwind_context`]: crate::unwind_context
33#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
34pub struct UnwindContextWithFmt<W: Write, T: Debug + DebugAnsiColored, P: PanicDetector> {
35    data: T,
36    writer: W,
37    panic_detector: P,
38    color_scheme: Option<&'static AnsiColorScheme>,
39    location: &'static Location<'static>,
40}
41
42impl<W: Write, T: Debug + DebugAnsiColored, P: PanicDetector> Drop
43    for UnwindContextWithFmt<W, T, P>
44{
45    #[inline]
46    fn drop(&mut self) {
47        if self.panic_detector.is_panicking() {
48            self.print();
49        }
50    }
51}
52
53impl<W: Write, T: Debug + DebugAnsiColored, P: PanicDetector> UnwindContextWithFmt<W, T, P> {
54    /// Create a new `UnwindContextWithFmt` with the provided
55    /// [`core::fmt::Write`] writer, context scope data, and color scheme.
56    ///
57    /// This function is not intended to be used directly. Consider using macros
58    /// like [`unwind_context_with_fmt`] instead.
59    ///
60    /// [`unwind_context_with_fmt`]: crate::unwind_context_with_fmt
61    #[inline]
62    #[must_use = "\
63        if unused, the `UnwindContextWithFmt` will immediately drop,
64        consider binding the `UnwindContextWithFmt` like `let _ctx = ...`.
65    "]
66    #[track_caller]
67    pub fn new(
68        data: T,
69        writer: W,
70        panic_detector: P,
71        color_scheme: Option<&'static AnsiColorScheme>,
72    ) -> Self {
73        Self {
74            data,
75            writer,
76            panic_detector,
77            color_scheme,
78            location: Location::caller(),
79        }
80    }
81
82    /// Print context to a writer specified in the `UnwindContextWithFmt`
83    /// constructor.
84    ///
85    /// This method is called when a panic detected.
86    #[cold]
87    #[inline(never)]
88    pub fn print(&mut self) {
89        if let Some(color_scheme) = self.color_scheme {
90            let _ = writeln!(
91                self.writer,
92                "{:?}\n    at {}{}:{}:{}{}",
93                AnsiColored::new(&self.data, color_scheme),
94                color_scheme.location,
95                self.location.file(),
96                self.location.line(),
97                self.location.column(),
98                color_scheme.default,
99            );
100        } else {
101            let _ = writeln!(
102                self.writer,
103                "{:?}\n    at {}:{}:{}",
104                self.data,
105                self.location.file(),
106                self.location.line(),
107                self.location.column(),
108            );
109        }
110    }
111}
112
113/// Creates [`UnwindContextWithFmt`] with a given [`core::fmt::Write`] writer,
114/// panic detector, color scheme, and a given function or scope context.
115///
116/// If not specified it uses [`get_default_color_scheme_if_enabled`] as a
117/// default color scheme.
118///
119/// The returned unwind context scope guard value should be kept alive as long
120/// as unwind context is needed. If unused, the [`UnwindContextWithFmt`] will
121/// immediately drop.
122///
123/// Passed context arguments are lazily formatted. The created wrapper takes
124/// ownership of the given arguments, so it may be necessary to use value
125/// references, clones, or pass the pre-prepared string representation. It also
126/// supports the `...` placeholder to show that some values have been omitted.
127///
128/// For more information about context argument, see
129/// [`build_unwind_context_data`].
130///
131/// # Examples
132///
133/// ```rust
134/// use unwind_context::unwind_context_with_fmt;
135///
136/// fn example1(foo: u32, bar: &str, secret: &str, custom_writer: &mut String) {
137///     let _ctx = unwind_context_with_fmt!(
138///         (fn(foo, bar, ...)),
139///         writer = custom_writer,
140///         panic_detector = unwind_context::StdPanicDetector,
141///         color_scheme = None,
142///     );
143///     // ...
144/// }
145/// ```
146///
147/// ```rust
148/// use unwind_context::{unwind_context_with_fmt, AnsiColorScheme};
149///
150/// fn example2<W: core::fmt::Write, P: unwind_context::PanicDetector>(
151///     foo: u32,
152///     bar: &str,
153///     custom_writer: &mut W,
154///     custom_panic_detector: P,
155///     custom_color_scheme: &'static AnsiColorScheme,
156/// ) {
157///     let _ctx = unwind_context_with_fmt!(
158///         (fn(foo, bar)),
159///         writer = custom_writer,
160///         panic_detector = custom_panic_detector,
161///         color_scheme = Some(custom_color_scheme),
162///     );
163///     // ...
164/// }
165/// ```
166///
167/// [`build_unwind_context_data`]: crate::build_unwind_context_data
168/// [`get_default_color_scheme_if_enabled`]: crate::get_default_color_scheme_if_enabled
169#[macro_export]
170macro_rules! unwind_context_with_fmt {
171    (
172        ( $( $context:tt )* )
173        , writer = $writer:expr
174        , panic_detector = $panic_detector:expr
175        $(, color_scheme = $color_scheme:expr )?
176        $(,)?
177    ) => {
178        $crate::UnwindContextWithFmt::new(
179            $crate::build_unwind_context_data!( $($context)* ),
180            $writer,
181            $panic_detector,
182            $crate::expr_or_default_expr!(
183                $( $color_scheme )?,
184                $crate::get_default_color_scheme_if_enabled()
185            ),
186        )
187    };
188}
189
190/// Creates [`UnwindContextWithFmt`] with a given [`core::fmt::Write`] writer,
191/// panic detector, color scheme, and a given function or scope context in debug
192/// builds only.
193///
194/// If not specified it uses [`get_default_color_scheme_if_enabled`] as a
195/// default color scheme.
196///
197/// The returned unwind context scope guard value should be kept alive as long
198/// as unwind context is needed. If unused, the [`UnwindContextWithFmt`] will
199/// immediately drop.
200///
201/// Passed context arguments are lazily formatted. The created wrapper takes
202/// ownership of the given arguments, so it may be necessary to use value
203/// references, clones, or pass the pre-prepared string representation. It also
204/// supports the `...` placeholder to show that some values have been omitted.
205///
206/// An optimized build will generate `()` unless `-C debug-assertions` is passed
207/// to the compiler. This makes this macro no-op with the default release
208/// profile.
209///
210/// For more information about macro arguments, see [`unwind_context_with_fmt`].
211/// For more information about context argument, see
212/// [`build_unwind_context_data`].
213///
214/// # Examples
215///
216/// ```rust
217/// use unwind_context::debug_unwind_context_with_fmt;
218///
219/// fn example1(foo: u32, bar: &str, secret: &str, custom_writer: &mut String) {
220///     let _ctx = debug_unwind_context_with_fmt!(
221///         (fn(foo, bar, ...)),
222///         writer = custom_writer,
223///         panic_detector = unwind_context::StdPanicDetector,
224///         color_scheme = None,
225///     );
226///     // ...
227/// }
228/// ```
229///
230/// ```rust
231/// use unwind_context::{debug_unwind_context_with_fmt, AnsiColorScheme};
232///
233/// fn example2<W: core::fmt::Write, P: unwind_context::PanicDetector>(
234///     foo: u32,
235///     bar: &str,
236///     custom_writer: &mut W,
237///     custom_panic_detector: P,
238///     custom_color_scheme: &'static AnsiColorScheme,
239/// ) {
240///     let _ctx = debug_unwind_context_with_fmt!(
241///         (fn(foo, bar)),
242///         writer = custom_writer,
243///         panic_detector = custom_panic_detector,
244///         color_scheme = Some(custom_color_scheme),
245///     );
246///     // ...
247/// }
248/// ```
249///
250/// [`unwind_context_with_fmt`]: crate::unwind_context_with_fmt
251/// [`build_unwind_context_data`]: crate::build_unwind_context_data
252/// [`get_default_color_scheme_if_enabled`]: crate::get_default_color_scheme_if_enabled
253#[macro_export]
254macro_rules! debug_unwind_context_with_fmt {
255    ( $( $tokens:tt )* ) => { $crate::debug_unwind_context_with_fmt_impl!( $($tokens)* ) };
256}
257
258#[doc(hidden)]
259#[cfg(debug_assertions)]
260#[macro_export]
261macro_rules! debug_unwind_context_with_fmt_impl {
262    ( $( $tokens:tt )* ) => { $crate::unwind_context_with_fmt!( $($tokens)* ) };
263}
264
265#[doc(hidden)]
266#[cfg(not(debug_assertions))]
267#[macro_export]
268macro_rules! debug_unwind_context_with_fmt_impl {
269    ($($tokens:tt)*) => {
270        ()
271    };
272}
273
274#[cfg(test)]
275mod tests {
276    #[cfg(feature = "std")]
277    use core::fmt::Result as FmtResult;
278    use core::fmt::Write as FmtWrite;
279    use core::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
280    #[cfg(feature = "std")]
281    use std::borrow::ToOwned;
282    #[cfg(feature = "std")]
283    use std::string::String;
284    #[cfg(feature = "std")]
285    use std::sync::mpsc;
286
287    use crate::test_common::{check_location_part, TEST_COLOR_SCHEME};
288    #[cfg(feature = "std")]
289    use crate::test_util::collect_string_from_recv;
290    use crate::test_util::{FixedBufWriter, PatternMatcher};
291    #[cfg(feature = "std")]
292    use crate::StdPanicDetector;
293    use crate::{AnsiColorScheme, PanicDetector};
294
295    #[derive(Clone, Debug)]
296    pub struct DummyPanicDetector<'a> {
297        is_panicking: &'a AtomicBool,
298    }
299
300    impl PanicDetector for DummyPanicDetector<'_> {
301        fn is_panicking(&self) -> bool {
302            self.is_panicking.load(AtomicOrdering::Relaxed)
303        }
304    }
305
306    #[cfg(feature = "std")]
307    #[derive(Clone, Debug)]
308    pub struct ChannelWriter(mpsc::Sender<String>);
309
310    #[cfg(feature = "std")]
311    impl FmtWrite for ChannelWriter {
312        #[allow(clippy::unwrap_used)]
313        fn write_str(&mut self, buf: &str) -> FmtResult {
314            self.0.send(buf.to_owned()).unwrap();
315            Ok(())
316        }
317    }
318
319    // This function should be ordered before the `func1`, `func2`, and `func3`
320    // functions.
321    fn get_min_line() -> u32 {
322        line!()
323    }
324
325    // Separate writers are used to test function without Rust standard library.
326    #[allow(clippy::unwrap_used)]
327    fn func1<W: FmtWrite, P: Clone + PanicDetector>(
328        foo: usize,
329        bar: &str,
330        writer1: &mut W,
331        writer2: &mut W,
332        writer3: &mut W,
333        panic_detector: P,
334        color_scheme: Option<&'static AnsiColorScheme>,
335    ) -> usize {
336        let _ctx = unwind_context_with_fmt!(
337            (fn(foo, bar)),
338            writer = writer1,
339            panic_detector = panic_detector.clone(),
340            color_scheme = color_scheme,
341        );
342        func2(
343            foo.checked_mul(2).unwrap(),
344            &bar[1..],
345            writer2,
346            writer3,
347            panic_detector,
348            color_scheme,
349        )
350    }
351
352    // Separate writers are used to test function without Rust standard library.
353    #[allow(clippy::unwrap_used)]
354    fn func2<W: FmtWrite, P: Clone + PanicDetector>(
355        foo: usize,
356        bar: &str,
357        writer2: &mut W,
358        writer3: &mut W,
359        panic_detector: P,
360        color_scheme: Option<&'static AnsiColorScheme>,
361    ) -> usize {
362        let _ctx = unwind_context_with_fmt!(
363            (fn(foo, bar)),
364            writer = writer2,
365            panic_detector = panic_detector.clone(),
366            color_scheme = color_scheme,
367        );
368        func3(
369            foo.checked_mul(3).unwrap(),
370            &bar[1..],
371            writer3,
372            panic_detector,
373            color_scheme,
374        )
375    }
376
377    #[allow(clippy::unwrap_used)]
378    fn func3<W: FmtWrite, P: PanicDetector>(
379        foo: usize,
380        bar: &str,
381        writer3: &mut W,
382        panic_detector: P,
383        color_scheme: Option<&'static AnsiColorScheme>,
384    ) -> usize {
385        let _ctx = unwind_context_with_fmt!(
386            (fn(foo, bar)),
387            writer = writer3,
388            panic_detector = panic_detector,
389            color_scheme = color_scheme,
390        );
391        foo.checked_sub(bar.len()).unwrap()
392    }
393
394    #[cfg(feature = "std")]
395    #[allow(clippy::unwrap_used)]
396    fn func_with_debug_unwind_context<W: FmtWrite, P: PanicDetector>(
397        foo: usize,
398        bar: &str,
399        #[allow(unused_variables)] writer: &mut W,
400        #[allow(unused_variables)] panic_detector: P,
401        #[allow(unused_variables)] color_scheme: Option<&'static AnsiColorScheme>,
402    ) -> usize {
403        let _ctx = debug_unwind_context_with_fmt!(
404            (fn(foo, bar)),
405            writer = writer,
406            panic_detector = panic_detector,
407            color_scheme = color_scheme,
408        );
409        foo.checked_sub(bar.len()).unwrap()
410    }
411
412    // This function should be ordered after the `func1`, `func2`, and `func3`
413    // functions.
414    fn get_max_line() -> u32 {
415        line!()
416    }
417
418    #[allow(clippy::unwrap_used)]
419    #[test]
420    fn test_unwind_context_with_fmt_without_unwind() {
421        let is_panicking = AtomicBool::new(false);
422        let dummy_panic_detector = DummyPanicDetector {
423            is_panicking: &is_panicking,
424        };
425
426        let mut buffer1 = [0; 128];
427        let mut buffer2 = [0; 128];
428        let mut buffer3 = [0; 128];
429
430        let mut writer1 = FixedBufWriter::new(&mut buffer1);
431        let mut writer2 = FixedBufWriter::new(&mut buffer2);
432        let mut writer3 = FixedBufWriter::new(&mut buffer3);
433        let result = func1(
434            1000,
435            "abcdef",
436            &mut writer1,
437            &mut writer2,
438            &mut writer3,
439            dummy_panic_detector.clone(),
440            None,
441        );
442        assert_eq!(result, 5996);
443        assert_eq!(writer1.into_str(), "");
444        assert_eq!(writer2.into_str(), "");
445        assert_eq!(writer3.into_str(), "");
446
447        let mut writer1 = FixedBufWriter::new(&mut buffer1);
448        let mut writer2 = FixedBufWriter::new(&mut buffer2);
449        let mut writer3 = FixedBufWriter::new(&mut buffer3);
450        let result = func1(
451            1000,
452            "ab",
453            &mut writer1,
454            &mut writer2,
455            &mut writer3,
456            dummy_panic_detector.clone(),
457            None,
458        );
459        assert_eq!(result, 6000);
460        assert_eq!(writer1.into_str(), "");
461        assert_eq!(writer2.into_str(), "");
462        assert_eq!(writer3.into_str(), "");
463
464        // Emulate panicking on the first scope guard drop without real panic.
465
466        is_panicking.store(true, AtomicOrdering::Relaxed);
467
468        let mut writer1 = FixedBufWriter::new(&mut buffer1);
469        let mut writer2 = FixedBufWriter::new(&mut buffer2);
470        let mut writer3 = FixedBufWriter::new(&mut buffer3);
471        let result = func1(
472            1000,
473            "ab",
474            &mut writer1,
475            &mut writer2,
476            &mut writer3,
477            dummy_panic_detector.clone(),
478            None,
479        );
480        assert_eq!(result, 6000);
481
482        let output = &mut writer1.into_str();
483        output
484            .expect_str("fn func1(foo: 1000, bar: \"ab\")\n")
485            .unwrap();
486        check_location_part(output, "", "", file!(), get_min_line(), get_max_line());
487        assert_eq!(*output, "");
488
489        let output = &mut writer2.into_str();
490        output
491            .expect_str("fn func2(foo: 2000, bar: \"b\")\n")
492            .unwrap();
493        check_location_part(output, "", "", file!(), get_min_line(), get_max_line());
494        assert_eq!(*output, "");
495
496        let output = &mut writer3.into_str();
497        output
498            .expect_str("fn func3(foo: 6000, bar: \"\")\n")
499            .unwrap();
500        check_location_part(output, "", "", file!(), get_min_line(), get_max_line());
501        assert_eq!(*output, "");
502    }
503
504    #[allow(clippy::unwrap_used)]
505    #[test]
506    fn test_unwind_context_with_fmt_without_unwind_with_colored_fmt() {
507        let is_panicking = AtomicBool::new(false);
508        let dummy_panic_detector = DummyPanicDetector {
509            is_panicking: &is_panicking,
510        };
511
512        let mut buffer1 = [0; 256];
513        let mut buffer2 = [0; 256];
514        let mut buffer3 = [0; 256];
515
516        // Emulate panicking on the first scope guard drop without real panic.
517
518        is_panicking.store(true, AtomicOrdering::Relaxed);
519
520        let mut writer1 = FixedBufWriter::new(&mut buffer1);
521        let mut writer2 = FixedBufWriter::new(&mut buffer2);
522        let mut writer3 = FixedBufWriter::new(&mut buffer3);
523        let result = func1(
524            1000,
525            "ab",
526            &mut writer1,
527            &mut writer2,
528            &mut writer3,
529            dummy_panic_detector.clone(),
530            Some(&TEST_COLOR_SCHEME),
531        );
532        assert_eq!(result, 6000);
533
534        let output = &mut writer1.into_str();
535        output
536            .expect_str(
537                "{FN}fn {FN_NAME}func1{FN_BRACE}({DEF}foo: {NUM}1000{DEF}, bar: \
538                 {QUOT}\"ab\"{DEF}{FN_BRACE}){DEF}\n",
539            )
540            .unwrap();
541        check_location_part(
542            output,
543            "{LOC}",
544            "{DEF}",
545            file!(),
546            get_min_line(),
547            get_max_line(),
548        );
549        assert_eq!(*output, "");
550
551        let output = &mut writer2.into_str();
552        output
553            .expect_str(
554                "{FN}fn {FN_NAME}func2{FN_BRACE}({DEF}foo: {NUM}2000{DEF}, bar: \
555                 {QUOT}\"b\"{DEF}{FN_BRACE}){DEF}\n",
556            )
557            .unwrap();
558        check_location_part(
559            output,
560            "{LOC}",
561            "{DEF}",
562            file!(),
563            get_min_line(),
564            get_max_line(),
565        );
566        assert_eq!(*output, "");
567
568        let output = &mut writer3.into_str();
569        output
570            .expect_str(
571                "{FN}fn {FN_NAME}func3{FN_BRACE}({DEF}foo: {NUM}6000{DEF}, bar: \
572                 {QUOT}\"\"{DEF}{FN_BRACE}){DEF}\n",
573            )
574            .unwrap();
575        check_location_part(
576            output,
577            "{LOC}",
578            "{DEF}",
579            file!(),
580            get_min_line(),
581            get_max_line(),
582        );
583        assert_eq!(*output, "");
584    }
585
586    #[cfg(feature = "std")]
587    #[allow(clippy::unwrap_used)]
588    #[test]
589    fn test_unwind_context_with_fmt_with_unwind() {
590        let panic_detector = StdPanicDetector;
591
592        let (sender, recv) = mpsc::channel();
593        let mut writer1 = ChannelWriter(sender);
594        let mut writer2 = writer1.clone();
595        let mut writer3 = writer1.clone();
596
597        let result = std::panic::catch_unwind(move || {
598            func1(
599                1000,
600                "a",
601                &mut writer1,
602                &mut writer2,
603                &mut writer3,
604                panic_detector,
605                None,
606            )
607        });
608        assert!(result.is_err());
609        let output = collect_string_from_recv(&recv);
610        let output = &mut output.as_str();
611        output
612            .expect_str("fn func2(foo: 2000, bar: \"\")\n")
613            .unwrap();
614        check_location_part(output, "", "", file!(), get_min_line(), get_max_line());
615        output
616            .expect_str("fn func1(foo: 1000, bar: \"a\")\n")
617            .unwrap();
618        check_location_part(output, "", "", file!(), get_min_line(), get_max_line());
619        assert_eq!(*output, "");
620
621        let (sender, recv) = mpsc::channel();
622        let mut writer1 = ChannelWriter(sender);
623        let mut writer2 = writer1.clone();
624        let mut writer3 = writer1.clone();
625
626        let result = std::panic::catch_unwind(move || {
627            func1(
628                1000,
629                "",
630                &mut writer1,
631                &mut writer2,
632                &mut writer3,
633                panic_detector,
634                None,
635            )
636        });
637        assert!(result.is_err());
638        let output = collect_string_from_recv(&recv);
639        let output = &mut output.as_str();
640        output
641            .expect_str("fn func1(foo: 1000, bar: \"\")\n")
642            .unwrap();
643        check_location_part(output, "", "", file!(), get_min_line(), get_max_line());
644        assert_eq!(*output, "");
645
646        let (sender, recv) = mpsc::channel();
647        let mut writer1 = ChannelWriter(sender);
648        let mut writer2 = writer1.clone();
649        let mut writer3 = writer1.clone();
650
651        let result = std::panic::catch_unwind(move || {
652            func1(
653                0,
654                "abcdef",
655                &mut writer1,
656                &mut writer2,
657                &mut writer3,
658                panic_detector,
659                None,
660            )
661        });
662        assert!(result.is_err());
663        let output = collect_string_from_recv(&recv);
664        let output = &mut output.as_str();
665        output
666            .expect_str("fn func3(foo: 0, bar: \"cdef\")\n")
667            .unwrap();
668        check_location_part(output, "", "", file!(), get_min_line(), get_max_line());
669        output
670            .expect_str("fn func2(foo: 0, bar: \"bcdef\")\n")
671            .unwrap();
672        check_location_part(output, "", "", file!(), get_min_line(), get_max_line());
673        output
674            .expect_str("fn func1(foo: 0, bar: \"abcdef\")\n")
675            .unwrap();
676        check_location_part(output, "", "", file!(), get_min_line(), get_max_line());
677        assert_eq!(*output, "");
678    }
679
680    #[cfg(feature = "std")]
681    #[allow(clippy::unwrap_used)]
682    #[test]
683    fn test_debug_unwind_context_with_io_without_unwind() {
684        let panic_detector = StdPanicDetector;
685
686        let (sender, recv) = mpsc::channel();
687        let mut writer = ChannelWriter(sender);
688
689        let result = std::panic::catch_unwind(move || {
690            func_with_debug_unwind_context(4, "abc", &mut writer, panic_detector, None)
691        });
692
693        assert_eq!(result.unwrap(), 1);
694        let output = collect_string_from_recv(&recv);
695        assert_eq!(output, "");
696    }
697
698    #[cfg(feature = "std")]
699    #[test]
700    fn test_debug_unwind_context_with_fmt_with_unwind() {
701        let panic_detector = StdPanicDetector;
702
703        let (sender, recv) = mpsc::channel();
704        let mut writer = ChannelWriter(sender);
705
706        let result = std::panic::catch_unwind(move || {
707            func_with_debug_unwind_context(2, "abc", &mut writer, panic_detector, None)
708        });
709        assert!(result.is_err());
710        let output = collect_string_from_recv(&recv);
711        let output = &mut output.as_str();
712
713        #[cfg(debug_assertions)]
714        {
715            output
716                .expect_str("fn func_with_debug_unwind_context(foo: 2, bar: \"abc\")\n")
717                .unwrap();
718            check_location_part(output, "", "", file!(), get_min_line(), get_max_line());
719        }
720        assert_eq!(*output, "");
721    }
722}