Skip to main content

deep_time/
an_err.rs

1use crate::LiteStr;
2use core::fmt;
3use core::fmt::Write;
4use core::panic::Location;
5
6/// Iterator over the error trace levels of an [`AnErr`].
7///
8/// Yields `(kind, location, reason)` tuples **from most recent context to oldest**
9/// (reverse chronological order). Only valid levels are returned.
10///
11/// The `reason` field is `Some` if a non-empty reason was supplied for that level,
12/// otherwise `None`.
13#[derive(Debug, Clone)]
14pub struct TraceIter<'a, K, const DEPTH: usize, const REASON_LEN: usize>
15where
16    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
17{
18    error: &'a AnErr<K, DEPTH, REASON_LEN>,
19    pos: usize,
20}
21
22impl<'a, K, const DEPTH: usize, const REASON_LEN: usize> Iterator
23    for TraceIter<'a, K, DEPTH, REASON_LEN>
24where
25    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
26{
27    type Item = (
28        K,
29        &'static Location<'static>,
30        Option<&'a LiteStr<REASON_LEN>>,
31    );
32
33    fn next(&mut self) -> Option<Self::Item> {
34        if self.pos >= self.error.len as usize {
35            return None;
36        }
37
38        let idx = (self.error.len as usize) - 1 - self.pos;
39        let kind = self.error.kinds[idx]?;
40        let loc = self.error.locations[idx]?;
41        let reason = self.error.reasons[idx].as_ref();
42
43        self.pos += 1;
44        Some((kind, loc, reason))
45    }
46
47    fn size_hint(&self) -> (usize, Option<usize>) {
48        let remaining = (self.error.len as usize).saturating_sub(self.pos);
49        (remaining, Some(remaining))
50    }
51}
52
53impl<'a, K, const DEPTH: usize, const REASON_LEN: usize> ExactSizeIterator
54    for TraceIter<'a, K, DEPTH, REASON_LEN>
55where
56    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
57{
58}
59
60/// A compact, `Copy`, zero-allocation error type that records a parallel stack
61/// of error kinds, source locations, and per-level human-readable reasons.
62///
63/// `AnErr` stores up to `DEPTH` levels of error context. Each level contains:
64/// - an error kind of type `K`,
65/// - the source location where the level was created,
66/// - an optional reason specific to that level (`LiteStr<REASON_LEN>`).
67///
68/// The kind enum provides the general error category while the per-level reason
69/// carries concrete details (e.g. a bad value, file path, token, etc.).
70///
71/// The type implements `Copy` and performs no heap allocation. Default memory
72/// footprint is small and fully controllable via the generic parameters.
73///
74/// ## Type Parameters
75///
76/// - `K`: Error kind type. Must implement `Copy + Clone + Debug + PartialEq + Eq`.
77/// - `DEPTH`: Maximum number of context levels (default `3`). Additional context
78///   beyond this limit is silently discarded.
79/// - `REASON_LEN`: Maximum length of each individual reason in bytes
80///   (default `29`). Longer reasons are silently truncated.
81///
82/// ## Construction
83///
84/// ```rust,ignore
85/// use an_error::{AnErr, an_err};
86///
87/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
88/// pub enum MyKind {
89///     Parse,
90///     Io,
91///     Validation,
92/// }
93///
94/// pub type MyError = AnErr<MyKind, 4, 64>;
95///
96/// fn parse() -> Result<(), MyError> {
97///     Err(an_err!(MyKind::Parse, "unexpected token at byte {}", 42))
98/// }
99///
100/// fn load(path: &str) -> Result<(), MyError> {
101///     let inner = parse()
102///         .map_err(|e| an_err!(MyKind::Io, "while loading config from {}", path => e))?;
103///     Ok(())
104/// }
105/// ```
106///
107/// All constructors and the `context` method capture the call site via `#[track_caller]`.
108///
109/// ## Display
110///
111/// The `Display` implementation produces output of the following form:
112///
113/// ```text
114/// --
115/// • Trace (2 levels):
116///    1. Io    @ src/io.rs:42:10    while loading config from /etc/foo
117///    2. Parse @ src/parser.rs:17:5  unexpected token at byte 42
118/// ```
119///
120/// Each trace level shows its own reason (if present) immediately after the location.
121///
122/// ## Invariants
123///
124/// Maintained by all constructors and `context`:
125///
126/// - `len` is always in `1..=DEPTH`.
127/// - For every `i` in `0..len`, `kinds[i]` and `locations[i]` are `Some`.
128/// - `reasons[i]` is `Some` only if a non-empty reason was supplied for that level.
129///
130/// ## Accessing the stack
131///
132/// In addition to the top-level convenience methods (`kind()`, `location()`, `reason()`),
133/// you can access any level directly or iterate the entire trace.
134///
135/// ### Direct access
136///
137/// ```rust,ignore
138/// let top_kind     = err.kind();           // most recent
139/// let top_loc      = err.location();
140/// let top_reason   = err.reason();
141///
142/// let root_kind    = err.root_kind();      // original error
143/// let root_loc     = err.root_location();
144/// let root_reason  = err.root_reason();
145///
146/// if let Some((kind, loc, reason)) = err.get(1) {
147///     // second level (index 0 = top, index 1 = next, ...)
148/// }
149/// ```
150///
151/// ### Iterating with `trace()`
152///
153/// The most common way to walk the full stack is with [`trace`](Self::trace):
154///
155/// ```rust,ignore
156/// for (kind, location, reason) in err.trace() {
157///     println!("{:?} @ {}:{}", kind, location.file(), location.line());
158///
159///     if let Some(r) = reason {
160///         println!("    reason: {}", r);
161///     }
162/// }
163/// ```
164///
165/// - Iteration order is **most recent → oldest** (same order as `Display`).
166/// - The iterator implements `ExactSizeIterator`, so you can call `.len()`, use it in `for` loops, etc.
167/// - No allocation — it just borrows the `AnErr`.
168#[derive(Clone, Copy, PartialEq, Eq)]
169#[must_use = "this error should be handled or converted to a different type e.g `pub type DtErr = AnErr<MyError, 2, 49>;`"]
170pub struct AnErr<K, const DEPTH: usize = 3, const REASON_LEN: usize = 29>
171where
172    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
173{
174    /// Per-level reasons. Only the first `len` entries are valid.
175    /// `None` means no reason (or an empty reason) was provided for that level.
176    pub reasons: [Option<LiteStr<REASON_LEN>>; DEPTH],
177
178    /// Parallel stack of source locations.
179    /// Only the first `len` entries are valid.
180    pub locations: [Option<&'static Location<'static>>; DEPTH],
181
182    /// Parallel stack of error kinds (one per call-stack level).
183    /// Only the first `len` entries are valid.
184    pub kinds: [Option<K>; DEPTH],
185
186    /// Current depth of the error trace (1 = original error).
187    pub len: u8,
188}
189
190impl<K, const DEPTH: usize, const REASON_LEN: usize> AnErr<K, DEPTH, REASON_LEN>
191where
192    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
193{
194    /// Creates a new error with the given kind and no reason.
195    #[inline]
196    #[track_caller]
197    pub fn new(kind: K) -> Self {
198        let mut kinds = [None; DEPTH];
199        let mut locs = [None; DEPTH];
200        let reasons = [None; DEPTH];
201
202        kinds[0] = Some(kind);
203        locs[0] = Some(Location::caller());
204
205        Self {
206            kinds,
207            locations: locs,
208            reasons,
209            len: 1,
210        }
211    }
212
213    /// Creates a new error with the given kind and reason.
214    ///
215    /// If the reason is empty, it is stored as `None`.
216    #[inline]
217    #[track_caller]
218    pub fn with_reason(kind: K, reason: LiteStr<REASON_LEN>) -> Self {
219        let mut kinds = [None; DEPTH];
220        let mut locs = [None; DEPTH];
221        let mut reasons = [None; DEPTH];
222
223        kinds[0] = Some(kind);
224        locs[0] = Some(Location::caller());
225        reasons[0] = if reason.len() == 0 {
226            None
227        } else {
228            Some(reason)
229        };
230
231        Self {
232            kinds,
233            locations: locs,
234            reasons,
235            len: 1,
236        }
237    }
238
239    /// Creates a new error with the given kind and a formatted reason.
240    ///
241    /// The formatted string is truncated if it exceeds `REASON_LEN` bytes.
242    #[inline]
243    #[track_caller]
244    pub fn with_fmt(kind: K, args: core::fmt::Arguments<'_>) -> Self {
245        let mut kinds = [None; DEPTH];
246        let mut locs = [None; DEPTH];
247        let mut reasons = [None; DEPTH];
248
249        kinds[0] = Some(kind);
250        locs[0] = Some(Location::caller());
251        let mut reason = LiteStr::<REASON_LEN>::default();
252        let _ = write!(&mut reason, "{}", args);
253        reasons[0] = if reason.len() == 0 {
254            None
255        } else {
256            Some(reason)
257        };
258
259        Self {
260            kinds,
261            locations: locs,
262            reasons,
263            len: 1,
264        }
265    }
266
267    /// Returns the current depth of the error trace.
268    #[inline]
269    pub fn depth(&self) -> u8 {
270        self.len
271    }
272
273    /// Returns the most recent error kind (the top of the trace).
274    #[inline]
275    pub fn kind(&self) -> Option<K> {
276        if self.len == 0 {
277            None
278        } else {
279            let idx = (self.len as usize) - 1;
280            self.kinds[idx]
281        }
282    }
283
284    /// Appends a new context level and optional reason to this error.
285    ///
286    /// If `new_reason` is empty, no reason is stored for the new level.
287    /// If the maximum depth is already reached, the call is a no-op.
288    #[inline]
289    #[track_caller]
290    pub fn context(&mut self, kind: K, new_reason: LiteStr<REASON_LEN>) {
291        let idx = self.len as usize;
292        if idx < DEPTH {
293            self.reasons[idx] = if new_reason.len() == 0 {
294                None
295            } else {
296                Some(new_reason)
297            };
298            self.push(kind, Location::caller());
299        }
300    }
301
302    /// Appends a new context level with a formatted reason.
303    ///
304    /// Used internally by the `an_err!` macro. The formatted string is
305    /// truncated if it exceeds `REASON_LEN` bytes.
306    #[inline]
307    #[track_caller]
308    pub fn context_fmt(&mut self, kind: K, args: core::fmt::Arguments<'_>) {
309        let idx = self.len as usize;
310        if idx < DEPTH {
311            let mut reason = LiteStr::<REASON_LEN>::default();
312            let _ = write!(&mut reason, "{}", args);
313
314            self.reasons[idx] = if reason.len() == 0 {
315                None
316            } else {
317                Some(reason)
318            };
319            self.push(kind, Location::caller());
320        }
321    }
322
323    /// Returns an iterator over the error trace, from most recent context
324    /// down to the original error.
325    ///
326    /// Each item is `(kind, location, reason)`. The iterator borrows `self`
327    /// with zero copying.
328    pub fn trace(&self) -> TraceIter<'_, K, DEPTH, REASON_LEN> {
329        TraceIter {
330            error: self,
331            pos: 0,
332        }
333    }
334
335    #[inline]
336    fn push(&mut self, kind: K, loc: &'static Location<'static>) {
337        if (self.len as usize) < DEPTH {
338            let idx = self.len as usize;
339            self.kinds[idx] = Some(kind);
340            self.locations[idx] = Some(loc);
341            self.len += 1;
342        }
343    }
344
345    /// Returns the data for a specific level in the error trace.
346    ///
347    /// `index == 0` is the **most recent** context (top of the stack / newest `context!`).
348    /// `index == self.depth() - 1` is the **root** (original) error.
349    ///
350    /// Returns `None` if `index >= self.depth()`.
351    #[inline]
352    pub fn get(
353        &self,
354        index: usize,
355    ) -> Option<(K, &'static Location<'static>, Option<&LiteStr<REASON_LEN>>)> {
356        let depth = self.len as usize;
357        if index >= depth {
358            return None;
359        }
360        let arr_idx = depth - 1 - index; // 0 in array = root, so we reverse
361        Some((
362            self.kinds[arr_idx]?,
363            self.locations[arr_idx]?,
364            self.reasons[arr_idx].as_ref(),
365        ))
366    }
367
368    /// Returns the source location where the most recent error/context was created.
369    #[inline]
370    pub fn location(&self) -> Option<&'static Location<'static>> {
371        self.get(0).map(|(_, loc, _)| loc)
372    }
373
374    /// Returns the reason (if any) attached to the most recent error/context.
375    #[inline]
376    pub fn reason(&self) -> Option<&LiteStr<REASON_LEN>> {
377        self.get(0).and_then(|(_, _, r)| r)
378    }
379
380    /// Returns the original (root) error kind.
381    #[inline]
382    pub fn root_kind(&self) -> Option<K> {
383        (self.len > 0).then(|| self.kinds[0]).flatten()
384    }
385
386    /// Returns the source location of the original (root) error.
387    #[inline]
388    pub fn root_location(&self) -> Option<&'static Location<'static>> {
389        (self.len > 0).then(|| self.locations[0]).flatten()
390    }
391
392    /// Returns the reason (if any) attached to the root error.
393    #[inline]
394    pub fn root_reason(&self) -> Option<&LiteStr<REASON_LEN>> {
395        (self.len > 0).then(|| self.reasons[0].as_ref()).flatten()
396    }
397}
398
399impl<K, const DEPTH: usize, const REASON_LEN: usize> From<K> for AnErr<K, DEPTH, REASON_LEN>
400where
401    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
402{
403    /// Converts a kind into a new [`AnErr`] with no reason.
404    #[inline]
405    #[track_caller]
406    fn from(kind: K) -> Self {
407        Self::new(kind)
408    }
409}
410
411impl<K, const DEPTH: usize, const REASON_LEN: usize> core::fmt::Display
412    for AnErr<K, DEPTH, REASON_LEN>
413where
414    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
415{
416    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
417        writeln!(f)?;
418        writeln!(f, "--")?;
419        writeln!(f, "Error:")?;
420
421        for (i, (kind, loc, reason_opt)) in self.trace().enumerate() {
422            let num = i + 1;
423
424            write!(f, "  {:>2}. {:?}", num, kind)?;
425
426            if let Some(reason) = reason_opt {
427                if let Ok(s) = reason.as_str() {
428                    write!(f, ": {}", s)?;
429                } else {
430                    write!(f, ": <invalid ascii>")?;
431                }
432            }
433
434            writeln!(f, " @ {}:{}:{}", loc.file(), loc.line(), loc.column())?;
435        }
436
437        Ok(())
438    }
439}
440
441impl<K, const DEPTH: usize, const REASON_LEN: usize> fmt::Debug for AnErr<K, DEPTH, REASON_LEN>
442where
443    K: Copy + Clone + fmt::Debug + PartialEq + Eq,
444{
445    /// Debug prints the same clean, human-readable trace as Display.
446    /// This makes `unwrap()`, `dbg!()`, and panic messages readable instead of
447    /// dumping giant byte arrays.
448    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
449        fmt::Display::fmt(self, f)
450    }
451}
452
453impl<K, const DEPTH: usize, const REASON_LEN: usize> core::error::Error
454    for AnErr<K, DEPTH, REASON_LEN>
455where
456    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
457{
458}
459
460/// Ergonomic constructor and chaining macro for [`AnErr`].
461///
462/// ## Forms
463///
464/// | Form                                              | Equivalent to                                      |
465/// |---------------------------------------------------|----------------------------------------------------|
466/// | `an_err!(Kind)`                                   | `AnErr::new(Kind)`                               |
467/// | `an_err!(Kind, "reason")`                         | `AnErr::with_fmt(Kind, ...)`                     |
468/// | `an_err!(Kind, "reason {}", arg, ...)`            | `AnErr::with_fmt(Kind, ...)`                     |
469/// | `an_err!(Kind, "reason" => inner)`                | `inner.context(Kind, ...)`                         |
470/// | `an_err!(Kind, "reason {}", arg => inner)`        | `inner.context(Kind, ...)`                         |
471///
472/// All forms capture the call site via `#[track_caller]`.
473#[macro_export]
474macro_rules! an_err {
475    // New error, no reason
476    ($kind:expr) => {
477        $crate::AnErr::new($kind)
478    };
479
480    // Chaining form (must appear before the new-error form)
481    ($kind:expr, $fmt:literal $(, $arg:expr)* => $inner:expr $(,)?) => {{
482        let mut e = $inner;
483        e.context_fmt(
484            $kind,
485            format_args!($fmt $(, $arg)*)
486        );
487        e
488    }};
489
490    // New error with reason (literal or formatted)
491    ($kind:expr, $fmt:literal $(, $arg:expr)* $(,)?) => {
492        $crate::AnErr::with_fmt($kind, format_args!($fmt $(, $arg)*))
493    };
494}
495
496#[cfg(feature = "wire")]
497impl<K, const DEPTH: usize, const REASON_LEN: usize> AnErr<K, DEPTH, REASON_LEN>
498where
499    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
500{
501    /// Serialize this error into a fixed-size byte buffer for transmission.
502    ///
503    /// The caller must provide a buffer that is at least `Self::WIRE_SIZE::<PATH_LEN>()` bytes long.
504    /// Returns the number of bytes actually written (always the same for a given `PATH_LEN`).
505    ///
506    /// Recommended usage:
507    /// ```rust,ignore
508    /// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
509    /// #[repr(u8)]   // or #[repr(u16)] for >256 variants
510    /// pub enum MyKind { ... }
511    ///
512    /// let mut buf = [0u8; AnErr::<MyKind, 3, 29>::wire_size::<80>()];
513    /// let written = my_error.to_wire_bytes::<80>(|k| k as u16, &mut buf);
514    /// let packet = &buf[..written];
515    /// ```
516    pub fn to_wire_bytes<const PATH_LEN: usize>(
517        &self,
518        kind_to_u16: impl Fn(K) -> u16,
519        buf: &mut [u8],
520    ) -> Result<usize, ()> {
521        let needed = Self::wire_size::<PATH_LEN>();
522        if buf.len() < needed {
523            return Err(());
524        }
525
526        let mut offset = 0;
527
528        // Header
529        buf[offset] = 1; // wire format version
530        offset += 1;
531        buf[offset] = self.len;
532        offset += 1;
533
534        for i in 0..DEPTH {
535            if i < self.len as usize {
536                // 1. Kind as u16
537                let kind_val = self.kinds[i].map_or(0, &kind_to_u16);
538                buf[offset..offset + 2].copy_from_slice(&kind_val.to_le_bytes());
539                offset += 2;
540
541                // 2. Reason
542                let defaultx = LiteStr::default();
543                let reason = self.reasons[i].as_ref().unwrap_or(&defaultx);
544                buf[offset..offset + REASON_LEN].copy_from_slice(&reason.to_bytes());
545                offset += REASON_LEN;
546
547                // 3. Location
548                if let Some(loc) = self.locations[i] {
549                    let file = LiteStr::<PATH_LEN>::from_str(loc.file());
550                    buf[offset..offset + PATH_LEN].copy_from_slice(&file.to_bytes());
551                    offset += PATH_LEN;
552
553                    buf[offset..offset + 4].copy_from_slice(&loc.line().to_le_bytes());
554                    offset += 4;
555                    buf[offset..offset + 4].copy_from_slice(&loc.column().to_le_bytes());
556                    offset += 4;
557                } else {
558                    offset += PATH_LEN + 8; // pad
559                }
560            } else {
561                // pad remaining levels
562                offset += 2 + REASON_LEN + PATH_LEN + 8;
563            }
564        }
565
566        Ok(needed)
567    }
568
569    /// Compile-time size of the wire representation for a given `PATH_LEN`.
570
571    pub const fn wire_size<const PATH_LEN: usize>() -> usize {
572        2 + DEPTH * (2 + REASON_LEN + PATH_LEN + 8)
573    }
574}
575
576/// Portable location for wire transmission.
577#[cfg(feature = "wire")]
578#[derive(Debug, Clone, Copy, PartialEq, Eq)]
579pub struct WireLocation<const N: usize> {
580    pub file: LiteStr<N>,
581    pub line: u32,
582    pub column: u32,
583}
584
585/// Fully portable, zero-allocation error for transmission/reception.
586#[cfg(feature = "wire")]
587#[derive(Debug, Clone, Copy, PartialEq, Eq)]
588pub struct WireErr<const DEPTH: usize = 3, const REASON_LEN: usize = 29, const FILE_LEN: usize = 80>
589{
590    pub len: u8,
591    pub kinds: [Option<u16>; DEPTH],
592    pub reasons: [Option<LiteStr<REASON_LEN>>; DEPTH],
593    pub locations: [Option<WireLocation<FILE_LEN>>; DEPTH],
594}
595
596#[cfg(feature = "wire")]
597impl<const DEPTH: usize, const REASON_LEN: usize, const FILE_LEN: usize>
598    WireErr<DEPTH, REASON_LEN, FILE_LEN>
599{
600    /// Fixed wire size (exactly matches `AnErr::wire_size::<FILE_LEN>()`).
601    pub const fn wire_size() -> usize {
602        const fn compute_size<const D: usize, const R: usize, const F: usize>() -> usize {
603            2 + D * (2 + R + F + 8)
604        }
605        compute_size::<DEPTH, REASON_LEN, FILE_LEN>()
606    }
607
608    /// Parse a wire buffer from `AnErr` into a `WireErr`.
609    ///
610    /// Returns `None` on any corruption, wrong size, unknown version,
611    /// or invalid `LiteStr` data.
612    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
613        if bytes.len() != Self::wire_size() {
614            return None;
615        }
616
617        let mut offset = 0;
618
619        // Version
620        let version = bytes[offset];
621        if version != 1 {
622            return None; // unknown wire format
623        }
624        offset += 1;
625
626        let len = bytes[offset];
627        if len == 0 || len as usize > DEPTH {
628            return None;
629        }
630        offset += 1;
631
632        let mut kinds = [None; DEPTH];
633        let mut reasons = [None; DEPTH];
634        let mut locations = [None; DEPTH];
635
636        for i in 0..(len as usize) {
637            // kind (u16)
638            let kind_bytes = <[u8; 2]>::try_from(&bytes[offset..offset + 2]).ok()?;
639            kinds[i] = Some(u16::from_le_bytes(kind_bytes));
640            offset += 2;
641
642            // reason
643            let reason_bytes = &bytes[offset..offset + REASON_LEN];
644            reasons[i] = LiteStr::from_bytes(reason_bytes).ok();
645            offset += REASON_LEN;
646
647            // location
648            let file_bytes = &bytes[offset..offset + FILE_LEN];
649            let file = LiteStr::from_bytes(file_bytes).ok()?;
650
651            offset += FILE_LEN;
652
653            let line_bytes = <[u8; 4]>::try_from(&bytes[offset..offset + 4]).ok()?;
654            let line = u32::from_le_bytes(line_bytes);
655            offset += 4;
656
657            let col_bytes = <[u8; 4]>::try_from(&bytes[offset..offset + 4]).ok()?;
658            let column = u32::from_le_bytes(col_bytes);
659            offset += 4;
660
661            locations[i] = Some(WireLocation { file, line, column });
662        }
663
664        // remaining bytes are padding (we already checked total length)
665
666        Some(WireErr {
667            len,
668            kinds,
669            reasons,
670            locations,
671        })
672    }
673}
674
675#[cfg(feature = "alloc")]
676#[cfg(test)]
677mod tests {
678    use super::*;
679    use alloc::format;
680    use alloc::vec::Vec;
681
682    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
683    #[repr(u8)]
684    enum TestKind {
685        Root,
686        Context1,
687        Context2,
688        Parse,
689        Io,
690    }
691
692    /// Helper for creating `LiteStr` reasons (turbofish required for const generic).
693    fn r<const N: usize>(s: &str) -> LiteStr<N> {
694        LiteStr::from_str(s)
695    }
696
697    // Use the crate's exact *default* parameters so the an_err! macro + constructors
698    // match perfectly and inference is unambiguous.
699    type E3 = AnErr<TestKind, 3, 29>;
700
701    #[test]
702    fn test_new_from_and_basic_properties() {
703        let e1: E3 = AnErr::new(TestKind::Root);
704        let e2: E3 = TestKind::Root.into();
705
706        // NOTE: We cannot use assert_eq!(e1, e2) because #[track_caller]
707        // captures different source locations (different lines in this test).
708        // The rest of the data is identical.
709        assert_eq!(e1.depth(), e2.depth());
710        assert_eq!(e1.kind(), e2.kind());
711        assert_eq!(e1.depth(), 1);
712        assert_eq!(e1.kind(), Some(TestKind::Root));
713
714        let mut trace = e1.trace();
715        let (kind, _loc, reason) = trace.next().unwrap();
716        assert_eq!(kind, TestKind::Root);
717        assert!(reason.is_none());
718        assert!(trace.next().is_none());
719    }
720
721    #[test]
722    fn test_with_reason_and_with_fmt() {
723        // Explicit type fixes const-generic inference (DEPTH cannot be inferred from LiteStr alone)
724        let e: E3 = AnErr::with_reason(TestKind::Parse, r::<29>("bad token"));
725        assert_eq!(e.depth(), 1);
726
727        let items: Vec<_> = e.trace().collect();
728        assert_eq!(items[0].2.unwrap().as_str().unwrap(), "bad token");
729
730        let e2: E3 = AnErr::with_fmt(
731            TestKind::Io,
732            format_args!("file not found: {}", "config.toml"),
733        );
734        let items2: Vec<_> = e2.trace().collect();
735        assert_eq!(
736            items2[0].2.unwrap().as_str().unwrap(),
737            "file not found: config.toml"
738        );
739    }
740
741    #[test]
742    fn test_an_err_macro_all_forms() {
743        let e1: E3 = an_err!(TestKind::Root);
744        assert_eq!(e1.kind(), Some(TestKind::Root));
745
746        let e2: E3 = an_err!(TestKind::Parse, "unexpected {}", "EOF");
747        assert_eq!(
748            e2.trace().next().unwrap().2.unwrap().as_str().unwrap(),
749            "unexpected EOF"
750        );
751
752        // Chaining form
753        let inner: E3 = an_err!(TestKind::Parse, "bad data");
754        let outer: E3 = an_err!(TestKind::Io, "while reading file" => inner);
755
756        assert_eq!(outer.depth(), 2);
757        let mut t = outer.trace();
758        let (k1, _, r1) = t.next().unwrap();
759        assert_eq!(k1, TestKind::Io);
760        assert_eq!(r1.unwrap().as_str().unwrap(), "while reading file");
761
762        let (k2, _, r2) = t.next().unwrap();
763        assert_eq!(k2, TestKind::Parse);
764        assert_eq!(r2.unwrap().as_str().unwrap(), "bad data");
765    }
766
767    #[test]
768    fn test_context_and_context_fmt() {
769        let mut e: E3 = an_err!(TestKind::Root, "initial");
770        e.context(TestKind::Context1, r::<29>("level 1"));
771        e.context_fmt(TestKind::Context2, format_args!("level {}", 2));
772
773        assert_eq!(e.depth(), 3);
774
775        let trace: Vec<_> = e.trace().collect();
776        // Most recent first
777        assert_eq!(trace[0].0, TestKind::Context2);
778        assert_eq!(trace[1].0, TestKind::Context1);
779        assert_eq!(trace[2].0, TestKind::Root);
780
781        assert_eq!(trace[0].2.unwrap().as_str().unwrap(), "level 2");
782        assert_eq!(trace[1].2.unwrap().as_str().unwrap(), "level 1");
783        assert_eq!(trace[2].2.unwrap().as_str().unwrap(), "initial");
784    }
785
786    #[test]
787    fn test_max_depth_is_no_op() {
788        let mut e: E3 = an_err!(TestKind::Root);
789        for i in 0..10 {
790            e.context(TestKind::Context1, r::<29>(&format!("extra {i}")));
791        }
792        assert_eq!(e.depth(), 3); // DEPTH limit reached, further calls ignored
793
794        let trace: Vec<_> = e.trace().collect();
795        assert_eq!(trace.len(), 3);
796        assert_eq!(trace[0].0, TestKind::Context1); // last successful context
797    }
798
799    #[test]
800    fn test_empty_reason_becomes_none() {
801        let e: E3 = an_err!(TestKind::Parse, "");
802        let (_, _, reason) = e.trace().next().unwrap();
803        assert!(reason.is_none());
804
805        let mut e2: E3 = an_err!(TestKind::Root);
806        e2.context(TestKind::Io, r::<29>("")); // empty literal -> None
807        let items: Vec<_> = e2.trace().collect();
808        assert!(items[0].2.is_none());
809    }
810
811    #[test]
812    fn test_trace_iter_order_exact_size_and_size_hint() {
813        let e: E3 = an_err!(TestKind::Root, "a" => an_err!(TestKind::Io, "b" => an_err!(TestKind::Parse, "c")));
814
815        let trace = e.trace();
816        assert_eq!(trace.len(), 3); // ExactSizeIterator
817        assert_eq!(trace.size_hint(), (3, Some(3)));
818
819        let collected: Vec<_> = trace.collect();
820        assert_eq!(collected.len(), 3);
821        assert_eq!(collected[0].0, TestKind::Root); // most recent
822        assert_eq!(collected[1].0, TestKind::Io);
823        assert_eq!(collected[2].0, TestKind::Parse); // original
824    }
825
826    #[test]
827    fn test_kind_returns_most_recent() {
828        let mut e: E3 = an_err!(TestKind::Parse);
829        e.context(TestKind::Context1, r::<29>("ctx1"));
830        e.context(TestKind::Context2, r::<29>("ctx2"));
831
832        assert_eq!(e.kind(), Some(TestKind::Context2)); // top of the trace
833    }
834
835    #[test]
836    fn test_display() {
837        let inner: E3 = an_err!(TestKind::Parse, "bad syntax");
838        let e: E3 = an_err!(TestKind::Io, "while loading config" => inner);
839
840        let display = format!("{}", e);
841        assert!(display.contains("--"));
842        assert!(display.contains("Error:"));
843        assert!(display.contains("Io"));
844        assert!(display.contains("while loading config"));
845        assert!(display.contains("Parse"));
846        assert!(display.contains("bad syntax"));
847    }
848
849    #[cfg(feature = "wire")]
850    type E4 = AnErr<TestKind, 4, 29>;
851    #[cfg(feature = "wire")]
852    use alloc::vec;
853
854    #[cfg(feature = "wire")]
855    #[test]
856    fn test_wire_roundtrip() {
857        let inner: E4 = an_err!(TestKind::Parse, "unexpected char");
858        let e: E4 = an_err!(TestKind::Io, "while processing file" => inner);
859
860        const FILE_LEN: usize = 64;
861        let wire_size = E4::wire_size::<FILE_LEN>();
862        let mut buf = vec![0u8; wire_size];
863
864        // Fixed: turbofish required for the const generic PATH_LEN
865        let written = e.to_wire_bytes::<FILE_LEN>(|k| k as u16, &mut buf).unwrap();
866        assert_eq!(written, wire_size);
867
868        let wire_err = WireErr::<4, 29, FILE_LEN>::from_wire_bytes(&buf[..written]).unwrap();
869
870        assert_eq!(wire_err.len, 2);
871
872        // Wire stores levels oldest-first (index 0 = root)
873        assert_eq!(wire_err.kinds[0], Some(TestKind::Parse as u16));
874        assert_eq!(wire_err.kinds[1], Some(TestKind::Io as u16));
875
876        assert_eq!(
877            wire_err.reasons[0].as_ref().unwrap().as_str().unwrap(),
878            "unexpected char"
879        );
880        assert_eq!(
881            wire_err.reasons[1].as_ref().unwrap().as_str().unwrap(),
882            "while processing file"
883        );
884    }
885
886    #[cfg(feature = "wire")]
887    #[test]
888    fn test_wire_invalid_cases() {
889        // Wrong size
890        assert!(WireErr::<3, 29, 64>::from_wire_bytes(&[0u8; 10]).is_none());
891
892        // Bad version
893        let mut buf = vec![0u8; E4::wire_size::<64>()];
894        buf[0] = 99; // invalid version
895        assert!(WireErr::<4, 29, 64>::from_wire_bytes(&buf).is_none());
896    }
897}