Skip to main content

deep_time/
an_err.rs

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