From 9aaa6ee15a0ceb4859c875096ba521bd5f553147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Wed, 30 Oct 2024 11:43:07 +0100 Subject: [PATCH] Backport fix displaying inner exceptions (#3965) --- .../Terminal/TerminalTestReporter.cs | 73 +++++++++++++------ .../OutputDevice/TerminalOutputDevice.cs | 20 ++--- .../Terminal/TerminalTestReporterTests.cs | 29 ++++---- 3 files changed, 78 insertions(+), 44 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs index c61803c262..0b1de6c275 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs @@ -373,7 +373,7 @@ internal void TestCompleted( TestOutcome outcome, TimeSpan duration, string? errorMessage, - string? errorStackTrace, + Exception? exception, string? expected, string? actual) { @@ -410,7 +410,7 @@ internal void TestCompleted( outcome, duration, errorMessage, - errorStackTrace, + exception, expected, actual)); } @@ -425,7 +425,7 @@ internal void TestCompleted( TestOutcome outcome, TimeSpan duration, string? errorMessage, - string? errorStackTrace, + Exception? exception, string? expected, string? actual) { @@ -469,9 +469,10 @@ internal void TestCompleted( terminal.AppendLine(); - FormatErrorMessage(terminal, errorMessage); + FormatErrorMessage(terminal, errorMessage, exception, outcome); FormatExpectedAndActual(terminal, expected, actual); - FormatStackTrace(terminal, errorStackTrace); + FormatStackTrace(terminal, exception); + FormatInnerExceptions(terminal, exception); } private static void AppendAssemblyLinkTargetFrameworkAndArchitecture(ITerminal terminal, string assembly, string? targetFramework, string? architecture) @@ -495,25 +496,16 @@ private static void AppendAssemblyLinkTargetFrameworkAndArchitecture(ITerminal t } } - private static void FormatStackTrace(ITerminal terminal, string? errorStackTrace) + private static void FormatStackTrace(ITerminal terminal, Exception? exception) { - if (RoslynString.IsNullOrWhiteSpace(errorStackTrace)) + if (exception?.StackTrace is not { } stackTrace) { return; } - terminal.SetColor(TerminalColor.Red); - terminal.Append(SingleIndentation); - terminal.Append(PlatformResources.StackTrace); - terminal.AppendLine(":"); - - if (!errorStackTrace.Contains('\n')) - { - AppendStackFrame(terminal, errorStackTrace); - return; - } + terminal.SetColor(TerminalColor.DarkGray); - string[] lines = errorStackTrace.Split(NewLineStrings, StringSplitOptions.None); + string[] lines = stackTrace.Split(NewLineStrings, StringSplitOptions.None); foreach (string line in lines) { AppendStackFrame(terminal, line); @@ -581,18 +573,57 @@ private static void FormatExpectedAndActual(ITerminal terminal, string? expected terminal.ResetColor(); } - private static void FormatErrorMessage(ITerminal terminal, string? errorMessage) + private static void FormatErrorMessage(ITerminal terminal, string? errorMessage, Exception? exception, TestOutcome outcome) { - if (RoslynString.IsNullOrWhiteSpace(errorMessage)) + if (RoslynString.IsNullOrWhiteSpace(errorMessage) && exception is null) { return; } terminal.SetColor(TerminalColor.Red); - AppendIndentedLine(terminal, errorMessage, SingleIndentation); + + if (exception is null) + { + AppendIndentedLine(terminal, errorMessage, SingleIndentation); + } + else if (outcome == TestOutcome.Fail) + { + // For failed tests, we don't prefix the message with the exception type because it is most likely an assertion specific exception like AssertionFailedException, and we prefer to show that without the exception type to avoid additional noise. + AppendIndentedLine(terminal, errorMessage ?? exception.Message, SingleIndentation); + } + else + { + AppendIndentedLine(terminal, $"{exception.GetType().FullName}: {errorMessage ?? exception.Message}", SingleIndentation); + } + terminal.ResetColor(); } + private static void FormatInnerExceptions(ITerminal terminal, Exception? exception) + { + IEnumerable aggregateExceptions = exception switch + { + AggregateException aggregate => aggregate.Flatten().InnerExceptions, + _ => [exception?.InnerException], + }; + + foreach (Exception? aggregate in aggregateExceptions) + { + Exception? currentException = aggregate; + while (currentException is not null) + { + terminal.SetColor(TerminalColor.Red); + terminal.Append(SingleIndentation); + terminal.Append("--->"); + FormatErrorMessage(terminal, null, currentException, TestOutcome.Error); + + FormatStackTrace(terminal, currentException); + + currentException = currentException.InnerException; + } + } + } + private static void AppendIndentedLine(ITerminal terminal, string? message, string indent) { if (RoslynString.IsNullOrWhiteSpace(message)) diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.cs index 5792660438..774eda95ad 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.cs @@ -411,8 +411,8 @@ public async Task ConsumeAsync(IDataProducer dataProducer, IData value, Cancella testNodeStateChanged.TestNode.DisplayName, TestOutcome.Error, duration, - errorMessage: errorState.Exception?.Message ?? errorState.Explanation, - errorState.Exception?.StackTrace, + errorState.Explanation, + errorState.Exception, expected: null, actual: null); break; @@ -425,8 +425,8 @@ public async Task ConsumeAsync(IDataProducer dataProducer, IData value, Cancella testNodeStateChanged.TestNode.DisplayName, TestOutcome.Fail, duration, - errorMessage: failedState.Exception?.Message ?? failedState.Explanation, - failedState.Exception?.StackTrace, + failedState.Explanation, + failedState.Exception, expected: failedState.Exception?.Data["assert.expected"] as string, actual: failedState.Exception?.Data["assert.actual"] as string); break; @@ -439,8 +439,8 @@ public async Task ConsumeAsync(IDataProducer dataProducer, IData value, Cancella testNodeStateChanged.TestNode.DisplayName, TestOutcome.Timeout, duration, - errorMessage: timeoutState.Exception?.Message ?? timeoutState.Explanation, - timeoutState.Exception?.StackTrace, + timeoutState.Explanation, + timeoutState.Exception, expected: null, actual: null); break; @@ -453,8 +453,8 @@ public async Task ConsumeAsync(IDataProducer dataProducer, IData value, Cancella testNodeStateChanged.TestNode.DisplayName, TestOutcome.Canceled, duration, - errorMessage: cancelledState.Exception?.Message ?? cancelledState.Explanation, - cancelledState.Exception?.StackTrace, + cancelledState.Explanation, + cancelledState.Exception, expected: null, actual: null); break; @@ -468,7 +468,7 @@ public async Task ConsumeAsync(IDataProducer dataProducer, IData value, Cancella outcome: TestOutcome.Passed, duration: duration, errorMessage: null, - errorStackTrace: null, + exception: null, expected: null, actual: null); break; @@ -482,7 +482,7 @@ public async Task ConsumeAsync(IDataProducer dataProducer, IData value, Cancella TestOutcome.Skipped, duration, errorMessage: null, - errorStackTrace: null, + exception: null, expected: null, actual: null); break; diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs index a4ca874c51..ced3ec2eb6 100644 --- a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs @@ -70,16 +70,16 @@ public void OutputFormattingIsCorrect() terminalReporter.AssemblyRunStarted(assembly, targetFramework, architecture); terminalReporter.TestCompleted(assembly, targetFramework, architecture, "PassedTest1", TestOutcome.Passed, TimeSpan.FromSeconds(10), - errorMessage: null, errorStackTrace: null, expected: null, actual: null); + errorMessage: null, exception: null, expected: null, actual: null); terminalReporter.TestCompleted(assembly, targetFramework, architecture, "SkippedTest1", TestOutcome.Skipped, TimeSpan.FromSeconds(10), - errorMessage: null, errorStackTrace: null, expected: null, actual: null); + errorMessage: null, exception: null, expected: null, actual: null); // timed out + cancelled + failed should all report as failed in summary terminalReporter.TestCompleted(assembly, targetFramework, architecture, "TimedoutTest1", TestOutcome.Timeout, TimeSpan.FromSeconds(10), - errorMessage: null, errorStackTrace: null, expected: null, actual: null); + errorMessage: null, exception: null, expected: null, actual: null); terminalReporter.TestCompleted(assembly, targetFramework, architecture, "CanceledTest1", TestOutcome.Canceled, TimeSpan.FromSeconds(10), - errorMessage: null, errorStackTrace: null, expected: null, actual: null); + errorMessage: null, exception: null, expected: null, actual: null); terminalReporter.TestCompleted(assembly, targetFramework, architecture, "FailedTest1", TestOutcome.Fail, TimeSpan.FromSeconds(10), - errorMessage: "Tests failed", errorStackTrace: @$" at FailingTest() in {folder}codefile.cs:line 10", expected: "ABC", actual: "DEF"); + errorMessage: "Tests failed", exception: new StackTraceException(@$" at FailingTest() in {folder}codefile.cs:line 10"), expected: "ABC", actual: "DEF"); terminalReporter.ArtifactAdded(outOfProcess: true, assembly, targetFramework, architecture, testName: null, @$"{folder}artifact1.txt"); terminalReporter.ArtifactAdded(outOfProcess: false, assembly, targetFramework, architecture, testName: null, @$"{folder}artifact2.txt"); terminalReporter.AssemblyRunCompleted(assembly, targetFramework, architecture); @@ -98,9 +98,8 @@ public void OutputFormattingIsCorrect() ABC Actual DEF - ␛[m␛[91m Stack Trace: - ␛[90mat ␛[m␛[91mFailingTest()␛[90m in ␛[90m␛]8;;file:///{folderLink}codefile.cs␛\{folder}codefile.cs:10␛]8;;␛\␛[m - + ␛[m␛[90m ␛[90mat ␛[m␛[91mFailingTest()␛[90m in ␛[90m␛]8;;file:///{folderLink}codefile.cs␛\{folder}codefile.cs:10␛]8;;␛\␛[m + ␛[m Out of process file artifacts produced: - ␛[90m␛]8;;file:///{folderLink}artifact1.txt␛\{folder}artifact1.txt␛]8;;␛\␛[m @@ -115,13 +114,10 @@ public void OutputFormattingIsCorrect() """; - EnsureAnsiMatch(expected, output); + Assert.AreEqual(expected, ShowEscape(output)); } - private void EnsureAnsiMatch(string expected, string actual) - => Assert.AreEqual(expected, ShowEscape(actual)); - - private string? ShowEscape(string? text) + private static string? ShowEscape(string? text) { string visibleEsc = "\x241b"; return text?.Replace(AnsiCodes.Esc, visibleEsc); @@ -229,4 +225,11 @@ public void SetColor(TerminalColor color) public void StopUpdate() => throw new NotImplementedException(); } + + private class StackTraceException : Exception + { + public StackTraceException(string stackTrace) => StackTrace = stackTrace; + + public override string? StackTrace { get; } + } }