1use std::borrow::Cow;
10use std::fmt;
11use std::panic::Location;
12
13use crate::trace::TraceEntry;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21#[non_exhaustive]
22pub enum ErrorKind {
23 Assertion,
25 Setup,
27 Timeout,
29 Snapshot,
31 Property,
33 Custom,
36}
37
38impl ErrorKind {
39 #[must_use]
41 pub fn headline(self) -> &'static str {
42 match self {
43 ErrorKind::Assertion => "assertion failed",
44 ErrorKind::Setup => "test setup failed",
45 ErrorKind::Timeout => "timed out",
46 ErrorKind::Snapshot => "snapshot mismatch",
47 ErrorKind::Property => "property failed",
48 ErrorKind::Custom => "test failed",
49 }
50 }
51}
52
53#[derive(Debug, Clone)]
58pub struct ContextFrame {
59 pub message: Cow<'static, str>,
61 pub location: Option<&'static Location<'static>>,
63}
64
65impl ContextFrame {
66 #[track_caller]
68 #[must_use]
69 pub fn new(message: impl Into<Cow<'static, str>>) -> Self {
70 Self {
71 message: message.into(),
72 location: Some(Location::caller()),
73 }
74 }
75}
76
77#[derive(Debug)]
79#[non_exhaustive]
80pub enum Payload {
81 ExpectedActual {
84 expected: String,
86 actual: String,
88 diff: Option<String>,
90 },
91 Multiple(Vec<TestError>),
93 Other(Box<dyn std::error::Error + Send + Sync>),
96}
97
98pub struct TestError {
110 pub kind: ErrorKind,
112 pub message: Option<Cow<'static, str>>,
114 pub location: &'static Location<'static>,
116 pub context: Vec<ContextFrame>,
118 pub trace: Vec<TraceEntry>,
121 pub payload: Option<Box<Payload>>,
127}
128
129impl TestError {
130 pub(crate) fn at(kind: ErrorKind, location: &'static Location<'static>) -> Self {
134 Self {
135 kind,
136 message: None,
137 location,
138 context: Vec::new(),
139 trace: crate::trace::snapshot(),
142 payload: None,
143 }
144 }
145
146 #[track_caller]
148 #[must_use]
149 pub fn new(kind: ErrorKind) -> Self {
150 Self::at(kind, Location::caller())
151 }
152
153 #[track_caller]
158 #[must_use]
159 pub fn assertion(message: impl Into<Cow<'static, str>>) -> Self {
160 Self::at(ErrorKind::Assertion, Location::caller()).with_message(message)
161 }
162
163 #[track_caller]
166 #[must_use]
167 pub fn custom(message: impl Into<Cow<'static, str>>) -> Self {
168 Self::at(ErrorKind::Custom, Location::caller()).with_message(message)
169 }
170
171 #[track_caller]
175 #[must_use]
176 pub fn from_expected_actual(expected: impl fmt::Debug, actual: impl fmt::Debug) -> Self {
177 Self::at(ErrorKind::Assertion, Location::caller()).with_payload(Payload::ExpectedActual {
178 expected: format!("{expected:?}"),
179 actual: format!("{actual:?}"),
180 diff: None,
181 })
182 }
183
184 #[must_use]
186 pub fn with_message(mut self, message: impl Into<Cow<'static, str>>) -> Self {
187 self.message = Some(message.into());
188 self
189 }
190
191 #[must_use]
198 pub fn with_kind(mut self, kind: ErrorKind) -> Self {
199 self.kind = kind;
200 self
201 }
202
203 #[must_use]
213 pub fn with_location(mut self, location: &'static Location<'static>) -> Self {
214 self.location = location;
215 self
216 }
217
218 #[must_use]
220 pub fn with_payload(mut self, payload: Payload) -> Self {
221 self.payload = Some(Box::new(payload));
222 self
223 }
224
225 #[must_use]
227 pub fn with_context_frame(mut self, frame: ContextFrame) -> Self {
228 self.context.push(frame);
229 self
230 }
231
232 pub fn push_context(&mut self, frame: ContextFrame) {
234 self.context.push(frame);
235 }
236}
237
238impl fmt::Display for TestError {
239 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241 crate::render::render(self, f, false)
242 }
243}
244
245impl fmt::Debug for TestError {
246 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257 crate::render::render(self, f, crate::color::color_enabled())?;
258 crate::runner::write_structured_marker(self, f)
259 }
260}
261
262impl std::error::Error for TestError {
263 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
264 match self.payload.as_deref() {
265 Some(Payload::Other(inner)) => Some(inner.as_ref()),
266 _ => None,
267 }
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use crate::{OrFail, TestResult};
275 use test_better_matchers::{check, eq, is_true};
276
277 #[track_caller]
278 fn sample_assertion() -> TestError {
279 TestError::new(ErrorKind::Assertion).with_message("values differ")
280 }
281
282 #[test]
283 fn new_captures_caller_location() -> TestResult {
284 let line = line!() + 1;
285 let error = TestError::new(ErrorKind::Assertion);
286 check!(error.location.line())
287 .satisfies(eq(line))
288 .or_fail()?;
289 check!(error.location.file().ends_with("error.rs"))
290 .satisfies(is_true())
291 .or_fail()?;
292 Ok(())
293 }
294
295 #[test]
296 fn display_includes_headline_message_and_location() -> TestResult {
297 let rendered = sample_assertion().to_string();
298 check!(rendered.contains("assertion failed: values differ"))
299 .satisfies(is_true())
300 .or_fail()?;
301 check!(rendered.contains(" at "))
302 .satisfies(is_true())
303 .or_fail()?;
304 Ok(())
305 }
306
307 #[test]
308 fn debug_matches_display() -> TestResult {
309 let _guard = crate::color::TEST_LOCK
312 .lock()
313 .unwrap_or_else(std::sync::PoisonError::into_inner);
314 let error = sample_assertion();
315 let debug = format!("{error:?}");
319 let human = debug
320 .split_once(crate::STRUCTURED_MARKER)
321 .map_or(debug.as_str(), |(before, _)| before.trim_end());
322 check!(human)
323 .satisfies(eq(format!("{error}").as_str()))
324 .or_fail()?;
325 Ok(())
326 }
327
328 #[test]
329 fn context_frames_render_in_order() -> TestResult {
330 let error = sample_assertion()
331 .with_context_frame(ContextFrame::new("creating user"))
332 .with_context_frame(ContextFrame::new("loading profile"));
333 let rendered = error.to_string();
334 let first = rendered
335 .find("creating user")
336 .or_fail_with("first frame present")?;
337 let second = rendered
338 .find("loading profile")
339 .or_fail_with("second frame present")?;
340 check!(first < second).satisfies(is_true()).or_fail()?;
341 Ok(())
342 }
343
344 #[test]
345 fn error_source_walks_into_payload_other() -> TestResult {
346 let io = std::io::Error::new(std::io::ErrorKind::NotFound, "missing file");
347 let error = TestError::new(ErrorKind::Custom).with_payload(Payload::Other(Box::new(io)));
348 let source =
349 std::error::Error::source(&error).or_fail_with("source is the wrapped io error")?;
350 check!(source.to_string())
351 .satisfies(eq("missing file".to_string()))
352 .or_fail()?;
353 Ok(())
354 }
355
356 #[test]
357 fn expected_actual_payload_renders_both_values() -> TestResult {
358 let error = TestError::new(ErrorKind::Assertion).with_payload(Payload::ExpectedActual {
359 expected: "4".to_string(),
360 actual: "5".to_string(),
361 diff: None,
362 });
363 let rendered = error.to_string();
364 check!(rendered.contains("expected: 4"))
365 .satisfies(is_true())
366 .or_fail()?;
367 check!(rendered.contains("actual: 5"))
368 .satisfies(is_true())
369 .or_fail()?;
370 Ok(())
371 }
372
373 #[test]
374 fn multiple_payload_renders_every_sub_failure() -> TestResult {
375 let error = TestError::new(ErrorKind::Assertion).with_payload(Payload::Multiple(vec![
376 TestError::new(ErrorKind::Assertion).with_message("first"),
377 TestError::new(ErrorKind::Assertion).with_message("second"),
378 ]));
379 let rendered = error.to_string();
380 check!(rendered.contains("first"))
381 .satisfies(is_true())
382 .or_fail()?;
383 check!(rendered.contains("second"))
384 .satisfies(is_true())
385 .or_fail()?;
386 Ok(())
387 }
388
389 #[test]
390 fn assertion_constructor_sets_kind_message_and_caller_location() -> TestResult {
391 let line = line!() + 1;
392 let error = TestError::assertion("values differ");
393 check!(error.kind)
394 .satisfies(eq(ErrorKind::Assertion))
395 .or_fail()?;
396 check!(error.message.as_deref())
397 .satisfies(eq(Some("values differ")))
398 .or_fail()?;
399 check!(error.location.line())
400 .satisfies(eq(line))
401 .or_fail()?;
402 check!(error.location.file().ends_with("error.rs"))
403 .satisfies(is_true())
404 .or_fail()?;
405 Ok(())
406 }
407
408 #[test]
409 fn custom_constructor_sets_kind_message_and_caller_location() -> TestResult {
410 let line = line!() + 1;
411 let error = TestError::custom("something off");
412 check!(error.kind)
413 .satisfies(eq(ErrorKind::Custom))
414 .or_fail()?;
415 check!(error.message.as_deref())
416 .satisfies(eq(Some("something off")))
417 .or_fail()?;
418 check!(error.location.line())
419 .satisfies(eq(line))
420 .or_fail()?;
421 check!(error.location.file().ends_with("error.rs"))
422 .satisfies(is_true())
423 .or_fail()?;
424 Ok(())
425 }
426
427 #[test]
428 fn from_expected_actual_captures_debug_values_and_caller_location() -> TestResult {
429 let line = line!() + 1;
430 let error = TestError::from_expected_actual(4, 5);
431 check!(error.kind)
432 .satisfies(eq(ErrorKind::Assertion))
433 .or_fail()?;
434 check!(error.location.line())
435 .satisfies(eq(line))
436 .or_fail()?;
437 match error.payload.map(|payload| *payload) {
438 Some(Payload::ExpectedActual {
439 expected,
440 actual,
441 diff,
442 }) => {
443 check!(expected).satisfies(eq("4".to_string())).or_fail()?;
444 check!(actual).satisfies(eq("5".to_string())).or_fail()?;
445 check!(diff.is_none()).satisfies(is_true()).or_fail()?;
446 }
447 other => panic!("expected ExpectedActual, got {other:?}"),
448 }
449 Ok(())
450 }
451}