Skip to main content

marque_utils/
error.rs

1// Adapted from code originally in [CocoIndex](https://CocoIndex)
2// Original code from CocoIndex is copyrighted by CocoIndex
3// and licensed under the Apache-2.0 License.
4// SPDX-License-Identifier: Apache-2.0
5// SPDX-FileCopyrightText: 2026 CocoIndex
6//
7// All modifications from the upstream for Marque are copyrighted by Knitli Inc.
8// SPDX-FileCopyrightText: 2026 Knitli Inc. (Marque)
9// SPDX-License-Identifier: LicenseRef-MarqueLicense-1.0
10
11//! The workspace error type and the helpers that build on it.
12//!
13//! [`Error`] sorts every failure into one of four kinds — caller mistakes
14//! ([`client`](Error::client)), internal faults ([`internal`](Error::internal)),
15//! errors from an embedding host language ([`host`](Error::host)), and a
16//! [`Context`](Error::Context) wrapper that records a human-readable trail. The
17//! [`ContextExt`] / [`StdContextExt`] traits add `.context(..)` to `Result` and
18//! `Option`, and the `client_*` / `internal_*` / `api_*` macros build-and-bail
19//! in one line.
20//!
21//! A few supporting types round it out: [`SError`] adapts [`Error`] to
22//! [`std::error::Error`] where a `'static` source is required, [`SharedError`]
23//! shares one error across many waiters (degrading to a message after the first
24//! takes ownership), and [`ApiError`] is the boundary type for API responses.
25
26use std::{
27    any::Any,
28    backtrace::Backtrace,
29    error::Error as StdError,
30    fmt::{Debug, Display},
31    sync::{Arc, Mutex},
32};
33
34/// Any foreign error that can be carried as an [`Error::HostLang`]. Blanket-
35/// implemented for every `Send + Sync + 'static` standard error.
36pub trait HostError: Any + StdError + Send + Sync + 'static {}
37impl<T: Any + StdError + Send + Sync + 'static> HostError for T {}
38
39/// The workspace error. Each variant marks where a failure originated.
40pub enum Error {
41    /// A message wrapping an inner error, forming a context trail.
42    Context { msg: String, source: Box<SError> },
43    /// An error surfaced from an embedding host language (e.g. Python).
44    HostLang(Box<dyn HostError>),
45    /// A caller mistake — bad input or a failed precondition. Carries a
46    /// backtrace and renders as `Invalid Request: ..`.
47    Client { msg: String, bt: Backtrace },
48    /// An internal fault, holding an [`anyhow::Error`] for flexible context.
49    Internal(anyhow::Error),
50}
51
52impl Display for Error {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        match self.format_context(f)? {
55            Error::Context { .. } => Ok(()),
56            Error::HostLang(e) => write!(f, "{}", e),
57            Error::Client { msg, .. } => write!(f, "Invalid Request: {}", msg),
58            Error::Internal(e) => write!(f, "{}", e),
59        }
60    }
61}
62impl Debug for Error {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        match self.format_context(f)? {
65            Error::Context { .. } => Ok(()),
66            Error::HostLang(e) => write!(f, "{:?}", e),
67            Error::Client { msg, bt } => {
68                write!(f, "Invalid Request: {msg}\n\n{bt}\n")
69            }
70            Error::Internal(e) => write!(f, "{e:?}"),
71        }
72    }
73}
74
75/// A [`Result`](std::result::Result) defaulting to the workspace [`Error`].
76pub type Result<T, E = Error> = std::result::Result<T, E>;
77
78/// Backwards-compatibility alias for [`Error`].
79pub type CError = Error;
80/// Backwards-compatibility alias for [`Result`].
81pub type CResult<T> = Result<T>;
82
83impl Error {
84    /// Wraps a host-language error as [`Error::HostLang`].
85    pub fn host(e: impl HostError) -> Self {
86        Self::HostLang(Box::new(e))
87    }
88
89    /// Builds a [`Error::Client`] from a message, capturing a backtrace.
90    pub fn client(msg: impl Into<String>) -> Self {
91        Self::Client {
92            msg: msg.into(),
93            bt: Backtrace::capture(),
94        }
95    }
96
97    /// Wraps any `Into<anyhow::Error>` as [`Error::Internal`].
98    pub fn internal(e: impl Into<anyhow::Error>) -> Self {
99        Self::Internal(e.into())
100    }
101
102    /// Builds an [`Error::Internal`] straight from a message.
103    pub fn internal_msg(msg: impl Into<String>) -> Self {
104        Self::Internal(anyhow::anyhow!("{}", msg.into()))
105    }
106
107    /// Returns the backtrace, if this error (or the error it wraps) captured
108    /// one. Host-language errors have none.
109    pub fn backtrace(&self) -> Option<&Backtrace> {
110        match self {
111            Error::Client { bt, .. } => Some(bt),
112            Error::Internal(e) => Some(e.backtrace()),
113            Error::Context { source, .. } => source.0.backtrace(),
114            Error::HostLang(_) => None,
115        }
116    }
117
118    /// Peels off any [`Context`](Error::Context) layers to reach the underlying
119    /// error.
120    pub fn without_contexts(&self) -> &Error {
121        match self {
122            Error::Context { source, .. } => source.0.without_contexts(),
123            other => other,
124        }
125    }
126
127    /// Returns this error's source, if any, for chain traversal.
128    pub fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
129        match self {
130            Error::Context { source, .. } => Some(source.as_ref()),
131            Error::HostLang(e) => Some(e.as_ref()),
132            Error::Internal(e) => e.source(),
133            Error::Client { .. } => None,
134        }
135    }
136
137    /// Wraps this error in a [`Context`](Error::Context) layer carrying `context`.
138    pub fn context<C: Into<String>>(self, context: C) -> Self {
139        Self::Context {
140            msg: context.into(),
141            source: Box::new(SError(self)),
142        }
143    }
144
145    /// Like [`context`](Self::context), but builds the message lazily — `f` runs
146    /// only on the error path.
147    pub fn with_context<C: Into<String>, F: FnOnce() -> C>(self, f: F) -> Self {
148        Self::Context {
149            msg: f().into(),
150            source: Box::new(SError(self)),
151        }
152    }
153
154    /// Wraps this error in an [`SError`] so it satisfies a `'static`
155    /// [`std::error::Error`] bound.
156    pub fn std_error(self) -> SError {
157        SError(self)
158    }
159
160    fn format_context(&self, f: &mut std::fmt::Formatter<'_>) -> Result<&Error, std::fmt::Error> {
161        let mut current = self;
162        if matches!(current, Error::Context { .. }) {
163            write!(f, "\nContext:\n")?;
164            let mut next_id = 1;
165            while let Error::Context { msg, source } = current {
166                writeln!(f, "  {next_id}: {msg}")?;
167                current = source.inner();
168                next_id += 1;
169            }
170        }
171        Ok(current)
172    }
173}
174
175impl StdError for Error {
176    fn source(&self) -> Option<&(dyn StdError + 'static)> {
177        self.source()
178    }
179}
180
181// A blanket `From<E: Into<anyhow::Error>>` would collide with the reflexive
182// `From<T> for T`, so the common conversions are spelled out one at a time
183// below instead.
184impl From<anyhow::Error> for Error {
185    fn from(e: anyhow::Error) -> Self {
186        Error::Internal(e)
187    }
188}
189
190impl From<std::io::Error> for Error {
191    fn from(e: std::io::Error) -> Self {
192        Error::Internal(e.into())
193    }
194}
195#[cfg(any(
196    feature = "concur_control",
197    feature = "retryable",
198    feature = "batching"
199))]
200impl From<tokio::task::JoinError> for Error {
201    fn from(e: tokio::task::JoinError) -> Self {
202        Error::Internal(e.into())
203    }
204}
205#[cfg(any(
206    feature = "concur_control",
207    feature = "retryable",
208    feature = "batching"
209))]
210impl From<tokio::sync::oneshot::error::RecvError> for Error {
211    fn from(e: tokio::sync::oneshot::error::RecvError) -> Self {
212        Error::Internal(e.into())
213    }
214}
215#[cfg(feature = "fingerprint")]
216impl From<base64::DecodeError> for Error {
217    fn from(e: base64::DecodeError) -> Self {
218        Error::Internal(e.into())
219    }
220}
221
222impl From<ResidualError> for Error {
223    fn from(e: ResidualError) -> Self {
224        Error::Internal(anyhow::Error::from(e))
225    }
226}
227#[cfg(feature = "fingerprint")]
228impl From<crate::fingerprint::FingerprinterError> for Error {
229    fn from(e: crate::fingerprint::FingerprinterError) -> Self {
230        Error::Internal(anyhow::Error::new(e))
231    }
232}
233
234impl From<ApiError> for Error {
235    fn from(e: ApiError) -> Self {
236        Error::Internal(e.err)
237    }
238}
239
240impl<T> From<std::sync::PoisonError<T>> for Error {
241    fn from(e: std::sync::PoisonError<T>) -> Self {
242        Error::Internal(anyhow::anyhow!("Mutex poison error: {}", e))
243    }
244}
245impl From<std::num::ParseIntError> for Error {
246    fn from(e: std::num::ParseIntError) -> Self {
247        Error::Internal(e.into())
248    }
249}
250
251impl From<std::str::ParseBoolError> for Error {
252    fn from(e: std::str::ParseBoolError) -> Self {
253        Error::Internal(e.into())
254    }
255}
256
257impl From<std::fmt::Error> for Error {
258    fn from(e: std::fmt::Error) -> Self {
259        Error::Internal(e.into())
260    }
261}
262
263impl From<std::string::FromUtf8Error> for Error {
264    fn from(e: std::string::FromUtf8Error) -> Self {
265        Error::Internal(e.into())
266    }
267}
268
269impl From<std::borrow::Cow<'_, str>> for Error {
270    fn from(e: std::borrow::Cow<'_, str>) -> Self {
271        Error::Internal(anyhow::anyhow!("{}", e))
272    }
273}
274#[cfg(any(
275    feature = "concur_control",
276    feature = "retryable",
277    feature = "batching"
278))]
279impl From<tokio::sync::AcquireError> for Error {
280    fn from(e: tokio::sync::AcquireError) -> Self {
281        Error::Internal(e.into())
282    }
283}
284#[cfg(any(
285    feature = "concur_control",
286    feature = "retryable",
287    feature = "batching"
288))]
289impl From<tokio::sync::watch::error::RecvError> for Error {
290    fn from(e: tokio::sync::watch::error::RecvError) -> Self {
291        Error::Internal(e.into())
292    }
293}
294
295/// Adds `.context(..)` / `.with_context(..)` to [`Result<T>`] and [`Option<T>`].
296///
297/// On a `Result` it wraps the existing error; on an `Option` a `None` becomes a
298/// [`client`](Error::client) error carrying the context message.
299pub trait ContextExt<T> {
300    /// Attaches `context` to the error path.
301    fn context<C: Into<String>>(self, context: C) -> Result<T>;
302    /// Attaches a lazily-built context message to the error path.
303    fn with_context<C: Into<String>, F: FnOnce() -> C>(self, f: F) -> Result<T>;
304}
305
306impl<T> ContextExt<T> for Result<T> {
307    fn context<C: Into<String>>(self, context: C) -> Result<T> {
308        self.map_err(|e| e.context(context))
309    }
310
311    fn with_context<C: Into<String>, F: FnOnce() -> C>(self, f: F) -> Result<T> {
312        self.map_err(|e| e.with_context(f))
313    }
314}
315
316/// Adds `.context(..)` / `.with_context(..)` to a `Result` carrying any foreign
317/// [`std::error::Error`], converting it to an [`Error::Internal`] along the way.
318pub trait StdContextExt<T, E> {
319    /// Converts the foreign error to [`Error`] and attaches `context`.
320    fn context<C: Into<String>>(self, context: C) -> Result<T>;
321    /// Converts the foreign error to [`Error`] and attaches a lazily-built
322    /// context message.
323    fn with_context<C: Into<String>, F: FnOnce() -> C>(self, f: F) -> Result<T>;
324}
325
326impl<T, E: StdError + Send + Sync + 'static> StdContextExt<T, E> for Result<T, E> {
327    fn context<C: Into<String>>(self, context: C) -> Result<T> {
328        self.map_err(|e| Error::internal(e).context(context))
329    }
330
331    fn with_context<C: Into<String>, F: FnOnce() -> C>(self, f: F) -> Result<T> {
332        self.map_err(|e| Error::internal(e).with_context(f))
333    }
334}
335
336impl<T> ContextExt<T> for Option<T> {
337    fn context<C: Into<String>>(self, context: C) -> Result<T> {
338        self.ok_or_else(|| Error::client(context))
339    }
340
341    fn with_context<C: Into<String>, F: FnOnce() -> C>(self, f: F) -> Result<T> {
342        self.ok_or_else(|| Error::client(f()))
343    }
344}
345
346/// Returns early with a formatted [`Error::client`].
347#[macro_export]
348macro_rules! client_bail {
349    ( $fmt:literal $(, $($arg:tt)*)?) => {
350        return Err($crate::error::Error::client(format!($fmt $(, $($arg)*)?)))
351    };
352}
353
354/// Builds a formatted [`Error::client`] without returning.
355#[macro_export]
356macro_rules! client_error {
357    ( $fmt:literal $(, $($arg:tt)*)?) => {
358        $crate::error::Error::client(format!($fmt $(, $($arg)*)?))
359    };
360}
361
362/// Returns early with a formatted [`Error::internal_msg`].
363#[macro_export]
364macro_rules! internal_bail {
365    ( $fmt:literal $(, $($arg:tt)*)?) => {
366        return Err($crate::error::Error::internal_msg(format!($fmt $(, $($arg)*)?)))
367    };
368}
369
370/// Builds a formatted [`Error::internal_msg`] without returning.
371#[macro_export]
372macro_rules! internal_error {
373    ( $fmt:literal $(, $($arg:tt)*)?) => {
374        $crate::error::Error::internal_msg(format!($fmt $(, $($arg)*)?))
375    };
376}
377
378/// Wraps [`Error`] so it satisfies the [`std::error::Error`] trait where a
379/// `'static` source is required (e.g. inside [`anyhow`]).
380pub struct SError(Error);
381
382impl SError {
383    /// Borrows the wrapped [`Error`].
384    pub fn inner(&self) -> &Error {
385        &self.0
386    }
387}
388
389impl Display for SError {
390    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
391        Display::fmt(&self.0, f)
392    }
393}
394
395impl Debug for SError {
396    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
397        Debug::fmt(&self.0, f)
398    }
399}
400
401impl std::error::Error for SError {
402    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
403        self.0.source()
404    }
405}
406
407struct ResidualErrorData {
408    message: String,
409    debug: String,
410}
411
412/// A cheap, cloneable snapshot of an error's rendered text.
413///
414/// When the original error cannot be cloned but its message must reach several
415/// recipients (e.g. every waiter on a failed batch), capture it once here and
416/// hand out clones.
417#[derive(Clone)]
418pub struct ResidualError(Arc<ResidualErrorData>);
419
420impl ResidualError {
421    /// Captures the `Display` and `Debug` renderings of `err`.
422    pub fn new<Err: Display + Debug>(err: &Err) -> Self {
423        Self(Arc::new(ResidualErrorData {
424            message: err.to_string(),
425            debug: err.to_string(),
426        }))
427    }
428}
429
430impl Display for ResidualError {
431    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
432        write!(f, "{}", self.0.message)
433    }
434}
435
436impl Debug for ResidualError {
437    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
438        write!(f, "{}", self.0.debug)
439    }
440}
441
442impl StdError for ResidualError {}
443
444enum SharedErrorState {
445    Error(Error),
446    ResidualErrorMessage(ResidualError),
447}
448
449/// One error shared across many holders.
450///
451/// The first caller to [`into_result`](SharedResultExt::into_result) takes the
452/// fully-typed [`Error`]; the slot then degrades to a [`ResidualError`], so
453/// later callers still get the message but as a generic internal error.
454#[derive(Clone)]
455pub struct SharedError(Arc<Mutex<SharedErrorState>>);
456
457impl SharedError {
458    /// Wraps `err` so it can be shared across clones.
459    pub fn new(err: Error) -> Self {
460        Self(Arc::new(Mutex::new(SharedErrorState::Error(err))))
461    }
462
463    fn extract_error(&self) -> Error {
464        let mut state = self.0.lock().unwrap();
465        let mut_state = &mut *state;
466
467        let residual_err = match mut_state {
468            SharedErrorState::ResidualErrorMessage(err) => {
469                // Already extracted; return a generic internal error with the residual message.
470                return Error::internal(err.clone());
471            }
472            SharedErrorState::Error(err) => ResidualError::new(err),
473        };
474
475        let orig_state = std::mem::replace(
476            mut_state,
477            SharedErrorState::ResidualErrorMessage(residual_err),
478        );
479        let SharedErrorState::Error(err) = orig_state else {
480            panic!("Expected shared error state to hold Error");
481        };
482        err
483    }
484}
485
486impl Debug for SharedError {
487    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
488        let state = self.0.lock().unwrap();
489        match &*state {
490            SharedErrorState::Error(err) => Debug::fmt(err, f),
491            SharedErrorState::ResidualErrorMessage(err) => Debug::fmt(err, f),
492        }
493    }
494}
495
496impl Display for SharedError {
497    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
498        let state = self.0.lock().unwrap();
499        match &*state {
500            SharedErrorState::Error(err) => Display::fmt(err, f),
501            SharedErrorState::ResidualErrorMessage(err) => Display::fmt(err, f),
502        }
503    }
504}
505
506impl From<Error> for SharedError {
507    fn from(err: Error) -> Self {
508        Self(Arc::new(Mutex::new(SharedErrorState::Error(err))))
509    }
510}
511
512/// Constructs an `Ok` in a [`SharedResult`], so call sites need not name
513/// [`SharedError`].
514pub fn shared_ok<T>(value: T) -> std::result::Result<T, SharedError> {
515    Ok(value)
516}
517
518/// A [`Result`](std::result::Result) whose error is a [`SharedError`].
519pub type SharedResult<T> = std::result::Result<T, SharedError>;
520
521/// Converts a [`SharedResult`] into a plain [`Result`] by extracting the shared
522/// error. See [`SharedError`] for the degrade-after-first-take behavior.
523pub trait SharedResultExt<T> {
524    /// Takes ownership and returns the typed error (or value).
525    fn into_result(self) -> Result<T>;
526}
527
528impl<T> SharedResultExt<T> for std::result::Result<T, SharedError> {
529    fn into_result(self) -> Result<T> {
530        match self {
531            Ok(value) => Ok(value),
532            Err(err) => Err(err.extract_error()),
533        }
534    }
535}
536
537/// Borrowing counterpart to [`SharedResultExt`] for `&SharedResult<T>`, yielding
538/// a borrowed value on success.
539pub trait SharedResultExtRef<'a, T> {
540    /// Extracts the shared error, or borrows the value.
541    fn into_result(self) -> Result<&'a T>;
542}
543
544impl<'a, T> SharedResultExtRef<'a, T> for &'a std::result::Result<T, SharedError> {
545    fn into_result(self) -> Result<&'a T> {
546        match self {
547            Ok(value) => Ok(value),
548            Err(err) => Err(err.extract_error()),
549        }
550    }
551}
552
553/// Builds a generic "Invariance violation" error for an unreachable state that
554/// nonetheless needs a value rather than a panic.
555pub fn invariance_violation() -> anyhow::Error {
556    anyhow::anyhow!("Invariance violation")
557}
558
559/// The error type returned at the API boundary, wrapping an [`anyhow::Error`].
560#[derive(Debug)]
561pub struct ApiError {
562    pub err: anyhow::Error,
563}
564
565impl ApiError {
566    /// Builds an API error from a message.
567    pub fn new(message: &str) -> Self {
568        Self {
569            err: anyhow::anyhow!("{}", message),
570        }
571    }
572}
573
574impl Display for ApiError {
575    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
576        Display::fmt(&self.err, f)
577    }
578}
579
580impl StdError for ApiError {
581    fn source(&self) -> Option<&(dyn StdError + 'static)> {
582        self.err.source()
583    }
584}
585
586impl From<anyhow::Error> for ApiError {
587    fn from(err: anyhow::Error) -> ApiError {
588        if err.is::<ApiError>() {
589            return err.downcast::<ApiError>().unwrap();
590        }
591        Self { err }
592    }
593}
594impl From<Error> for ApiError {
595    fn from(err: Error) -> ApiError {
596        ApiError {
597            err: anyhow::Error::from(err.std_error()),
598        }
599    }
600}
601/// Returns early with a formatted [`ApiError`], converted into the caller's
602/// error type via `.into()`.
603#[macro_export]
604macro_rules! api_bail {
605    ( $fmt:literal $(, $($arg:tt)*)?) => {
606        return Err($crate::error::ApiError::new(&format!($fmt $(, $($arg)*)?)).into())
607    };
608}
609
610/// Builds a formatted [`ApiError`] without returning.
611#[macro_export]
612macro_rules! api_error {
613    ( $fmt:literal $(, $($arg:tt)*)?) => {
614        $crate::error::ApiError::new(&format!($fmt $(, $($arg)*)?))
615    };
616}
617
618#[cfg(test)]
619#[cfg_attr(coverage_nightly, coverage(off))]
620mod tests {
621    use super::*;
622    use std::backtrace::BacktraceStatus;
623    use std::io;
624
625    #[derive(Debug)]
626    struct MockHostError(String);
627
628    impl Display for MockHostError {
629        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
630            write!(f, "MockHostError: {}", self.0)
631        }
632    }
633
634    impl StdError for MockHostError {}
635
636    #[test]
637    fn test_client_error_creation() {
638        let err = Error::client("invalid input");
639        assert!(matches!(&err, Error::Client { msg, .. } if msg == "invalid input"));
640        assert!(matches!(err.without_contexts(), Error::Client { .. }));
641    }
642
643    #[test]
644    fn test_internal_error_creation() {
645        let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
646        let err: Error = io_err.into();
647        assert!(matches!(err, Error::Internal { .. }));
648    }
649
650    #[test]
651    fn test_internal_msg_error_creation() {
652        let err = Error::internal_msg("something went wrong");
653        assert!(matches!(err, Error::Internal { .. }));
654        assert_eq!(err.to_string(), "something went wrong");
655    }
656
657    #[test]
658    fn test_host_error_creation_and_detection() {
659        let mock = MockHostError("test error".to_string());
660        let err = Error::host(mock);
661        assert!(matches!(err.without_contexts(), Error::HostLang(_)));
662
663        if let Error::HostLang(host_err) = err.without_contexts() {
664            let any: &dyn Any = host_err.as_ref();
665            let downcasted = any.downcast_ref::<MockHostError>();
666            assert!(downcasted.is_some());
667            assert_eq!(downcasted.unwrap().0, "test error");
668        } else {
669            panic!("Expected HostLang variant");
670        }
671    }
672
673    #[test]
674    fn test_context_chaining() {
675        let inner = Error::client("base error");
676        let with_context: Result<()> = Err(inner);
677        let wrapped = ContextExt::context(
678            ContextExt::context(ContextExt::context(with_context, "layer 1"), "layer 2"),
679            "layer 3",
680        );
681
682        let err = wrapped.unwrap_err();
683        assert!(matches!(&err, Error::Context { msg, .. } if msg == "layer 3"));
684
685        if let Error::Context { source, .. } = &err {
686            assert!(
687                matches!(source.as_ref(), SError(Error::Context { msg, .. }) if msg == "layer 2")
688            );
689        }
690        assert_eq!(
691            err.to_string(),
692            "\nContext:\
693             \n  1: layer 3\
694             \n  2: layer 2\
695             \n  3: layer 1\
696             \nInvalid Request: base error"
697        );
698    }
699
700    #[test]
701    fn test_context_preserves_host_error() {
702        let mock = MockHostError("original python error".to_string());
703        let err = Error::host(mock);
704        let wrapped: Result<()> = Err(err);
705        let with_context = ContextExt::context(wrapped, "while processing request");
706
707        let final_err = with_context.unwrap_err();
708        assert!(matches!(final_err.without_contexts(), Error::HostLang(_)));
709
710        if let Error::HostLang(host_err) = final_err.without_contexts() {
711            let any: &dyn Any = host_err.as_ref();
712            let downcasted = any.downcast_ref::<MockHostError>();
713            assert!(downcasted.is_some());
714            assert_eq!(downcasted.unwrap().0, "original python error");
715        } else {
716            panic!("Expected HostLang variant");
717        }
718    }
719
720    #[test]
721    fn test_backtrace_captured_for_client_error() {
722        let err = Error::client("test");
723        let bt = err.backtrace();
724        assert!(bt.is_some());
725        let status = bt.unwrap().status();
726        assert!(
727            status == BacktraceStatus::Captured
728                || status == BacktraceStatus::Disabled
729                || status == BacktraceStatus::Unsupported
730        );
731    }
732
733    #[test]
734    fn test_backtrace_captured_for_internal_error() {
735        let err = Error::internal_msg("test internal");
736        let bt = err.backtrace();
737        assert!(bt.is_some());
738    }
739
740    #[test]
741    fn test_backtrace_traverses_context() {
742        let inner = Error::internal_msg("base");
743        let wrapped: Result<()> = Err(inner);
744        let with_context = ContextExt::context(wrapped, "context");
745
746        let err = with_context.unwrap_err();
747        let bt = err.backtrace();
748        assert!(bt.is_some());
749    }
750
751    #[test]
752    fn test_option_context_ext() {
753        let opt: Option<i32> = None;
754        let result = opt.context("value was missing");
755
756        assert!(result.is_err());
757        let err = result.unwrap_err();
758        assert!(matches!(err.without_contexts(), Error::Client { .. }));
759        assert!(matches!(&err, Error::Client { msg, .. } if msg == "value was missing"));
760    }
761
762    #[test]
763    fn test_error_display_formats() {
764        let client_err = Error::client("bad input");
765        assert_eq!(client_err.to_string(), "Invalid Request: bad input");
766
767        let internal_err = Error::internal_msg("db connection failed");
768        assert_eq!(internal_err.to_string(), "db connection failed");
769
770        let host_err = Error::host(MockHostError("py error".to_string()));
771        assert_eq!(host_err.to_string(), "MockHostError: py error");
772    }
773
774    #[test]
775    fn test_error_source_chain() {
776        let inner = Error::internal_msg("root cause");
777        let wrapped: Result<()> = Err(inner);
778        let outer = ContextExt::context(wrapped, "outer context").unwrap_err();
779
780        let source = outer.source();
781        assert!(source.is_some());
782    }
783
784    #[test]
785    fn test_internal_from_anyhow() {
786        let err = Error::internal(anyhow::anyhow!("boom"));
787        assert!(matches!(err, Error::Internal(_)));
788        assert_eq!(err.to_string(), "boom");
789    }
790
791    #[test]
792    fn test_source_per_variant() {
793        assert!(Error::client("x").source().is_none());
794        assert!(Error::host(MockHostError("x".into())).source().is_some());
795    }
796
797    #[test]
798    fn test_backtrace_none_for_host_error() {
799        let err = Error::host(MockHostError("x".into()));
800        assert!(err.backtrace().is_none());
801    }
802
803    #[test]
804    fn test_with_context_is_lazy() {
805        let inner = Error::client("base");
806        let wrapped: Result<()> = Err(inner);
807        let err = ContextExt::with_context(wrapped, || "lazy ctx").unwrap_err();
808        assert!(matches!(&err, Error::Context { msg, .. } if msg == "lazy ctx"));
809    }
810
811    #[test]
812    fn test_option_with_context_is_lazy() {
813        let opt: Option<i32> = None;
814        let err = ContextExt::with_context(opt, || "missing value").unwrap_err();
815        assert!(matches!(&err, Error::Client { msg, .. } if msg == "missing value"));
816    }
817
818    #[test]
819    fn test_std_context_ext_wraps_foreign_error() {
820        let r: std::result::Result<(), io::Error> = Err(io::Error::other("io failure"));
821        let err = StdContextExt::context(r, "while doing io").unwrap_err();
822        assert!(matches!(&err, Error::Context { msg, .. } if msg == "while doing io"));
823        assert!(matches!(err.without_contexts(), Error::Internal(_)));
824    }
825
826    #[test]
827    fn test_std_context_ext_with_context_is_lazy() {
828        let r: std::result::Result<(), io::Error> = Err(io::Error::other("io failure"));
829        let err = StdContextExt::with_context(r, || "lazy io ctx").unwrap_err();
830        assert!(matches!(&err, Error::Context { msg, .. } if msg == "lazy io ctx"));
831    }
832
833    #[test]
834    fn test_std_error_wrapper_roundtrip() {
835        let serr = Error::client("boom").std_error();
836        assert_eq!(serr.to_string(), "Invalid Request: boom");
837        assert!(matches!(serr.inner(), Error::Client { .. }));
838        assert!(format!("{serr:?}").contains("boom"));
839    }
840
841    #[test]
842    fn test_invariance_violation_message() {
843        assert_eq!(invariance_violation().to_string(), "Invariance violation");
844    }
845
846    // --- macros ------------------------------------------------------------
847
848    #[test]
849    fn test_client_macros() {
850        fn bail() -> Result<()> {
851            client_bail!("bad {}", 42);
852        }
853        assert!(matches!(&bail().unwrap_err(), Error::Client { msg, .. } if msg == "bad 42"));
854
855        let e = client_error!("oops {}", 1);
856        assert!(matches!(&e, Error::Client { msg, .. } if msg == "oops 1"));
857    }
858
859    #[test]
860    fn test_internal_macros() {
861        fn bail() -> Result<()> {
862            internal_bail!("internal {}", 7);
863        }
864        let err = bail().unwrap_err();
865        assert!(matches!(err, Error::Internal(_)));
866        assert_eq!(err.to_string(), "internal 7");
867
868        let e = internal_error!("ierr {}", 2);
869        assert_eq!(e.to_string(), "ierr 2");
870    }
871
872    #[test]
873    fn test_api_macros() {
874        fn bail() -> std::result::Result<(), ApiError> {
875            api_bail!("api {}", 9);
876        }
877        assert_eq!(bail().unwrap_err().to_string(), "api 9");
878        assert_eq!(api_error!("aerr {}", 3).to_string(), "aerr 3");
879    }
880
881    // --- ResidualError -----------------------------------------------------
882
883    #[test]
884    fn test_residual_error_formats_and_converts() {
885        let base = Error::client("residual base");
886        let residual = ResidualError::new(&base);
887        assert!(residual.to_string().contains("residual base"));
888        assert!(format!("{residual:?}").contains("residual base"));
889
890        let err: Error = residual.into();
891        assert!(matches!(err, Error::Internal(_)));
892    }
893
894    // --- SharedError -------------------------------------------------------
895
896    #[test]
897    fn test_shared_error_degrades_after_first_extraction() {
898        let shared = SharedError::new(Error::client("shared boom"));
899        assert!(shared.to_string().contains("shared boom"));
900        assert!(format!("{shared:?}").contains("shared boom"));
901
902        // First extraction returns the real, fully-typed error.
903        let first: SharedResult<()> = Err(shared.clone());
904        let extracted = first.into_result().unwrap_err();
905        assert!(matches!(extracted.without_contexts(), Error::Client { .. }));
906
907        // After extraction the shared slot holds only the residual message, but
908        // still renders the original text.
909        assert!(shared.to_string().contains("shared boom"));
910        let second: SharedResult<()> = Err(shared.clone());
911        assert!(matches!(
912            second.into_result().unwrap_err(),
913            Error::Internal(_)
914        ));
915    }
916
917    #[test]
918    fn test_shared_ok_and_ref_extension() {
919        assert_eq!(shared_ok::<i32>(5).into_result().unwrap(), 5);
920
921        let ok: SharedResult<i32> = Ok(10);
922        assert_eq!(*(&ok).into_result().unwrap(), 10);
923
924        let errored: SharedResult<i32> = Err(SharedError::new(Error::client("e")));
925        assert!((&errored).into_result().is_err());
926    }
927
928    #[test]
929    fn test_from_error_for_shared_error() {
930        let shared: SharedError = Error::internal_msg("x").into();
931        assert!(shared.to_string().contains("x"));
932    }
933
934    // --- ApiError ----------------------------------------------------------
935
936    #[test]
937    fn test_api_error_new_display_and_into_error() {
938        let api = ApiError::new("api boom");
939        assert_eq!(api.to_string(), "api boom");
940
941        let err: Error = api.into();
942        assert!(matches!(err, Error::Internal(_)));
943    }
944
945    #[test]
946    fn test_api_error_from_anyhow_passthrough_and_downcast() {
947        let api: ApiError = anyhow::anyhow!("plain").into();
948        assert_eq!(api.to_string(), "plain");
949
950        // An anyhow error already wrapping an ApiError downcasts back to it.
951        let any = anyhow::Error::new(ApiError::new("nested"));
952        let api2: ApiError = any.into();
953        assert_eq!(api2.to_string(), "nested");
954    }
955
956    #[test]
957    fn test_api_error_from_core_error() {
958        let api: ApiError = Error::client("bad request").into();
959        assert!(api.to_string().contains("bad request"));
960    }
961
962    // --- From conversions --------------------------------------------------
963
964    #[test]
965    fn test_from_std_library_errors() {
966        let _: Error = "5x".parse::<i32>().unwrap_err().into();
967        let _: Error = "notbool".parse::<bool>().unwrap_err().into();
968        let _: Error = std::fmt::Error.into();
969        let _: Error = String::from_utf8(vec![0xff, 0xfe]).unwrap_err().into();
970
971        let cow: std::borrow::Cow<str> = std::borrow::Cow::Borrowed("cow error");
972        let e: Error = cow.into();
973        assert!(e.to_string().contains("cow error"));
974    }
975
976    #[test]
977    fn test_from_poison_error() {
978        use std::sync::{Arc, Mutex};
979        let m = Arc::new(Mutex::new(0));
980        let m2 = m.clone();
981        // Poison the mutex by panicking while the lock is held.
982        let _ = std::thread::spawn(move || {
983            let _guard = m2.lock().unwrap();
984            panic!("intentional poison");
985        })
986        .join();
987
988        let poison = m.lock().unwrap_err();
989        let err: Error = poison.into();
990        assert!(matches!(err, Error::Internal(_)));
991        assert!(err.to_string().contains("Mutex poison"));
992    }
993
994    #[cfg(feature = "fingerprint")]
995    #[test]
996    fn test_from_base64_decode_error() {
997        use base64::Engine as _;
998        let decode_err = base64::prelude::BASE64_STANDARD.decode("a").unwrap_err();
999        let err: Error = decode_err.into();
1000        assert!(matches!(err, Error::Internal(_)));
1001    }
1002
1003    #[cfg(feature = "fingerprint")]
1004    #[test]
1005    fn test_from_fingerprinter_error() {
1006        use serde::ser::Error as _;
1007        let fe = crate::fingerprint::FingerprinterError::custom("fp boom");
1008        let err: Error = fe.into();
1009        assert!(matches!(err, Error::Internal(_)));
1010    }
1011
1012    #[cfg(any(
1013        feature = "concur_control",
1014        feature = "retryable",
1015        feature = "batching"
1016    ))]
1017    #[tokio::test]
1018    async fn test_from_tokio_errors() {
1019        // oneshot RecvError
1020        let (tx, rx) = tokio::sync::oneshot::channel::<i32>();
1021        drop(tx);
1022        let _: Error = rx.await.unwrap_err().into();
1023
1024        // AcquireError from a closed semaphore
1025        let sem = tokio::sync::Semaphore::new(1);
1026        sem.close();
1027        let _: Error = sem.acquire().await.unwrap_err().into();
1028
1029        // JoinError from a panicking task
1030        let handle = tokio::spawn(async { panic!("boom") });
1031        let _: Error = handle.await.unwrap_err().into();
1032
1033        // watch RecvError when all senders drop
1034        let (wtx, mut wrx) = tokio::sync::watch::channel(1);
1035        drop(wtx);
1036        let _: Error = wrx.changed().await.unwrap_err().into();
1037    }
1038}