Skip to main content

test_better_core/
or_fail.rs

1//! [`OrFail`]: turn a `Result` or `Option` into a [`TestResult`] at the call
2//! site, the `?`-friendly alternative to panicking on failure.
3//!
4//! Panicking on failure provides a location but no story; `.or_fail()?` produces a
5//! [`TestError`] that names what was expected and carries the underlying
6//! error's chain. In the happy path the two are
7//! interchangeable; in the failure path `or_fail` is strictly more informative.
8
9use std::borrow::Cow;
10use std::error::Error;
11
12use crate::context::coerce;
13use crate::error::{ErrorKind, TestError};
14use crate::result::TestResult;
15
16/// Converts a fallible value into a [`TestResult`], producing a [`TestError`]
17/// on the failure path.
18pub trait OrFail<T> {
19    /// Converts the failure path into a [`TestError`] with a default message.
20    fn or_fail(self) -> TestResult<T>;
21
22    /// Like [`or_fail`](OrFail::or_fail), but with a caller-supplied message.
23    fn or_fail_with(self, message: impl Into<Cow<'static, str>>) -> TestResult<T>;
24}
25
26/// The error produced when [`OrFail::or_fail`] is called on a [`None`].
27#[track_caller]
28fn missing_value<T>() -> TestError {
29    TestError::new(ErrorKind::Assertion).with_message(format!(
30        "expected Some({}), got None",
31        std::any::type_name::<T>()
32    ))
33}
34
35impl<T> OrFail<T> for Option<T> {
36    #[track_caller]
37    fn or_fail(self) -> TestResult<T> {
38        match self {
39            Some(value) => Ok(value),
40            None => Err(missing_value::<T>()),
41        }
42    }
43
44    #[track_caller]
45    fn or_fail_with(self, message: impl Into<Cow<'static, str>>) -> TestResult<T> {
46        match self {
47            Some(value) => Ok(value),
48            None => Err(TestError::new(ErrorKind::Assertion).with_message(message)),
49        }
50    }
51}
52
53impl<T, E> OrFail<T> for Result<T, E>
54where
55    E: Error + Send + Sync + 'static,
56{
57    #[track_caller]
58    fn or_fail(self) -> TestResult<T> {
59        self.map_err(coerce)
60    }
61
62    /// The supplied message is attached as a context frame, so the underlying
63    /// error's own message and source chain are preserved.
64    #[track_caller]
65    fn or_fail_with(self, message: impl Into<Cow<'static, str>>) -> TestResult<T> {
66        match self {
67            Ok(value) => Ok(value),
68            Err(error) => {
69                Err(coerce(error).with_context_frame(crate::error::ContextFrame::new(message)))
70            }
71        }
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use crate::error::Payload;
79    use crate::{OrFail, TestResult};
80    use test_better_matchers::{check, eq, is_true};
81
82    fn io_error() -> std::io::Error {
83        std::io::Error::new(std::io::ErrorKind::NotFound, "missing file")
84    }
85
86    #[test]
87    fn or_fail_passes_through_some_like_unwrap() -> TestResult {
88        let some: Option<i32> = Some(7);
89        check!(some.or_fail()?).satisfies(eq(7)).or_fail()?;
90        Ok(())
91    }
92
93    #[test]
94    fn or_fail_passes_through_ok_like_unwrap() -> TestResult {
95        let ok: Result<i32, std::io::Error> = Ok(7);
96        check!(ok.or_fail()?).satisfies(eq(7)).or_fail()?;
97        Ok(())
98    }
99
100    #[test]
101    fn or_fail_on_none_names_the_expected_type_and_caller_location() -> TestResult {
102        let missing: Option<i32> = None;
103        let line = line!() + 1;
104        let result = missing.or_fail();
105        let error = result.expect_err("err path");
106        check!(error.kind)
107            .satisfies(eq(ErrorKind::Assertion))
108            .or_fail()?;
109        check!(error.location.line())
110            .satisfies(eq(line))
111            .or_fail()?;
112        let message = error.message.as_deref().or_fail_with("message present")?;
113        check!(message.starts_with("expected Some("))
114            .satisfies(is_true())
115            .or_fail()?;
116        check!(message.ends_with("i32), got None"))
117            .satisfies(is_true())
118            .or_fail()?;
119        Ok(())
120    }
121
122    #[test]
123    fn or_fail_with_on_none_uses_the_supplied_message() -> TestResult {
124        let missing: Option<i32> = None;
125        let error = missing
126            .or_fail_with("the user should have been seeded")
127            .expect_err("err path");
128        check!(error.message.as_deref())
129            .satisfies(eq(Some("the user should have been seeded")))
130            .or_fail()?;
131        Ok(())
132    }
133
134    #[test]
135    fn or_fail_on_err_preserves_the_underlying_error() -> TestResult {
136        let failing: Result<(), std::io::Error> = Err(io_error());
137        let error = failing.or_fail().expect_err("err path");
138        check!(error.kind)
139            .satisfies(eq(ErrorKind::Custom))
140            .or_fail()?;
141        match error.payload.as_deref() {
142            Some(Payload::Other(inner)) => {
143                check!(inner.to_string())
144                    .satisfies(eq("missing file".to_string()))
145                    .or_fail()?;
146            }
147            other => panic!("expected Other payload, got {other:?}"),
148        }
149        Ok(())
150    }
151
152    #[test]
153    fn or_fail_does_not_double_wrap_a_test_error() -> TestResult {
154        let original = TestError::assertion("values differ");
155        let original_line = original.location.line();
156        let failing: Result<(), TestError> = Err(original);
157        let error = failing.or_fail().expect_err("err path");
158        check!(error.kind)
159            .satisfies(eq(ErrorKind::Assertion))
160            .or_fail()?;
161        check!(error.location.line())
162            .satisfies(eq(original_line))
163            .or_fail()?;
164        check!(error.message.as_deref())
165            .satisfies(eq(Some("values differ")))
166            .or_fail()?;
167        check!(error.payload.is_none())
168            .satisfies(is_true())
169            .or_fail()?;
170        Ok(())
171    }
172
173    #[test]
174    fn or_fail_with_on_err_keeps_the_chain_and_adds_context() -> TestResult {
175        let failing: Result<(), std::io::Error> = Err(io_error());
176        let error = failing
177            .or_fail_with("loading the config file")
178            .expect_err("err path");
179        check!(matches!(error.payload.as_deref(), Some(Payload::Other(_))))
180            .satisfies(is_true())
181            .or_fail()?;
182        check!(error.context.len()).satisfies(eq(1)).or_fail()?;
183        check!(error.context[0].message.as_ref())
184            .satisfies(eq("loading the config file"))
185            .or_fail()?;
186        Ok(())
187    }
188}