diskann/error/ranked.rs
1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6//! Multi-Modal Error Handling
7//!
8//! # Background
9//!
10//! Sometime, data providers can experience spurious failures during operations such as vector
11//! retrieval.
12//!
13//! A primary culprit is access to deleted vectors (if vectors are eagerly deleted rather
14//! than deferred until graph clean up), which may returns an error but is more or
15//! less expected and should not take down the DiskANN algorithm.
16//!
17//! However, not all classes of errors can be ignored in this way, such as errors indicating
18//! that DiskANN should abort processing.
19//!
20//! Furthermore, DiskANN should not be in the habit of ignoring all errors from providers as
21//! many users may want such errors to be propagated.
22//!
23//! This means we need a multi-level error handling mechanism that achieves several design
24//! goals:
25//!
26//! 1. Disambiguate between "transient" errors (those which are expected to resolve eventually
27//! or can be ignored) and "hard" errors (those that should stop the current task and be
28//! propagated) from user error types.
29//!
30//! 2. Require algorithmic code to explicitly acknowledge or escalate "transient" errors with
31//! minimal probability of silently dropping an error or implicitly escalating.
32//!
33//! In this context:
34//!
35//! * "acknowledge": Notify the error that it has been observed and that the algorithm
36//! is choosing to continue.
37//!
38//! * "escalate": Notify the error that is has been observed and is being escalated by
39//! the algorithm into a hard error.
40//!
41//! 3. Provide light-weight callback mechanisms for error acknowledgement or escalation
42//! to enable telemetry in user types.
43//!
44//! Practically, this means that user error types can use diagnostic logging to record
45//! acknowledgements or escalations to assist in debugging efforts.
46//!
47//! 4. Ideally be minimal or no overhead for error types that are never transient.
48//!
49//! # How Does This Work?
50//!
51//! User error types (those implemented by the [`DataProvider`] and (eventually)
52//! [`NeighborProviderAsync`]) are expected to implement [`ToRanked`], which converts the
53//! error type to a [`RankedError`] consisting of a `Transient` and full `Error` alternatives.
54//! The [`RankedError::Transient`] alternative must implement [`TransientError`], which
55//! provides callback mechanisms for transient error acknowledgement or escalation.
56//! These callback are provided with a concrete reason for the decision to aid in debugging
57//! or program telemetry.
58//!
59//! The trait [`ErrorExt`] is then defined which takes `Result`s of such [`ToRanked`] types
60//! and provides the `acknowledge`/`escalate` interface to these `Result`s.
61//!
62//! So, how does this satisfy our goals:
63//!
64//! 1. User types explicitly mark themselves as either "transient" or "hard" when constructing
65//! the [`RankedError`] enum through [`ToRanked`].
66//!
67//! 2. Generic algorithmic code should constrain error types to be just [`ToRanked`]. This
68//! means that the `?` operator will not apply by default, requiring explicit handling
69//! through either [`ErrorExt::acknowledge`] or [`ErrorExt::escalate`].
70//!
71//! 3. The methods in [`ErrorExt`] require reasons for the decision, which are forwarded
72//! the implementation of [`TransientError`] and therefore made available to user types.
73//!
74//! Now, what about low-overhead?
75//!
76//! User types `T` that wish to **always** escalate can use the [`diskann::always_escalate`]
77//! macro. This works be defining the `Transient` alternative of [`ToRanked`] to be the
78//! unconstructable enum [`diskann::error::NeverTransient`]. This ensures at compile time
79//! that the [`RankedError::Transient`] alternative is unreachable and therefore the layout of
80//! `RankedError<NeverTransient, T>` is the same as `T` and all the transient error handling
81//! goes away.
82
83use std::fmt::{Debug, Display};
84
85use crate::{ANNError, ANNResult};
86
87pub trait TransientError<T>: Sized + std::fmt::Debug + Send + Sync {
88 /// Consume self, acknowledging the transient error but proceeding with program logic.
89 ///
90 /// This method accepts a parameter describing the reason for the acknowledgement.
91 fn acknowledge<D>(self, why: D)
92 where
93 D: Display;
94
95 /// Consume self, acknowledging the transient error but proceeding with program logic.
96 ///
97 /// The closure `why` provides a deferred method for evaluating the acknowledgement
98 /// reason and can be used by error types that do not log acknowledgements to avoid
99 /// evaluating the reason all-together.
100 ///
101 /// # Track Caller
102 ///
103 /// The provided implementation is annotated with `track_caller`.
104 #[track_caller]
105 fn acknowledge_with<F, D>(self, why: F)
106 where
107 F: FnOnce() -> D,
108 D: Display,
109 {
110 self.acknowledge(why())
111 }
112
113 /// Report to `self` that transient errors are not acceptable in the current context
114 /// and that the transient error is being upgraded to a full error.
115 ///
116 /// This method accepts a parameter describing the reason for the escalation.
117 fn escalate<D>(self, why: D) -> T
118 where
119 D: Display;
120
121 /// Report to `self` that transient errors are not acceptable in the current context
122 /// and that the transient error is being upgraded to a full error.
123 ///
124 /// The closure `why` provides a deferred method for evaluating the acknowledgement
125 /// reason and can be used by error types that do not log acknowledgements to avoid
126 /// evaluating the reason all-together.
127 ///
128 /// # Track Caller
129 ///
130 /// The provided implementation is annotated with `track_caller`.
131 #[track_caller]
132 fn escalate_with<F, D>(self, why: F) -> T
133 where
134 F: FnOnce() -> D,
135 D: Display,
136 {
137 self.escalate(why())
138 }
139}
140
141/// Allow conversion from an error type to a [`RankedError`].
142///
143/// This trait bound is applied to providers that may return transient or non-critical
144/// failures. The [`RankedError`] allows DiskANN algorithmic logic to determine whether
145/// an error can be suppressed safely or needs to be escalated.
146///
147/// * See also: [`always_escalate!`], [`ErrorExt`].
148pub trait ToRanked {
149 type Transient: TransientError<Self::Error>;
150 type Error: Into<ANNError> + std::fmt::Debug + Send + Sync;
151
152 /// Convert `self` into a `RankedError`.
153 fn to_ranked(self) -> RankedError<Self::Transient, Self::Error>;
154
155 /// Construct `Self` from its transient variant.
156 fn from_transient(transient: Self::Transient) -> Self;
157
158 /// Construct `Self` from its error variant.
159 fn from_error(error: Self::Error) -> Self;
160}
161
162/// An error type consisting of a transient (non-critical) error and an unignorable error.
163#[must_use]
164#[derive(Debug)]
165pub enum RankedError<R, E>
166where
167 R: TransientError<E>,
168{
169 /// This error can be ignored if the allowed by the caller code. Such errors should
170 /// be acknowledged (see [`TransientError::acknowledge`] before being dropped, but
171 /// aren't necessarily required to be.
172 Transient(R),
173
174 /// This error cannot be ignored and should be propagated. The caller should **never**
175 /// suppress values in this alternative.
176 Error(E),
177}
178
179impl<R, E> ToRanked for RankedError<R, E>
180where
181 R: TransientError<E>,
182 E: Into<ANNError> + std::fmt::Debug + Send + Sync,
183{
184 type Transient = R;
185 type Error = E;
186
187 fn to_ranked(self) -> Self {
188 self
189 }
190
191 fn from_transient(transient: <Self as ToRanked>::Transient) -> Self {
192 Self::Transient(transient)
193 }
194
195 fn from_error(error: <Self as ToRanked>::Error) -> Self {
196 Self::Error(error)
197 }
198}
199
200/// A zero-sized type that is unconstructable.
201///
202/// This is used as the [`TransientError`] type for error types using the
203/// [`always_escalate!`] macro to opt-out of transient error handling.
204#[derive(Debug)]
205pub enum NeverTransient {}
206
207/// Mark the type `T` as "Always Escalating". This will implement `ToRanked for $T` in a
208/// way that:
209///
210/// 1. Ensures `to_ranked` **always** returns [`RankedError::Error`].
211///
212/// 2. Defines the "transient" portion of `ToRanked` in such a way that it is impossible to
213/// instantiate. This means that the layout of [`RankedError`] is identical to the layout
214/// of `$T` and allows the methods in [`ErrorExt`] to optimize out all transient error
215/// handling.
216#[macro_export]
217macro_rules! always_escalate {
218 ($T:ty) => {
219 impl $crate::error::TransientError<$T> for $crate::error::NeverTransient {
220 fn acknowledge<D>(self, _: D)
221 where
222 D: std::fmt::Display,
223 {
224 unreachable!("NeverTransient is an unconstructable type");
225 }
226 fn acknowledge_with<F, D>(self, _: F)
227 where
228 F: FnOnce() -> D,
229 D: std::fmt::Display,
230 {
231 unreachable!("NeverTransient is an unconstructable type");
232 }
233 fn escalate<D>(self, _: D) -> $T
234 where
235 D: std::fmt::Display,
236 {
237 unreachable!("NeverTransient is an unconstructable type");
238 }
239 fn escalate_with<F, D>(self, _: F) -> $T
240 where
241 F: FnOnce() -> D,
242 D: std::fmt::Display,
243 {
244 unreachable!("NeverTransient is an unconstructable type");
245 }
246 }
247
248 impl $crate::error::ToRanked for $T {
249 type Transient = $crate::error::NeverTransient;
250 type Error = Self;
251
252 fn to_ranked(self) -> $crate::error::RankedError<Self::Transient, Self::Error> {
253 $crate::error::RankedError::Error(self)
254 }
255
256 fn from_transient(_: $crate::error::NeverTransient) -> Self {
257 unreachable!("NeverTransient is an unconstructable type");
258 }
259
260 fn from_error(error: Self) -> Self {
261 error
262 }
263 }
264 };
265}
266
267/// An infallible error type so that the compiler knows this error can
268/// never be thrown.
269#[derive(Debug, Clone, PartialEq, Eq)]
270pub enum Infallible {}
271
272impl From<Infallible> for ANNError {
273 fn from(_: Infallible) -> Self {
274 // This is unreachable but we need to implement it to keep the compiler happy when
275 // the error type for an associated type must implement `Into<ANNError>` and is
276 // `Infallible`. E.g. `VectorRepr` impl for f32.
277 unreachable!()
278 }
279}
280
281impl std::fmt::Display for Infallible {
282 fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283 unreachable!()
284 }
285}
286
287impl std::error::Error for Infallible {}
288
289impl Infallible {
290 /// Match result containing `Infallible` error type.
291 ///
292 /// This function is a utility for unwrapping a `Result` that has an `Infallible` error type.
293 /// Since `Infallible` represents a type that cannot be instantiated, any `Result<T, Infallible>`
294 /// is guaranteed to be `Ok(T)`. This function safely extracts the value from such a result.
295 ///
296 /// # Arguments
297 ///
298 /// * `x` - A result with an infallible error type.
299 ///
300 /// # Returns
301 ///
302 /// The inner value `T` from the `Ok` variant.
303 ///
304 /// # Example
305 ///
306 /// ```
307 /// use diskann::error::Infallible;
308 ///
309 /// let result: Result<i32, Infallible> = Ok(42);
310 /// let value = Infallible::match_infallible(result);
311 /// assert_eq!(value, 42);
312 /// ```
313 pub fn match_infallible<T>(x: Result<T, Infallible>) -> T {
314 x.unwrap_or_else(|inf| match inf {})
315 }
316}
317
318always_escalate!(Infallible);
319
320/// Provide explicit error handling for compatible result types.
321///
322/// This trait requires the caller to differentiate between callsites that can handle
323/// transient errors and those that cannot.
324///
325/// Explicit acknowledgement or escalation of transient errors is accompanied by a reason
326/// why to assist with telementry.
327pub trait ErrorExt<T> {
328 /// Acknowledge and drop transient errors while propagating critical errors.
329 ///
330 /// * If `self` is not an error, return `Ok(Some(v))`.
331 /// * If `self` is a transient error, acknowledge it with the provided reason and return
332 /// `Ok(None)`.
333 /// * Otherwise, return an error.
334 fn allow_transient<D>(self, why: D) -> ANNResult<Option<T>>
335 where
336 D: Display;
337
338 /// Acknowledge and drop transient errors while propagating critical errors.
339 ///
340 /// * If `self` is not an error, return `Ok(Some(v))`.
341 /// * If `self` is a transient error, acknowledge it with the provided reason and return
342 /// `Ok(None)`.
343 /// * Otherwise, return an error.
344 fn allow_transient_with<F, D>(self, why: F) -> ANNResult<Option<T>>
345 where
346 F: FnOnce() -> D,
347 D: Display;
348
349 /// Escalate transient errors into full errors.
350 ///
351 /// * If `self` is not an error, return `Ok(v)`.
352 /// * If `self` is a transient error, escalate with the provided reason and return the
353 /// escalated error.
354 /// * Otherwise, return the critical error.
355 fn escalate<D>(self, why: D) -> ANNResult<T>
356 where
357 D: Display;
358
359 /// Escalate transient errors into full errors.
360 ///
361 /// * If `self` is not an error, return `Ok(v)`.
362 /// * If `self` is a transient error, escalate with the provided reason and return the
363 /// escalated error.
364 /// * Otherwise, return the critical error.
365 fn escalate_with<F, D>(self, why: F) -> ANNResult<T>
366 where
367 F: FnOnce() -> D,
368 D: Display;
369}
370
371impl<T, E> ErrorExt<T> for Result<T, E>
372where
373 E: ToRanked,
374{
375 #[track_caller]
376 fn allow_transient<D>(self, why: D) -> ANNResult<Option<T>>
377 where
378 D: Display,
379 {
380 match self {
381 Ok(v) => Ok(Some(v)),
382 Err(err) => match err.to_ranked() {
383 RankedError::Transient(transient) => {
384 transient.acknowledge(why);
385 Ok(None)
386 }
387 RankedError::Error(err) => Err(err.into()),
388 },
389 }
390 }
391
392 #[track_caller]
393 fn allow_transient_with<F, D>(self, why: F) -> ANNResult<Option<T>>
394 where
395 F: FnOnce() -> D,
396 D: Display,
397 {
398 match self {
399 Ok(v) => Ok(Some(v)),
400 Err(err) => match err.to_ranked() {
401 RankedError::Transient(transient) => {
402 transient.acknowledge_with(why);
403 Ok(None)
404 }
405 RankedError::Error(err) => Err(err.into()),
406 },
407 }
408 }
409
410 #[track_caller]
411 fn escalate<D>(self, why: D) -> ANNResult<T>
412 where
413 D: Display,
414 {
415 match self {
416 Ok(v) => Ok(v),
417 Err(err) => match err.to_ranked() {
418 RankedError::Transient(transient) => Err(transient.escalate(why).into()),
419 RankedError::Error(err) => Err(err.into()),
420 },
421 }
422 }
423
424 #[track_caller]
425 fn escalate_with<F, D>(self, why: F) -> ANNResult<T>
426 where
427 F: FnOnce() -> D,
428 D: Display,
429 {
430 match self {
431 Ok(v) => Ok(v),
432 Err(err) => match err.to_ranked() {
433 RankedError::Transient(transient) => Err(transient.escalate_with(why).into()),
434 RankedError::Error(err) => Err(err.into()),
435 },
436 }
437 }
438}
439
440///////////
441// Tests //
442///////////
443
444#[cfg(test)]
445mod tests {
446 use std::sync::Mutex;
447
448 use thiserror::Error;
449
450 use super::*;
451
452 // Check that the layout of Ranked "Always Escalate" types `T` is the same as the layout
453 // for `T`
454 #[derive(Debug, Clone, Copy, Error)]
455 #[error("generic error message: {0}")]
456 struct AlwaysEscalate(usize);
457
458 impl From<AlwaysEscalate> for ANNError {
459 fn from(value: AlwaysEscalate) -> ANNError {
460 ANNError::log_index_error(value)
461 }
462 }
463
464 always_escalate!(AlwaysEscalate);
465
466 #[test]
467 fn test_always_escalate() {
468 // The goal here is to ensure that the ranked version of a type that has opted in
469 // to `always_escalate!` has the same layout (i.e., same size) as the error type
470 // itself.
471 //
472 // The goal is to make ranking always-escalate errors
473 assert_eq!(
474 std::mem::size_of::<RankedError<NeverTransient, AlwaysEscalate>>(),
475 std::mem::size_of::<AlwaysEscalate>()
476 );
477
478 let r = AlwaysEscalate(10).to_ranked();
479 assert!(matches!(r, RankedError::Error(AlwaysEscalate(10))));
480 }
481
482 #[derive(Debug, Error)]
483 #[error(
484 "Bomb: value = {}, ack = {}, escalated = {}",
485 value,
486 acknowledged,
487 escalated
488 )]
489 struct Bomb<'a> {
490 messages: &'a Mutex<Vec<(String, u32)>>,
491 acknowledged: bool,
492 escalated: bool,
493 value: u64,
494 }
495
496 impl<'a> Bomb<'a> {
497 fn new(messages: &'a Mutex<Vec<(String, u32)>>, value: u64) -> Self {
498 Self {
499 messages,
500 acknowledged: false,
501 escalated: false,
502 value,
503 }
504 }
505 }
506
507 impl Drop for Bomb<'_> {
508 fn drop(&mut self) {
509 if !self.acknowledged && !self.escalated {
510 panic!("Bomb error was neither acknowledged nor escalated");
511 }
512 if self.acknowledged && self.escalated {
513 panic!("Bomb error was both acknowledged and escalated");
514 }
515 }
516 }
517
518 #[derive(Debug, Error)]
519 #[error("Disarmed: value = {}", value)]
520 struct Disarmed<'a> {
521 messages: &'a Mutex<Vec<(String, u32)>>,
522 value: u64,
523 }
524
525 impl<'a> Disarmed<'a> {
526 fn new(messages: &'a Mutex<Vec<(String, u32)>>, value: u64) -> Self {
527 Self { messages, value }
528 }
529 }
530
531 impl<'a> TransientError<Disarmed<'a>> for Bomb<'a> {
532 #[track_caller]
533 fn acknowledge<D>(mut self, why: D)
534 where
535 D: Display,
536 {
537 self.acknowledged = true;
538 let mut v = self.messages.lock().unwrap();
539 let location = std::panic::Location::caller();
540 v.push((format!("acknowledged: {}", why), location.line()))
541 }
542
543 #[track_caller]
544 fn escalate<D>(mut self, why: D) -> Disarmed<'a>
545 where
546 D: Display,
547 {
548 self.escalated = true;
549 let mut v = self.messages.lock().unwrap();
550 let location = std::panic::Location::caller();
551 v.push((format!("escalated: {}", why), location.line()));
552
553 Disarmed {
554 messages: self.messages,
555 value: self.value,
556 }
557 }
558 }
559
560 impl From<Disarmed<'_>> for ANNError {
561 #[track_caller]
562 fn from(value: Disarmed<'_>) -> ANNError {
563 ANNError::log_index_error(&value)
564 }
565 }
566
567 struct MaybeTransient<'a> {
568 messages: &'a Mutex<Vec<(String, u32)>>,
569 value: u64,
570 transient: bool,
571 }
572
573 impl<'a> MaybeTransient<'a> {
574 fn new(messages: &'a Mutex<Vec<(String, u32)>>, value: u64, transient: bool) -> Self {
575 Self {
576 messages,
577 value,
578 transient,
579 }
580 }
581 }
582
583 impl<'a> ToRanked for MaybeTransient<'a> {
584 type Transient = Bomb<'a>;
585 type Error = Disarmed<'a>;
586
587 fn to_ranked(self) -> RankedError<Self::Transient, Self::Error> {
588 if self.transient {
589 RankedError::Transient(Bomb::new(self.messages, self.value))
590 } else {
591 RankedError::Error(Disarmed::new(self.messages, self.value))
592 }
593 }
594
595 fn from_transient(mut transient: Bomb<'a>) -> Self {
596 transient.acknowledged = true;
597 Self::new(transient.messages, transient.value, true)
598 }
599
600 fn from_error(error: Disarmed<'a>) -> Self {
601 Self::new(error.messages, error.value, false)
602 }
603 }
604
605 #[test]
606 fn to_ranked_idempotent() {
607 let messages = Mutex::new(Vec::<(String, u32)>::new());
608
609 let v = MaybeTransient::new(&messages, 10, true).to_ranked();
610 assert!(matches!(v, RankedError::Transient(..)));
611
612 // Disarm the bomb
613 match v.to_ranked() {
614 RankedError::Transient(v) => v.acknowledge(""),
615 _ => panic!("wrong variant"),
616 }
617
618 let v = MaybeTransient::new(&messages, 10, false).to_ranked();
619 assert!(matches!(v, RankedError::Error(..)));
620 let v = v.to_ranked();
621 assert!(matches!(v, RankedError::Error(..)));
622 }
623
624 #[test]
625 fn error_ext_allow_transient_ok() {
626 let messages = Mutex::new(Vec::<(String, u32)>::new());
627
628 // Non-Error Path.
629 let v: usize = Result::<usize, MaybeTransient<'_>>::Ok(10)
630 .allow_transient("hello")
631 .unwrap()
632 .unwrap();
633 assert_eq!(v, 10);
634 assert!(messages.lock().unwrap().is_empty());
635 }
636
637 #[test]
638 fn error_ext_allow_transient_with_ok() {
639 let messages = Mutex::new(Vec::<(String, u32)>::new());
640
641 // Non-Error Path.
642 let v: usize = Result::<usize, MaybeTransient<'_>>::Ok(10)
643 .allow_transient_with(|| -> &str {
644 panic!("this should not be called!");
645 })
646 .unwrap()
647 .unwrap();
648 assert_eq!(v, 10);
649 assert!(messages.lock().unwrap().is_empty());
650 }
651
652 #[test]
653 fn error_ext_escalate_ok() {
654 let messages = Mutex::new(Vec::<(String, u32)>::new());
655
656 // Non-Error Path.
657 let v: usize = Result::<usize, MaybeTransient<'_>>::Ok(10)
658 .escalate("hello")
659 .unwrap();
660 assert_eq!(v, 10);
661 assert!(messages.lock().unwrap().is_empty());
662 }
663
664 #[test]
665 fn error_ext_escalate_with_ok() {
666 let messages = Mutex::new(Vec::<(String, u32)>::new());
667
668 // Non-Error Path.
669 let v: usize = Result::<usize, MaybeTransient<'_>>::Ok(10)
670 .escalate_with(|| -> &str {
671 panic!("this should not be called");
672 })
673 .unwrap();
674
675 assert_eq!(v, 10);
676 assert!(messages.lock().unwrap().is_empty());
677 }
678
679 #[test]
680 fn error_ext_allow_transient_transient() {
681 let messages = Mutex::new(Vec::<(String, u32)>::new());
682
683 // Error path - transient
684 let why = "foo";
685 let line = line!();
686 assert!(
687 Result::<usize, MaybeTransient<'_>>::Err(MaybeTransient::new(&messages, 10, true))
688 .allow_transient(why)
689 .unwrap()
690 .is_none()
691 );
692
693 let m = messages.lock().unwrap();
694 assert_eq!(m.len(), 1);
695 assert_eq!(m[0].1, line + 3);
696 assert_eq!(m[0].0, format!("acknowledged: {}", why));
697 }
698
699 #[test]
700 fn error_ext_allow_transient_with_transient() {
701 let messages = Mutex::new(Vec::<(String, u32)>::new());
702
703 // Error path - transient
704 let why = "foo";
705 let mut called: bool = false;
706 let line = line!();
707 assert!(
708 Result::<usize, MaybeTransient<'_>>::Err(MaybeTransient::new(&messages, 10, true))
709 .allow_transient_with(|| {
710 called = true;
711 why
712 })
713 .unwrap()
714 .is_none()
715 );
716
717 assert!(called);
718 let m = messages.lock().unwrap();
719 assert_eq!(m.len(), 1);
720 assert_eq!(m[0].1, line + 3);
721 assert_eq!(m[0].0, format!("acknowledged: {}", why));
722 }
723
724 #[test]
725 fn error_ext_escalate_transient() {
726 let messages = Mutex::new(Vec::<(String, u32)>::new());
727
728 // Error path - transient
729 let why = "foo";
730 let line = line!();
731 assert!(
732 Result::<usize, MaybeTransient<'_>>::Err(MaybeTransient::new(&messages, 10, true))
733 .escalate(why)
734 .is_err()
735 );
736
737 let m = messages.lock().unwrap();
738 assert_eq!(m.len(), 1);
739 assert_eq!(m[0].1, line + 3);
740 assert_eq!(m[0].0, format!("escalated: {}", why));
741 }
742
743 #[test]
744 fn error_ext_escalate_with_transient() {
745 let messages = Mutex::new(Vec::<(String, u32)>::new());
746
747 // Error path - transient
748 let why = "foo";
749 let mut called: bool = false;
750 let line = line!();
751 assert!(
752 Result::<usize, MaybeTransient<'_>>::Err(MaybeTransient::new(&messages, 10, true))
753 .escalate_with(|| {
754 called = true;
755 why
756 })
757 .is_err()
758 );
759
760 assert!(called);
761 let m = messages.lock().unwrap();
762 assert_eq!(m.len(), 1);
763 assert_eq!(m[0].1, line + 3);
764 assert_eq!(m[0].0, format!("escalated: {}", why));
765 }
766
767 #[test]
768 fn error_ext_allow_transient_error() {
769 let messages = Mutex::new(Vec::<(String, u32)>::new());
770
771 // Error path - error
772 let why = "foo";
773 assert!(
774 Result::<usize, MaybeTransient<'_>>::Err(MaybeTransient::new(&messages, 10, false))
775 .allow_transient(why)
776 .is_err()
777 );
778
779 assert!(messages.lock().unwrap().is_empty());
780 }
781
782 #[test]
783 fn error_ext_allow_transient_with_error() {
784 let messages = Mutex::new(Vec::<(String, u32)>::new());
785
786 // Error path - error
787 assert!(
788 Result::<usize, MaybeTransient<'_>>::Err(MaybeTransient::new(&messages, 10, false))
789 .allow_transient_with(|| -> &str {
790 panic!("should not be called");
791 })
792 .is_err()
793 );
794
795 assert!(messages.lock().unwrap().is_empty());
796 }
797
798 #[test]
799 fn error_ext_escalate_error() {
800 let messages = Mutex::new(Vec::<(String, u32)>::new());
801
802 // Error path - error
803 let why = "foo";
804 assert!(
805 Result::<usize, MaybeTransient<'_>>::Err(MaybeTransient::new(&messages, 10, false))
806 .escalate(why)
807 .is_err()
808 );
809
810 assert!(messages.lock().unwrap().is_empty());
811 }
812
813 #[test]
814 fn error_ext_escalate_with_error() {
815 let messages = Mutex::new(Vec::<(String, u32)>::new());
816
817 // Error path - error
818 assert!(
819 Result::<usize, MaybeTransient<'_>>::Err(MaybeTransient::new(&messages, 10, false))
820 .escalate_with(|| -> &str {
821 panic!("should not be called");
822 })
823 .is_err()
824 );
825
826 assert!(messages.lock().unwrap().is_empty());
827 }
828
829 #[test]
830 fn test_infallible() {
831 // Test that match_infallible can extract values from Ok results
832 let result: Result<i32, Infallible> = Ok(42);
833 let value = Infallible::match_infallible(result);
834 assert_eq!(value, 42);
835
836 // Test with different types
837 let result: Result<String, Infallible> = Ok("hello".to_string());
838 let value = Infallible::match_infallible(result);
839 assert_eq!(value, "hello");
840
841 // Test that Infallible converts to ANNError (though this should never happen in practice)
842 // We can't actually construct an Infallible value to test this directly since it's unconstructable
843 // But we can verify the From implementation exists by checking the type constraint
844 fn _test_infallible_into_ann_error(_: Infallible) -> ANNError {
845 ANNError::log_index_error("This should never be called")
846 }
847 }
848}