1use std::panic::Location;
12
13use crate::error::{ErrorKind, Payload, TestError};
14use crate::trace::TraceEntry;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20pub struct SourceLocation {
21 pub file: String,
23 pub line: u32,
25 pub column: u32,
27}
28
29impl SourceLocation {
30 fn from_std(location: &Location<'_>) -> Self {
31 Self {
32 file: location.file().to_string(),
33 line: location.line(),
34 column: location.column(),
35 }
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
41#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
42pub struct StructuredContextFrame {
43 pub message: String,
45 pub location: Option<SourceLocation>,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
54#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
55pub enum StructuredPayload {
56 ExpectedActual {
58 expected: String,
60 actual: String,
62 diff: Option<String>,
64 },
65 Multiple(Vec<StructuredError>),
67 Other {
69 message: String,
71 chain: Vec<String>,
73 },
74}
75
76impl StructuredPayload {
77 fn from_payload(payload: &Payload) -> Self {
78 match payload {
79 Payload::ExpectedActual {
80 expected,
81 actual,
82 diff,
83 } => StructuredPayload::ExpectedActual {
84 expected: expected.clone(),
85 actual: actual.clone(),
86 diff: diff.clone(),
87 },
88 Payload::Multiple(errors) => {
89 StructuredPayload::Multiple(errors.iter().map(TestError::to_structured).collect())
90 }
91 Payload::Other(inner) => {
92 let mut chain = Vec::new();
93 let mut source = inner.source();
94 while let Some(current) = source {
95 chain.push(current.to_string());
96 source = current.source();
97 }
98 StructuredPayload::Other {
99 message: inner.to_string(),
100 chain,
101 }
102 }
103 }
104 }
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
109#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
110pub struct StructuredError {
111 pub kind: ErrorKind,
113 pub message: Option<String>,
115 pub location: SourceLocation,
117 pub context: Vec<StructuredContextFrame>,
119 pub trace: Vec<TraceEntry>,
122 pub payload: Option<StructuredPayload>,
124}
125
126impl TestError {
127 #[must_use]
132 pub fn to_structured(&self) -> StructuredError {
133 StructuredError {
134 kind: self.kind,
135 message: self.message.as_ref().map(ToString::to_string),
136 location: SourceLocation::from_std(self.location),
137 context: self
138 .context
139 .iter()
140 .map(|frame| StructuredContextFrame {
141 message: frame.message.to_string(),
142 location: frame.location.map(SourceLocation::from_std),
143 })
144 .collect(),
145 trace: self.trace.clone(),
146 payload: self.payload.as_deref().map(StructuredPayload::from_payload),
147 }
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use crate::error::ContextFrame;
155 use crate::{OrFail, TestResult, Trace};
156 use test_better_matchers::{eq, expect, is_true};
157
158 fn all_kinds() -> [ErrorKind; 6] {
159 [
160 ErrorKind::Assertion,
161 ErrorKind::Setup,
162 ErrorKind::Timeout,
163 ErrorKind::Snapshot,
164 ErrorKind::Property,
165 ErrorKind::Custom,
166 ]
167 }
168
169 #[test]
170 fn every_kind_round_trips_through_structured() -> TestResult {
171 for kind in all_kinds() {
172 let error = TestError::new(kind).with_message("boom");
173 let structured = error.to_structured();
174 expect!(structured.kind).to(eq(kind)).or_fail()?;
175 expect!(structured.message.as_deref())
176 .to(eq(Some("boom")))
177 .or_fail()?;
178 }
179 Ok(())
180 }
181
182 #[test]
183 fn structured_captures_location_and_context() -> TestResult {
184 let error =
185 TestError::new(ErrorKind::Assertion).with_context_frame(ContextFrame::new("step one"));
186 let structured = error.to_structured();
187 expect!(structured.context.len()).to(eq(1)).or_fail()?;
188 expect!(structured.context[0].message.as_str())
189 .to(eq("step one"))
190 .or_fail()?;
191 expect!(structured.location.file.ends_with("structured.rs"))
192 .to(is_true())
193 .or_fail()?;
194 expect!(structured.location.line > 0)
195 .to(is_true())
196 .or_fail()?;
197 Ok(())
198 }
199
200 #[test]
201 fn structured_carries_the_trace() -> TestResult {
202 let mut trace = Trace::new();
203 trace.step("step one");
204 trace.kv("answer", 42);
205 let error = TestError::new(ErrorKind::Assertion);
206 drop(trace);
207
208 let structured = error.to_structured();
209 expect!(structured.trace.len()).to(eq(2)).or_fail()?;
210 expect!(structured.trace[0].clone())
211 .to(eq(TraceEntry::Step("step one".into())))
212 .or_fail()?;
213 Ok(())
214 }
215
216 #[test]
217 fn expected_actual_payload_round_trips() -> TestResult {
218 let error = TestError::new(ErrorKind::Assertion).with_payload(Payload::ExpectedActual {
219 expected: "1".to_string(),
220 actual: "2".to_string(),
221 diff: Some("- 1\n+ 2".to_string()),
222 });
223 match error.to_structured().payload {
224 Some(StructuredPayload::ExpectedActual {
225 expected,
226 actual,
227 diff,
228 }) => {
229 expect!(expected).to(eq("1".to_string())).or_fail()?;
230 expect!(actual).to(eq("2".to_string())).or_fail()?;
231 expect!(diff.as_deref())
232 .to(eq(Some("- 1\n+ 2")))
233 .or_fail()?;
234 }
235 other => panic!("expected ExpectedActual, got {other:?}"),
236 }
237 Ok(())
238 }
239
240 #[test]
241 fn multiple_payload_round_trips_recursively() -> TestResult {
242 let error = TestError::new(ErrorKind::Assertion).with_payload(Payload::Multiple(vec![
243 TestError::new(ErrorKind::Assertion).with_message("a"),
244 TestError::new(ErrorKind::Setup).with_message("b"),
245 ]));
246 match error.to_structured().payload {
247 Some(StructuredPayload::Multiple(subs)) => {
248 expect!(subs.len()).to(eq(2)).or_fail()?;
249 expect!(subs[0].message.as_deref())
250 .to(eq(Some("a")))
251 .or_fail()?;
252 expect!(subs[1].kind).to(eq(ErrorKind::Setup)).or_fail()?;
253 }
254 other => panic!("expected Multiple, got {other:?}"),
255 }
256 Ok(())
257 }
258
259 #[test]
260 fn other_payload_flattens_error_chain() -> TestResult {
261 let io = std::io::Error::new(std::io::ErrorKind::NotFound, "missing");
262 let error = TestError::new(ErrorKind::Custom).with_payload(Payload::Other(Box::new(io)));
263 match error.to_structured().payload {
264 Some(StructuredPayload::Other { message, chain }) => {
265 expect!(message).to(eq("missing".to_string())).or_fail()?;
266 expect!(chain.is_empty()).to(is_true()).or_fail()?;
267 }
268 other => panic!("expected Other, got {other:?}"),
269 }
270 Ok(())
271 }
272
273 #[cfg(feature = "serde")]
274 #[test]
275 fn structured_error_json_round_trips() -> TestResult {
276 let error = TestError::new(ErrorKind::Property)
277 .with_message("shrunk input failed")
278 .with_context_frame(ContextFrame::new("checking the round-trip property"))
279 .with_payload(Payload::ExpectedActual {
280 expected: "Ok(\"x\")".to_string(),
281 actual: "Err(..)".to_string(),
282 diff: None,
283 });
284 let structured = error.to_structured();
285 let json = serde_json::to_string(&structured).or_fail_with("serialize")?;
286 let back: StructuredError = serde_json::from_str(&json).or_fail_with("deserialize")?;
287 expect!(structured).to(eq(back)).or_fail()?;
288 Ok(())
289 }
290}