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::{eq, check, 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()).satisfies(eq(line)).or_fail()?;
287 check!(error.location.file().ends_with("error.rs"))
288 .satisfies(is_true())
289 .or_fail()?;
290 Ok(())
291 }
292
293 #[test]
294 fn display_includes_headline_message_and_location() -> TestResult {
295 let rendered = sample_assertion().to_string();
296 check!(rendered.contains("assertion failed: values differ"))
297 .satisfies(is_true())
298 .or_fail()?;
299 check!(rendered.contains(" at "))
300 .satisfies(is_true())
301 .or_fail()?;
302 Ok(())
303 }
304
305 #[test]
306 fn debug_matches_display() -> TestResult {
307 let _guard = crate::color::TEST_LOCK
310 .lock()
311 .unwrap_or_else(std::sync::PoisonError::into_inner);
312 let error = sample_assertion();
313 let debug = format!("{error:?}");
317 let human = debug
318 .split_once(crate::STRUCTURED_MARKER)
319 .map_or(debug.as_str(), |(before, _)| before.trim_end());
320 check!(human)
321 .satisfies(eq(format!("{error}").as_str()))
322 .or_fail()?;
323 Ok(())
324 }
325
326 #[test]
327 fn context_frames_render_in_order() -> TestResult {
328 let error = sample_assertion()
329 .with_context_frame(ContextFrame::new("creating user"))
330 .with_context_frame(ContextFrame::new("loading profile"));
331 let rendered = error.to_string();
332 let first = rendered
333 .find("creating user")
334 .or_fail_with("first frame present")?;
335 let second = rendered
336 .find("loading profile")
337 .or_fail_with("second frame present")?;
338 check!(first < second).satisfies(is_true()).or_fail()?;
339 Ok(())
340 }
341
342 #[test]
343 fn error_source_walks_into_payload_other() -> TestResult {
344 let io = std::io::Error::new(std::io::ErrorKind::NotFound, "missing file");
345 let error = TestError::new(ErrorKind::Custom).with_payload(Payload::Other(Box::new(io)));
346 let source =
347 std::error::Error::source(&error).or_fail_with("source is the wrapped io error")?;
348 check!(source.to_string())
349 .satisfies(eq("missing file".to_string()))
350 .or_fail()?;
351 Ok(())
352 }
353
354 #[test]
355 fn expected_actual_payload_renders_both_values() -> TestResult {
356 let error = TestError::new(ErrorKind::Assertion).with_payload(Payload::ExpectedActual {
357 expected: "4".to_string(),
358 actual: "5".to_string(),
359 diff: None,
360 });
361 let rendered = error.to_string();
362 check!(rendered.contains("expected: 4"))
363 .satisfies(is_true())
364 .or_fail()?;
365 check!(rendered.contains("actual: 5"))
366 .satisfies(is_true())
367 .or_fail()?;
368 Ok(())
369 }
370
371 #[test]
372 fn multiple_payload_renders_every_sub_failure() -> TestResult {
373 let error = TestError::new(ErrorKind::Assertion).with_payload(Payload::Multiple(vec![
374 TestError::new(ErrorKind::Assertion).with_message("first"),
375 TestError::new(ErrorKind::Assertion).with_message("second"),
376 ]));
377 let rendered = error.to_string();
378 check!(rendered.contains("first"))
379 .satisfies(is_true())
380 .or_fail()?;
381 check!(rendered.contains("second"))
382 .satisfies(is_true())
383 .or_fail()?;
384 Ok(())
385 }
386
387 #[test]
388 fn assertion_constructor_sets_kind_message_and_caller_location() -> TestResult {
389 let line = line!() + 1;
390 let error = TestError::assertion("values differ");
391 check!(error.kind).satisfies(eq(ErrorKind::Assertion)).or_fail()?;
392 check!(error.message.as_deref())
393 .satisfies(eq(Some("values differ")))
394 .or_fail()?;
395 check!(error.location.line()).satisfies(eq(line)).or_fail()?;
396 check!(error.location.file().ends_with("error.rs"))
397 .satisfies(is_true())
398 .or_fail()?;
399 Ok(())
400 }
401
402 #[test]
403 fn custom_constructor_sets_kind_message_and_caller_location() -> TestResult {
404 let line = line!() + 1;
405 let error = TestError::custom("something off");
406 check!(error.kind).satisfies(eq(ErrorKind::Custom)).or_fail()?;
407 check!(error.message.as_deref())
408 .satisfies(eq(Some("something off")))
409 .or_fail()?;
410 check!(error.location.line()).satisfies(eq(line)).or_fail()?;
411 check!(error.location.file().ends_with("error.rs"))
412 .satisfies(is_true())
413 .or_fail()?;
414 Ok(())
415 }
416
417 #[test]
418 fn from_expected_actual_captures_debug_values_and_caller_location() -> TestResult {
419 let line = line!() + 1;
420 let error = TestError::from_expected_actual(4, 5);
421 check!(error.kind).satisfies(eq(ErrorKind::Assertion)).or_fail()?;
422 check!(error.location.line()).satisfies(eq(line)).or_fail()?;
423 match error.payload.map(|payload| *payload) {
424 Some(Payload::ExpectedActual {
425 expected,
426 actual,
427 diff,
428 }) => {
429 check!(expected).satisfies(eq("4".to_string())).or_fail()?;
430 check!(actual).satisfies(eq("5".to_string())).or_fail()?;
431 check!(diff.is_none()).satisfies(is_true()).or_fail()?;
432 }
433 other => panic!("expected ExpectedActual, got {other:?}"),
434 }
435 Ok(())
436 }
437}