Skip to main content

oopsie_core/
welp.rs

1//! See [`Welp`] for the overview.
2
3use alloc::boxed::Box;
4use alloc::string::String;
5use core::error::Error as StdError;
6use core::fmt;
7
8use crate::{Backtrace, Capturable as _, Diagnostic, SpanTrace};
9
10/// A boxed `std::error::Error` that's `Send + Sync + 'static`.
11type BoxError = Box<dyn StdError + Send + Sync + 'static>;
12
13/// A string-shaped error type for prototypes, one-off errors, and quick
14/// string contexts.
15///
16/// The typed-selector flow generated by `#[oopsie]` is the project's primary
17/// error-handling story. `Welp` covers the cases where a structured variant
18/// would be overkill: prototyping, exactly-once failure modes, and adding a
19/// plain-string context to a foreign error.
20///
21/// ```
22/// use oopsie_core::{Welp, WelpResultExt as _};
23///
24/// fn read_config(path: &str) -> Result<String, Welp> {
25///     std::fs::read_to_string(path).welp_context("could not read config")
26/// }
27/// ```
28///
29/// # Construction
30///
31/// - [`Welp::new`] — fresh string error, captures a backtrace (and a span-trace
32///   when the `tracing` feature is enabled).
33/// - [`Welp::wrap`] — wrap an existing error with a string message.
34/// - [`Welp::wrap_boxed`] — same, for an already-boxed `dyn Error`.
35/// - [`Welp::from_error`] — wrap an existing error with *no* message; its
36///   `Display` delegates to the source.
37///
38/// # Adding context to a `Result` / `Option`
39///
40/// Bring [`WelpResultExt`] / [`WelpOptionExt`] into scope (or import from
41/// `oopsie::prelude`) and use [`welp_context`](WelpResultExt::welp_context) /
42/// [`with_welp_context`](WelpResultExt::with_welp_context).
43///
44/// Use [`welp`](WelpResultExt::welp) for the message-free conversion — the
45/// `?`-style escape hatch `anyhow`/`eyre` give you for free. `result.welp()?`
46/// turns any foreign `Error` into a `Welp` without inventing a string;
47/// `result.welp_context("…")?` is for when a message genuinely adds information
48/// the source doesn't already carry. Together they make
49/// `fn main() -> Report<Welp>` (with `Welp` from the prelude) the catch-all
50/// top-level error story.
51///
52/// # Layout
53///
54/// `Welp` is a small enum. Every variant carries a boxed `(Backtrace, _)`
55/// captured at construction (the second slot is the span-trace under the
56/// `tracing` feature and is otherwise empty) and an inline `&'static Location`
57/// of the call site. `Traced` adds a `Box<str>` message; `Sourced` adds an
58/// optional `Box<str>` message and the wrapped `Box<dyn Error>`. The
59/// message-free form ([`from_error`](Self::from_error) /
60/// [`welp`](WelpResultExt::welp)) is a `Sourced` with no message, whose
61/// `Display` delegates to the source.
62///
63/// A `Sourced` `Welp`'s [`Diagnostic`] accessors prefer the source's traces
64/// (reached via the Provider API under the
65/// `unstable-error-generic-member-access` feature) and fall back to the
66/// wrap-site ones.
67#[must_use = "this `Welp` error should be returned or propagated, not discarded"]
68pub struct Welp(WelpRepr);
69
70enum WelpRepr {
71    Sourced {
72        // `None` is the message-free form (`from_error` / `.welp()`): `Display`
73        // delegates to `source`. The `Box<str>` data pointer's null niche
74        // absorbs the `Option` tag, so this stays one machine word.
75        message: Option<Box<str>>,
76        source: BoxError,
77        traces: Box<(Backtrace, SpanTrace)>,
78        location: &'static core::panic::Location<'static>,
79    },
80    Traced {
81        message: Box<str>,
82        traces: Box<(Backtrace, SpanTrace)>,
83        location: &'static core::panic::Location<'static>,
84    },
85}
86
87impl Welp {
88    /// Build a fresh `Welp` from a string message. Captures a backtrace (and a
89    /// span-trace when the `tracing` feature is enabled) at the call site.
90    ///
91    /// ```
92    /// use oopsie_core::Welp;
93    ///
94    /// let err = Welp::new(format!("port {} out of range", 70_000));
95    /// assert_eq!(err.to_string(), "port 70000 out of range");
96    /// ```
97    #[track_caller]
98    pub fn new(message: impl Into<String>) -> Self {
99        Self(WelpRepr::Traced {
100            message: message.into().into_boxed_str(),
101            traces: Box::new((Backtrace::capture(), SpanTrace::capture())),
102            location: core::panic::Location::caller(),
103        })
104    }
105
106    /// Wrap an existing error with a string message. Captures a backtrace (and a
107    /// span-trace when the `tracing` feature is enabled) at the wrap site; when
108    /// the source provides its own captured
109    /// traces (via the Provider API under the
110    /// `unstable-error-generic-member-access` feature) the [`Diagnostic`]
111    /// accessors surface those instead, so the origin-most captured trace
112    /// wins.
113    ///
114    /// ```
115    /// use oopsie_core::Welp;
116    ///
117    /// let io_err = std::io::Error::other("disk full");
118    /// let err = Welp::wrap(io_err, "could not write");
119    /// assert_eq!(err.to_string(), "could not write");
120    /// assert!(std::error::Error::source(&err).is_some());
121    /// ```
122    #[track_caller]
123    pub fn wrap<E>(source: E, message: impl Into<String>) -> Self
124    where
125        E: StdError + Send + Sync + 'static,
126    {
127        // No construction-time extraction: `wrap` is generic, so the
128        // `CaptureProbe` autoref trick cannot see the source's `Diagnostic`
129        // impl here (method resolution happens against this body's bounds,
130        // not per instantiation). The origin trace is reached at accessor
131        // time instead, through the source-first Provider lookup.
132        Self(WelpRepr::Sourced {
133            message: Some(message.into().into_boxed_str()),
134            source: Box::new(source),
135            traces: Box::new((Backtrace::capture(), SpanTrace::capture())),
136            location: core::panic::Location::caller(),
137        })
138    }
139
140    /// Wrap an already-boxed trait-object error with a string message.
141    ///
142    /// Use this when the source comes from an API returning
143    /// `Box<dyn Error + Send + Sync + 'static>` — that type is `?Sized` and
144    /// can't satisfy [`wrap`](Self::wrap)'s `Sized` bound, but it's exactly
145    /// the shape `Welp` stores internally, so no rewrapping is needed.
146    ///
147    /// ```
148    /// use oopsie_core::Welp;
149    ///
150    /// let boxed: Box<dyn std::error::Error + Send + Sync + 'static> =
151    ///     Box::new(std::io::Error::other("disk full"));
152    /// let err = Welp::wrap_boxed(boxed, "could not write");
153    /// assert_eq!(err.to_string(), "could not write");
154    /// assert!(std::error::Error::source(&err).is_some());
155    /// ```
156    #[track_caller]
157    pub fn wrap_boxed(source: BoxError, message: impl Into<String>) -> Self {
158        Self(WelpRepr::Sourced {
159            message: Some(message.into().into_boxed_str()),
160            source,
161            traces: Box::new((Backtrace::capture(), SpanTrace::capture())),
162            location: core::panic::Location::caller(),
163        })
164    }
165
166    /// Wrap an existing error with no user message: this `Welp`'s [`Display`]
167    /// delegates to the source's, and the source stays reachable through
168    /// [`Error::source`]. Use it for the message-free `?`-style conversion of a
169    /// foreign error; reach for [`wrap`](Self::wrap) when a string context adds
170    /// information.
171    ///
172    /// Captures a backtrace (and a span-trace under the `tracing` feature) at
173    /// the call site; like [`wrap`](Self::wrap), the [`Diagnostic`] accessors
174    /// surface the source's captured traces (via the Provider API under the
175    /// `unstable-error-generic-member-access` feature) when it has them, so the
176    /// origin-most trace wins.
177    ///
178    /// [`Display`]: fmt::Display
179    /// [`Error::source`]: StdError::source
180    ///
181    /// ```
182    /// use oopsie_core::Welp;
183    ///
184    /// let err = Welp::from_error(std::io::Error::other("disk full"));
185    /// assert_eq!(err.to_string(), "disk full");
186    /// assert!(std::error::Error::source(&err).is_some());
187    /// ```
188    #[track_caller]
189    pub fn from_error<E>(source: E) -> Self
190    where
191        E: StdError + Send + Sync + 'static,
192    {
193        Self(WelpRepr::Sourced {
194            message: None,
195            source: Box::new(source),
196            traces: Box::new((Backtrace::capture(), SpanTrace::capture())),
197            location: core::panic::Location::caller(),
198        })
199    }
200
201    /// The user message attached to this error, or `None` when it carries none
202    /// (a `Welp` from [`from_error`](Self::from_error) / [`welp`] delegates its
203    /// [`Display`] to the wrapped source instead of owning a message).
204    ///
205    /// [`welp`]: WelpResultExt::welp
206    /// [`Display`]: fmt::Display
207    #[must_use]
208    #[inline]
209    pub fn message(&self) -> Option<&str> {
210        match &self.0 {
211            WelpRepr::Sourced { message, .. } => message.as_deref(),
212            WelpRepr::Traced { message, .. } => Some(message),
213        }
214    }
215
216    /// Borrow the wrapped source as `&E` when it is exactly that type.
217    ///
218    /// Inspects the *direct* source only, not the whole chain. A `Welp` with no
219    /// source ([`new`](Self::new) / [`welp_context`](crate::WelpOptionExt::welp_context)
220    /// on an `Option`) returns `None`. To search the whole chain, compose with
221    /// [`chain`](crate::ErrorChainExt::chain):
222    ///
223    /// ```
224    /// use oopsie_core::{ErrorChainExt as _, Welp};
225    /// use std::error::Error as _;
226    ///
227    /// let inner = std::io::Error::other("disk full");
228    /// let err = Welp::wrap(Welp::wrap(inner, "mid"), "outer");
229    ///
230    /// // The direct source is the middle `Welp`, not the io::Error.
231    /// assert!(err.downcast_ref::<std::io::Error>().is_none());
232    ///
233    /// // Reach the buried io::Error by walking the chain.
234    /// let io = err
235    ///     .chain()
236    ///     .find_map(|e| e.downcast_ref::<std::io::Error>())
237    ///     .expect("io::Error is somewhere in the chain");
238    /// assert_eq!(io.to_string(), "disk full");
239    /// ```
240    #[must_use]
241    #[inline]
242    pub fn downcast_ref<E: StdError + 'static>(&self) -> Option<&E> {
243        match &self.0 {
244            WelpRepr::Sourced { source, .. } => source.downcast_ref::<E>(),
245            WelpRepr::Traced { .. } => None,
246        }
247    }
248
249    /// Mutably borrow the wrapped source as `&mut E` when it is exactly that
250    /// type. Like [`downcast_ref`](Self::downcast_ref), inspects the direct
251    /// source only.
252    #[must_use]
253    #[inline]
254    pub fn downcast_mut<E: StdError + 'static>(&mut self) -> Option<&mut E> {
255        match &mut self.0 {
256            WelpRepr::Sourced { source, .. } => source.downcast_mut::<E>(),
257            WelpRepr::Traced { .. } => None,
258        }
259    }
260
261    /// Consume this `Welp` and recover the wrapped source as `E` when it is
262    /// exactly that type, returning the `Welp` unchanged otherwise.
263    ///
264    /// Targets the *direct* source only. A message-free [`Welp`] is the source's
265    /// transparent wrapper, so this recovers the original foreign error:
266    ///
267    /// ```
268    /// use oopsie_core::{Welp, WelpResultExt as _};
269    ///
270    /// let r: Result<(), std::io::Error> = Err(std::io::Error::other("disk full"));
271    /// let welp = r.welp().unwrap_err();
272    /// let io: std::io::Error = welp.downcast().expect("source is an io::Error");
273    /// assert_eq!(io.to_string(), "disk full");
274    /// ```
275    ///
276    /// # Errors
277    ///
278    /// Returns `Err(self)` — message, location, and captured traces intact —
279    /// when there is no source or it is not of type `E`.
280    pub fn downcast<E: StdError + 'static>(self) -> Result<E, Self> {
281        match self.0 {
282            WelpRepr::Sourced {
283                message,
284                source,
285                traces,
286                location,
287            } => match source.downcast::<E>() {
288                Ok(source) => Ok(*source),
289                Err(source) => Err(Self(WelpRepr::Sourced {
290                    message,
291                    source,
292                    traces,
293                    location,
294                })),
295            },
296            traced @ WelpRepr::Traced { .. } => Err(Self(traced)),
297        }
298    }
299}
300
301impl fmt::Debug for Welp {
302    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
303        let mut dbg = f.debug_struct("Welp");
304        if let Some(message) = self.message() {
305            dbg.field("message", &message);
306        }
307        match &self.0 {
308            WelpRepr::Sourced { source, .. } => {
309                dbg.field("source", source);
310            }
311            WelpRepr::Traced { .. } => {}
312        }
313        dbg.finish()
314    }
315}
316
317impl fmt::Display for Welp {
318    #[inline]
319    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
320        match &self.0 {
321            // No message is the delegating form: render the source as if it
322            // were the top error.
323            WelpRepr::Sourced {
324                message: None,
325                source,
326                ..
327            } => source.fmt(f),
328            WelpRepr::Sourced {
329                message: Some(message),
330                ..
331            }
332            | WelpRepr::Traced { message, .. } => f.write_str(message),
333        }
334    }
335}
336
337impl StdError for Welp {
338    fn source(&self) -> Option<&(dyn StdError + 'static)> {
339        match &self.0 {
340            WelpRepr::Sourced { source, .. } => Some(&**source),
341            WelpRepr::Traced { .. } => None,
342        }
343    }
344
345    #[cfg(feature = "unstable-error-generic-member-access")]
346    fn provide<'a>(&'a self, request: &mut core::error::Request<'a>) {
347        let traces = match &self.0 {
348            WelpRepr::Sourced { source, traces, .. } => {
349                // `Request` is first-wins: forwarding the source first keeps
350                // its deeper trace ahead of the wrap-site one. An empty trace
351                // is never provided, so it cannot shadow a captured one in an
352                // outer layer.
353                source.provide(request);
354                traces
355            }
356            WelpRepr::Traced { traces, .. } => traces,
357        };
358        if traces.0.is_captured() {
359            request.provide_ref::<Backtrace>(&traces.0);
360        }
361        if traces.1.is_captured() {
362            request.provide_ref::<SpanTrace>(&traces.1);
363        }
364    }
365}
366
367impl Diagnostic for Welp {
368    fn oopsie_backtrace(&self) -> Option<&Backtrace> {
369        match &self.0 {
370            WelpRepr::Sourced { source, traces, .. } => {
371                crate::__private::source_backtrace(&**source).or(Some(&traces.0))
372            }
373            WelpRepr::Traced { traces, .. } => Some(&traces.0),
374        }
375    }
376
377    fn oopsie_spantrace(&self) -> Option<&SpanTrace> {
378        match &self.0 {
379            WelpRepr::Sourced { source, traces, .. } => {
380                crate::__private::source_spantrace(&**source).or(Some(&traces.1))
381            }
382            WelpRepr::Traced { traces, .. } => Some(&traces.1),
383        }
384    }
385
386    fn oopsie_location(&self) -> Option<&'static core::panic::Location<'static>> {
387        match &self.0 {
388            WelpRepr::Sourced { location, .. } | WelpRepr::Traced { location, .. } => {
389                Some(location)
390            }
391        }
392    }
393
394    fn oopsie_exit_code(&self) -> Option<core::num::NonZeroU8> {
395        // Surface the wrapped error's declared exit code, so a `#[oopsie]`
396        // origin's code survives `.welp()` wrapping. The source is type-erased,
397        // so this reaches it through the Provider API and yields `None` on
398        // stable — `Welp` keeps no exit code of its own to fall back to.
399        match &self.0 {
400            WelpRepr::Sourced { source, .. } => crate::__private::source_exit_code(&**source),
401            WelpRepr::Traced { .. } => None,
402        }
403    }
404}
405
406/// Extension trait on [`Result`] for attaching a string message that produces
407/// a [`Welp`].
408///
409/// Bring this into scope via `use oopsie::prelude::*` (or `use
410/// oopsie_core::WelpResultExt`).
411///
412/// ```
413/// use oopsie_core::{Welp, WelpResultExt as _};
414///
415/// fn read(path: &str) -> Result<String, Welp> {
416///     std::fs::read_to_string(path).welp_context("could not read")
417/// }
418/// ```
419pub trait WelpResultExt<T, E>: Sized {
420    /// Convert the error into a [`Welp`] with no user message — the kind of
421    /// message-free `?`-style conversion `anyhow`/`eyre` give you for free. The
422    /// resulting `Welp`'s [`Display`](std::fmt::Display) delegates to the
423    /// source's, and the source stays reachable through
424    /// [`Error::source`](std::error::Error::source). Reach for
425    /// [`welp_context`](Self::welp_context) instead when a string adds
426    /// information the source doesn't already carry.
427    fn welp(self) -> Result<T, Welp>;
428
429    /// Wrap the error in a [`Welp`] with the given message.
430    ///
431    /// Note: the `message` argument is evaluated at the call site per Rust's
432    /// eager-argument rules — `welp_context(format!(...))` allocates the
433    /// formatted `String` even on the `Ok` branch. Use [`with_welp_context`]
434    /// when the message is built from a non-trivial expression that should
435    /// only run on `Err`.
436    ///
437    /// [`with_welp_context`]: Self::with_welp_context
438    fn welp_context(self, message: impl Into<String>) -> Result<T, Welp>;
439
440    /// Wrap the error in a [`Welp`] with a lazily-built message. The closure
441    /// runs only on the `Err` branch and receives a shared reference to the
442    /// source error.
443    fn with_welp_context<S, F>(self, f: F) -> Result<T, Welp>
444    where
445        S: Into<String>,
446        F: FnOnce(&E) -> S;
447}
448
449impl<T, E> WelpResultExt<T, E> for Result<T, E>
450where
451    E: StdError + Send + Sync + 'static,
452{
453    #[inline]
454    #[track_caller]
455    fn welp(self) -> Result<T, Welp> {
456        // Build directly (no `map_err` closure) so `Welp::from_error`'s
457        // `#[track_caller]` reports the `.welp` call site.
458        match self {
459            Ok(value) => Ok(value),
460            Err(e) => Err(Welp::from_error(e)),
461        }
462    }
463
464    #[inline]
465    #[track_caller]
466    fn welp_context(self, message: impl Into<String>) -> Result<T, Welp> {
467        // Build directly (no `map_err` closure) so `Welp::wrap`'s
468        // `#[track_caller]` reports the `.welp_context` call site.
469        match self {
470            Ok(value) => Ok(value),
471            Err(e) => Err(Welp::wrap(e, message)),
472        }
473    }
474
475    #[inline]
476    #[track_caller]
477    fn with_welp_context<S, F>(self, f: F) -> Result<T, Welp>
478    where
479        S: Into<String>,
480        F: FnOnce(&E) -> S,
481    {
482        match self {
483            Ok(value) => Ok(value),
484            Err(e) => {
485                let msg = f(&e);
486                Err(Welp::wrap(e, msg))
487            }
488        }
489    }
490}
491
492/// Extension trait on [`Option`] for converting `None` into a [`Welp`].
493///
494/// Bring this into scope via `use oopsie::prelude::*` (or `use
495/// oopsie_core::WelpOptionExt`).
496///
497/// ```
498/// use oopsie_core::{Welp, WelpOptionExt as _};
499///
500/// fn first_word(s: &str) -> Result<&str, Welp> {
501///     s.split_whitespace().next().welp_context("string is empty")
502/// }
503/// ```
504pub trait WelpOptionExt<T>: Sized {
505    /// Convert `None` into a [`Welp`] with the given message.
506    ///
507    /// Note: the `message` argument is evaluated at the call site per Rust's
508    /// eager-argument rules — `welp_context(format!(...))` allocates the
509    /// formatted `String` even on the `Some` branch. Use [`with_welp_context`]
510    /// when the message is built from a non-trivial expression that should
511    /// only run on `None`.
512    ///
513    /// [`with_welp_context`]: Self::with_welp_context
514    fn welp_context(self, message: impl Into<String>) -> Result<T, Welp>;
515
516    /// Convert `None` into a [`Welp`] with a lazily-built message. The
517    /// closure runs only on the `None` branch.
518    fn with_welp_context<S, F>(self, f: F) -> Result<T, Welp>
519    where
520        S: Into<String>,
521        F: FnOnce() -> S;
522}
523
524impl<T> WelpOptionExt<T> for Option<T> {
525    #[inline]
526    #[track_caller]
527    fn welp_context(self, message: impl Into<String>) -> Result<T, Welp> {
528        // Build directly (no `ok_or_else` closure) so `Welp::new`'s
529        // `#[track_caller]` reports the `.welp_context` call site.
530        match self {
531            Some(value) => Ok(value),
532            None => Err(Welp::new(message)),
533        }
534    }
535
536    #[inline]
537    #[track_caller]
538    fn with_welp_context<S, F>(self, f: F) -> Result<T, Welp>
539    where
540        S: Into<String>,
541        F: FnOnce() -> S,
542    {
543        match self {
544            Some(value) => Ok(value),
545            None => Err(Welp::new(f())),
546        }
547    }
548}
549
550#[cfg(test)]
551mod tests {
552    use super::*;
553    use crate::{RustBacktrace, with_rust_backtrace_override};
554
555    #[cfg(feature = "tracing")]
556    fn with_error_subscriber<R>(f: impl FnOnce() -> R) -> R {
557        use tracing_subscriber::prelude::*;
558        let subscriber = tracing_subscriber::registry().with(tracing_error::ErrorLayer::default());
559        tracing::subscriber::with_default(subscriber, f)
560    }
561
562    #[test]
563    fn new_captures_message() {
564        let err = Welp::new("hello");
565        assert_eq!(err.message(), Some("hello"));
566        assert_eq!(err.to_string(), "hello");
567    }
568
569    #[test]
570    fn new_accepts_string_and_str() {
571        let _: Welp = Welp::new("static");
572        let _: Welp = Welp::new(String::from("owned"));
573        let _: Welp = Welp::new(format!("formatted {}", 1));
574    }
575
576    #[test]
577    fn new_captures_traces() {
578        let err = Welp::new("oops");
579        assert!(err.oopsie_backtrace().is_some());
580        assert!(err.oopsie_spantrace().is_some());
581    }
582
583    #[test]
584    fn new_has_no_source() {
585        let err = Welp::new("oops");
586        assert!(StdError::source(&err).is_none());
587    }
588
589    #[test]
590    fn captures_location_at_call_site() {
591        let new_line = line!() + 1;
592        let traced = Welp::new("solo");
593        let loc = traced.oopsie_location().expect("traced location");
594        assert!(loc.file().ends_with("welp.rs"));
595        assert_eq!(loc.line(), new_line);
596
597        let wrap_line = line!() + 1;
598        let sourced = Welp::wrap(std::io::Error::other("x"), "outer");
599        let loc = sourced.oopsie_location().expect("sourced location");
600        assert_eq!(loc.line(), wrap_line);
601    }
602
603    #[test]
604    fn wrap_carries_source() {
605        let inner = std::io::Error::other("disk full");
606        let err = Welp::wrap(inner, "could not write");
607        assert_eq!(err.to_string(), "could not write");
608        let source = StdError::source(&err).expect("source missing");
609        assert_eq!(source.to_string(), "disk full");
610    }
611
612    #[test]
613    fn wrap_captures_traces_at_wrap_site() {
614        let err = Welp::wrap(std::io::Error::other("x"), "msg");
615        assert!(err.oopsie_backtrace().is_some());
616        assert!(err.oopsie_spantrace().is_some());
617    }
618
619    #[test]
620    fn wrap_boxed_carries_source() {
621        let boxed: BoxError = Box::new(std::io::Error::other("disk full"));
622        let err = Welp::wrap_boxed(boxed, "could not write");
623        assert_eq!(err.to_string(), "could not write");
624        assert_eq!(
625            StdError::source(&err).expect("source missing").to_string(),
626            "disk full"
627        );
628    }
629
630    #[test]
631    fn wrap_boxed_captures_location_at_call_site() {
632        let boxed: BoxError = Box::new(std::io::Error::other("x"));
633        let wrap_line = line!() + 1;
634        let err = Welp::wrap_boxed(boxed, "outer");
635        let loc = err.oopsie_location().expect("wrap_boxed location");
636        assert!(loc.file().ends_with("welp.rs"));
637        assert_eq!(loc.line(), wrap_line);
638    }
639
640    #[test]
641    fn wrap_boxed_captures_traces_at_wrap_site() {
642        let boxed: BoxError = Box::new(std::io::Error::other("x"));
643        let err = Welp::wrap_boxed(boxed, "msg");
644        assert!(err.oopsie_backtrace().is_some());
645        assert!(err.oopsie_spantrace().is_some());
646    }
647
648    #[test]
649    fn from_error_delegates_display_to_source() {
650        let err = Welp::from_error(std::io::Error::other("disk full"));
651        assert_eq!(err.to_string(), "disk full");
652        assert!(err.message().is_none());
653        let source = StdError::source(&err).expect("source missing");
654        assert_eq!(source.to_string(), "disk full");
655    }
656
657    #[test]
658    fn from_error_source_downcasts_to_original() {
659        let err = Welp::from_error(std::io::Error::new(std::io::ErrorKind::NotFound, "missing"));
660        let io = StdError::source(&err)
661            .and_then(|s| s.downcast_ref::<std::io::Error>())
662            .expect("source is the original io::Error");
663        assert_eq!(io.kind(), std::io::ErrorKind::NotFound);
664    }
665
666    #[test]
667    fn welp_on_ok_is_passthrough() {
668        let r: Result<i32, std::io::Error> = Ok(42);
669        assert_eq!(r.welp().unwrap(), 42);
670    }
671
672    #[test]
673    fn welp_on_err_delegates_display_and_keeps_source() {
674        let r: Result<i32, std::io::Error> = Err(std::io::Error::other("io fail"));
675        let err = r.welp().unwrap_err();
676        assert_eq!(err.to_string(), "io fail");
677        assert!(err.message().is_none());
678        assert_eq!(StdError::source(&err).unwrap().to_string(), "io fail");
679    }
680
681    #[test]
682    fn welp_captures_location_at_call_site() {
683        let call_line = line!() + 2;
684        let r: Result<i32, std::io::Error> = Err(std::io::Error::other("x"));
685        let err = r.welp().unwrap_err();
686        let loc = err.oopsie_location().expect("welp location");
687        assert!(loc.file().ends_with("welp.rs"));
688        assert_eq!(loc.line(), call_line);
689    }
690
691    #[cfg(feature = "tracing")]
692    #[test]
693    fn welp_extracts_origin_trace_from_diagnostic_source() {
694        // The inner traces must actually capture (live span + backtraces
695        // enabled), otherwise the skip-empty fallback surfaces the wrap-site
696        // traces and the ptr-eq checks below are vacuous.
697        let outer = with_rust_backtrace_override(RustBacktrace::Enabled, || {
698            with_error_subscriber(|| {
699                let _g = tracing::info_span!("origin").entered();
700                let inner: Result<(), Welp> = Err(Welp::new("inner"));
701                inner.welp().unwrap_err()
702            })
703        });
704        let bt = outer.oopsie_backtrace().expect("backtrace");
705        assert!(!bt.frames().is_empty());
706        #[cfg(feature = "unstable-error-generic-member-access")]
707        {
708            let source = StdError::source(&outer)
709                .and_then(|s| s.downcast_ref::<Welp>())
710                .expect("source is the inner Welp");
711            let inner_bt = source.oopsie_backtrace().expect("inner backtrace");
712            assert!(
713                std::ptr::eq(
714                    std::ptr::from_ref::<Backtrace>(bt),
715                    std::ptr::from_ref::<Backtrace>(inner_bt)
716                ),
717                "outer should surface the source's (origin-most) backtrace"
718            );
719        }
720    }
721
722    #[test]
723    fn welp_context_on_ok_is_passthrough() {
724        let r: Result<i32, std::io::Error> = Ok(42);
725        let mapped: Result<i32, Welp> = r.welp_context("nope");
726        assert_eq!(mapped.unwrap(), 42);
727    }
728
729    #[test]
730    fn welp_context_on_err_produces_sourced_welp() {
731        let r: Result<i32, std::io::Error> = Err(std::io::Error::other("io fail"));
732        let err = r.welp_context("read failed").unwrap_err();
733        assert_eq!(err.to_string(), "read failed");
734        assert_eq!(StdError::source(&err).unwrap().to_string(), "io fail");
735    }
736
737    #[test]
738    fn with_welp_context_does_not_run_closure_on_ok() {
739        let r: Result<i32, std::io::Error> = Ok(1);
740        let mapped: Result<i32, Welp> = r.with_welp_context(|_| -> String {
741            panic!("should not run");
742        });
743        mapped.unwrap();
744    }
745
746    #[test]
747    fn with_welp_context_runs_closure_on_err() {
748        let r: Result<i32, std::io::Error> = Err(std::io::Error::other("inner"));
749        let err = r
750            .with_welp_context(|e| format!("wrapped: {e}"))
751            .unwrap_err();
752        assert_eq!(err.to_string(), "wrapped: inner");
753    }
754
755    #[test]
756    fn option_welp_context_on_some_is_passthrough() {
757        let opt: Option<i32> = Some(7);
758        assert_eq!(opt.welp_context("missing").unwrap(), 7);
759    }
760
761    #[test]
762    fn option_welp_context_on_none_produces_traced_welp() {
763        let opt: Option<i32> = None;
764        let err = opt.welp_context("missing").unwrap_err();
765        assert_eq!(err.to_string(), "missing");
766        assert!(err.oopsie_backtrace().is_some());
767        assert!(StdError::source(&err).is_none());
768    }
769
770    #[test]
771    fn option_with_welp_context_does_not_run_closure_on_some() {
772        let opt: Option<i32> = Some(1);
773        let mapped: Result<i32, Welp> =
774            opt.with_welp_context(|| -> String { panic!("should not run") });
775        mapped.unwrap();
776    }
777
778    #[test]
779    fn option_with_welp_context_runs_closure_on_none() {
780        let opt: Option<i32> = None;
781        let err = opt
782            .with_welp_context(|| format!("missing key {}", "host"))
783            .unwrap_err();
784        assert_eq!(err.to_string(), "missing key host");
785    }
786
787    #[test]
788    fn chain_walks_welp_and_its_sources_self_first() {
789        use crate::ErrorChainExt as _;
790
791        let err = Welp::wrap(
792            Welp::wrap(std::io::Error::other("disk full"), "mid"),
793            "outer",
794        );
795        let messages: Vec<String> = err.chain().map(ToString::to_string).collect();
796        assert_eq!(messages, ["outer", "mid", "disk full"]);
797        assert_eq!(err.root_cause().to_string(), "disk full");
798    }
799
800    #[test]
801    fn downcast_ref_hits_matching_direct_source() {
802        let err = Welp::wrap(std::io::Error::other("disk full"), "outer");
803        let io = err.downcast_ref::<std::io::Error>().expect("source is io");
804        assert_eq!(io.to_string(), "disk full");
805    }
806
807    #[test]
808    fn downcast_ref_misses_on_wrong_type_and_traced() {
809        let sourced = Welp::wrap(std::io::Error::other("x"), "outer");
810        assert!(sourced.downcast_ref::<std::fmt::Error>().is_none());
811
812        let traced = Welp::new("solo");
813        assert!(traced.downcast_ref::<std::io::Error>().is_none());
814    }
815
816    #[test]
817    fn downcast_mut_borrows_matching_source() {
818        let mut err = Welp::wrap(std::io::Error::other("x"), "outer");
819        assert!(err.downcast_mut::<std::io::Error>().is_some());
820        assert!(err.downcast_mut::<std::fmt::Error>().is_none());
821    }
822
823    #[test]
824    fn downcast_recovers_matching_source() {
825        let err = Welp::wrap(
826            std::io::Error::new(std::io::ErrorKind::NotFound, "missing"),
827            "outer",
828        );
829        let io: std::io::Error = err.downcast().expect("source is io");
830        assert_eq!(io.kind(), std::io::ErrorKind::NotFound);
831    }
832
833    #[test]
834    fn downcast_only_targets_the_direct_source() {
835        // The direct source is the middle `Welp`; the io::Error is one layer
836        // deeper and must not be reached by a direct downcast.
837        let err = Welp::wrap(Welp::wrap(std::io::Error::other("x"), "mid"), "outer");
838        let err = err
839            .downcast::<std::io::Error>()
840            .expect_err("io is not the direct source");
841        assert!(err.downcast_ref::<Welp>().is_some());
842    }
843
844    #[test]
845    fn downcast_err_preserves_message_location_and_traces() {
846        let line = line!() + 1;
847        let err = Welp::wrap(std::io::Error::other("inner"), "outer");
848        let recovered = err
849            .downcast::<std::fmt::Error>()
850            .expect_err("wrong target type");
851        assert_eq!(recovered.message(), Some("outer"));
852        assert_eq!(recovered.to_string(), "outer");
853        let loc = recovered.oopsie_location().expect("location preserved");
854        assert_eq!(loc.line(), line);
855        assert!(recovered.oopsie_backtrace().is_some());
856        // The original source survives the failed round-trip.
857        assert_eq!(StdError::source(&recovered).unwrap().to_string(), "inner");
858    }
859
860    #[test]
861    fn downcast_on_traced_returns_self() {
862        let err = Welp::new("solo");
863        let recovered = err
864            .downcast::<std::io::Error>()
865            .expect_err("traced has no source");
866        assert_eq!(recovered.message(), Some("solo"));
867    }
868
869    #[test]
870    fn debug_includes_source_for_sourced() {
871        let err = Welp::wrap(std::io::Error::other("inner"), "outer");
872        let dbg = format!("{err:?}");
873        assert!(dbg.contains("outer"));
874        assert!(dbg.contains("inner"));
875    }
876
877    #[test]
878    fn debug_omits_source_for_traced() {
879        let err = Welp::new("solo");
880        let dbg = format!("{err:?}");
881        assert!(dbg.contains("solo"));
882        assert!(!dbg.contains("source"));
883    }
884
885    #[cfg(all(feature = "unstable-error-generic-member-access", feature = "tracing"))]
886    #[test]
887    fn traced_provides_traces_via_request_ref() {
888        // Only captured traces are provided, so capture must succeed here.
889        let err = with_rust_backtrace_override(RustBacktrace::Enabled, || {
890            with_error_subscriber(|| {
891                let _g = tracing::info_span!("solo").entered();
892                Welp::new("solo")
893            })
894        });
895        assert!(core::error::request_ref::<Backtrace>(&err).is_some());
896        assert!(core::error::request_ref::<SpanTrace>(&err).is_some());
897    }
898
899    #[cfg(feature = "unstable-error-generic-member-access")]
900    #[test]
901    fn empty_traces_are_not_provided() {
902        // No subscriber and backtraces disabled → both traces are empty and
903        // must be withheld from the (first-wins) provider chain. Without the
904        // tracing feature the span-trace stub is never captured, so its slot
905        // stays empty too.
906        let err = with_rust_backtrace_override(RustBacktrace::Disabled, || Welp::new("solo"));
907        assert!(core::error::request_ref::<Backtrace>(&err).is_none());
908        assert!(core::error::request_ref::<SpanTrace>(&err).is_none());
909    }
910
911    #[cfg(feature = "tracing")]
912    #[test]
913    fn sourced_recovers_traces_from_diagnostic_source() {
914        // The inner traces must actually capture something (live span +
915        // backtraces enabled), otherwise the skip-empty filters surface the
916        // wrap-site traces instead and the ptr-eq assertions are meaningless.
917        let outer = with_rust_backtrace_override(RustBacktrace::Enabled, || {
918            with_error_subscriber(|| {
919                let _g = tracing::info_span!("origin").entered();
920                Welp::wrap(Welp::new("inner"), "outer")
921            })
922        });
923        let bt = outer.oopsie_backtrace().expect("backtrace");
924        let st = outer.oopsie_spantrace().expect("spantrace");
925        // On nightly the Provider API is available, so the source's own
926        // (deeper) trace must win over the wrap-site capture.
927        #[cfg(feature = "unstable-error-generic-member-access")]
928        {
929            let source = StdError::source(&outer)
930                .and_then(|s| s.downcast_ref::<Welp>())
931                .expect("source is the inner Welp");
932            let inner_bt = source.oopsie_backtrace().expect("inner backtrace");
933            let inner_st = source.oopsie_spantrace().expect("inner spantrace");
934            assert!(
935                std::ptr::eq(
936                    std::ptr::from_ref::<Backtrace>(bt),
937                    std::ptr::from_ref::<Backtrace>(inner_bt)
938                ),
939                "outer should surface the source's backtrace, not the wrap-site one"
940            );
941            assert!(
942                std::ptr::eq(
943                    std::ptr::from_ref::<SpanTrace>(st),
944                    std::ptr::from_ref::<SpanTrace>(inner_st)
945                ),
946                "outer should surface the source's spantrace, not the wrap-site one"
947            );
948        }
949        #[cfg(not(feature = "unstable-error-generic-member-access"))]
950        {
951            let _ = (bt, st);
952        }
953    }
954
955    #[cfg(all(feature = "unstable-error-generic-member-access", feature = "tracing"))]
956    #[test]
957    fn sourced_forwards_source_provide() {
958        // Only captured traces are provided, so capture must succeed here.
959        let outer = with_rust_backtrace_override(RustBacktrace::Enabled, || {
960            with_error_subscriber(|| {
961                let _g = tracing::info_span!("origin").entered();
962                Welp::wrap(Welp::new("inner"), "outer")
963            })
964        });
965        assert!(core::error::request_ref::<Backtrace>(&outer).is_some());
966        assert!(core::error::request_ref::<SpanTrace>(&outer).is_some());
967    }
968
969    #[test]
970    fn sourced_falls_back_to_own_traces_for_foreign_source() {
971        let outer = Welp::wrap(std::io::Error::other("x"), "outer");
972        assert!(outer.oopsie_backtrace().is_some());
973        assert!(outer.oopsie_spantrace().is_some());
974    }
975
976    #[cfg(feature = "tracing")]
977    #[test]
978    fn empty_source_spantrace_does_not_shadow_wrap_site_capture() {
979        let inner = Welp::new("inner"); // no subscriber → empty spantrace
980        assert!(!inner.oopsie_spantrace().unwrap().is_captured());
981        let outer = with_error_subscriber(|| {
982            let _g = tracing::info_span!("wrap_site").entered();
983            Welp::wrap(inner, "outer")
984        });
985        assert!(outer.oopsie_spantrace().unwrap().is_captured());
986    }
987
988    #[test]
989    fn empty_source_backtrace_does_not_shadow_wrap_site_capture() {
990        let inner = with_rust_backtrace_override(RustBacktrace::Disabled, || Welp::new("inner"));
991        let outer =
992            with_rust_backtrace_override(RustBacktrace::Enabled, || Welp::wrap(inner, "outer"));
993        assert!(!outer.oopsie_backtrace().unwrap().frames().is_empty());
994    }
995
996    // Reaching the source's trace requires the Provider API: `wrap` is
997    // generic, so no construction-time extraction is possible (see `wrap`),
998    // and on stable the accessors cannot descend into the type-erased source.
999    #[cfg(all(feature = "unstable-error-generic-member-access", feature = "tracing"))]
1000    #[test]
1001    fn wrap_surfaces_captured_source_spantrace() {
1002        let inner = with_error_subscriber(|| {
1003            let _g = tracing::info_span!("origin").entered();
1004            Welp::new("inner")
1005        });
1006        assert!(inner.oopsie_spantrace().unwrap().is_captured());
1007        let outer = Welp::wrap(inner, "outer"); // no live span here
1008        assert!(outer.oopsie_spantrace().unwrap().is_captured());
1009    }
1010
1011    // Compile-time guarantees about `Welp`'s shape.
1012    const _: () = {
1013        const fn assert_send<T: Send>() {}
1014        const fn assert_sync<T: Sync>() {}
1015        const fn assert_static<T: 'static>() {}
1016        assert_send::<Welp>();
1017        assert_sync::<Welp>();
1018        assert_static::<Welp>();
1019    };
1020
1021    #[test]
1022    fn welp_size_is_pinned() {
1023        // `Sourced` dominates: fat `Box<str>` message + fat `Box<dyn Error>`
1024        // source + thin boxed traces + the inline `&'static Location` — six
1025        // words, with no spare niche for the discriminant. The message-free form
1026        // reuses `Sourced` with a `None` message — the `Box<str>` null niche
1027        // absorbs the `Option` tag — so it adds no width.
1028        assert_eq!(std::mem::size_of::<Welp>(), 48);
1029    }
1030}