#[cfg(test)]
mod tests {
use crate::effect::prelude::*;
use crate::predicate::PredicateExt;
#[derive(Debug, Clone, PartialEq)]
enum TestError {
Recoverable(String),
Fatal(String),
}
impl TestError {
fn is_recoverable(&self) -> bool {
matches!(self, TestError::Recoverable(_))
}
}
#[tokio::test]
async fn test_recover_on_success() {
let effect = pure::<_, TestError, ()>(5).recover(
|_: &TestError| panic!("predicate should not be called"),
|_: TestError| pure::<i32, TestError, ()>(0),
);
assert_eq!(effect.execute(&()).await, Ok(5));
}
#[tokio::test]
async fn test_recover_on_matching_error() {
let effect = fail::<i32, _, ()>(TestError::Recoverable("oops".into()))
.recover(|e: &TestError| e.is_recoverable(), |_| pure(42));
assert_eq!(effect.execute(&()).await, Ok(42));
}
#[tokio::test]
async fn test_recover_on_non_matching_error() {
let effect = fail::<i32, _, ()>(TestError::Fatal("boom".into()))
.recover(|e: &TestError| e.is_recoverable(), |_| pure(42));
assert_eq!(
effect.execute(&()).await,
Err(TestError::Fatal("boom".into()))
);
}
#[tokio::test]
async fn test_recover_not_called_on_success() {
let effect = pure::<_, TestError, ()>(5).recover(
|_: &TestError| panic!("predicate should not be called"),
|_: TestError| pure::<i32, TestError, ()>(0),
);
assert_eq!(effect.execute(&()).await, Ok(5));
}
#[tokio::test]
async fn test_recover_with_result() {
let effect = fail::<i32, _, ()>(TestError::Recoverable("oops".into()))
.recover_with(|e: &TestError| e.is_recoverable(), |_| Ok(42));
assert_eq!(effect.execute(&()).await, Ok(42));
}
#[tokio::test]
async fn test_recover_with_transforms_error() {
let effect = fail::<i32, _, ()>(TestError::Recoverable("oops".into())).recover_with(
|e: &TestError| e.is_recoverable(),
|_| Err(TestError::Fatal("transformed".into())),
);
assert_eq!(
effect.execute(&()).await,
Err(TestError::Fatal("transformed".into()))
);
}
#[tokio::test]
async fn test_recover_some_matches() {
let effect =
fail::<i32, _, ()>(TestError::Recoverable("oops".into())).recover_some(|e| match e {
TestError::Recoverable(_) => Some(pure(42)),
_ => None,
});
assert_eq!(effect.execute(&()).await, Ok(42));
}
#[tokio::test]
async fn test_recover_some_no_match() {
let effect =
fail::<i32, _, ()>(TestError::Fatal("boom".into())).recover_some(|e| match e {
TestError::Recoverable(_) => Some(pure(42)),
_ => None,
});
assert_eq!(
effect.execute(&()).await,
Err(TestError::Fatal("boom".into()))
);
}
#[tokio::test]
async fn test_fallback() {
let effect = fail::<i32, _, ()>(TestError::Fatal("boom".into())).fallback(0);
assert_eq!(effect.execute(&()).await, Ok(0));
}
#[tokio::test]
async fn test_fallback_not_used_on_success() {
let effect = pure::<_, TestError, ()>(5).fallback(0);
assert_eq!(effect.execute(&()).await, Ok(5));
}
#[tokio::test]
async fn test_fallback_to() {
let effect = fail::<i32, _, ()>(TestError::Fatal("boom".into())).fallback_to(pure(42));
assert_eq!(effect.execute(&()).await, Ok(42));
}
#[tokio::test]
async fn test_fallback_to_not_used_on_success() {
let effect = pure::<_, TestError, ()>(5)
.fallback_to(fail(TestError::Fatal("should not run".into())));
assert_eq!(effect.execute(&()).await, Ok(5));
}
#[tokio::test]
async fn test_chained_recover() {
let effect = fail::<i32, _, ()>(TestError::Recoverable("oops".into()))
.recover(
|e: &TestError| matches!(e, TestError::Fatal(_)),
|_| pure(0),
)
.recover(
|e: &TestError| matches!(e, TestError::Recoverable(_)),
|_| pure(42),
);
assert_eq!(effect.execute(&()).await, Ok(42));
}
#[tokio::test]
async fn test_first_matching_recover_handles_error() {
let effect = fail::<i32, _, ()>(TestError::Recoverable("oops".into()))
.recover(
|e: &TestError| matches!(e, TestError::Recoverable(_)),
|_| pure(1),
)
.recover(|_: &TestError| true, |_| pure(2));
assert_eq!(effect.execute(&()).await, Ok(1));
}
#[tokio::test]
async fn test_unmatched_errors_propagate() {
let effect = fail::<i32, _, ()>(TestError::Fatal("boom".into()))
.recover(
|e: &TestError| matches!(e, TestError::Recoverable(_)),
|_| pure(1),
)
.recover(
|e: &TestError| matches!(e, TestError::Recoverable(_)),
|_| pure(2),
);
assert_eq!(
effect.execute(&()).await,
Err(TestError::Fatal("boom".into()))
);
}
#[tokio::test]
async fn test_recover_with_other_combinators() {
let effect = pure::<_, TestError, ()>(5)
.map(|x| x * 2)
.and_then(|x| {
if x > 5 {
fail(TestError::Recoverable("too big".into())).boxed()
} else {
pure(x).boxed()
}
})
.recover(|e: &TestError| e.is_recoverable(), |_| pure(5))
.map(|x| x + 1);
assert_eq!(effect.execute(&()).await, Ok(6));
}
#[tokio::test]
async fn test_recover_with_predicate_composition() {
let is_recoverable = |e: &TestError| matches!(e, TestError::Recoverable(_));
let is_timeout =
|e: &TestError| matches!(e, TestError::Recoverable(s) if s.contains("timeout"));
let should_retry = is_recoverable.and(is_timeout);
let effect = fail::<i32, _, ()>(TestError::Recoverable("timeout".into()))
.recover(should_retry, |_| pure(42));
assert_eq!(effect.execute(&()).await, Ok(42));
let effect = fail::<i32, _, ()>(TestError::Recoverable("other".into()))
.recover(should_retry, |_| pure(42));
assert!(effect.execute(&()).await.is_err());
}
#[tokio::test]
async fn test_recover_with_or_predicate() {
let is_recoverable = |e: &TestError| matches!(e, TestError::Recoverable(_));
let is_specific_fatal =
|e: &TestError| matches!(e, TestError::Fatal(s) if s.contains("retryable"));
let can_retry = is_recoverable.or(is_specific_fatal);
let effect = fail::<i32, _, ()>(TestError::Recoverable("err".into()))
.recover(can_retry, |_| pure(1));
assert_eq!(effect.execute(&()).await, Ok(1));
let effect = fail::<i32, _, ()>(TestError::Fatal("retryable".into()))
.recover(can_retry, |_| pure(2));
assert_eq!(effect.execute(&()).await, Ok(2));
let effect = fail::<i32, _, ()>(TestError::Fatal("permanent".into()))
.recover(can_retry, |_| pure(3));
assert!(effect.execute(&()).await.is_err());
}
}