Skip to main content

oopsie_core/
traits.rs

1use alloc::boxed::Box;
2use alloc::rc::Rc;
3use alloc::sync::Arc;
4use core::error;
5use core::panic::Location;
6
7/// Builds a target error from a context selector and a source error of type `E`.
8///
9/// Implemented automatically by the context selector structs generated by
10/// `#[derive(Oopsie)]`. You rarely implement this manually, and most code never
11/// names it directly — [`ResultExt::context`] and [`OptionExt::context`] drive
12/// it. Reach for it only when building an error outside a `Result`/`Option`,
13/// e.g. `selector.build_error(source)`.
14#[diagnostic::on_unimplemented(
15    message = "`{Self}` cannot build an error from a source of type `{E}`",
16    label = "this selector does not accept a `{E}` source",
17    note = "a selector whose variant has a `source` field builds from that field's exact type — \
18            `.context(...)` on a `Result` requires the selector's source type to match the \
19            `Result`'s error",
20    note = "a selector with no `source` field is a leaf: build it with `.build()` / `.fail()`, or \
21            attach it to an `Option` with `.context(...)` — not to a `Result`'s error"
22)]
23pub trait Contextual<E> {
24    /// The destination error type being built by this context selector.
25    type Destination;
26
27    /// Build the target error from this context selector and the source error.
28    #[track_caller]
29    fn build_error(self, source: E) -> Self::Destination;
30}
31
32/// A value that fills itself in when an error is constructed.
33///
34/// A field marked `#[oopsie(capture)]` is populated by calling [`capture`](Self::capture) at
35/// construction time, so it is dropped from the context selector and never supplied by the
36/// caller. The library implements `Capturable` for backtraces, span-traces (with the `tracing`
37/// feature), [`SystemTime`](std::time::SystemTime)/[`Instant`](std::time::Instant) timestamps
38/// (plus `chrono::DateTime<Local>` under the `chrono` feature and `jiff::Timestamp`/`jiff::Zoned`
39/// under the `jiff` feature), the caller [`Location`], and the environment
40/// snapshots in the `extras` module. Implement it for your own type to capture anything
41/// else, such as a request or thread id.
42///
43/// It also covers `Box`, `Rc`, `Arc`, and tuples, so one field can wrap or combine several
44/// captured values.
45pub trait Capturable {
46    /// Capture a fresh value at the point the error is constructed.
47    #[track_caller]
48    fn capture() -> Self;
49
50    /// Capture for an error that wraps `source`, reusing equivalent data already on `source`
51    /// when that is more useful than a fresh capture.
52    ///
53    /// The default ignores `source` and calls [`capture`](Self::capture); trace types override
54    /// it so wrapping another diagnostic preserves the source's original call site instead of
55    /// recording the wrap site. Rarely called or overridden by hand.
56    #[track_caller]
57    #[inline]
58    fn capture_or_extract(source: &dyn crate::Diagnostic) -> Self
59    where
60        Self: Sized,
61    {
62        let _ = source;
63        Self::capture()
64    }
65}
66
67impl<T: Capturable> Capturable for Box<T> {
68    #[track_caller]
69    #[inline]
70    fn capture() -> Self {
71        Self::new(T::capture())
72    }
73
74    #[track_caller]
75    #[inline]
76    fn capture_or_extract(source: &dyn crate::Diagnostic) -> Self {
77        Self::new(T::capture_or_extract(source))
78    }
79}
80
81impl<T: Capturable> Capturable for Rc<T> {
82    #[track_caller]
83    #[inline]
84    fn capture() -> Self {
85        Self::new(T::capture())
86    }
87
88    #[track_caller]
89    #[inline]
90    fn capture_or_extract(source: &dyn crate::Diagnostic) -> Self {
91        Self::new(T::capture_or_extract(source))
92    }
93}
94
95impl<T: Capturable> Capturable for Arc<T> {
96    #[track_caller]
97    #[inline]
98    fn capture() -> Self {
99        Self::new(T::capture())
100    }
101
102    #[track_caller]
103    #[inline]
104    fn capture_or_extract(source: &dyn crate::Diagnostic) -> Self {
105        Self::new(T::capture_or_extract(source))
106    }
107}
108
109macro_rules! impl_capturable_tuples {
110    // entry: kick off with an empty accumulator
111    ($($name:ident),* $(,)?) => {
112        impl_capturable_tuples!(@acc [] $($name,)*);
113    };
114    // move one ident into the accumulator, emit for the accumulator + it
115    (@acc [$($acc:ident,)*] $head:ident, $($rest:ident,)*) => {
116        impl_capturable_tuples!(@impl $($acc,)* $head,);
117        impl_capturable_tuples!(@acc [$($acc,)* $head,] $($rest,)*);
118    };
119    // remaining list empty: done
120    (@acc [$($acc:ident,)*]) => {};
121    (@impl $($name:ident,)+) => {
122        impl<$($name: Capturable),+> Capturable for ($($name,)+) {
123            #[track_caller]
124            #[inline]
125            fn capture() -> Self {
126                ($($name::capture(),)+)
127            }
128            #[track_caller]
129            #[inline]
130            fn capture_or_extract(source: &dyn crate::Diagnostic) -> Self {
131                ($($name::capture_or_extract(source),)+)
132            }
133        }
134    };
135}
136impl_capturable_tuples!(A, B, C, D, E, F, G, H, I);
137
138#[cfg(feature = "std")]
139impl Capturable for std::time::SystemTime {
140    #[inline]
141    fn capture() -> Self {
142        Self::now()
143    }
144}
145
146#[cfg(feature = "std")]
147impl Capturable for std::time::Instant {
148    #[inline]
149    fn capture() -> Self {
150        Self::now()
151    }
152}
153impl Capturable for &'static Location<'static> {
154    #[inline]
155    #[track_caller]
156    fn capture() -> Self {
157        Location::caller()
158    }
159
160    #[track_caller]
161    #[inline]
162    fn capture_or_extract(source: &dyn crate::Diagnostic) -> Self {
163        // Capture eagerly so `#[track_caller]` resolves to this call's caller; a
164        // fn-pointer passed to `unwrap_or_else` would lose that and report a
165        // stdlib frame instead.
166        let here = Location::caller();
167        source.oopsie_location().unwrap_or(here)
168    }
169}
170
171#[cfg(feature = "chrono")]
172impl Capturable for chrono::DateTime<chrono::Local> {
173    #[inline]
174    fn capture() -> Self {
175        chrono::Local::now()
176    }
177}
178
179#[cfg(feature = "jiff")]
180impl Capturable for jiff::Timestamp {
181    #[inline]
182    fn capture() -> Self {
183        Self::now()
184    }
185}
186
187#[cfg(feature = "jiff")]
188impl Capturable for jiff::Zoned {
189    #[inline]
190    fn capture() -> Self {
191        Self::now()
192    }
193}
194
195/// The stand-in "source" a leaf selector (one with no `source` field) builds
196/// from. `Option::context` uses it as the source when there is no error value to
197/// attach. You normally reach a leaf error through `.build()` / `.fail()` /
198/// `Option::context`, so you rarely name this directly.
199#[derive(Debug, Copy, Clone, PartialEq, Eq)]
200pub struct NoSource;
201
202/// Normalize any source-error value to a `&(dyn Error + 'static)`.
203pub trait AsErrorSource {
204    /// Borrow this value as a `&(dyn Error + 'static)`.
205    fn as_error_source(&self) -> &(dyn error::Error + 'static);
206}
207
208impl<T: error::Error + 'static> AsErrorSource for T {
209    #[inline]
210    fn as_error_source(&self) -> &(dyn error::Error + 'static) {
211        self
212    }
213}
214
215impl AsErrorSource for dyn error::Error + 'static {
216    #[inline]
217    fn as_error_source(&self) -> &(dyn error::Error + 'static) {
218        self
219    }
220}
221
222impl AsErrorSource for dyn error::Error + Send + 'static {
223    #[inline]
224    fn as_error_source(&self) -> &(dyn error::Error + 'static) {
225        self
226    }
227}
228
229impl AsErrorSource for dyn error::Error + Send + Sync + 'static {
230    #[inline]
231    fn as_error_source(&self) -> &(dyn error::Error + 'static) {
232        self
233    }
234}
235
236/// Extension trait on [`Result`] for ergonomic error context wrapping.
237///
238/// Import via `use oopsie::prelude::*` or `use oopsie::ResultExt`.
239///
240/// # Example
241///
242/// ```
243/// use oopsie::{Oopsie, ResultExt as _};
244///
245/// #[derive(Debug, Oopsie)]
246/// #[oopsie(module(false))]
247/// enum IoError {
248///     #[oopsie("IO failed: {source}")]
249///     Failed { source: std::io::Error },
250/// }
251///
252/// # fn main() {
253/// let result: Result<(), std::io::Error> = Err(std::io::Error::other("disk full"));
254/// let err = result.context(Failed).unwrap_err();
255/// assert_eq!(err.to_string(), "IO failed: disk full");
256/// # }
257/// ```
258pub trait ResultExt<T, E> {
259    /// Wrap the error with an eagerly-evaluated context selector.
260    fn context<C>(self, context: C) -> Result<T, C::Destination>
261    where
262        C: Contextual<E>;
263
264    /// Wrap the error with a lazily-evaluated context selector.
265    fn with_context<F, C>(self, context: F) -> Result<T, C::Destination>
266    where
267        F: FnOnce(&E) -> C,
268        C: Contextual<E>;
269
270    /// Convert the error into a boxed trait object (`Box<dyn Error + Send + Sync>`).
271    ///
272    /// If `E` is itself a `Box<C>`, the result nests boxes: downcasting must
273    /// target `Box<C>`, not `C`.
274    fn boxed(self) -> Result<T, Box<dyn error::Error + Send + Sync + 'static>>
275    where
276        E: error::Error + Send + Sync + 'static;
277
278    /// Convert the error into a boxed trait object (no `Send`/`Sync` bounds).
279    ///
280    /// If `E` is itself a `Box<C>`, the result nests boxes: downcasting must
281    /// target `Box<C>`, not `C`.
282    fn boxed_local(self) -> Result<T, Box<dyn error::Error + 'static>>
283    where
284        E: error::Error + 'static;
285}
286
287impl<T, E> ResultExt<T, E> for Result<T, E> {
288    #[inline]
289    #[track_caller]
290    fn context<C>(self, context: C) -> Result<T, C::Destination>
291    where
292        C: Contextual<E>,
293    {
294        // Call `build_error` directly (not inside a `map_err` closure) so
295        // `#[track_caller]` forwards the caller's `Location` through to any
296        // captured location field.
297        match self {
298            Ok(value) => Ok(value),
299            Err(error) => Err(context.build_error(error)),
300        }
301    }
302
303    #[inline]
304    #[track_caller]
305    fn with_context<F, C>(self, context: F) -> Result<T, C::Destination>
306    where
307        F: FnOnce(&E) -> C,
308        C: Contextual<E>,
309    {
310        match self {
311            Ok(value) => Ok(value),
312            Err(error) => {
313                let selector = context(&error);
314                Err(selector.build_error(error))
315            }
316        }
317    }
318
319    #[inline]
320    fn boxed(self) -> Result<T, Box<dyn error::Error + Send + Sync + 'static>>
321    where
322        E: error::Error + Send + Sync + 'static,
323    {
324        self.map_err(|error| Box::new(error) as Box<dyn error::Error + Send + Sync + 'static>)
325    }
326
327    #[inline]
328    fn boxed_local(self) -> Result<T, Box<dyn error::Error + 'static>>
329    where
330        E: error::Error + 'static,
331    {
332        self.map_err(|error| Box::new(error) as Box<dyn error::Error + 'static>)
333    }
334}
335
336/// Extension trait on [`Option`] for converting `None` into a typed error.
337///
338/// Import via `use oopsie::prelude::*` or `use oopsie::OptionExt`.
339///
340/// # Example
341///
342/// ```
343/// use oopsie::{Oopsie, OptionExt as _};
344///
345/// #[derive(Debug, Oopsie)]
346/// #[oopsie(module(false))]
347/// enum LookupError {
348///     #[oopsie("Key not found: {key}")]
349///     Missing { key: String },
350/// }
351///
352/// # fn main() {
353/// let opt: Option<i32> = None;
354/// let err = opt.context(Missing { key: "host" }).unwrap_err();
355/// assert_eq!(err.to_string(), "Key not found: host");
356/// # }
357/// ```
358pub trait OptionExt<T> {
359    /// Convert `None` into an error with an eagerly-evaluated context selector.
360    fn context<C>(self, context: C) -> Result<T, C::Destination>
361    where
362        C: Contextual<NoSource>;
363
364    /// Convert `None` into an error with a lazily-evaluated context selector.
365    fn with_context<F, C>(self, context: F) -> Result<T, C::Destination>
366    where
367        F: FnOnce() -> C,
368        C: Contextual<NoSource>;
369}
370
371impl<T> OptionExt<T> for Option<T> {
372    #[inline]
373    #[track_caller]
374    fn context<C>(self, context: C) -> Result<T, C::Destination>
375    where
376        C: Contextual<NoSource>,
377    {
378        match self {
379            Some(value) => Ok(value),
380            None => Err(context.build_error(NoSource)),
381        }
382    }
383
384    #[inline]
385    #[track_caller]
386    fn with_context<F, C>(self, context: F) -> Result<T, C::Destination>
387    where
388        F: FnOnce() -> C,
389        C: Contextual<NoSource>,
390    {
391        match self {
392            Some(value) => Ok(value),
393            None => Err(context().build_error(NoSource)),
394        }
395    }
396}
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401    use std::error::Error as StdError;
402    use std::fmt;
403    use std::time::SystemTime;
404
405    #[cfg(feature = "jiff")]
406    #[test]
407    fn captures_current_jiff_timestamp() {
408        // Lower bound only: wall-clock `now()` is non-monotonic, so an upper
409        // bound could flake on a backward clock step.
410        let before = jiff::Timestamp::now();
411        let captured = <jiff::Timestamp as Capturable>::capture();
412        assert!(captured >= before, "captured {captured} predates {before}");
413    }
414
415    #[cfg(feature = "jiff")]
416    #[test]
417    fn captures_current_jiff_zoned() {
418        let before = jiff::Timestamp::now();
419        let captured = <jiff::Zoned as Capturable>::capture().timestamp();
420        assert!(captured >= before, "captured {captured} predates {before}");
421    }
422
423    #[derive(Debug)]
424    struct DiagOnly;
425
426    impl fmt::Display for DiagOnly {
427        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
428            f.write_str("diag only")
429        }
430    }
431
432    impl std::error::Error for DiagOnly {}
433    impl crate::Diagnostic for DiagOnly {}
434
435    // Test error types for trait implementations
436    #[derive(Debug)]
437    struct SimpleError {
438        message: String,
439    }
440
441    impl fmt::Display for SimpleError {
442        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
443            write!(f, "{}", self.message)
444        }
445    }
446
447    impl std::error::Error for SimpleError {}
448
449    #[derive(Debug)]
450    struct SourceError {
451        message: String,
452    }
453
454    impl fmt::Display for SourceError {
455        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
456            write!(f, "{}", self.message)
457        }
458    }
459
460    impl std::error::Error for SourceError {}
461
462    #[derive(Debug)]
463    struct ChainError {
464        message: String,
465        source: Box<SourceError>,
466    }
467
468    impl fmt::Display for ChainError {
469        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
470            write!(f, "{}", self.message)
471        }
472    }
473
474    impl std::error::Error for ChainError {
475        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
476            Some(&*self.source)
477        }
478    }
479
480    // Test Contextual selector implementations
481    struct SimpleSelector;
482
483    impl Contextual<NoSource> for SimpleSelector {
484        type Destination = SimpleError;
485
486        fn build_error(self, _source: NoSource) -> SimpleError {
487            SimpleError {
488                message: "simple error".to_owned(),
489            }
490        }
491    }
492
493    struct ChainSelector;
494
495    impl Contextual<SourceError> for ChainSelector {
496        type Destination = ChainError;
497
498        fn build_error(self, source: SourceError) -> ChainError {
499            ChainError {
500                message: "chain error".to_owned(),
501                source: Box::new(source),
502            }
503        }
504    }
505
506    #[test]
507    fn test_result_ext_context_ok() {
508        let result: Result<i32, SourceError> = Ok(42);
509        let chained: Result<i32, ChainError> = result.context(ChainSelector);
510        assert!(chained.is_ok());
511        assert_eq!(chained.unwrap(), 42);
512    }
513
514    #[test]
515    fn test_result_ext_context_err() {
516        let result: Result<i32, SourceError> = Err(SourceError {
517            message: "source failed".to_owned(),
518        });
519        let chained: Result<i32, ChainError> = result.context(ChainSelector);
520        assert!(chained.is_err());
521        let err = chained.unwrap_err();
522        assert_eq!(err.message, "chain error");
523        assert_eq!(StdError::source(&err).unwrap().to_string(), "source failed");
524    }
525
526    #[test]
527    fn test_result_ext_with_context_ok() {
528        let result: Result<i32, SourceError> = Ok(42);
529        let chained: Result<i32, ChainError> = result.with_context(|_| ChainSelector);
530        assert!(chained.is_ok());
531        assert_eq!(chained.unwrap(), 42);
532    }
533
534    #[test]
535    fn test_result_ext_with_context_err_closure_called() {
536        let mut closure_called = false;
537        let result: Result<i32, SourceError> = Err(SourceError {
538            message: "source failed".to_owned(),
539        });
540        let chained: Result<i32, ChainError> = result.with_context(|source| {
541            closure_called = true;
542            // Verify we can access the source in the closure
543            assert_eq!(source.message, "source failed");
544            ChainSelector
545        });
546        assert!(closure_called);
547        chained.unwrap_err();
548    }
549
550    #[test]
551    fn test_option_ext_context_some() {
552        let option: Option<i32> = Some(42);
553        let result: Result<i32, SimpleError> = option.context(SimpleSelector);
554        assert!(result.is_ok());
555        assert_eq!(result.unwrap(), 42);
556    }
557
558    #[test]
559    fn test_option_ext_context_none() {
560        let option: Option<i32> = None;
561        let result: Result<i32, SimpleError> = option.context(SimpleSelector);
562        assert!(result.is_err());
563        assert_eq!(result.unwrap_err().message, "simple error");
564    }
565
566    #[test]
567    fn test_option_ext_with_context_some() {
568        let option: Option<i32> = Some(42);
569        let result: Result<i32, SimpleError> = option.with_context(|| SimpleSelector);
570        assert!(result.is_ok());
571        assert_eq!(result.unwrap(), 42);
572    }
573
574    #[test]
575    fn test_option_ext_with_context_none_closure_called() {
576        let mut closure_called = false;
577        let option: Option<i32> = None;
578        let result: Result<i32, SimpleError> = option.with_context(|| {
579            closure_called = true;
580            SimpleSelector
581        });
582        assert!(closure_called);
583        result.unwrap_err();
584    }
585
586    const _: () = {
587        // Verify that Box<T> implements Capturable when T: Capturable
588        const fn is_capturable<T: Capturable>() {}
589        is_capturable::<Box<crate::Backtrace>>();
590    };
591
592    #[cfg(feature = "tracing")]
593    #[test]
594    fn tuple_capture_produces_both_elements() {
595        use crate::{Backtrace, SpanTrace};
596        // Tuple capture yields both traces; neither call panics.
597        let (bt, st): (Backtrace, SpanTrace) = <(Backtrace, SpanTrace) as Capturable>::capture();
598        let _ = bt.frames();
599        let _ = st.status();
600    }
601
602    #[cfg(feature = "tracing")]
603    const _: () = {
604        const fn is_capturable<T: Capturable>() {}
605        is_capturable::<Box<(crate::Backtrace, crate::SpanTrace)>>();
606        is_capturable::<(crate::Backtrace, crate::SpanTrace)>();
607    };
608
609    #[cfg(feature = "tracing")]
610    #[test]
611    fn tuple_capture_ext_extracts_both_from_source() {
612        use crate::{
613            Backtrace, Capturable, Diagnostic, RustBacktrace, SpanTrace,
614            with_rust_backtrace_override,
615        };
616        use std::fmt;
617
618        #[derive(Debug)]
619        struct Src {
620            backtrace: Backtrace,
621            spantrace: SpanTrace,
622        }
623        impl fmt::Display for Src {
624            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
625                f.write_str("src")
626            }
627        }
628        impl std::error::Error for Src {}
629        impl Diagnostic for Src {
630            fn oopsie_backtrace(&self) -> Option<&Backtrace> {
631                Some(&self.backtrace)
632            }
633            fn oopsie_spantrace(&self) -> Option<&SpanTrace> {
634                Some(&self.spantrace)
635            }
636        }
637
638        with_rust_backtrace_override(RustBacktrace::Enabled, || {
639            let src = Src {
640                backtrace: Backtrace::capture(),
641                spantrace: SpanTrace::capture(),
642            };
643            let source_frames = src.backtrace.frames().len();
644            assert!(
645                source_frames > 0,
646                "backtrace must be enabled for this test to be probative"
647            );
648            let extracted = <(Backtrace, SpanTrace) as Capturable>::capture_or_extract(&src);
649            // The extracted backtrace must reuse the source's frame count, proving
650            // extraction (not a fresh capture).
651            assert_eq!(extracted.0.frames().len(), source_frames);
652        });
653    }
654
655    #[cfg(feature = "tracing")]
656    const _: () = {
657        const fn is_capturable<T: Capturable>() {}
658        is_capturable::<Box<(crate::Backtrace, crate::SpanTrace)>>();
659        is_capturable::<(crate::Backtrace, crate::SpanTrace)>();
660    };
661
662    // Destination error whose source is a type-erased boxed error, exercising
663    // the `result.boxed().context(...)` chaining ergonomic. The `Send + Sync`
664    // bound on the field is what `boxed` (vs `boxed_local`) makes possible.
665    #[derive(Debug)]
666    struct BoxedChainError {
667        message: String,
668        source: Box<dyn StdError + Send + Sync + 'static>,
669    }
670
671    impl fmt::Display for BoxedChainError {
672        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
673            write!(f, "{}", self.message)
674        }
675    }
676
677    impl StdError for BoxedChainError {
678        fn source(&self) -> Option<&(dyn StdError + 'static)> {
679            Some(&*self.source)
680        }
681    }
682
683    struct BoxedSelector;
684
685    impl Contextual<Box<dyn StdError + Send + Sync + 'static>> for BoxedSelector {
686        type Destination = BoxedChainError;
687
688        fn build_error(self, source: Box<dyn StdError + Send + Sync + 'static>) -> BoxedChainError {
689            BoxedChainError {
690                message: "boxed chain error".to_owned(),
691                source,
692            }
693        }
694    }
695
696    #[derive(Debug)]
697    struct BoxedLocalChainError {
698        source: Box<dyn StdError + 'static>,
699    }
700
701    impl fmt::Display for BoxedLocalChainError {
702        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
703            write!(f, "boxed local chain error")
704        }
705    }
706
707    impl StdError for BoxedLocalChainError {
708        fn source(&self) -> Option<&(dyn StdError + 'static)> {
709            Some(&*self.source)
710        }
711    }
712
713    struct BoxedLocalSelector;
714
715    impl Contextual<Box<dyn StdError + 'static>> for BoxedLocalSelector {
716        type Destination = BoxedLocalChainError;
717
718        fn build_error(self, source: Box<dyn StdError + 'static>) -> BoxedLocalChainError {
719            BoxedLocalChainError { source }
720        }
721    }
722
723    #[test]
724    fn boxed_passes_through_ok() {
725        let result: Result<i32, SourceError> = Ok(7);
726        assert_eq!(result.boxed().unwrap(), 7);
727    }
728
729    #[test]
730    fn boxed_erases_err_but_preserves_display_and_concrete_type() {
731        let result: Result<i32, SourceError> = Err(SourceError {
732            message: "boom".to_owned(),
733        });
734        let err = result.boxed().unwrap_err();
735        assert_eq!(err.to_string(), "boom");
736        // Erasure is reversible: the concrete type is still recoverable.
737        assert!(err.downcast_ref::<SourceError>().is_some());
738    }
739
740    #[test]
741    fn boxed_local_erases_err_but_preserves_display_and_concrete_type() {
742        let result: Result<i32, SourceError> = Err(SourceError {
743            message: "boom".to_owned(),
744        });
745        let err = result.boxed_local().unwrap_err();
746        assert_eq!(err.to_string(), "boom");
747        assert!(err.downcast_ref::<SourceError>().is_some());
748    }
749
750    #[test]
751    fn boxed_then_context_chains_erased_source() {
752        let result: Result<i32, SourceError> = Err(SourceError {
753            message: "io broke".to_owned(),
754        });
755        let chained: Result<i32, BoxedChainError> = result.boxed().context(BoxedSelector);
756        let err = chained.unwrap_err();
757        assert_eq!(err.message, "boxed chain error");
758        assert_eq!(StdError::source(&err).unwrap().to_string(), "io broke");
759    }
760
761    #[test]
762    fn boxed_then_context_passes_through_ok() {
763        let result: Result<i32, SourceError> = Ok(42);
764        let chained: Result<i32, BoxedChainError> = result.boxed().context(BoxedSelector);
765        assert_eq!(chained.unwrap(), 42);
766    }
767
768    #[test]
769    fn boxed_then_with_context_chains_erased_source() {
770        let result: Result<i32, SourceError> = Err(SourceError {
771            message: "io broke".to_owned(),
772        });
773        let chained: Result<i32, BoxedChainError> = result.boxed().with_context(|source| {
774            // The closure sees the already-erased boxed error.
775            assert_eq!(source.to_string(), "io broke");
776            BoxedSelector
777        });
778        assert_eq!(
779            StdError::source(&chained.unwrap_err()).unwrap().to_string(),
780            "io broke"
781        );
782    }
783
784    #[test]
785    fn boxed_local_then_context_chains_erased_source() {
786        let result: Result<i32, SourceError> = Err(SourceError {
787            message: "io broke".to_owned(),
788        });
789        let chained: Result<i32, BoxedLocalChainError> =
790            result.boxed_local().context(BoxedLocalSelector);
791        assert_eq!(
792            StdError::source(&chained.unwrap_err()).unwrap().to_string(),
793            "io broke"
794        );
795    }
796
797    #[test]
798    fn boxed_unifies_heterogeneous_error_types() {
799        // `boxed()?` collapses distinct concrete error types to one boxed type,
800        // letting a single function signature absorb either.
801        fn run(fail_with_source: bool) -> Result<(), Box<dyn StdError + Send + Sync + 'static>> {
802            if fail_with_source {
803                Err(SourceError {
804                    message: "source".to_owned(),
805                })
806                .boxed()?;
807            } else {
808                Err(SimpleError {
809                    message: "simple".to_owned(),
810                })
811                .boxed()?;
812            }
813            Ok(())
814        }
815        assert_eq!(run(true).unwrap_err().to_string(), "source");
816        assert_eq!(run(false).unwrap_err().to_string(), "simple");
817    }
818
819    #[test]
820    fn boxed_on_boxed_error_downcasts_to_the_box() {
821        let result: Result<(), Box<SourceError>> = Err(Box::new(SourceError {
822            message: "boom".to_owned(),
823        }));
824        let err = result.boxed().unwrap_err();
825        assert!(err.downcast_ref::<SourceError>().is_none());
826        assert!(err.downcast_ref::<Box<SourceError>>().is_some());
827    }
828
829    const _: () = {
830        // `boxed` yields a thread-portable error; `boxed_local` does not.
831        const fn is_send_sync<T: Send + Sync>() {}
832        is_send_sync::<Box<dyn StdError + Send + Sync + 'static>>();
833    };
834
835    #[derive(Debug)]
836    struct WithLoc(&'static std::panic::Location<'static>);
837
838    impl fmt::Display for WithLoc {
839        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
840            f.write_str("with loc")
841        }
842    }
843
844    impl StdError for WithLoc {}
845
846    impl crate::Diagnostic for WithLoc {
847        fn oopsie_location(&self) -> Option<&'static std::panic::Location<'static>> {
848            Some(self.0)
849        }
850    }
851
852    #[test]
853    fn location_captures_caller_and_extracts_from_source() {
854        // `capture` records the call site.
855        let line = line!() + 1;
856        let loc = <&'static std::panic::Location<'static> as Capturable>::capture();
857        assert!(loc.file().ends_with("traits.rs"));
858        assert_eq!(loc.line(), line);
859
860        // `capture_or_extract` prefers a source that exposes a location.
861        let src = WithLoc(std::panic::Location::caller());
862        let extracted =
863            <&'static std::panic::Location<'static> as Capturable>::capture_or_extract(&src);
864        assert!(
865            std::ptr::eq(extracted, src.0),
866            "must reuse the source's location"
867        );
868
869        // A source with no location falls back to a fresh capture.
870        let fresh =
871            <&'static std::panic::Location<'static> as Capturable>::capture_or_extract(&DiagOnly);
872        assert!(fresh.file().ends_with("traits.rs"));
873    }
874
875    #[test]
876    fn system_time_captures_now_and_never_extracts() {
877        let before = SystemTime::now();
878        let captured = <SystemTime as Capturable>::capture();
879        assert!(captured >= before);
880        let extracted = <SystemTime as Capturable>::capture_or_extract(&DiagOnly);
881        assert!(extracted >= before);
882    }
883
884    #[test]
885    fn location_captures() {
886        let location = <&'static Location<'static> as Capturable>::capture();
887        let expected_line = line!() - 1; // capture() is 1 line above
888        assert_eq!(location.line(), expected_line);
889        assert_eq!(location.file(), file!());
890    }
891
892    #[test]
893    fn location_captures_nested() {
894        #[track_caller]
895        fn capture_location() -> &'static Location<'static> {
896            <&'static Location<'static> as Capturable>::capture()
897        }
898        let location = capture_location();
899        let expected_line = line!() - 1; // capture() is 1 line above
900        assert_eq!(location.line(), expected_line);
901        assert_eq!(location.file(), file!());
902    }
903
904    #[test]
905    fn complex_location_captures() {
906        type Loc = &'static Location<'static>;
907        type LocTimeLoc = (Loc, SystemTime, Loc);
908        let location =
909            <(Box<LocTimeLoc>, Rc<LocTimeLoc>, Arc<LocTimeLoc>) as Capturable>::capture();
910        let expected_line = line!() - 1; // capture() is 1 line above
911        let expected_col = 3 * 4 + 1; // column of the first `location` in the tuple
912        let (box_loc, rc_loc, arc_loc) = location;
913        for (loc1, _, loc2) in [&*box_loc, &*rc_loc, &*arc_loc] {
914            for loc in [loc1, loc2] {
915                assert_eq!(loc.file(), file!());
916                assert_eq!(loc.line(), expected_line);
917                assert_eq!(loc.column(), expected_col);
918            }
919        }
920    }
921}