Add support for rendering exceptions

This commit is contained in:
Patrik Svensson
2020-10-03 02:08:31 +02:00
committed by Patrik Svensson
parent 971f9032ba
commit 3c3afe7439
35 changed files with 926 additions and 41 deletions

View File

@@ -24,3 +24,6 @@ dotnet_diagnostic.CA2000.severity = none
# SA1118: Parameter should not span multiple lines
dotnet_diagnostic.SA1118.severity = none
# CA1031: Do not catch general exception types
dotnet_diagnostic.CA1031.severity = none

View File

@@ -0,0 +1,23 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace Spectre.Console.Tests.Data
{
public static class TestExceptions
{
[SuppressMessage("Usage", "CA1801:Review unused parameters", Justification = "<Pending>")]
public static bool MethodThatThrows(int? number) => throw new InvalidOperationException("Throwing!");
public static void ThrowWithInnerException()
{
try
{
MethodThatThrows(null);
}
catch (Exception ex)
{
throw new InvalidOperationException("Something threw!", ex);
}
}
}
}

View File

@@ -1,13 +1,34 @@
using System;
using System.Text.RegularExpressions;
namespace Spectre.Console.Tests
{
public static class StringExtensions
{
private static readonly Regex _lineNumberRegex = new Regex(":\\d+", RegexOptions.Singleline);
private static readonly Regex _filenameRegex = new Regex("\\sin\\s.*cs:nn", RegexOptions.Multiline);
public static string NormalizeLineEndings(this string text)
{
return text?.Replace("\r\n", "\n", StringComparison.OrdinalIgnoreCase)
?.Replace("\r", string.Empty, StringComparison.OrdinalIgnoreCase);
}
public static string NormalizeStackTrace(this string text)
{
text = _lineNumberRegex.Replace(text, match =>
{
return ":nn";
});
return _filenameRegex.Replace(text, match =>
{
var value = match.Value;
var index = value.LastIndexOfAny(new[] { '\\', '/' });
var filename = value.Substring(index + 1, value.Length - index - 1);
return $" in /xyz/{filename}";
});
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.IO;
using System.Text;
using Spectre.Console.Rendering;
namespace Spectre.Console.Tests.Tools
{
public sealed class MarkupConsoleFixture : IDisposable, IAnsiConsole
{
private readonly StringWriter _writer;
private readonly IAnsiConsole _console;
public string Output => _writer.ToString().TrimEnd('\n');
public Capabilities Capabilities => _console.Capabilities;
public Encoding Encoding => _console.Encoding;
public int Width { get; }
public int Height => _console.Height;
public MarkupConsoleFixture(ColorSystem system, AnsiSupport ansi = AnsiSupport.Yes, int width = 80)
{
_writer = new StringWriter();
_console = AnsiConsole.Create(new AnsiConsoleSettings
{
Ansi = ansi,
ColorSystem = (ColorSystemSupport)system,
Out = _writer,
LinkIdentityGenerator = new TestLinkIdentityGenerator(),
});
Width = width;
}
public void Dispose()
{
_writer?.Dispose();
}
public void Write(Segment segment)
{
_console.Write(segment);
}
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Spectre.Console.Rendering;
@@ -50,5 +51,16 @@ namespace Spectre.Console.Tests
Writer.Write(segment.Text);
}
public string[] WriteExceptionAndGetLines(Exception ex, ExceptionFormats formats = ExceptionFormats.None)
{
this.WriteException(ex, formats);
return Output.NormalizeStackTrace()
.NormalizeLineEndings()
.Split(new char[] { '\n' })
.Select(line => line.TrimEnd())
.ToArray();
}
}
}

View File

@@ -0,0 +1,99 @@
using System;
using Shouldly;
using Spectre.Console.Tests.Data;
using Xunit;
namespace Spectre.Console.Tests.Unit
{
public sealed class ExceptionTests
{
[Fact]
public void Should_Write_Exception()
{
// Given
var console = new PlainConsole(width: 1024);
var dex = GetException(() => TestExceptions.MethodThatThrows(null));
// When
var result = console.WriteExceptionAndGetLines(dex);
// Then
result.Length.ShouldBe(4);
result[0].ShouldBe("System.InvalidOperationException: Throwing!");
result[1].ShouldBe(" at Spectre.Console.Tests.Data.TestExceptions.MethodThatThrows(Nullable`1 number) in /xyz/Exceptions.cs:nn");
result[2].ShouldBe(" at Spectre.Console.Tests.Unit.ExceptionTests.<>c.<Should_Write_Exception>b__0_0() in /xyz/ExceptionTests.cs:nn");
result[3].ShouldBe(" at Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn");
}
[Fact]
public void Should_Write_Exception_With_Shortened_Types()
{
// Given
var console = new PlainConsole(width: 1024);
var dex = GetException(() => TestExceptions.MethodThatThrows(null));
// When
var result = console.WriteExceptionAndGetLines(dex, ExceptionFormats.ShortenTypes);
// Then
result.Length.ShouldBe(4);
result[0].ShouldBe("InvalidOperationException: Throwing!");
result[1].ShouldBe(" at Spectre.Console.Tests.Data.TestExceptions.MethodThatThrows(Nullable`1 number) in /xyz/Exceptions.cs:nn");
result[2].ShouldBe(" at Spectre.Console.Tests.Unit.ExceptionTests.<>c.<Should_Write_Exception_With_Shortened_Types>b__1_0() in /xyz/ExceptionTests.cs:nn");
result[3].ShouldBe(" at Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn");
}
[Fact]
public void Should_Write_Exception_With_Shortened_Methods()
{
// Given
var console = new PlainConsole(width: 1024);
var dex = GetException(() => TestExceptions.MethodThatThrows(null));
// When
var result = console.WriteExceptionAndGetLines(dex, ExceptionFormats.ShortenMethods);
// Then
result.Length.ShouldBe(4);
result[0].ShouldBe("System.InvalidOperationException: Throwing!");
result[1].ShouldBe(" at MethodThatThrows(Nullable`1 number) in /xyz/Exceptions.cs:nn");
result[2].ShouldBe(" at <Should_Write_Exception_With_Shortened_Methods>b__2_0() in /xyz/ExceptionTests.cs:nn");
result[3].ShouldBe(" at GetException(Action action) in /xyz/ExceptionTests.cs:nn");
}
[Fact]
public void Should_Write_Exception_With_Inner_Exception()
{
// Given
var console = new PlainConsole(width: 1024);
var dex = GetException(() => TestExceptions.ThrowWithInnerException());
// When
var result = console.WriteExceptionAndGetLines(dex);
// Then
result.Length.ShouldBe(7);
result[0].ShouldBe("System.InvalidOperationException: Something threw!");
result[1].ShouldBe(" System.InvalidOperationException: Throwing!");
result[2].ShouldBe(" at Spectre.Console.Tests.Data.TestExceptions.MethodThatThrows(Nullable`1 number) in /xyz/Exceptions.cs:nn");
result[3].ShouldBe(" at Spectre.Console.Tests.Data.TestExceptions.ThrowWithInnerException() in /xyz/Exceptions.cs:nn");
result[4].ShouldBe(" at Spectre.Console.Tests.Data.TestExceptions.ThrowWithInnerException() in /xyz/Exceptions.cs:nn");
result[5].ShouldBe(" at Spectre.Console.Tests.Unit.ExceptionTests.<>c.<Should_Write_Exception_With_Inner_Exception>b__3_0() in /xyz/ExceptionTests.cs:nn");
result[6].ShouldBe(" at Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn");
}
public static Exception GetException(Action action)
{
try
{
action?.Invoke();
}
catch (Exception e)
{
return e;
}
throw new InvalidOperationException("Exception harness failed");
}
}
}

View File

@@ -1,4 +1,6 @@
using System.Collections.Generic;
using Shouldly;
using Spectre.Console.Rendering;
using Xunit;
namespace Spectre.Console.Tests.Unit
@@ -298,5 +300,33 @@ namespace Spectre.Console.Tests.Unit
console.Lines[3].ShouldBe("│ └─────────────┘ │");
console.Lines[4].ShouldBe("└─────────────────┘");
}
[Fact]
public void Should_Wrap_Content_Correctly()
{
// Given
var console = new PlainConsole(width: 84);
var rows = new List<IRenderable>();
var grid = new Grid();
grid.AddColumn(new GridColumn().PadLeft(2).PadRight(0));
grid.AddColumn(new GridColumn().PadLeft(1).PadRight(0));
grid.AddRow("at", "[grey]System.Runtime.CompilerServices.TaskAwaiter.[/][yellow]HandleNonSuccessAndDebuggerNotification[/]([blue]Task[/] task)");
rows.Add(grid);
var panel = new Panel(grid)
.Expand().RoundedBorder()
.SetBorderStyle(Style.WithForeground(Color.Grey))
.SetHeader("Short paths ", Style.WithForeground(Color.Grey));
// When
console.Render(panel);
// Then
console.Lines.Count.ShouldBe(4);
console.Lines[0].ShouldBe("╭─Short paths ─────────────────────────────────────────────────────────────────────╮");
console.Lines[1].ShouldBe("│ at System.Runtime.CompilerServices.TaskAwaiter. │");
console.Lines[2].ShouldBe("│ HandleNonSuccessAndDebuggerNotification(Task task) │");
console.Lines[3].ShouldBe("╰──────────────────────────────────────────────────────────────────────────────────╯");
}
}
}