diff --git a/TUnit.Engine.Tests/GitHubReporterTests.cs b/TUnit.Engine.Tests/GitHubReporterTests.cs index 81cf2382c9..19a25df118 100644 --- a/TUnit.Engine.Tests/GitHubReporterTests.cs +++ b/TUnit.Engine.Tests/GitHubReporterTests.cs @@ -1,6 +1,7 @@ using Microsoft.Testing.Platform.Extensions.Messages; using Microsoft.Testing.Platform.TestHost; using Shouldly; +using TUnit.Engine.Exceptions; using TUnit.Engine.Reporters; namespace TUnit.Engine.Tests; @@ -260,6 +261,27 @@ await FeedTestMessages(reporter, output.ShouldContain("Timeout"); } + [Test] + public async Task AfterRunAsync_Unwraps_TestFailedException_For_Grouping() + { + var (reporter, outputFile) = await SetupReporter(); + + var inner1 = new InvalidOperationException("Docker image not created"); + var inner2 = new InvalidOperationException("Docker image not created"); + await FeedTestMessages(reporter, + CreateFailedTestMessage("1", "T1", "Svc", new TestFailedException(inner1)), + CreateFailedTestMessage("2", "T2", "Svc", new TestFailedException(inner2)) + ); + + await reporter.AfterRunAsync(1, CancellationToken.None); + + var output = await File.ReadAllTextAsync(outputFile); + output.ShouldContain("InvalidOperationException (2 tests)"); + output.ShouldNotContain("TestFailedException ("); + output.ShouldContain("Docker image not created"); + output.ShouldContain("2 × `InvalidOperationException`"); + } + [Test] public async Task AfterRunAsync_Other_NonPassing_Tests_Remain_Separate() { diff --git a/TUnit.Engine/Exceptions/TUnitFailedException.cs b/TUnit.Engine/Exceptions/TUnitFailedException.cs index 32ab50321f..8cad6602d2 100644 --- a/TUnit.Engine/Exceptions/TUnitFailedException.cs +++ b/TUnit.Engine/Exceptions/TUnitFailedException.cs @@ -1,4 +1,5 @@ -using TUnit.Core.Exceptions; +using System.Diagnostics.CodeAnalysis; +using TUnit.Core.Exceptions; using TUnit.Core.Helpers; using TUnit.Engine.CommandLineProviders; @@ -6,8 +7,11 @@ namespace TUnit.Engine.Exceptions; public abstract class TUnitFailedException : TUnitException { + public Exception? WrappedException { get; } + protected TUnitFailedException(Exception exception) : base($"{exception.GetType().Name}: {exception.Message}", exception.InnerException) { + WrappedException = exception; StackTrace = FilterStackTrace(exception.StackTrace); } @@ -18,6 +22,16 @@ protected TUnitFailedException(string? message, Exception? innerException) : bas public override string StackTrace { get; } + [return: NotNullIfNotNull(nameof(exception))] + public static Exception? Unwrap(Exception? exception) + { + while (exception is TUnitFailedException { WrappedException: { } wrapped }) + { + exception = wrapped; + } + return exception; + } + // The hint only mentions --detailed-stacktrace because filtering is bypassed // entirely when --log-level Debug/Trace is set (see TUnitMessageBus.SimplifyStacktrace), // so users on debug logging will never see this message. diff --git a/TUnit.Engine/Reporters/GitHubReporter.cs b/TUnit.Engine/Reporters/GitHubReporter.cs index 0ed6afd379..da742484a0 100644 --- a/TUnit.Engine/Reporters/GitHubReporter.cs +++ b/TUnit.Engine/Reporters/GitHubReporter.cs @@ -9,6 +9,7 @@ using Microsoft.Testing.Platform.Extensions.TestHost; using TUnit.Engine.Configuration; using TUnit.Engine.Constants; +using TUnit.Engine.Exceptions; using TUnit.Engine.Framework; using TUnit.Engine.Helpers; @@ -528,9 +529,9 @@ or TimeoutTestNodeStateProperty return stateProperty switch { FailedTestNodeStateProperty failedTestNodeStateProperty => - GetTruncatedExceptionMessage(failedTestNodeStateProperty.Exception) ?? "Test failed", + GetTruncatedExceptionMessage(TUnitFailedException.Unwrap(failedTestNodeStateProperty.Exception)) ?? "Test failed", ErrorTestNodeStateProperty errorTestNodeStateProperty => - GetTruncatedExceptionMessage(errorTestNodeStateProperty.Exception) ?? "Test failed", + GetTruncatedExceptionMessage(TUnitFailedException.Unwrap(errorTestNodeStateProperty.Exception)) ?? "Test failed", TimeoutTestNodeStateProperty timeoutTestNodeStateProperty => timeoutTestNodeStateProperty.Explanation, #pragma warning disable CS0618 // CancelledTestNodeStateProperty is obsolete CancelledTestNodeStateProperty => "Test was cancelled", @@ -637,8 +638,8 @@ internal void SetReporterStyle(GitHubReporterStyle style) private static string GetExceptionTypeName(IProperty? stateProperty) => stateProperty switch { - FailedTestNodeStateProperty f => f.Exception?.GetType().Name ?? "Unknown", - ErrorTestNodeStateProperty e => e.Exception?.GetType().Name ?? "Unknown", + FailedTestNodeStateProperty f => TUnitFailedException.Unwrap(f.Exception)?.GetType().Name ?? "Unknown", + ErrorTestNodeStateProperty e => TUnitFailedException.Unwrap(e.Exception)?.GetType().Name ?? "Unknown", TimeoutTestNodeStateProperty => "Timeout", _ => "Unknown" }; diff --git a/TUnit.Engine/Reporters/Html/HtmlReporter.cs b/TUnit.Engine/Reporters/Html/HtmlReporter.cs index 98bbc2eee4..5c3213503f 100644 --- a/TUnit.Engine/Reporters/Html/HtmlReporter.cs +++ b/TUnit.Engine/Reporters/Html/HtmlReporter.cs @@ -13,6 +13,7 @@ using TUnit.Core; using TUnit.Engine.Configuration; using TUnit.Engine.Constants; +using TUnit.Engine.Exceptions; using TUnit.Engine.Framework; #pragma warning disable TPEXP @@ -593,6 +594,8 @@ private static (string Status, ReportExceptionData? Exception, string? SkipReaso return null; } + ex = TUnitFailedException.Unwrap(ex); + return new ReportExceptionData { Type = ex.GetType().FullName ?? ex.GetType().Name, diff --git a/TUnit.Engine/Xml/JUnitXmlWriter.cs b/TUnit.Engine/Xml/JUnitXmlWriter.cs index bb7b27c015..978fe65a70 100644 --- a/TUnit.Engine/Xml/JUnitXmlWriter.cs +++ b/TUnit.Engine/Xml/JUnitXmlWriter.cs @@ -5,6 +5,7 @@ using System.Text; using System.Xml; using Microsoft.Testing.Platform.Extensions.Messages; +using TUnit.Engine.Exceptions; namespace TUnit.Engine.Xml; @@ -245,7 +246,7 @@ private static void WriteFailure(XmlWriter writer, FailedTestNodeStateProperty f { writer.WriteStartElement("failure"); - var exception = failed.Exception; + var exception = TUnitFailedException.Unwrap(failed.Exception); if (exception != null) { writer.WriteAttributeString("message", SanitizeForXml(exception.Message)); @@ -267,7 +268,7 @@ private static void WriteError(XmlWriter writer, ErrorTestNodeStateProperty erro { writer.WriteStartElement("error"); - var exception = error.Exception; + var exception = TUnitFailedException.Unwrap(error.Exception); if (exception != null) { writer.WriteAttributeString("message", SanitizeForXml(exception.Message));