use std::panic::Location;
use crate::error::{ErrorKind, Payload, TestError};
use crate::trace::TraceEntry;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SourceLocation {
pub file: String,
pub line: u32,
pub column: u32,
}
impl SourceLocation {
fn from_std(location: &Location<'_>) -> Self {
Self {
file: location.file().to_string(),
line: location.line(),
column: location.column(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct StructuredContextFrame {
pub message: String,
pub location: Option<SourceLocation>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum StructuredPayload {
ExpectedActual {
expected: String,
actual: String,
diff: Option<String>,
},
Multiple(Vec<StructuredError>),
Other {
message: String,
chain: Vec<String>,
},
}
impl StructuredPayload {
fn from_payload(payload: &Payload) -> Self {
match payload {
Payload::ExpectedActual {
expected,
actual,
diff,
} => StructuredPayload::ExpectedActual {
expected: expected.clone(),
actual: actual.clone(),
diff: diff.clone(),
},
Payload::Multiple(errors) => {
StructuredPayload::Multiple(errors.iter().map(TestError::to_structured).collect())
}
Payload::Other(inner) => {
let mut chain = Vec::new();
let mut source = inner.source();
while let Some(current) = source {
chain.push(current.to_string());
source = current.source();
}
StructuredPayload::Other {
message: inner.to_string(),
chain,
}
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct StructuredError {
pub kind: ErrorKind,
pub message: Option<String>,
pub location: SourceLocation,
pub context: Vec<StructuredContextFrame>,
pub trace: Vec<TraceEntry>,
pub payload: Option<StructuredPayload>,
}
impl TestError {
#[must_use]
pub fn to_structured(&self) -> StructuredError {
StructuredError {
kind: self.kind,
message: self.message.as_ref().map(ToString::to_string),
location: SourceLocation::from_std(self.location),
context: self
.context
.iter()
.map(|frame| StructuredContextFrame {
message: frame.message.to_string(),
location: frame.location.map(SourceLocation::from_std),
})
.collect(),
trace: self.trace.clone(),
payload: self.payload.as_deref().map(StructuredPayload::from_payload),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::ContextFrame;
use crate::{OrFail, TestResult, Trace};
use test_better_matchers::{check, eq, is_true};
fn all_kinds() -> [ErrorKind; 6] {
[
ErrorKind::Assertion,
ErrorKind::Setup,
ErrorKind::Timeout,
ErrorKind::Snapshot,
ErrorKind::Property,
ErrorKind::Custom,
]
}
#[test]
fn every_kind_round_trips_through_structured() -> TestResult {
for kind in all_kinds() {
let error = TestError::new(kind).with_message("boom");
let structured = error.to_structured();
check!(structured.kind).satisfies(eq(kind)).or_fail()?;
check!(structured.message.as_deref())
.satisfies(eq(Some("boom")))
.or_fail()?;
}
Ok(())
}
#[test]
fn structured_captures_location_and_context() -> TestResult {
let error =
TestError::new(ErrorKind::Assertion).with_context_frame(ContextFrame::new("step one"));
let structured = error.to_structured();
check!(structured.context.len())
.satisfies(eq(1))
.or_fail()?;
check!(structured.context[0].message.as_str())
.satisfies(eq("step one"))
.or_fail()?;
check!(structured.location.file.ends_with("structured.rs"))
.satisfies(is_true())
.or_fail()?;
check!(structured.location.line > 0)
.satisfies(is_true())
.or_fail()?;
Ok(())
}
#[test]
fn structured_carries_the_trace() -> TestResult {
let mut trace = Trace::new();
trace.step("step one");
trace.kv("answer", 42);
let error = TestError::new(ErrorKind::Assertion);
drop(trace);
let structured = error.to_structured();
check!(structured.trace.len()).satisfies(eq(2)).or_fail()?;
check!(structured.trace[0].clone())
.satisfies(eq(TraceEntry::Step("step one".into())))
.or_fail()?;
Ok(())
}
#[test]
fn expected_actual_payload_round_trips() -> TestResult {
let error = TestError::new(ErrorKind::Assertion).with_payload(Payload::ExpectedActual {
expected: "1".to_string(),
actual: "2".to_string(),
diff: Some("- 1\n+ 2".to_string()),
});
match error.to_structured().payload {
Some(StructuredPayload::ExpectedActual {
expected,
actual,
diff,
}) => {
check!(expected).satisfies(eq("1".to_string())).or_fail()?;
check!(actual).satisfies(eq("2".to_string())).or_fail()?;
check!(diff.as_deref())
.satisfies(eq(Some("- 1\n+ 2")))
.or_fail()?;
}
other => panic!("expected ExpectedActual, got {other:?}"),
}
Ok(())
}
#[test]
fn multiple_payload_round_trips_recursively() -> TestResult {
let error = TestError::new(ErrorKind::Assertion).with_payload(Payload::Multiple(vec![
TestError::new(ErrorKind::Assertion).with_message("a"),
TestError::new(ErrorKind::Setup).with_message("b"),
]));
match error.to_structured().payload {
Some(StructuredPayload::Multiple(subs)) => {
check!(subs.len()).satisfies(eq(2)).or_fail()?;
check!(subs[0].message.as_deref())
.satisfies(eq(Some("a")))
.or_fail()?;
check!(subs[1].kind)
.satisfies(eq(ErrorKind::Setup))
.or_fail()?;
}
other => panic!("expected Multiple, got {other:?}"),
}
Ok(())
}
#[test]
fn other_payload_flattens_error_chain() -> TestResult {
let io = std::io::Error::new(std::io::ErrorKind::NotFound, "missing");
let error = TestError::new(ErrorKind::Custom).with_payload(Payload::Other(Box::new(io)));
match error.to_structured().payload {
Some(StructuredPayload::Other { message, chain }) => {
check!(message)
.satisfies(eq("missing".to_string()))
.or_fail()?;
check!(chain.is_empty()).satisfies(is_true()).or_fail()?;
}
other => panic!("expected Other, got {other:?}"),
}
Ok(())
}
#[cfg(feature = "serde")]
#[test]
fn structured_error_json_round_trips() -> TestResult {
let error = TestError::new(ErrorKind::Property)
.with_message("shrunk input failed")
.with_context_frame(ContextFrame::new("checking the round-trip property"))
.with_payload(Payload::ExpectedActual {
expected: "Ok(\"x\")".to_string(),
actual: "Err(..)".to_string(),
diff: None,
});
let structured = error.to_structured();
let json = serde_json::to_string(&structured).or_fail_with("serialize")?;
let back: StructuredError = serde_json::from_str(&json).or_fail_with("deserialize")?;
check!(structured).satisfies(eq(back)).or_fail()?;
Ok(())
}
}