use std::borrow::Cow;
use std::error::Error;
use crate::error::{ContextFrame, ErrorKind, Payload, TestError};
use crate::result::TestResult;
pub trait ContextExt<T> {
fn context(self, message: impl Into<Cow<'static, str>>) -> TestResult<T>;
fn with_context<F, S>(self, f: F) -> TestResult<T>
where
F: FnOnce() -> S,
S: Into<Cow<'static, str>>;
}
#[track_caller]
pub(crate) fn coerce<E>(error: E) -> TestError
where
E: Error + Send + Sync + 'static,
{
let boxed: Box<dyn Error + Send + Sync> = Box::new(error);
match boxed.downcast::<TestError>() {
Ok(test_error) => *test_error,
Err(other) => TestError::new(ErrorKind::Custom).with_payload(Payload::Other(other)),
}
}
#[track_caller]
fn none_error() -> TestError {
TestError::new(ErrorKind::Custom).with_message("value was None")
}
impl<T, E> ContextExt<T> for Result<T, E>
where
E: Error + Send + Sync + 'static,
{
#[track_caller]
fn context(self, message: impl Into<Cow<'static, str>>) -> TestResult<T> {
match self {
Ok(value) => Ok(value),
Err(error) => Err(coerce(error).with_context_frame(ContextFrame::new(message))),
}
}
#[track_caller]
fn with_context<F, S>(self, f: F) -> TestResult<T>
where
F: FnOnce() -> S,
S: Into<Cow<'static, str>>,
{
match self {
Ok(value) => Ok(value),
Err(error) => Err(coerce(error).with_context_frame(ContextFrame::new(f()))),
}
}
}
impl<T> ContextExt<T> for Option<T> {
#[track_caller]
fn context(self, message: impl Into<Cow<'static, str>>) -> TestResult<T> {
match self {
Some(value) => Ok(value),
None => Err(none_error().with_context_frame(ContextFrame::new(message))),
}
}
#[track_caller]
fn with_context<F, S>(self, f: F) -> TestResult<T>
where
F: FnOnce() -> S,
S: Into<Cow<'static, str>>,
{
match self {
Some(value) => Ok(value),
None => Err(none_error().with_context_frame(ContextFrame::new(f()))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{OrFail, TestResult};
use std::cell::Cell;
use test_better_matchers::{check, eq, is_true};
fn io_error() -> std::io::Error {
std::io::Error::new(std::io::ErrorKind::NotFound, "missing file")
}
#[test]
fn context_passes_through_ok() -> TestResult {
let value: TestResult<i32> = Ok::<i32, std::io::Error>(7).context("unused");
check!(value?).satisfies(eq(7)).or_fail()?;
Ok(())
}
#[test]
fn context_passes_through_some() -> TestResult {
let value: TestResult<i32> = Some(7).context("unused");
check!(value?).satisfies(eq(7)).or_fail()?;
Ok(())
}
#[test]
fn context_wraps_foreign_error_as_other_payload() -> TestResult {
let failing: Result<(), std::io::Error> = Err(io_error());
let line = line!() + 1;
let result = failing.context("reading the fixture");
let error = result.expect_err("err path");
check!(error.kind)
.satisfies(eq(ErrorKind::Custom))
.or_fail()?;
check!(error.location.line())
.satisfies(eq(line))
.or_fail()?;
check!(matches!(error.payload.as_deref(), Some(Payload::Other(_))))
.satisfies(is_true())
.or_fail()?;
check!(error.context.len()).satisfies(eq(1)).or_fail()?;
check!(error.context[0].message.as_ref())
.satisfies(eq("reading the fixture"))
.or_fail()?;
Ok(())
}
#[test]
fn context_does_not_double_wrap_a_test_error() -> TestResult {
let original = TestError::assertion("values differ");
let original_line = original.location.line();
let error = Err::<(), _>(original)
.context("comparing the results")
.expect_err("err path");
check!(error.kind)
.satisfies(eq(ErrorKind::Assertion))
.or_fail()?;
check!(error.location.line())
.satisfies(eq(original_line))
.or_fail()?;
check!(error.payload.is_none())
.satisfies(is_true())
.or_fail()?;
check!(error.message.as_deref())
.satisfies(eq(Some("values differ")))
.or_fail()?;
check!(error.context.len()).satisfies(eq(1)).or_fail()?;
check!(error.context[0].message.as_ref())
.satisfies(eq("comparing the results"))
.or_fail()?;
Ok(())
}
#[test]
fn context_frames_accumulate_in_order() -> TestResult {
let error = Err::<(), _>(io_error())
.context("inner step")
.context("outer step")
.expect_err("err path");
let messages: Vec<_> = error.context.iter().map(|f| f.message.as_ref()).collect();
check!(messages)
.satisfies(eq(vec!["inner step", "outer step"]))
.or_fail()?;
Ok(())
}
#[test]
fn none_gains_context_and_caller_location() -> TestResult {
let missing: Option<i32> = None;
let line = line!() + 1;
let result = missing.context("looking up the user");
let error = result.expect_err("err path");
check!(error.kind)
.satisfies(eq(ErrorKind::Custom))
.or_fail()?;
check!(error.location.line())
.satisfies(eq(line))
.or_fail()?;
check!(error.context[0].message.as_ref())
.satisfies(eq("looking up the user"))
.or_fail()?;
Ok(())
}
#[test]
fn with_context_runs_the_closure_only_on_failure() -> TestResult {
let calls = Cell::new(0);
let ok: TestResult<i32> = Ok::<i32, std::io::Error>(1).with_context(|| {
calls.set(calls.get() + 1);
"unused"
});
check!(ok?).satisfies(eq(1)).or_fail()?;
check!(calls.get()).satisfies(eq(0)).or_fail()?;
let err = Err::<(), _>(io_error())
.with_context(|| {
calls.set(calls.get() + 1);
"computed context"
})
.expect_err("err path");
check!(calls.get()).satisfies(eq(1)).or_fail()?;
check!(err.context[0].message.as_ref())
.satisfies(eq("computed context"))
.or_fail()?;
Ok(())
}
}