diff --git a/src/Spectre.Console.Tests/Expectations/Exception/CallSite.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Exception/CallSite.Output.verified.txt index 51919ab53..60a989ab1 100644 --- a/src/Spectre.Console.Tests/Expectations/Exception/CallSite.Output.verified.txt +++ b/src/Spectre.Console.Tests/Expectations/Exception/CallSite.Output.verified.txt @@ -1,7 +1,7 @@ -System.InvalidOperationException: Something threw! - System.InvalidOperationException: Throwing! - at bool Spectre.Console.Tests.Data.TestExceptions.GenericMethodThatThrows(int? number) in /xyz/Exceptions.cs:nn - at void Spectre.Console.Tests.Data.TestExceptions.ThrowWithGenericInnerException() in /xyz/Exceptions.cs:nn - at void Spectre.Console.Tests.Data.TestExceptions.ThrowWithGenericInnerException() in /xyz/Exceptions.cs:nn - at void Spectre.Console.Tests.Unit.ExceptionTests.<>c.b__4_0() in /xyz/ExceptionTests.cs:nn - at Exception Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn +System.InvalidOperationException: Something threw! + System.InvalidOperationException: Throwing! + at bool Spectre.Console.Tests.Data.TestExceptions.GenericMethodThatThrows(int? number) in /xyz/Exceptions.cs:100 + at void Spectre.Console.Tests.Data.TestExceptions.ThrowWithGenericInnerException() in /xyz/Exceptions.cs:200 + at void Spectre.Console.Tests.Data.TestExceptions.ThrowWithGenericInnerException() in /xyz/Exceptions.cs:300 + at void Spectre.Console.Tests.Unit.ExceptionTests.<>c.b__4_0() in /xyz/ExceptionTests.cs:400 + at Exception Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:500 diff --git a/src/Spectre.Console.Tests/Expectations/Exception/Default.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Exception/Default.Output.verified.txt index be7c93f45..08cb599e9 100644 --- a/src/Spectre.Console.Tests/Expectations/Exception/Default.Output.verified.txt +++ b/src/Spectre.Console.Tests/Expectations/Exception/Default.Output.verified.txt @@ -1,4 +1,4 @@ System.InvalidOperationException: Throwing! - at bool Spectre.Console.Tests.Data.TestExceptions.MethodThatThrows(int? number) in /xyz/Exceptions.cs:nn - at void Spectre.Console.Tests.Unit.ExceptionTests.<>c.b__0_0() in /xyz/ExceptionTests.cs:nn - at Exception Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn + at bool Spectre.Console.Tests.Data.TestExceptions.MethodThatThrows(int? number) in /xyz/Exceptions.cs:100 + at void Spectre.Console.Tests.Unit.ExceptionTests.<>c.b__0_0() in /xyz/ExceptionTests.cs:200 + at Exception Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:300 diff --git a/src/Spectre.Console.Tests/Expectations/Exception/Expanded_Panel.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Exception/Expanded_Panel.Output.verified.txt new file mode 100644 index 000000000..3364ddd7b --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/Exception/Expanded_Panel.Output.verified.txt @@ -0,0 +1,6 @@ +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ GenericException: Throwing! │ +│ at bool MethodThatThrowsGenericException() in Exceptions.cs:100 │ +│ at void b__10_0() in ExceptionTests.cs:200 │ +│ at Exception GetException(Action action) in ExceptionTests.cs:300 │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/src/Spectre.Console.Tests/Expectations/Exception/GenericException.Output_exceptionFormats=Default.verified.txt b/src/Spectre.Console.Tests/Expectations/Exception/GenericException.Output_exceptionFormats=Default.verified.txt index c9b6b1ec8..ca33fbdc6 100644 --- a/src/Spectre.Console.Tests/Expectations/Exception/GenericException.Output_exceptionFormats=Default.verified.txt +++ b/src/Spectre.Console.Tests/Expectations/Exception/GenericException.Output_exceptionFormats=Default.verified.txt @@ -1,4 +1,4 @@ Spectre.Console.Tests.Data.GenericException: Throwing! - at bool Spectre.Console.Tests.Data.TestExceptions.MethodThatThrowsGenericException() in /xyz/Exceptions.cs:nn - at void Spectre.Console.Tests.Unit.ExceptionTests.<>c.b__8_0() in /xyz/ExceptionTests.cs:nn - at Exception Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn + at bool Spectre.Console.Tests.Data.TestExceptions.MethodThatThrowsGenericException() in /xyz/Exceptions.cs:100 + at void Spectre.Console.Tests.Unit.ExceptionTests.<>c.b__8_0() in /xyz/ExceptionTests.cs:200 + at Exception Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:300 diff --git a/src/Spectre.Console.Tests/Expectations/Exception/GenericException.Output_exceptionFormats=ShortenEverything.verified.txt b/src/Spectre.Console.Tests/Expectations/Exception/GenericException.Output_exceptionFormats=ShortenEverything.verified.txt index 07f6a7b7e..6bce0f514 100644 --- a/src/Spectre.Console.Tests/Expectations/Exception/GenericException.Output_exceptionFormats=ShortenEverything.verified.txt +++ b/src/Spectre.Console.Tests/Expectations/Exception/GenericException.Output_exceptionFormats=ShortenEverything.verified.txt @@ -1,4 +1,4 @@ GenericException: Throwing! - at bool MethodThatThrowsGenericException() in /xyz/ in Exceptions.cs:nn - at void b__8_0() in /xyz/ in ExceptionTests.cs:nn - at Exception GetException(Action action) in /xyz/ in ExceptionTests.cs:nn + at bool MethodThatThrowsGenericException() in Exceptions.cs:100 + at void b__8_0() in ExceptionTests.cs:200 + at Exception GetException(Action action) in ExceptionTests.cs:300 diff --git a/src/Spectre.Console.Tests/Expectations/Exception/GenericException.Output_exceptionFormats=ShortenMethods.verified.txt b/src/Spectre.Console.Tests/Expectations/Exception/GenericException.Output_exceptionFormats=ShortenMethods.verified.txt index 612171096..c9aece636 100644 --- a/src/Spectre.Console.Tests/Expectations/Exception/GenericException.Output_exceptionFormats=ShortenMethods.verified.txt +++ b/src/Spectre.Console.Tests/Expectations/Exception/GenericException.Output_exceptionFormats=ShortenMethods.verified.txt @@ -1,4 +1,4 @@ Spectre.Console.Tests.Data.GenericException: Throwing! - at bool MethodThatThrowsGenericException() in /xyz/Exceptions.cs:nn - at void b__8_0() in /xyz/ExceptionTests.cs:nn - at Exception GetException(Action action) in /xyz/ExceptionTests.cs:nn + at bool MethodThatThrowsGenericException() in /xyz/Exceptions.cs:100 + at void b__8_0() in /xyz/ExceptionTests.cs:200 + at Exception GetException(Action action) in /xyz/ExceptionTests.cs:300 diff --git a/src/Spectre.Console.Tests/Expectations/Exception/GenericException.Output_exceptionFormats=ShortenTypes.verified.txt b/src/Spectre.Console.Tests/Expectations/Exception/GenericException.Output_exceptionFormats=ShortenTypes.verified.txt index 045d77711..0d79bb544 100644 --- a/src/Spectre.Console.Tests/Expectations/Exception/GenericException.Output_exceptionFormats=ShortenTypes.verified.txt +++ b/src/Spectre.Console.Tests/Expectations/Exception/GenericException.Output_exceptionFormats=ShortenTypes.verified.txt @@ -1,4 +1,4 @@ GenericException: Throwing! - at bool Spectre.Console.Tests.Data.TestExceptions.MethodThatThrowsGenericException() in /xyz/Exceptions.cs:nn - at void Spectre.Console.Tests.Unit.ExceptionTests.<>c.b__8_0() in /xyz/ExceptionTests.cs:nn - at Exception Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn + at bool Spectre.Console.Tests.Data.TestExceptions.MethodThatThrowsGenericException() in /xyz/Exceptions.cs:100 + at void Spectre.Console.Tests.Unit.ExceptionTests.<>c.b__8_0() in /xyz/ExceptionTests.cs:200 + at Exception Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:300 diff --git a/src/Spectre.Console.Tests/Expectations/Exception/InnerException.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Exception/InnerException.Output.verified.txt index 2783df503..4e6f669d9 100644 --- a/src/Spectre.Console.Tests/Expectations/Exception/InnerException.Output.verified.txt +++ b/src/Spectre.Console.Tests/Expectations/Exception/InnerException.Output.verified.txt @@ -1,7 +1,7 @@ -System.InvalidOperationException: Something threw! - System.InvalidOperationException: Throwing! - at bool Spectre.Console.Tests.Data.TestExceptions.MethodThatThrows(int? number) in /xyz/Exceptions.cs:nn - at void Spectre.Console.Tests.Data.TestExceptions.ThrowWithInnerException() in /xyz/Exceptions.cs:nn - at void Spectre.Console.Tests.Data.TestExceptions.ThrowWithInnerException() in /xyz/Exceptions.cs:nn - at void Spectre.Console.Tests.Unit.ExceptionTests.<>c.b__3_0() in /xyz/ExceptionTests.cs:nn - at Exception Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn +System.InvalidOperationException: Something threw! + System.InvalidOperationException: Throwing! + at bool Spectre.Console.Tests.Data.TestExceptions.MethodThatThrows(int? number) in /xyz/Exceptions.cs:100 + at void Spectre.Console.Tests.Data.TestExceptions.ThrowWithInnerException() in /xyz/Exceptions.cs:200 + at void Spectre.Console.Tests.Data.TestExceptions.ThrowWithInnerException() in /xyz/Exceptions.cs:300 + at void Spectre.Console.Tests.Unit.ExceptionTests.<>c.b__3_0() in /xyz/ExceptionTests.cs:400 + at Exception Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:500 diff --git a/src/Spectre.Console.Tests/Expectations/Exception/MinimumSpace.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Exception/MinimumSpace.Output.verified.txt new file mode 100644 index 000000000..8d08a7ef4 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/Exception/MinimumSpace.Output.verified.txt @@ -0,0 +1,6 @@ +┌────────────────────────────────────────────────────────────────────────────────────────┐ +│ GenericException: Throwing! │ +│ at bool MethodThatThrowsGenericException() in Exceptions.cs:100 │ +│ at void b__9_0() in ExceptionTests.cs:200 │ +│ at Exception GetException(Action action) in ExceptionTests.cs:300 │ +└────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/src/Spectre.Console.Tests/Expectations/Exception/OutParam.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Exception/OutParam.Output.verified.txt index 6c9871f05..80f9dcfee 100644 --- a/src/Spectre.Console.Tests/Expectations/Exception/OutParam.Output.verified.txt +++ b/src/Spectre.Console.Tests/Expectations/Exception/OutParam.Output.verified.txt @@ -1,4 +1,4 @@ InvalidOperationException: Throwing! - at List Spectre.Console.Tests.Data.TestExceptions.GenericMethodWithOutThatThrows(out List firstFewItems) in /xyz/Exceptions.cs:nn - at void Spectre.Console.Tests.Unit.ExceptionTests.<>c.b__5_0() in /xyz/ExceptionTests.cs:nn - at Exception Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn + at List Spectre.Console.Tests.Data.TestExceptions.GenericMethodWithOutThatThrows(out List firstFewItems) in /xyz/Exceptions.cs:100 + at void Spectre.Console.Tests.Unit.ExceptionTests.<>c.b__5_0() in /xyz/ExceptionTests.cs:200 + at Exception Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:300 diff --git a/src/Spectre.Console.Tests/Expectations/Exception/ShortenedMethods.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Exception/ShortenedMethods.Output.verified.txt index 53620276d..048155910 100644 --- a/src/Spectre.Console.Tests/Expectations/Exception/ShortenedMethods.Output.verified.txt +++ b/src/Spectre.Console.Tests/Expectations/Exception/ShortenedMethods.Output.verified.txt @@ -1,4 +1,4 @@ System.InvalidOperationException: Throwing! - at bool MethodThatThrows(int? number) in /xyz/Exceptions.cs:nn - at void b__2_0() in /xyz/ExceptionTests.cs:nn - at Exception GetException(Action action) in /xyz/ExceptionTests.cs:nn + at bool MethodThatThrows(int? number) in /xyz/Exceptions.cs:100 + at void b__2_0() in /xyz/ExceptionTests.cs:200 + at Exception GetException(Action action) in /xyz/ExceptionTests.cs:300 diff --git a/src/Spectre.Console.Tests/Expectations/Exception/ShortenedTypes.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Exception/ShortenedTypes.Output.verified.txt index 860c11caa..fe31fb8f7 100644 --- a/src/Spectre.Console.Tests/Expectations/Exception/ShortenedTypes.Output.verified.txt +++ b/src/Spectre.Console.Tests/Expectations/Exception/ShortenedTypes.Output.verified.txt @@ -1,4 +1,4 @@ InvalidOperationException: Throwing! - at bool Spectre.Console.Tests.Data.TestExceptions.MethodThatThrows(int? number) in /xyz/Exceptions.cs:nn - at void Spectre.Console.Tests.Unit.ExceptionTests.<>c.b__1_0() in /xyz/ExceptionTests.cs:nn - at Exception Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn + at bool Spectre.Console.Tests.Data.TestExceptions.MethodThatThrows(int? number) in /xyz/Exceptions.cs:100 + at void Spectre.Console.Tests.Unit.ExceptionTests.<>c.b__1_0() in /xyz/ExceptionTests.cs:200 + at Exception Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:300 diff --git a/src/Spectre.Console.Tests/Expectations/Exception/Tuple.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Exception/Tuple.Output.verified.txt index acd2a363f..b2a659276 100644 --- a/src/Spectre.Console.Tests/Expectations/Exception/Tuple.Output.verified.txt +++ b/src/Spectre.Console.Tests/Expectations/Exception/Tuple.Output.verified.txt @@ -1,5 +1,5 @@ -InvalidOperationException: Throwing! - at bool Spectre.Console.Tests.Data.TestExceptions.MethodThatThrows(int? number) in /xyz/Exceptions.cs:nn - at (string Key, List Values) Spectre.Console.Tests.Data.TestExceptions.GetTuplesWithInnerException((int First, string Second) myValue) in /xyz/Exceptions.cs:nn - at void Spectre.Console.Tests.Unit.ExceptionTests.<>c.b__6_0() in /xyz/ExceptionTests.cs:nn - at Exception Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn +InvalidOperationException: Throwing! + at bool Spectre.Console.Tests.Data.TestExceptions.MethodThatThrows(int? number) in /xyz/Exceptions.cs:100 + at (string Key, List Values) Spectre.Console.Tests.Data.TestExceptions.GetTuplesWithInnerException((int First, string Second) myValue) in /xyz/Exceptions.cs:200 + at void Spectre.Console.Tests.Unit.ExceptionTests.<>c.b__6_0() in /xyz/ExceptionTests.cs:300 + at Exception Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:400 diff --git a/src/Spectre.Console.Tests/Expectations/Public_API.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Public_API.Output.verified.txt index 4b55062cc..8e1777a8c 100644 --- a/src/Spectre.Console.Tests/Expectations/Public_API.Output.verified.txt +++ b/src/Spectre.Console.Tests/Expectations/Public_API.Output.verified.txt @@ -1897,11 +1897,24 @@ ShortenEverything = 7, NoStackTrace = 16, } + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2070:RequiresUnreferencedCode")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2075:RequiresUnreferencedCode")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode")] + public class ExceptionInfoResolver + { + public ExceptionInfoResolver() { } + public virtual int GetFileLineNumber(System.Diagnostics.StackFrame frame) { } + public virtual string? GetFileName(System.Diagnostics.StackFrame frame) { } + public virtual string GetMethodName(System.Reflection.MethodBase method) { } + public virtual string GetParameterName(System.Reflection.ParameterInfo parameter) { } + } public sealed class ExceptionSettings { public ExceptionSettings() { } - public Spectre.Console.ExceptionFormats Format { get; set; } - public Spectre.Console.ExceptionStyle Style { get; set; } + public Spectre.Console.ExceptionFormats Format { get; init; } + public Spectre.Console.ExceptionInfoResolver Resolver { get; init; } + public Spectre.Console.ExceptionStyle Style { get; init; } } public sealed class ExceptionStyle { diff --git a/src/Spectre.Console.Tests/Unit/ExceptionTests.cs b/src/Spectre.Console.Tests/Unit/ExceptionTests.cs index 0a58f755f..62aea7b39 100644 --- a/src/Spectre.Console.Tests/Unit/ExceptionTests.cs +++ b/src/Spectre.Console.Tests/Unit/ExceptionTests.cs @@ -12,10 +12,15 @@ public Task Should_Write_Exception() var dex = GetException(() => TestExceptions.MethodThatThrows(null)); // When - var result = console.WriteNormalizedException(dex); + console.WriteException(dex, new ExceptionSettings + { + Format = ExceptionFormats.Default, + Resolver = new ExceptionScrubber(), + }); + // Then - return Verifier.Verify(result); + return Verifier.Verify(console.Output); } [Fact] @@ -27,10 +32,14 @@ public Task Should_Write_Exception_With_Shortened_Types() var dex = GetException(() => TestExceptions.MethodThatThrows(null)); // When - var result = console.WriteNormalizedException(dex, ExceptionFormats.ShortenTypes); + console.WriteException(dex, new ExceptionSettings + { + Format = ExceptionFormats.ShortenTypes, + Resolver = new ExceptionScrubber(), + }); // Then - return Verifier.Verify(result); + return Verifier.Verify(console.Output); } [Fact] @@ -42,10 +51,14 @@ public Task Should_Write_Exception_With_Shortened_Methods() var dex = GetException(() => TestExceptions.MethodThatThrows(null)); // When - var result = console.WriteNormalizedException(dex, ExceptionFormats.ShortenMethods); + console.WriteException(dex, new ExceptionSettings + { + Format = ExceptionFormats.ShortenMethods, + Resolver = new ExceptionScrubber(), + }); // Then - return Verifier.Verify(result); + return Verifier.Verify(console.Output); } [Fact] @@ -57,10 +70,14 @@ public Task Should_Write_Exception_With_Inner_Exception() var dex = GetException(() => TestExceptions.ThrowWithInnerException()); // When - var result = console.WriteNormalizedException(dex); + console.WriteException(dex, new ExceptionSettings + { + Format = ExceptionFormats.Default, + Resolver = new ExceptionScrubber(), + }); // Then - return Verifier.Verify(result); + return Verifier.Verify(console.Output); } [Fact] @@ -72,10 +89,14 @@ public Task Should_Write_Exceptions_With_Generic_Type_Parameters_In_Callsite_As_ var dex = GetException(() => TestExceptions.ThrowWithGenericInnerException()); // When - var result = console.WriteNormalizedException(dex); + console.WriteException(dex, new ExceptionSettings + { + Format = ExceptionFormats.Default, + Resolver = new ExceptionScrubber(), + }); // Then - return Verifier.Verify(result); + return Verifier.Verify(console.Output); } [Fact] @@ -87,10 +108,14 @@ public Task Should_Write_Exception_With_Output_Param() var dex = GetException(() => TestExceptions.GenericMethodWithOutThatThrows(out _)); // When - var result = console.WriteNormalizedException(dex, ExceptionFormats.ShortenTypes); + console.WriteException(dex, new ExceptionSettings + { + Format = ExceptionFormats.ShortenTypes, + Resolver = new ExceptionScrubber(), + }); // Then - return Verifier.Verify(result); + return Verifier.Verify(console.Output); } [Fact] @@ -102,10 +127,14 @@ public Task Should_Write_Exception_With_Tuple_Return() var dex = GetException(() => TestExceptions.GetTuplesWithInnerException((0, "value"))); // When - var result = console.WriteNormalizedException(dex, ExceptionFormats.ShortenTypes); + console.WriteException(dex, new ExceptionSettings + { + Format = ExceptionFormats.ShortenTypes, + Resolver = new ExceptionScrubber(), + }); // Then - return Verifier.Verify(result); + return Verifier.Verify(console.Output); } [Fact] @@ -117,10 +146,14 @@ public Task Should_Write_Exception_With_No_StackTrace() var dex = GetException(TestExceptions.ThrowWithInnerException); // When - var result = console.WriteNormalizedException(dex, ExceptionFormats.NoStackTrace); + console.WriteException(dex, new ExceptionSettings + { + Format = ExceptionFormats.NoStackTrace, + Resolver = new ExceptionScrubber(), + }); // Then - return Verifier.Verify(result); + return Verifier.Verify(console.Output); } [Theory] @@ -136,13 +169,60 @@ public Task Should_Write_GenericException(ExceptionFormats exceptionFormats) var dex = GetException(() => TestExceptions.MethodThatThrowsGenericException()); // When - var result = console.WriteNormalizedException(dex, exceptionFormats); + console.WriteException(dex, new ExceptionSettings + { + Format = exceptionFormats, + Resolver = new ExceptionScrubber(), + }); + + // Then + return Verifier.Verify(console.Output).UseParameters(exceptionFormats); + } + + [Fact] + [Expectation("MinimumSpace")] + public Task Should_Take_Up_Minimum_Space_When_Wrapped() + { + // Given + var console = new TestConsole().Width(1024); + var dex = GetException(() => TestExceptions.MethodThatThrowsGenericException()); + + // When + console.Write( + new Panel( + dex.GetRenderable(new ExceptionSettings + { + Format = ExceptionFormats.ShortenEverything, + Resolver = new ExceptionScrubber(), + }))); + + // Then + return Verifier.Verify(console.Output); + } + + [Fact] + [Expectation("Expanded_Panel")] + public Task Exception_Within_Expanded_Panel_Should_Expand_As_Expected() + { + // Given + var console = new TestConsole().Width(120); + var dex = GetException(() => TestExceptions.MethodThatThrowsGenericException()); + + // When + console.Write( + new Panel( + dex.GetRenderable(new ExceptionSettings + { + Format = ExceptionFormats.ShortenEverything, + Resolver = new ExceptionScrubber(), + })) + .Expand()); // Then - return Verifier.Verify(result).UseParameters(exceptionFormats); + return Verifier.Verify(console.Output); } - public static Exception GetException(Action action) + private static Exception GetException(Action action) { try { diff --git a/src/Spectre.Console.Tests/Utilities/ExceptionScrubber.cs b/src/Spectre.Console.Tests/Utilities/ExceptionScrubber.cs new file mode 100644 index 000000000..63530ffd7 --- /dev/null +++ b/src/Spectre.Console.Tests/Utilities/ExceptionScrubber.cs @@ -0,0 +1,31 @@ +using System.Diagnostics; + +namespace Spectre.Console.Tests; + +public sealed class ExceptionScrubber : ExceptionInfoResolver +{ + private int _lineNumber; + + private static readonly Regex _filenameRegex = new Regex(".*cs:?.*", RegexOptions.Multiline); + private static readonly Regex _pathSeparatorRegex = new Regex(@"[/\\]+"); + + public override int GetFileLineNumber(StackFrame frame) + { + return _lineNumber += 100; + } + + public override string? GetFileName(StackFrame frame) + { + var text = base.GetFileName(frame) ?? string.Empty; + text = _filenameRegex.Replace(text, match => + { + var value = match.Value; + var index = value.LastIndexOfAny(['\\', '/']); + var filename = value.Substring(index + 1, value.Length - index - 1); + + return $"/xyz/{filename}"; + }); + + return _pathSeparatorRegex.Replace(text, "/"); + } +} \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Utilities/TestConsoleExtensions.cs b/src/Spectre.Console.Tests/Utilities/TestConsoleExtensions.cs deleted file mode 100644 index 7758ebff0..000000000 --- a/src/Spectre.Console.Tests/Utilities/TestConsoleExtensions.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace Spectre.Console.Tests; - -public static class TestConsoleExtensions -{ - private static readonly Regex _lineNumberRegex = new Regex(":\\d+", RegexOptions.Singleline); - private static readonly Regex _filenameRegex = new Regex("\\sin\\s.*cs:nn", RegexOptions.Multiline); - private static readonly Regex _pathSeparatorRegex = new Regex(@"[/\\]+"); - - public static string WriteNormalizedException(this TestConsole console, Exception ex, ExceptionFormats formats = ExceptionFormats.Default) - { - if (!string.IsNullOrWhiteSpace(console.Output)) - { - throw new InvalidOperationException("Output buffer is not empty."); - } - - console.WriteException(ex, formats); - - return string.Join("\n", NormalizeStackTrace(console.Output) - .NormalizeLineEndings() - .Split(['\n']) - .Select(line => line.TrimEnd())) - .Replace(Path.DirectorySeparatorChar, '/'); - } - - public static string NormalizeStackTrace(string text) - { - // First normalize line numbers - text = _lineNumberRegex.Replace(text, ":nn"); - - // Then normalize paths and filenames - text = _filenameRegex.Replace(text, match => - { - var value = match.Value; - var index = value.LastIndexOfAny(['\\', '/']); - var filename = value.Substring(index + 1, value.Length - index - 1); - - return $" in /xyz/{filename}"; - }); - - // Finally normalize any remaining path separators - return _pathSeparatorRegex.Replace(text, "/"); - } -} \ No newline at end of file diff --git a/src/Spectre.Console/AnsiConsole.Exceptions.cs b/src/Spectre.Console/AnsiConsole.Exceptions.cs index de3a47333..e4b6103aa 100644 --- a/src/Spectre.Console/AnsiConsole.Exceptions.cs +++ b/src/Spectre.Console/AnsiConsole.Exceptions.cs @@ -10,7 +10,7 @@ public static partial class AnsiConsole /// /// The exception to write to the console. /// The exception format options. - [RequiresDynamicCode(ExceptionFormatter.AotWarning)] + [RequiresDynamicCode(ExceptionRenderableBuilder.AotWarning)] public static void WriteException(Exception exception, ExceptionFormats format = ExceptionFormats.Default) { Console.WriteException(exception, format); @@ -21,7 +21,7 @@ public static void WriteException(Exception exception, ExceptionFormats format = /// /// The exception to write to the console. /// The exception settings. - [RequiresDynamicCode(ExceptionFormatter.AotWarning)] + [RequiresDynamicCode(ExceptionRenderableBuilder.AotWarning)] public static void WriteException(Exception exception, ExceptionSettings settings) { Console.WriteException(exception, settings); diff --git a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Exceptions.cs b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Exceptions.cs index 43df16940..5ea0a04ae 100644 --- a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Exceptions.cs +++ b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Exceptions.cs @@ -11,7 +11,7 @@ public static partial class AnsiConsoleExtensions /// The console. /// The exception to write to the console. /// The exception format options. - [RequiresDynamicCode(ExceptionFormatter.AotWarning)] + [RequiresDynamicCode(ExceptionRenderableBuilder.AotWarning)] public static void WriteException(this IAnsiConsole console, Exception exception, ExceptionFormats format = ExceptionFormats.Default) { @@ -26,7 +26,7 @@ public static void WriteException(this IAnsiConsole console, Exception exception /// The console. /// The exception to write to the console. /// The exception settings. - [RequiresDynamicCode(ExceptionFormatter.AotWarning)] + [RequiresDynamicCode(ExceptionRenderableBuilder.AotWarning)] public static void WriteException(this IAnsiConsole console, Exception exception, ExceptionSettings settings) { ArgumentNullException.ThrowIfNull(console); diff --git a/src/Spectre.Console/Extensions/Bcl/ExceptionExtensions.cs b/src/Spectre.Console/Extensions/Bcl/ExceptionExtensions.cs index 76e1866b6..2215cb37d 100644 --- a/src/Spectre.Console/Extensions/Bcl/ExceptionExtensions.cs +++ b/src/Spectre.Console/Extensions/Bcl/ExceptionExtensions.cs @@ -11,7 +11,7 @@ public static class ExceptionExtensions /// The exception to format. /// The exception format options. /// A representing the exception. - [RequiresDynamicCode(ExceptionFormatter.AotWarning)] + [RequiresDynamicCode(ExceptionRenderableBuilder.AotWarning)] public static IRenderable GetRenderable(this Exception exception, ExceptionFormats format = ExceptionFormats.Default) { ArgumentNullException.ThrowIfNull(exception); @@ -28,13 +28,13 @@ public static IRenderable GetRenderable(this Exception exception, ExceptionForma /// The exception to format. /// The exception settings. /// A representing the exception. - [RequiresDynamicCode(ExceptionFormatter.AotWarning)] + [RequiresDynamicCode(ExceptionRenderableBuilder.AotWarning)] public static IRenderable GetRenderable(this Exception exception, ExceptionSettings settings) { ArgumentNullException.ThrowIfNull(exception); ArgumentNullException.ThrowIfNull(settings); - return ExceptionFormatter.Format(exception, settings); + return ExceptionRenderableBuilder.Format(exception, settings); } } \ No newline at end of file diff --git a/src/Spectre.Console/Widgets/Exceptions/ExceptionInfoResolver.cs b/src/Spectre.Console/Widgets/Exceptions/ExceptionInfoResolver.cs new file mode 100644 index 000000000..4df592401 --- /dev/null +++ b/src/Spectre.Console/Widgets/Exceptions/ExceptionInfoResolver.cs @@ -0,0 +1,176 @@ +namespace Spectre.Console; + +/// +/// Used to resolve information from an . +/// +[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode")] +[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2070:RequiresUnreferencedCode")] +[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2075:RequiresUnreferencedCode")] +[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode")] +public class ExceptionInfoResolver +{ + internal static ExceptionInfoResolver Shared { get; } = new(); + + /// + /// Gets the name of a method. + /// + /// The instance to get the name for. + /// The method's name. + public virtual string GetMethodName(MethodBase method) + { + var builder = new StringBuilder(256); + + var fullName = method.DeclaringType?.FullName; + if (fullName != null) + { + // See https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackTrace.cs#L247-L253 + builder.Append(fullName.Replace('+', '.')); + builder.Append('.'); + } + + builder.Append(method.Name); + if (method.IsGenericMethod) + { + builder.Append('<'); + builder.Append(string.Join(",", method.GetGenericArguments().Select(t => t.Name))); + builder.Append('>'); + } + + return builder.ToString(); + } + + /// + /// Gets the name of a parameter. + /// + /// The instance to get the name for. + /// The parameter's name. + public virtual string GetParameterName(ParameterInfo parameter) + { + var prefix = GetParameterPrefix(parameter); + var parameterType = parameter.ParameterType; + + string typeName; + if (parameterType.IsGenericType && TryGetTupleName(parameter, parameterType, out var s)) + { + typeName = s; + } + else + { + if (parameterType.IsByRef && parameterType.GetElementType() is { } elementType) + { + parameterType = elementType; + } + + typeName = TypeNameHelper.GetTypeDisplayName(parameterType); + } + + return string.IsNullOrWhiteSpace(prefix) ? typeName : $"{prefix} {typeName}"; + } + + /// + /// Gets the file name of a . + /// + /// The to get the file name for. + /// The file name. + public virtual string? GetFileName(StackFrame frame) + { + return frame.GetFileName(); + } + + /// + /// Gets the line number of a . + /// + /// The to get the line number for. + /// The line number. + public virtual int GetFileLineNumber(StackFrame frame) + { + return frame.GetFileLineNumber(); + } + + private static string GetParameterPrefix(ParameterInfo parameter) + { + if (Attribute.IsDefined(parameter, typeof(ParamArrayAttribute), false)) + { + return "params"; + } + + if (parameter.IsOut) + { + return "out"; + } + + if (parameter.IsIn) + { + return "in"; + } + + if (parameter.ParameterType.IsByRef) + { + return "ref"; + } + + return string.Empty; + } + + private static bool TryGetTupleName(ParameterInfo parameter, Type parameterType, + [NotNullWhen(true)] out string? tupleName) + { + var customAttribs = parameter.GetCustomAttributes(inherit: false); + + var tupleNameAttribute = customAttribs + .OfType() + .FirstOrDefault(a => + { + var attributeType = a.GetType(); + return attributeType.Namespace == "System.Runtime.CompilerServices" && + attributeType.Name == "TupleElementNamesAttribute"; + }); + + if (tupleNameAttribute != null) + { + var propertyInfo = tupleNameAttribute.GetType() + .GetProperty("TransformNames", BindingFlags.Instance | BindingFlags.Public)!; + var tupleNames = propertyInfo.GetValue(tupleNameAttribute) as IList; + if (tupleNames?.Count > 0) + { + var args = parameterType.GetGenericArguments(); + var sb = new StringBuilder(); + + sb.Append('('); + for (var i = 0; i < args.Length; i++) + { + if (i > 0) + { + sb.Append(", "); + } + + sb.Append(TypeNameHelper.GetTypeDisplayName(args[i])); + + if (i >= tupleNames.Count) + { + continue; + } + + var argName = tupleNames[i]; + + sb.Append(' '); + sb.Append(argName); + } + + sb.Append(')'); + + tupleName = sb.ToString(); + return true; + } + } + else if (parameterType.Namespace == "System" && parameterType.Name.Contains("ValueTuple`")) + { + var args = parameterType.GetGenericArguments().Select(i => TypeNameHelper.GetTypeDisplayName(i)); + tupleName = $"({string.Join(", ", args)})"; + return true; + } + + tupleName = null; + return false; + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Widgets/Exceptions/ExceptionFormatter.cs b/src/Spectre.Console/Widgets/Exceptions/ExceptionRenderableBuilder.cs similarity index 66% rename from src/Spectre.Console/Widgets/Exceptions/ExceptionFormatter.cs rename to src/Spectre.Console/Widgets/Exceptions/ExceptionRenderableBuilder.cs index 6c5546962..58d18c803 100644 --- a/src/Spectre.Console/Widgets/Exceptions/ExceptionFormatter.cs +++ b/src/Spectre.Console/Widgets/Exceptions/ExceptionRenderableBuilder.cs @@ -5,7 +5,7 @@ namespace Spectre.Console; [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2070:RequiresUnreferencedCode")] [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2075:RequiresUnreferencedCode")] [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode")] -internal static class ExceptionFormatter +internal static class ExceptionRenderableBuilder { public const string AotWarning = "ExceptionFormatter is currently not supported for AOT."; @@ -20,14 +20,20 @@ private static IRenderable GetException(Exception exception, ExceptionSettings s { ArgumentNullException.ThrowIfNull(exception); - return new Rows(GetMessage(exception, settings), GetStackFrames(exception, settings)).Expand(); + var renderable = new Rows( + GetMessage(exception, settings), + GetStackFrames(exception, settings)) + .Collapse(); + + return renderable; } private static Markup GetMessage(Exception ex, ExceptionSettings settings) { var shortenTypes = (settings.Format & ExceptionFormats.ShortenTypes) != 0; var exceptionType = ex.GetType(); - var exceptionTypeName = TypeNameHelper.GetTypeDisplayName(exceptionType, fullName: !shortenTypes, includeSystemNamespace: true); + var exceptionTypeName = + TypeNameHelper.GetTypeDisplayName(exceptionType, fullName: !shortenTypes, includeSystemNamespace: true); var type = new StringBuilder(); Emphasize(type, exceptionTypeName, ['.'], settings.Style.Exception, shortenTypes, settings, limit: '<'); @@ -38,6 +44,7 @@ private static Markup GetMessage(Exception ex, ExceptionSettings settings) private static Grid GetStackFrames(Exception ex, ExceptionSettings settings) { var styles = settings.Style; + var resolver = settings.Resolver ?? new ExceptionInfoResolver(); var grid = new Grid(); grid.AddColumn(new GridColumn().PadLeft(2).PadRight(0).NoWrap()); @@ -83,7 +90,7 @@ private static Grid GetStackFrames(Exception ex, ExceptionSettings settings) continue; } - var methodName = GetMethodName(ref method, out var isAsync); + var methodName = GetMethodName(resolver, ref method, out var isAsync); if (isAsync) { builder.Append("async "); @@ -92,16 +99,17 @@ private static Grid GetStackFrames(Exception ex, ExceptionSettings settings) if (method is MethodInfo mi) { var returnParameter = mi.ReturnParameter; - builder.AppendWithStyle(styles.ParameterType, GetParameterName(returnParameter).EscapeMarkup()); + builder.AppendWithStyle(styles.ParameterType, + resolver.GetParameterName(returnParameter).EscapeMarkup()); builder.Append(' '); } Emphasize(builder, methodName, ['.'], styles.Method, shortenMethods, settings); builder.AppendWithStyle(styles.Parenthesis, "("); - AppendParameters(builder, method, settings); + AppendParameters(resolver, builder, method, settings); builder.AppendWithStyle(styles.Parenthesis, ")"); - var path = frame.GetFileName(); + var path = resolver.GetFileName(frame); if (path != null) { builder.Append(' '); @@ -112,7 +120,7 @@ private static Grid GetStackFrames(Exception ex, ExceptionSettings settings) AppendPath(builder, path, settings); // Line number - var lineNumber = frame.GetFileLineNumber(); + var lineNumber = resolver.GetFileLineNumber(frame); if (lineNumber != 0) { builder.AppendWithStyle(styles.Dimmed, ":"); @@ -145,12 +153,14 @@ private static void WriteAotFrames(Grid grid, StackFrame?[] frames, ExceptionSty } } - private static void AppendParameters(StringBuilder builder, MethodBase? method, ExceptionSettings settings) + private static void AppendParameters(ExceptionInfoResolver resolver, StringBuilder builder, MethodBase? method, + ExceptionSettings settings) { var typeColor = settings.Style.ParameterType.ToMarkup(); var nameColor = settings.Style.ParameterName.ToMarkup(); var parameters = method?.GetParameters() - .Select(x => $"[{typeColor}]{GetParameterName(x).EscapeMarkup()}[/] [{nameColor}]{x.Name?.EscapeMarkup()}[/]"); + .Select(x => + $"[{typeColor}]{resolver.GetParameterName(x).EscapeMarkup()}[/] [{nameColor}]{x.Name?.EscapeMarkup()}[/]"); if (parameters != null) { @@ -192,7 +202,9 @@ private static void Emphasize(StringBuilder builder, string input, char[] separa { var limitIndex = limit.HasValue ? input.IndexOf(limit.Value) : -1; - var index = limitIndex != -1 ? input[..limitIndex].LastIndexOfAny(separators) : input.LastIndexOfAny(separators); + var index = limitIndex != -1 + ? input[..limitIndex].LastIndexOfAny(separators) + : input.LastIndexOfAny(separators); if (index != -1) { if (!compact) @@ -271,116 +283,7 @@ private static IEnumerable FilterStackFrames(this IEnumerable() - .FirstOrDefault(a => - { - var attributeType = a.GetType(); - return attributeType.Namespace == "System.Runtime.CompilerServices" && - attributeType.Name == "TupleElementNamesAttribute"; - }); - - if (tupleNameAttribute != null) - { - var propertyInfo = tupleNameAttribute.GetType() - .GetProperty("TransformNames", BindingFlags.Instance | BindingFlags.Public)!; - var tupleNames = propertyInfo.GetValue(tupleNameAttribute) as IList; - if (tupleNames?.Count > 0) - { - var args = parameterType.GetGenericArguments(); - var sb = new StringBuilder(); - - sb.Append('('); - for (var i = 0; i < args.Length; i++) - { - if (i > 0) - { - sb.Append(", "); - } - - sb.Append(TypeNameHelper.GetTypeDisplayName(args[i])); - - if (i >= tupleNames.Count) - { - continue; - } - - var argName = tupleNames[i]; - - sb.Append(' '); - sb.Append(argName); - } - - sb.Append(')'); - - tupleName = sb.ToString(); - return true; - } - } - else if (parameterType.Namespace == "System" && parameterType.Name.Contains("ValueTuple`")) - { - var args = parameterType.GetGenericArguments().Select(i => TypeNameHelper.GetTypeDisplayName(i)); - tupleName = $"({string.Join(", ", args)})"; - return true; - } - - tupleName = null; - return false; - } - - private static string GetMethodName(ref MethodBase method, out bool isAsync) + private static string GetMethodName(ExceptionInfoResolver resolver, ref MethodBase method, out bool isAsync) { var declaringType = method.DeclaringType; @@ -389,7 +292,7 @@ private static string GetMethodName(ref MethodBase method, out bool isAsync) isAsync = typeof(IAsyncStateMachine).IsAssignableFrom(declaringType); if (isAsync || typeof(IEnumerator).IsAssignableFrom(declaringType)) { - TryResolveStateMachineMethod(ref method, out declaringType); + TryResolveStateMachineMethod(method, out declaringType); } } else @@ -397,29 +300,11 @@ private static string GetMethodName(ref MethodBase method, out bool isAsync) isAsync = false; } - var builder = new StringBuilder(256); - - var fullName = method.DeclaringType?.FullName; - if (fullName != null) - { - // See https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackTrace.cs#L247-L253 - builder.Append(fullName.Replace('+', '.')); - builder.Append('.'); - } - - builder.Append(method.Name); - if (method.IsGenericMethod) - { - builder.Append('<'); - builder.Append(string.Join(",", method.GetGenericArguments().Select(t => t.Name))); - builder.Append('>'); - } - - return builder.ToString(); + return resolver.GetMethodName(method); } - [RequiresDynamicCode(ExceptionFormatter.AotWarning)] - private static bool TryResolveStateMachineMethod(ref MethodBase method, out Type declaringType) + [RequiresDynamicCode(ExceptionRenderableBuilder.AotWarning)] + private static bool TryResolveStateMachineMethod(MethodBase method, out Type declaringType) { // https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackTrace.cs#L400-L455 declaringType = method.DeclaringType ?? diff --git a/src/Spectre.Console/Widgets/Exceptions/ExceptionSettings.cs b/src/Spectre.Console/Widgets/Exceptions/ExceptionSettings.cs index 4ea0c07ad..dffc924c3 100644 --- a/src/Spectre.Console/Widgets/Exceptions/ExceptionSettings.cs +++ b/src/Spectre.Console/Widgets/Exceptions/ExceptionSettings.cs @@ -8,12 +8,18 @@ public sealed class ExceptionSettings /// /// Gets or sets the exception format. /// - public ExceptionFormats Format { get; set; } + public ExceptionFormats Format { get; init; } /// /// Gets or sets the exception style. /// - public ExceptionStyle Style { get; set; } + public ExceptionStyle Style { get; init; } + + /// + /// Gets or sets the exception scrubber. + /// Useful for removing sensitive data and for testing. + /// + public ExceptionInfoResolver Resolver { get; init; } /// /// Initializes a new instance of the class. @@ -22,5 +28,6 @@ public ExceptionSettings() { Format = ExceptionFormats.Default; Style = new ExceptionStyle(); + Resolver = ExceptionInfoResolver.Shared; } } \ No newline at end of file