Skip to main content

oopsie_core/
traits.rs

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