From 4f5ed443713d6e7fbbdbd401d1ca5f3792a12870 Mon Sep 17 00:00:00 2001 From: Ralph Ursprung Date: Thu, 17 Jul 2025 11:12:29 +0200 Subject: [PATCH] `TestAssertionFailure`: retain full error chain in message so far the conversion from an `Error` to a `TestAssertionFailure` meant that all information about the source errors were lost. often, the `Display` implementation for `Error` implementations does not include the source information since that should instead be retrieved via `source`. while this has not yet been made into an official API guideline (see [rust-lang/api-guidelines#210]) this is nevertheless being followed. various crates like `anyhow` or `eyere` take care of pretty-printing the error chain on failure, however this does not work with `googletest` since `googletest::Result` has `TestAssertionFailure` as the error type which swallows any `Error` and only keeps the message. to resolve this a simple error chain implementation is added to the `From` implementation which pretty-prints the error chain. example: ``` Error: test3 Caused by: 1: test2 2: test1 ``` fixes #657 [rust-lang/api-guidelines#210]: https://github.com/rust-lang/api-guidelines/pull/210 --- googletest/src/internal/test_outcome.rs | 47 ++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/googletest/src/internal/test_outcome.rs b/googletest/src/internal/test_outcome.rs index 341eb72f..a13f0a6a 100644 --- a/googletest/src/internal/test_outcome.rs +++ b/googletest/src/internal/test_outcome.rs @@ -236,7 +236,21 @@ impl Debug for TestAssertionFailure { impl From for TestAssertionFailure { #[track_caller] fn from(value: T) -> Self { - TestAssertionFailure::create(format!("{value}")) + // print the full error chain, not just the first error. + let mut description = String::new(); + description.push_str(&format!("Error: {value}\n")); + let mut source = value.source(); + if source.is_some() { + description.push_str("\nCaused by:"); + let mut i = 1; + while let Some(e) = source { + description.push_str(&format!("\n{i}: {e}")); + source = e.source(); + i += 1; + } + } + + TestAssertionFailure::create(description) } } @@ -246,3 +260,34 @@ impl From for proptest::test_runner::TestCaseError { proptest::test_runner::TestCaseError::Fail(format!("{value}").into()) } } + +#[cfg(test)] +mod tests { + use crate::internal::test_outcome::TestAssertionFailure; + use std::fmt::{Debug, Display, Formatter}; + + #[test] + fn error_chain_from_error() { + #[derive(Debug)] + struct CustomError { + message: String, + source: Option>, + } + impl Display for CustomError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.message) + } + } + impl std::error::Error for CustomError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.source.as_ref().map(|e| e.as_ref() as _) + } + } + + let source1 = CustomError { message: "test1".to_string(), source: None }; + let source2 = CustomError { message: "test2".to_string(), source: Some(source1.into()) }; + let error = CustomError { message: "test3".to_string(), source: Some(source2.into()) }; + let assertion_failure = TestAssertionFailure::from(error); + assert_eq!(assertion_failure.description, "Error: test3\n\nCaused by:\n1: test2\n2: test1"); + } +}