use errlanes::{Denied, Fail, Fatal, Fault, ResultExt, Transient, TransientKind, lanes};
type Tf = lanes!(Transient, Fatal);
type Df = lanes!(Denied, Fatal);
#[derive(Debug, Clone, PartialEq, Eq, errlanes::Rejection)]
#[rejection(code = "SMALL")]
struct Small;
fn execute(
mut inner: impl FnMut() -> Result<u64, Fault<lanes!(Transient, Fatal)>>,
) -> Result<u64, Fault<lanes!(Fatal)>> {
let mut attempts = 0;
loop {
attempts += 1;
match inner() {
Ok(value) => return Ok(value),
Err(failure) if failure.is_transient() && attempts < 3 => continue,
Err(failure) => return Err(failure.narrow_transient(attempts)),
}
}
}
#[test]
fn retry_hands_back_the_narrowed_profile() {
let mut attempts = 0;
let value = execute(|| {
attempts += 1;
if attempts == 1 {
Err(Transient::new(TransientKind::Deadlock).into())
} else {
Ok(42)
}
})
.unwrap();
assert_eq!(value, 42);
assert_eq!(attempts, 2);
let err = execute(|| Err(Fatal::invariant("broken state").into())).unwrap_err();
let Fault::Fatal(fatal) = err;
assert_eq!(fatal.context.as_deref(), Some("broken state"));
let err = execute(|| Err(Transient::new(TransientKind::Deadlock).into())).unwrap_err();
let Fault::Fatal(fatal) = err;
assert_eq!(fatal.kind, errlanes::FatalKind::Exhausted);
assert!(
std::error::Error::source(&fatal)
.unwrap()
.is::<errlanes::Exhausted>()
);
}
#[test]
fn result_narrow_rejected_turns_fail_into_fault() {
let result: Result<u8, Fail<Small, Tf>> = Err(Fail::Rejected(Small));
let narrowed: Result<u8, Fault<Tf>> = result.narrow_rejected();
match narrowed {
Err(Fault::Fatal(fatal)) => {
assert_eq!(fatal.kind, errlanes::FatalKind::Invariant);
assert!(
std::error::Error::source(&fatal)
.unwrap()
.downcast_ref::<Small>()
.is_some(),
"the rejection must still be reachable for a handler or test to downcast"
);
let message = fatal.to_string();
assert!(
!message.contains("SMALL"),
"narrow_rejected's rejection Display must never reach an operator-facing \
message; got {message:?}"
);
}
other => panic!("expected Err(Fault::Fatal(Invariant)), got {other:?}"),
}
}
#[test]
fn result_narrow_rejected_on_a_bare_rejection_through_question_mark() {
fn listen() -> Result<u8, Small> {
Err(Small)
}
fn into_fault() -> Result<u8, Fault<Tf>> {
listen().narrow_rejected()?;
Ok(1)
}
fn into_fail() -> Result<u8, Fail<Small, lanes!(Fatal)>> {
listen().narrow_rejected()?;
Ok(1)
}
fn into_bare_fatal() -> Result<u8, Fatal> {
listen().narrow_rejected()
}
let fault = into_fault().unwrap_err();
let Fault::Fatal(fatal) = &fault else {
panic!("expected Fatal(Invariant), got {fault:?}")
};
assert_eq!(fatal.kind, errlanes::FatalKind::Invariant);
assert!(
std::error::Error::source(fatal)
.unwrap()
.downcast_ref::<Small>()
.is_some(),
"the rejection must still be reachable for a handler or test to downcast"
);
let message = fault.message();
assert!(
!message.contains("SMALL"),
"a bare rejection's Display must never reach an operator-facing message; \
got {message:?}"
);
assert!(matches!(into_fail().unwrap_err(), Fail::Fatal(_)));
assert_eq!(
into_bare_fatal().unwrap_err().kind,
errlanes::FatalKind::Invariant
);
}
#[test]
fn result_rejected_splits_the_rejection_from_the_faults() {
let ok: Result<u8, Fail<Small, Tf>> = Ok(7);
assert_eq!(ok.rejected().unwrap().unwrap(), 7);
let rejected: Result<u8, Fail<Small, Tf>> = Err(Fail::Rejected(Small));
assert_eq!(rejected.rejected().unwrap().unwrap_err(), Small);
let transient: Result<u8, Fail<Small, Tf>> =
Err(Transient::new(TransientKind::Deadlock).into());
match transient.rejected() {
Err(Fault::Transient(t)) => assert_eq!(t.kind, TransientKind::Deadlock),
other => panic!("expected Err(Fault::Transient), got {other:?}"),
}
}
#[test]
fn result_rejected_propagates_faults_through_question_mark() {
#[derive(Debug, PartialEq, Eq, errlanes::Rejection)]
#[rejection(code = "TIMED_OUT")]
struct TimedOut;
fn wait_forever(
mut await_once: impl FnMut() -> Result<u64, Fail<TimedOut, Tf>>,
) -> Result<u64, Fail<TimedOut, Tf>> {
loop {
match await_once().rejected()? {
Ok(outcome) => break Ok(outcome),
Err(TimedOut) => continue,
}
}
}
let mut calls = 0;
let outcome = wait_forever(|| {
calls += 1;
if calls < 3 {
Err(Fail::Rejected(TimedOut))
} else {
Ok(42)
}
});
assert_eq!(outcome.unwrap(), 42);
assert_eq!(calls, 3);
let fatal = wait_forever(|| Err(Fatal::invariant("router not started").into())).unwrap_err();
match fatal {
Fail::Fatal(f) => assert_eq!(f.context.as_deref(), Some("router not started")),
other => panic!("expected Fail::Fatal, got {other:?}"),
}
}
#[test]
fn result_map_rejected_touches_only_the_rejected_lane() {
#[derive(Debug, PartialEq, Eq, errlanes::Rejection)]
#[rejection(code = "ATTEMPTED")]
struct Attempted(u8);
let rejected: Result<u8, Fail<Small, Tf>> = Err(Fail::Rejected(Small));
let mapped: Result<u8, Fail<Attempted, Tf>> = rejected.map_rejected(|Small| Attempted(9));
assert!(matches!(mapped, Err(Fail::Rejected(Attempted(9)))));
let transient: Result<u8, Fail<Small, Tf>> =
Err(Transient::new(TransientKind::Deadlock).into());
match transient.map_rejected(|Small| Attempted(9)) {
Err(Fail::Transient(t)) => assert_eq!(t.kind, TransientKind::Deadlock),
other => panic!("expected Err(Fail::Transient), got {other:?}"),
}
let ok: Result<u8, Fail<Small, Tf>> = Ok(7);
assert_eq!(
ok.map_rejected(|Small| unreachable!("Ok never reaches the closure"))
.unwrap(),
7
);
}
#[test]
fn result_narrow_transient_on_both_carriers() {
let fault: Result<u8, Fault<Tf>> = Err(Transient::new(TransientKind::Deadlock).into());
match fault.narrow_transient(3) {
Err(Fault::Fatal(f)) => assert_eq!(f.kind, errlanes::FatalKind::Exhausted),
other => panic!("expected Err(Fault::Fatal(Exhausted)), got {other:?}"),
}
let fail: Result<u8, Fail<Small, Tf>> = Err(Transient::new(TransientKind::Deadlock).into());
match fail.narrow_transient(3) {
Err(Fail::Fatal(f)) => assert_eq!(f.kind, errlanes::FatalKind::Exhausted),
other => panic!("expected Err(Fail::Fatal(Exhausted)), got {other:?}"),
}
}
#[test]
fn result_narrow_denied_on_both_carriers() {
let fault: Result<u8, Fault<Df>> = Err(Denied::default().into());
match fault.narrow_denied() {
Err(Fault::Fatal(f)) => assert_eq!(f.kind, errlanes::FatalKind::Denied),
other => panic!("expected Err(Fault::Fatal(Denied)), got {other:?}"),
}
let fail: Result<u8, Fail<Small, Df>> = Err(Denied::default().into());
match fail.narrow_denied() {
Err(Fail::Fatal(f)) => assert_eq!(f.kind, errlanes::FatalKind::Denied),
other => panic!("expected Err(Fail::Fatal(Denied)), got {other:?}"),
}
}
#[test]
fn result_expand_infers_destination_through_question_mark() {
fn inner() -> Result<u8, Fail<Small, lanes!(Fatal)>> {
Err(Fail::Rejected(Small))
}
fn outer() -> Result<u8, Fail<Small, Tf>> {
let value = inner().lift()?;
Ok(value)
}
match outer() {
Err(Fail::Rejected(Small)) => {}
other => panic!("expected Err(Fail::Rejected(Small)), got {other:?}"),
}
}
#[test]
fn result_expand_accepts_turbofish() {
let r: Result<u8, Fail<Small, lanes!(Fatal)>> = Err(Fail::Rejected(Small));
let converted = r.lift::<Small>();
match converted {
Err(Fail::Rejected(Small)) => {}
other => panic!("expected Err(Fail::Rejected(Small)), got {other:?}"),
}
}
#[cfg(feature = "tracing")]
#[test]
fn result_classify_and_record_behave_as_before() {
#[derive(Debug, errlanes::Classify)]
#[classify(fatal(CorruptState), from)]
struct Stored(std::io::Error);
fn decode() -> Result<u8, std::io::Error> {
Err(std::io::Error::other("x"))
}
fn fault_fn() -> Result<u8, Fault<Tf>> {
let _ = decode().classify::<Stored>()?;
Ok(0)
}
match fault_fn() {
Err(Fault::Fatal(f)) => assert_eq!(f.kind, errlanes::FatalKind::CorruptState),
other => panic!("expected Err(Fault::Fatal(CorruptState)), got {other:?}"),
}
let span = tracing::info_span!(
"boundary",
error = tracing::field::Empty,
error.lane = tracing::field::Empty,
error.code = tracing::field::Empty,
error.level = tracing::field::Empty,
exception.message = tracing::field::Empty,
exception.type = tracing::field::Empty,
);
let _enter = span.enter();
let result: Result<(), Fail<Small, Tf>> = Err(Fail::Rejected(Small)).record();
assert!(matches!(result, Err(Fail::Rejected(Small))));
let ok: Result<u8, Fail<Small, Tf>> = Ok(7).record();
assert_eq!(ok.unwrap(), 7);
}