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::{eq, check, 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).satisfies(eq(ErrorKind::Assertion)).or_fail()?;
107        check!(error.location.line()).satisfies(eq(line)).or_fail()?;
108        let message = error.message.as_deref().or_fail_with("message present")?;
109        check!(message.starts_with("expected Some("))
110            .satisfies(is_true())
111            .or_fail()?;
112        check!(message.ends_with("i32), got None"))
113            .satisfies(is_true())
114            .or_fail()?;
115        Ok(())
116    }
117
118    #[test]
119    fn or_fail_with_on_none_uses_the_supplied_message() -> TestResult {
120        let missing: Option<i32> = None;
121        let error = missing
122            .or_fail_with("the user should have been seeded")
123            .expect_err("err path");
124        check!(error.message.as_deref())
125            .satisfies(eq(Some("the user should have been seeded")))
126            .or_fail()?;
127        Ok(())
128    }
129
130    #[test]
131    fn or_fail_on_err_preserves_the_underlying_error() -> TestResult {
132        let failing: Result<(), std::io::Error> = Err(io_error());
133        let error = failing.or_fail().expect_err("err path");
134        check!(error.kind).satisfies(eq(ErrorKind::Custom)).or_fail()?;
135        match error.payload.as_deref() {
136            Some(Payload::Other(inner)) => {
137                check!(inner.to_string())
138                    .satisfies(eq("missing file".to_string()))
139                    .or_fail()?;
140            }
141            other => panic!("expected Other payload, got {other:?}"),
142        }
143        Ok(())
144    }
145
146    #[test]
147    fn or_fail_does_not_double_wrap_a_test_error() -> TestResult {
148        let original = TestError::assertion("values differ");
149        let original_line = original.location.line();
150        let failing: Result<(), TestError> = Err(original);
151        let error = failing.or_fail().expect_err("err path");
152        check!(error.kind).satisfies(eq(ErrorKind::Assertion)).or_fail()?;
153        check!(error.location.line())
154            .satisfies(eq(original_line))
155            .or_fail()?;
156        check!(error.message.as_deref())
157            .satisfies(eq(Some("values differ")))
158            .or_fail()?;
159        check!(error.payload.is_none()).satisfies(is_true()).or_fail()?;
160        Ok(())
161    }
162
163    #[test]
164    fn or_fail_with_on_err_keeps_the_chain_and_adds_context() -> TestResult {
165        let failing: Result<(), std::io::Error> = Err(io_error());
166        let error = failing
167            .or_fail_with("loading the config file")
168            .expect_err("err path");
169        check!(matches!(error.payload.as_deref(), Some(Payload::Other(_))))
170            .satisfies(is_true())
171            .or_fail()?;
172        check!(error.context.len()).satisfies(eq(1)).or_fail()?;
173        check!(error.context[0].message.as_ref())
174            .satisfies(eq("loading the config file"))
175            .or_fail()?;
176        Ok(())
177    }
178}