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#[derive(Clone, Copy, PartialEq, Eq)]
129#[must_use = "this error should be handled or converted to a different type"]
130pub struct AnErr<K, const DEPTH: usize = 3, const REASON_LEN: usize = 29>
131where
132    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
133{
134    /// Per-level reasons. Only the first `len` entries are valid.
135    /// `None` means no reason (or an empty reason) was provided for that level.
136    pub reasons: [Option<AsciiStr<REASON_LEN>>; DEPTH],
137
138    /// Parallel stack of source locations.
139    /// Only the first `len` entries are valid.
140    pub locations: [Option<&'static Location<'static>>; DEPTH],
141
142    /// Parallel stack of error kinds (one per call-stack level).
143    /// Only the first `len` entries are valid.
144    pub kinds: [Option<K>; DEPTH],
145
146    /// Current depth of the error trace (1 = original error).
147    pub len: u8,
148}
149
150impl<K, const DEPTH: usize, const REASON_LEN: usize> AnErr<K, DEPTH, REASON_LEN>
151where
152    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
153{
154    /// Creates a new error with the given kind and no reason.
155    #[inline]
156    #[track_caller]
157    pub fn new(kind: K) -> Self {
158        let mut kinds = [None; DEPTH];
159        let mut locs = [None; DEPTH];
160        let reasons = [None; DEPTH];
161
162        kinds[0] = Some(kind);
163        locs[0] = Some(Location::caller());
164
165        Self {
166            kinds,
167            locations: locs,
168            reasons,
169            len: 1,
170        }
171    }
172
173    /// Creates a new error with the given kind and reason.
174    ///
175    /// If the reason is empty, it is stored as `None`.
176    #[inline]
177    #[track_caller]
178    pub fn with_reason(kind: K, reason: AsciiStr<REASON_LEN>) -> Self {
179        let mut kinds = [None; DEPTH];
180        let mut locs = [None; DEPTH];
181        let mut reasons = [None; DEPTH];
182
183        kinds[0] = Some(kind);
184        locs[0] = Some(Location::caller());
185        reasons[0] = if reason.is_empty() {
186            None
187        } else {
188            Some(reason)
189        };
190
191        Self {
192            kinds,
193            locations: locs,
194            reasons,
195            len: 1,
196        }
197    }
198
199    /// Creates a new error with the given kind and a formatted reason.
200    ///
201    /// The formatted string is truncated if it exceeds `REASON_LEN` bytes.
202    #[inline]
203    #[track_caller]
204    pub fn with_fmt(kind: K, args: core::fmt::Arguments<'_>) -> Self {
205        let mut kinds = [None; DEPTH];
206        let mut locs = [None; DEPTH];
207        let mut reasons = [None; DEPTH];
208
209        kinds[0] = Some(kind);
210        locs[0] = Some(Location::caller());
211        let reason = AsciiStr::from_fmt(args);
212        reasons[0] = if reason.is_empty() {
213            None
214        } else {
215            Some(reason)
216        };
217
218        Self {
219            kinds,
220            locations: locs,
221            reasons,
222            len: 1,
223        }
224    }
225
226    /// Returns the current depth of the error trace.
227    #[inline]
228    pub fn depth(&self) -> u8 {
229        self.len
230    }
231
232    /// Returns the most recent error kind (the top of the trace).
233    #[inline]
234    pub fn kind(&self) -> Option<K> {
235        if self.len == 0 {
236            None
237        } else {
238            let idx = (self.len as usize) - 1;
239            self.kinds[idx]
240        }
241    }
242
243    /// Appends a new context level and optional reason to this error.
244    ///
245    /// If `new_reason` is empty, no reason is stored for the new level.
246    /// If the maximum depth is already reached, the call is a no-op.
247    #[inline]
248    #[track_caller]
249    pub fn context(&mut self, kind: K, new_reason: AsciiStr<REASON_LEN>) {
250        let idx = self.len as usize;
251        if idx < DEPTH {
252            self.reasons[idx] = if new_reason.is_empty() {
253                None
254            } else {
255                Some(new_reason)
256            };
257            self.push(kind, Location::caller());
258        }
259    }
260
261    /// Appends a new context level with a formatted reason.
262    ///
263    /// Used internally by the `an_err!` macro. The formatted string is
264    /// truncated if it exceeds `REASON_LEN` bytes.
265    #[inline]
266    #[track_caller]
267    pub fn context_fmt(&mut self, kind: K, args: core::fmt::Arguments<'_>) {
268        let idx = self.len as usize;
269        if idx < DEPTH {
270            let reason = AsciiStr::from_fmt(args);
271            self.reasons[idx] = if reason.is_empty() {
272                None
273            } else {
274                Some(reason)
275            };
276            self.push(kind, Location::caller());
277        }
278    }
279
280    /// Returns an iterator over the error trace, from most recent context
281    /// down to the original error.
282    ///
283    /// Each item is `(kind, location, reason)`. The iterator borrows `self`
284    /// with zero copying.
285    pub fn trace(&self) -> TraceIter<'_, K, DEPTH, REASON_LEN> {
286        TraceIter {
287            error: self,
288            pos: 0,
289        }
290    }
291
292    #[inline]
293    fn push(&mut self, kind: K, loc: &'static Location<'static>) {
294        if (self.len as usize) < DEPTH {
295            let idx = self.len as usize;
296            self.kinds[idx] = Some(kind);
297            self.locations[idx] = Some(loc);
298            self.len += 1;
299        }
300    }
301
302    /// Serialize this error into a fixed-size byte buffer for transmission.
303    ///
304    /// The caller must provide a buffer that is at least `Self::WIRE_SIZE::<PATH_LEN>()` bytes long.
305    /// Returns the number of bytes actually written (always the same for a given `PATH_LEN`).
306    ///
307    /// Recommended usage:
308    /// ```rust,ignore
309    /// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
310    /// #[repr(u8)]   // or #[repr(u16)] for >256 variants
311    /// pub enum MyKind { ... }
312    ///
313    /// let mut buf = [0u8; AnErr::<MyKind, 3, 29>::wire_size::<80>()];
314    /// let written = my_error.to_wire_bytes::<80>(|k| k as u16, &mut buf);
315    /// let packet = &buf[..written];
316    /// ```
317    #[cfg(feature = "wire")]
318    pub fn to_wire_bytes<const PATH_LEN: usize>(
319        &self,
320        kind_to_u16: impl Fn(K) -> u16,
321        buf: &mut [u8],
322    ) -> Result<usize, ()> {
323        let needed = Self::wire_size::<PATH_LEN>();
324        if buf.len() < needed {
325            return Err(());
326        }
327
328        let mut offset = 0;
329
330        // Header
331        buf[offset] = 1; // wire format version
332        offset += 1;
333        buf[offset] = self.len;
334        offset += 1;
335
336        for i in 0..DEPTH {
337            if i < self.len as usize {
338                // 1. Kind as u16
339                let kind_val = self.kinds[i].map_or(0, |k| kind_to_u16(k));
340                buf[offset..offset + 2].copy_from_slice(&kind_val.to_le_bytes());
341                offset += 2;
342
343                // 2. Reason
344                let reason = self.reasons[i]
345                    .as_ref()
346                    .unwrap_or_else(|| &AsciiStr::DEFAULT);
347                buf[offset..offset + REASON_LEN].copy_from_slice(&reason.to_wire_bytes());
348                offset += REASON_LEN;
349
350                // 3. Location
351                if let Some(loc) = self.locations[i] {
352                    let file = AsciiStr::<PATH_LEN>::from_str_truncate(loc.file());
353                    buf[offset..offset + PATH_LEN].copy_from_slice(&file.to_wire_bytes());
354                    offset += PATH_LEN;
355
356                    buf[offset..offset + 4].copy_from_slice(&loc.line().to_le_bytes());
357                    offset += 4;
358                    buf[offset..offset + 4].copy_from_slice(&loc.column().to_le_bytes());
359                    offset += 4;
360                } else {
361                    offset += PATH_LEN + 8; // pad
362                }
363            } else {
364                // pad remaining levels
365                offset += 2 + REASON_LEN + PATH_LEN + 8;
366            }
367        }
368
369        Ok(needed)
370    }
371
372    /// Compile-time size of the wire representation for a given `PATH_LEN`.
373    #[cfg(feature = "wire")]
374    pub const fn wire_size<const PATH_LEN: usize>() -> usize {
375        2 + DEPTH * (2 + REASON_LEN + PATH_LEN + 8)
376    }
377}
378
379impl<K, const DEPTH: usize, const REASON_LEN: usize> From<K> for AnErr<K, DEPTH, REASON_LEN>
380where
381    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
382{
383    /// Converts a kind into a new [`AnErr`] with no reason.
384    #[inline]
385    #[track_caller]
386    fn from(kind: K) -> Self {
387        Self::new(kind)
388    }
389}
390
391impl<K, const DEPTH: usize, const REASON_LEN: usize> core::fmt::Display
392    for AnErr<K, DEPTH, REASON_LEN>
393where
394    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
395{
396    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
397        writeln!(f)?;
398        writeln!(f, "--")?;
399        writeln!(f, "Error:")?;
400
401        for (i, (kind, loc, reason_opt)) in self.trace().enumerate() {
402            let num = i + 1;
403
404            write!(f, "  {:>2}. {:?}", num, kind)?;
405
406            if let Some(reason) = reason_opt {
407                if let Ok(s) = reason.as_str() {
408                    write!(f, ": {}", s)?;
409                } else {
410                    write!(f, ": <invalid ascii>")?;
411                }
412            }
413
414            writeln!(f, " @ {}:{}:{}", loc.file(), loc.line(), loc.column())?;
415        }
416
417        Ok(())
418    }
419}
420
421impl<K, const DEPTH: usize, const REASON_LEN: usize> fmt::Debug for AnErr<K, DEPTH, REASON_LEN>
422where
423    K: Copy + Clone + fmt::Debug + PartialEq + Eq,
424{
425    /// Debug prints the same clean, human-readable trace as Display.
426    /// This makes `unwrap()`, `dbg!()`, and panic messages readable instead of
427    /// dumping giant byte arrays.
428    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
429        fmt::Display::fmt(self, f)
430    }
431}
432
433impl<K, const DEPTH: usize, const REASON_LEN: usize> core::error::Error
434    for AnErr<K, DEPTH, REASON_LEN>
435where
436    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
437{
438}
439
440/// Ergonomic constructor and chaining macro for [`AnErr`].
441///
442/// # Forms
443///
444/// | Form                                              | Equivalent to                                      |
445/// |---------------------------------------------------|----------------------------------------------------|
446/// | `an_err!(Kind)`                                   | `AnErr::new(Kind)`                               |
447/// | `an_err!(Kind, "reason")`                         | `AnErr::with_fmt(Kind, ...)`                     |
448/// | `an_err!(Kind, "reason {}", arg, ...)`            | `AnErr::with_fmt(Kind, ...)`                     |
449/// | `an_err!(Kind, "reason" => inner)`                | `inner.context(Kind, ...)`                         |
450/// | `an_err!(Kind, "reason {}", arg => inner)`        | `inner.context(Kind, ...)`                         |
451///
452/// All forms capture the call site via `#[track_caller]`.
453#[macro_export]
454macro_rules! an_err {
455    // New error, no reason
456    ($kind:expr) => {
457        $crate::AnErr::new($kind)
458    };
459
460    // Chaining form (must appear before the new-error form)
461    ($kind:expr, $fmt:literal $(, $arg:expr)* => $inner:expr $(,)?) => {{
462        let mut e = $inner;
463        e.context_fmt(
464            $kind,
465            format_args!($fmt $(, $arg)*)
466        );
467        e
468    }};
469
470    // New error with reason (literal or formatted)
471    ($kind:expr, $fmt:literal $(, $arg:expr)* $(,)?) => {
472        $crate::AnErr::with_fmt($kind, format_args!($fmt $(, $arg)*))
473    };
474}
475
476/// Portable location for wire transmission.
477#[cfg(feature = "wire")]
478#[derive(Debug, Clone, Copy, PartialEq, Eq)]
479pub struct WireLocation<const N: usize> {
480    pub file: AsciiStr<N>,
481    pub line: u32,
482    pub column: u32,
483}
484
485/// Fully portable, zero-allocation error for transmission/reception.
486#[cfg(feature = "wire")]
487#[derive(Debug, Clone, Copy, PartialEq, Eq)]
488pub struct WireErr<const DEPTH: usize = 3, const REASON_LEN: usize = 29, const FILE_LEN: usize = 80>
489{
490    pub len: u8,
491    pub kinds: [Option<u16>; DEPTH],
492    pub reasons: [Option<AsciiStr<REASON_LEN>>; DEPTH],
493    pub locations: [Option<WireLocation<FILE_LEN>>; DEPTH],
494}
495
496#[cfg(feature = "wire")]
497impl<const DEPTH: usize, const REASON_LEN: usize, const FILE_LEN: usize>
498    WireErr<DEPTH, REASON_LEN, FILE_LEN>
499{
500    /// Fixed wire size (exactly matches `AnErr::wire_size::<FILE_LEN>()`).
501    pub const fn wire_size() -> usize {
502        const fn compute_size<const D: usize, const R: usize, const F: usize>() -> usize {
503            2 + D * (2 + R + F + 8)
504        }
505        compute_size::<DEPTH, REASON_LEN, FILE_LEN>()
506    }
507
508    /// Parse a wire buffer from `AnErr` into a `WireErr`.
509    ///
510    /// Returns `None` on any corruption, wrong size, unknown version,
511    /// or invalid `AsciiStr` data.
512    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
513        if bytes.len() != Self::wire_size() {
514            return None;
515        }
516
517        let mut offset = 0;
518
519        // Version
520        let version = bytes[offset];
521        if version != 1 {
522            return None; // unknown wire format
523        }
524        offset += 1;
525
526        let len = bytes[offset];
527        if len == 0 || len as usize > DEPTH {
528            return None;
529        }
530        offset += 1;
531
532        let mut kinds = [None; DEPTH];
533        let mut reasons = [None; DEPTH];
534        let mut locations = [None; DEPTH];
535
536        for i in 0..(len as usize) {
537            // kind (u16)
538            let kind_bytes = <[u8; 2]>::try_from(&bytes[offset..offset + 2]).ok()?;
539            kinds[i] = Some(u16::from_le_bytes(kind_bytes));
540            offset += 2;
541
542            // reason
543            let reason_bytes = &bytes[offset..offset + REASON_LEN];
544            reasons[i] = AsciiStr::from_wire_bytes(reason_bytes);
545            offset += REASON_LEN;
546
547            // location
548            let file_bytes = &bytes[offset..offset + FILE_LEN];
549            let file = AsciiStr::from_wire_bytes(file_bytes)?;
550
551            offset += FILE_LEN;
552
553            let line_bytes = <[u8; 4]>::try_from(&bytes[offset..offset + 4]).ok()?;
554            let line = u32::from_le_bytes(line_bytes);
555            offset += 4;
556
557            let col_bytes = <[u8; 4]>::try_from(&bytes[offset..offset + 4]).ok()?;
558            let column = u32::from_le_bytes(col_bytes);
559            offset += 4;
560
561            locations[i] = Some(WireLocation { file, line, column });
562        }
563
564        // remaining bytes are padding (we already checked total length)
565
566        Some(WireErr {
567            len,
568            kinds,
569            reasons,
570            locations,
571        })
572    }
573}
574
575#[cfg(feature = "alloc")]
576#[cfg(test)]
577mod tests {
578    use super::*;
579    use alloc::format;
580    use alloc::vec::Vec;
581
582    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
583    #[repr(u8)]
584    enum TestKind {
585        Root,
586        Context1,
587        Context2,
588        Parse,
589        Io,
590    }
591
592    /// Helper for creating `AsciiStr` reasons (turbofish required for const generic).
593    fn r<const N: usize>(s: &str) -> AsciiStr<N> {
594        AsciiStr::from_str_truncate(s)
595    }
596
597    // Use the crate's exact *default* parameters so the an_err! macro + constructors
598    // match perfectly and inference is unambiguous.
599    type E3 = AnErr<TestKind, 3, 29>;
600
601    #[test]
602    fn test_new_from_and_basic_properties() {
603        let e1: E3 = AnErr::new(TestKind::Root);
604        let e2: E3 = TestKind::Root.into();
605
606        // NOTE: We cannot use assert_eq!(e1, e2) because #[track_caller]
607        // captures different source locations (different lines in this test).
608        // The rest of the data is identical.
609        assert_eq!(e1.depth(), e2.depth());
610        assert_eq!(e1.kind(), e2.kind());
611        assert_eq!(e1.depth(), 1);
612        assert_eq!(e1.kind(), Some(TestKind::Root));
613
614        let mut trace = e1.trace();
615        let (kind, _loc, reason) = trace.next().unwrap();
616        assert_eq!(kind, TestKind::Root);
617        assert!(reason.is_none());
618        assert!(trace.next().is_none());
619    }
620
621    #[test]
622    fn test_with_reason_and_with_fmt() {
623        // Explicit type fixes const-generic inference (DEPTH cannot be inferred from AsciiStr alone)
624        let e: E3 = AnErr::with_reason(TestKind::Parse, r::<29>("bad token"));
625        assert_eq!(e.depth(), 1);
626
627        let items: Vec<_> = e.trace().collect();
628        assert_eq!(items[0].2.unwrap().as_str().unwrap(), "bad token");
629
630        let e2: E3 = AnErr::with_fmt(
631            TestKind::Io,
632            format_args!("file not found: {}", "config.toml"),
633        );
634        let items2: Vec<_> = e2.trace().collect();
635        assert_eq!(
636            items2[0].2.unwrap().as_str().unwrap(),
637            "file not found: config.toml"
638        );
639    }
640
641    #[test]
642    fn test_an_err_macro_all_forms() {
643        let e1: E3 = an_err!(TestKind::Root);
644        assert_eq!(e1.kind(), Some(TestKind::Root));
645
646        let e2: E3 = an_err!(TestKind::Parse, "unexpected {}", "EOF");
647        assert_eq!(
648            e2.trace().next().unwrap().2.unwrap().as_str().unwrap(),
649            "unexpected EOF"
650        );
651
652        // Chaining form
653        let inner: E3 = an_err!(TestKind::Parse, "bad data");
654        let outer: E3 = an_err!(TestKind::Io, "while reading file" => inner);
655
656        assert_eq!(outer.depth(), 2);
657        let mut t = outer.trace();
658        let (k1, _, r1) = t.next().unwrap();
659        assert_eq!(k1, TestKind::Io);
660        assert_eq!(r1.unwrap().as_str().unwrap(), "while reading file");
661
662        let (k2, _, r2) = t.next().unwrap();
663        assert_eq!(k2, TestKind::Parse);
664        assert_eq!(r2.unwrap().as_str().unwrap(), "bad data");
665    }
666
667    #[test]
668    fn test_context_and_context_fmt() {
669        let mut e: E3 = an_err!(TestKind::Root, "initial");
670        e.context(TestKind::Context1, r::<29>("level 1"));
671        e.context_fmt(TestKind::Context2, format_args!("level {}", 2));
672
673        assert_eq!(e.depth(), 3);
674
675        let trace: Vec<_> = e.trace().collect();
676        // Most recent first
677        assert_eq!(trace[0].0, TestKind::Context2);
678        assert_eq!(trace[1].0, TestKind::Context1);
679        assert_eq!(trace[2].0, TestKind::Root);
680
681        assert_eq!(trace[0].2.unwrap().as_str().unwrap(), "level 2");
682        assert_eq!(trace[1].2.unwrap().as_str().unwrap(), "level 1");
683        assert_eq!(trace[2].2.unwrap().as_str().unwrap(), "initial");
684    }
685
686    #[test]
687    fn test_max_depth_is_no_op() {
688        let mut e: E3 = an_err!(TestKind::Root);
689        for i in 0..10 {
690            e.context(TestKind::Context1, r::<29>(&format!("extra {i}")));
691        }
692        assert_eq!(e.depth(), 3); // DEPTH limit reached, further calls ignored
693
694        let trace: Vec<_> = e.trace().collect();
695        assert_eq!(trace.len(), 3);
696        assert_eq!(trace[0].0, TestKind::Context1); // last successful context
697    }
698
699    #[test]
700    fn test_empty_reason_becomes_none() {
701        let e: E3 = an_err!(TestKind::Parse, "");
702        let (_, _, reason) = e.trace().next().unwrap();
703        assert!(reason.is_none());
704
705        let mut e2: E3 = an_err!(TestKind::Root);
706        e2.context(TestKind::Io, r::<29>("")); // empty literal -> None
707        let items: Vec<_> = e2.trace().collect();
708        assert!(items[0].2.is_none());
709    }
710
711    #[test]
712    fn test_trace_iter_order_exact_size_and_size_hint() {
713        let e: E3 = an_err!(TestKind::Root, "a" => an_err!(TestKind::Io, "b" => an_err!(TestKind::Parse, "c")));
714
715        let trace = e.trace();
716        assert_eq!(trace.len(), 3); // ExactSizeIterator
717        assert_eq!(trace.size_hint(), (3, Some(3)));
718
719        let collected: Vec<_> = trace.collect();
720        assert_eq!(collected.len(), 3);
721        assert_eq!(collected[0].0, TestKind::Root); // most recent
722        assert_eq!(collected[1].0, TestKind::Io);
723        assert_eq!(collected[2].0, TestKind::Parse); // original
724    }
725
726    #[test]
727    fn test_kind_returns_most_recent() {
728        let mut e: E3 = an_err!(TestKind::Parse);
729        e.context(TestKind::Context1, r::<29>("ctx1"));
730        e.context(TestKind::Context2, r::<29>("ctx2"));
731
732        assert_eq!(e.kind(), Some(TestKind::Context2)); // top of the trace
733    }
734
735    #[test]
736    fn test_display() {
737        let inner: E3 = an_err!(TestKind::Parse, "bad syntax");
738        let e: E3 = an_err!(TestKind::Io, "while loading config" => inner);
739
740        let display = format!("{}", e);
741        assert!(display.contains("--"));
742        assert!(display.contains("Error:"));
743        assert!(display.contains("Io"));
744        assert!(display.contains("while loading config"));
745        assert!(display.contains("Parse"));
746        assert!(display.contains("bad syntax"));
747    }
748
749    #[cfg(feature = "wire")]
750    type E4 = AnErr<TestKind, 4, 29>;
751    #[cfg(feature = "wire")]
752    use alloc::vec;
753
754    #[cfg(feature = "wire")]
755    #[test]
756    fn test_wire_roundtrip() {
757        let inner: E4 = an_err!(TestKind::Parse, "unexpected char");
758        let e: E4 = an_err!(TestKind::Io, "while processing file" => inner);
759
760        const FILE_LEN: usize = 64;
761        let wire_size = E4::wire_size::<FILE_LEN>();
762        let mut buf = vec![0u8; wire_size];
763
764        // Fixed: turbofish required for the const generic PATH_LEN
765        let written = e.to_wire_bytes::<FILE_LEN>(|k| k as u16, &mut buf).unwrap();
766        assert_eq!(written, wire_size);
767
768        let wire_err = WireErr::<4, 29, FILE_LEN>::from_wire_bytes(&buf[..written]).unwrap();
769
770        assert_eq!(wire_err.len, 2);
771
772        // Wire stores levels oldest-first (index 0 = root)
773        assert_eq!(wire_err.kinds[0], Some(TestKind::Parse as u16));
774        assert_eq!(wire_err.kinds[1], Some(TestKind::Io as u16));
775
776        assert_eq!(
777            wire_err.reasons[0].as_ref().unwrap().as_str().unwrap(),
778            "unexpected char"
779        );
780        assert_eq!(
781            wire_err.reasons[1].as_ref().unwrap().as_str().unwrap(),
782            "while processing file"
783        );
784    }
785
786    #[cfg(feature = "wire")]
787    #[test]
788    fn test_wire_invalid_cases() {
789        // Wrong size
790        assert!(WireErr::<3, 29, 64>::from_wire_bytes(&[0u8; 10]).is_none());
791
792        // Bad version
793        let mut buf = vec![0u8; E4::wire_size::<64>()];
794        buf[0] = 99; // invalid version
795        assert!(WireErr::<4, 29, 64>::from_wire_bytes(&buf).is_none());
796    }
797}