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::{check, eq, 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 check!(structured.kind).satisfies(eq(kind)).or_fail()?;
175 check!(structured.message.as_deref())
176 .satisfies(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 check!(structured.context.len())
188 .satisfies(eq(1))
189 .or_fail()?;
190 check!(structured.context[0].message.as_str())
191 .satisfies(eq("step one"))
192 .or_fail()?;
193 check!(structured.location.file.ends_with("structured.rs"))
194 .satisfies(is_true())
195 .or_fail()?;
196 check!(structured.location.line > 0)
197 .satisfies(is_true())
198 .or_fail()?;
199 Ok(())
200 }
201
202 #[test]
203 fn structured_carries_the_trace() -> TestResult {
204 let mut trace = Trace::new();
205 trace.step("step one");
206 trace.kv("answer", 42);
207 let error = TestError::new(ErrorKind::Assertion);
208 drop(trace);
209
210 let structured = error.to_structured();
211 check!(structured.trace.len()).satisfies(eq(2)).or_fail()?;
212 check!(structured.trace[0].clone())
213 .satisfies(eq(TraceEntry::Step("step one".into())))
214 .or_fail()?;
215 Ok(())
216 }
217
218 #[test]
219 fn expected_actual_payload_round_trips() -> TestResult {
220 let error = TestError::new(ErrorKind::Assertion).with_payload(Payload::ExpectedActual {
221 expected: "1".to_string(),
222 actual: "2".to_string(),
223 diff: Some("- 1\n+ 2".to_string()),
224 });
225 match error.to_structured().payload {
226 Some(StructuredPayload::ExpectedActual {
227 expected,
228 actual,
229 diff,
230 }) => {
231 check!(expected).satisfies(eq("1".to_string())).or_fail()?;
232 check!(actual).satisfies(eq("2".to_string())).or_fail()?;
233 check!(diff.as_deref())
234 .satisfies(eq(Some("- 1\n+ 2")))
235 .or_fail()?;
236 }
237 other => panic!("expected ExpectedActual, got {other:?}"),
238 }
239 Ok(())
240 }
241
242 #[test]
243 fn multiple_payload_round_trips_recursively() -> TestResult {
244 let error = TestError::new(ErrorKind::Assertion).with_payload(Payload::Multiple(vec![
245 TestError::new(ErrorKind::Assertion).with_message("a"),
246 TestError::new(ErrorKind::Setup).with_message("b"),
247 ]));
248 match error.to_structured().payload {
249 Some(StructuredPayload::Multiple(subs)) => {
250 check!(subs.len()).satisfies(eq(2)).or_fail()?;
251 check!(subs[0].message.as_deref())
252 .satisfies(eq(Some("a")))
253 .or_fail()?;
254 check!(subs[1].kind)
255 .satisfies(eq(ErrorKind::Setup))
256 .or_fail()?;
257 }
258 other => panic!("expected Multiple, got {other:?}"),
259 }
260 Ok(())
261 }
262
263 #[test]
264 fn other_payload_flattens_error_chain() -> TestResult {
265 let io = std::io::Error::new(std::io::ErrorKind::NotFound, "missing");
266 let error = TestError::new(ErrorKind::Custom).with_payload(Payload::Other(Box::new(io)));
267 match error.to_structured().payload {
268 Some(StructuredPayload::Other { message, chain }) => {
269 check!(message)
270 .satisfies(eq("missing".to_string()))
271 .or_fail()?;
272 check!(chain.is_empty()).satisfies(is_true()).or_fail()?;
273 }
274 other => panic!("expected Other, got {other:?}"),
275 }
276 Ok(())
277 }
278
279 #[cfg(feature = "serde")]
280 #[test]
281 fn structured_error_json_round_trips() -> TestResult {
282 let error = TestError::new(ErrorKind::Property)
283 .with_message("shrunk input failed")
284 .with_context_frame(ContextFrame::new("checking the round-trip property"))
285 .with_payload(Payload::ExpectedActual {
286 expected: "Ok(\"x\")".to_string(),
287 actual: "Err(..)".to_string(),
288 diff: None,
289 });
290 let structured = error.to_structured();
291 let json = serde_json::to_string(&structured).or_fail_with("serialize")?;
292 let back: StructuredError = serde_json::from_str(&json).or_fail_with("deserialize")?;
293 check!(structured).satisfies(eq(back)).or_fail()?;
294 Ok(())
295 }
296}