Skip to main content

oopsie_core/
welp.rs

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