use std::borrow::Cow;
use std::fmt;
use std::panic::Location;
use crate::trace::TraceEntry;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum ErrorKind {
Assertion,
Setup,
Timeout,
Snapshot,
Property,
Custom,
}
impl ErrorKind {
#[must_use]
pub fn headline(self) -> &'static str {
match self {
ErrorKind::Assertion => "assertion failed",
ErrorKind::Setup => "test setup failed",
ErrorKind::Timeout => "timed out",
ErrorKind::Snapshot => "snapshot mismatch",
ErrorKind::Property => "property failed",
ErrorKind::Custom => "test failed",
}
}
}
#[derive(Debug, Clone)]
pub struct ContextFrame {
pub message: Cow<'static, str>,
pub location: Option<&'static Location<'static>>,
}
impl ContextFrame {
#[track_caller]
#[must_use]
pub fn new(message: impl Into<Cow<'static, str>>) -> Self {
Self {
message: message.into(),
location: Some(Location::caller()),
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum Payload {
ExpectedActual {
expected: String,
actual: String,
diff: Option<String>,
},
Multiple(Vec<TestError>),
Other(Box<dyn std::error::Error + Send + Sync>),
}
pub struct TestError {
pub kind: ErrorKind,
pub message: Option<Cow<'static, str>>,
pub location: &'static Location<'static>,
pub context: Vec<ContextFrame>,
pub trace: Vec<TraceEntry>,
pub payload: Option<Box<Payload>>,
}
impl TestError {
pub(crate) fn at(kind: ErrorKind, location: &'static Location<'static>) -> Self {
Self {
kind,
message: None,
location,
context: Vec::new(),
trace: crate::trace::snapshot(),
payload: None,
}
}
#[track_caller]
#[must_use]
pub fn new(kind: ErrorKind) -> Self {
Self::at(kind, Location::caller())
}
#[track_caller]
#[must_use]
pub fn assertion(message: impl Into<Cow<'static, str>>) -> Self {
Self::at(ErrorKind::Assertion, Location::caller()).with_message(message)
}
#[track_caller]
#[must_use]
pub fn custom(message: impl Into<Cow<'static, str>>) -> Self {
Self::at(ErrorKind::Custom, Location::caller()).with_message(message)
}
#[track_caller]
#[must_use]
pub fn from_expected_actual(expected: impl fmt::Debug, actual: impl fmt::Debug) -> Self {
Self::at(ErrorKind::Assertion, Location::caller()).with_payload(Payload::ExpectedActual {
expected: format!("{expected:?}"),
actual: format!("{actual:?}"),
diff: None,
})
}
#[must_use]
pub fn with_message(mut self, message: impl Into<Cow<'static, str>>) -> Self {
self.message = Some(message.into());
self
}
#[must_use]
pub fn with_kind(mut self, kind: ErrorKind) -> Self {
self.kind = kind;
self
}
#[must_use]
pub fn with_location(mut self, location: &'static Location<'static>) -> Self {
self.location = location;
self
}
#[must_use]
pub fn with_payload(mut self, payload: Payload) -> Self {
self.payload = Some(Box::new(payload));
self
}
#[must_use]
pub fn with_context_frame(mut self, frame: ContextFrame) -> Self {
self.context.push(frame);
self
}
pub fn push_context(&mut self, frame: ContextFrame) {
self.context.push(frame);
}
}
impl fmt::Display for TestError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
crate::render::render(self, f, false)
}
}
impl fmt::Debug for TestError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
crate::render::render(self, f, crate::color::color_enabled())?;
crate::runner::write_structured_marker(self, f)
}
}
impl std::error::Error for TestError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self.payload.as_deref() {
Some(Payload::Other(inner)) => Some(inner.as_ref()),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{OrFail, TestResult};
use test_better_matchers::{check, eq, is_true};
#[track_caller]
fn sample_assertion() -> TestError {
TestError::new(ErrorKind::Assertion).with_message("values differ")
}
#[test]
fn new_captures_caller_location() -> TestResult {
let line = line!() + 1;
let error = TestError::new(ErrorKind::Assertion);
check!(error.location.line())
.satisfies(eq(line))
.or_fail()?;
check!(error.location.file().ends_with("error.rs"))
.satisfies(is_true())
.or_fail()?;
Ok(())
}
#[test]
fn display_includes_headline_message_and_location() -> TestResult {
let rendered = sample_assertion().to_string();
check!(rendered.contains("assertion failed: values differ"))
.satisfies(is_true())
.or_fail()?;
check!(rendered.contains(" at "))
.satisfies(is_true())
.or_fail()?;
Ok(())
}
#[test]
fn debug_matches_display() -> TestResult {
let _guard = crate::color::TEST_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let error = sample_assertion();
let debug = format!("{error:?}");
let human = debug
.split_once(crate::STRUCTURED_MARKER)
.map_or(debug.as_str(), |(before, _)| before.trim_end());
check!(human)
.satisfies(eq(format!("{error}").as_str()))
.or_fail()?;
Ok(())
}
#[test]
fn context_frames_render_in_order() -> TestResult {
let error = sample_assertion()
.with_context_frame(ContextFrame::new("creating user"))
.with_context_frame(ContextFrame::new("loading profile"));
let rendered = error.to_string();
let first = rendered
.find("creating user")
.or_fail_with("first frame present")?;
let second = rendered
.find("loading profile")
.or_fail_with("second frame present")?;
check!(first < second).satisfies(is_true()).or_fail()?;
Ok(())
}
#[test]
fn error_source_walks_into_payload_other() -> TestResult {
let io = std::io::Error::new(std::io::ErrorKind::NotFound, "missing file");
let error = TestError::new(ErrorKind::Custom).with_payload(Payload::Other(Box::new(io)));
let source =
std::error::Error::source(&error).or_fail_with("source is the wrapped io error")?;
check!(source.to_string())
.satisfies(eq("missing file".to_string()))
.or_fail()?;
Ok(())
}
#[test]
fn expected_actual_payload_renders_both_values() -> TestResult {
let error = TestError::new(ErrorKind::Assertion).with_payload(Payload::ExpectedActual {
expected: "4".to_string(),
actual: "5".to_string(),
diff: None,
});
let rendered = error.to_string();
check!(rendered.contains("expected: 4"))
.satisfies(is_true())
.or_fail()?;
check!(rendered.contains("actual: 5"))
.satisfies(is_true())
.or_fail()?;
Ok(())
}
#[test]
fn multiple_payload_renders_every_sub_failure() -> TestResult {
let error = TestError::new(ErrorKind::Assertion).with_payload(Payload::Multiple(vec![
TestError::new(ErrorKind::Assertion).with_message("first"),
TestError::new(ErrorKind::Assertion).with_message("second"),
]));
let rendered = error.to_string();
check!(rendered.contains("first"))
.satisfies(is_true())
.or_fail()?;
check!(rendered.contains("second"))
.satisfies(is_true())
.or_fail()?;
Ok(())
}
#[test]
fn assertion_constructor_sets_kind_message_and_caller_location() -> TestResult {
let line = line!() + 1;
let error = TestError::assertion("values differ");
check!(error.kind)
.satisfies(eq(ErrorKind::Assertion))
.or_fail()?;
check!(error.message.as_deref())
.satisfies(eq(Some("values differ")))
.or_fail()?;
check!(error.location.line())
.satisfies(eq(line))
.or_fail()?;
check!(error.location.file().ends_with("error.rs"))
.satisfies(is_true())
.or_fail()?;
Ok(())
}
#[test]
fn custom_constructor_sets_kind_message_and_caller_location() -> TestResult {
let line = line!() + 1;
let error = TestError::custom("something off");
check!(error.kind)
.satisfies(eq(ErrorKind::Custom))
.or_fail()?;
check!(error.message.as_deref())
.satisfies(eq(Some("something off")))
.or_fail()?;
check!(error.location.line())
.satisfies(eq(line))
.or_fail()?;
check!(error.location.file().ends_with("error.rs"))
.satisfies(is_true())
.or_fail()?;
Ok(())
}
#[test]
fn from_expected_actual_captures_debug_values_and_caller_location() -> TestResult {
let line = line!() + 1;
let error = TestError::from_expected_actual(4, 5);
check!(error.kind)
.satisfies(eq(ErrorKind::Assertion))
.or_fail()?;
check!(error.location.line())
.satisfies(eq(line))
.or_fail()?;
match error.payload.map(|payload| *payload) {
Some(Payload::ExpectedActual {
expected,
actual,
diff,
}) => {
check!(expected).satisfies(eq("4".to_string())).or_fail()?;
check!(actual).satisfies(eq("5".to_string())).or_fail()?;
check!(diff.is_none()).satisfies(is_true()).or_fail()?;
}
other => panic!("expected ExpectedActual, got {other:?}"),
}
Ok(())
}
}