From 42c276f68bb916f679f8f6050eab4747557b1157 Mon Sep 17 00:00:00 2001 From: Pascal Berger Date: Tue, 23 Sep 2025 23:46:26 +0200 Subject: [PATCH 01/10] Add test case --- ...Cake.Issues.Reporting.Console.Tests.csproj | 5 + .../ConsoleIssueReportFixture.cs | 39 ++--- .../ConsoleIssueReportGeneratorTests.cs | 112 +++++++++++-- ...ork_With_Issue_On_End_Of_Line.verified.txt | 8 + .../packages.lock.json | 148 +++++++++++++++--- .../ConsoleIssueReportFormatAliases.cs | 3 +- .../ConsoleIssueReportGenerator.cs | 37 +++-- 7 files changed, 279 insertions(+), 73 deletions(-) create mode 100644 src/Cake.Issues.Reporting.Console.Tests/TheInternalCreateReportMethod.WithShowDiagnosticsEnabled.Should_Work_With_Issue_On_End_Of_Line.verified.txt diff --git a/src/Cake.Issues.Reporting.Console.Tests/Cake.Issues.Reporting.Console.Tests.csproj b/src/Cake.Issues.Reporting.Console.Tests/Cake.Issues.Reporting.Console.Tests.csproj index e768422f9..8a94ebc6e 100644 --- a/src/Cake.Issues.Reporting.Console.Tests/Cake.Issues.Reporting.Console.Tests.csproj +++ b/src/Cake.Issues.Reporting.Console.Tests/Cake.Issues.Reporting.Console.Tests.csproj @@ -3,6 +3,11 @@ Tests for the Cake.Issues.Reporting.Console addin + + + + + diff --git a/src/Cake.Issues.Reporting.Console.Tests/ConsoleIssueReportFixture.cs b/src/Cake.Issues.Reporting.Console.Tests/ConsoleIssueReportFixture.cs index a4e0b041b..034f926f5 100644 --- a/src/Cake.Issues.Reporting.Console.Tests/ConsoleIssueReportFixture.cs +++ b/src/Cake.Issues.Reporting.Console.Tests/ConsoleIssueReportFixture.cs @@ -4,14 +4,17 @@ using Cake.Core.Diagnostics; using Cake.Core.IO; using Cake.Issues.Serialization; +using Spectre.Console.Testing; internal class ConsoleIssueReportFixture { + public TestConsole Console { get; set; } = new(); + public FakeLog Log { get; set; } = new() { Verbosity = Verbosity.Normal }; public ConsoleIssueReportFormatSettings ConsoleIssueReportFormatSettings { get; set; } = new(); - public string CreateReportForTestfile(string fileResourceName, DirectoryPath repositoryRootPath) + public void CreateReportForTestfile(string fileResourceName, DirectoryPath repositoryRootPath) { fileResourceName.NotNullOrWhiteSpace(); @@ -28,22 +31,23 @@ public string CreateReportForTestfile(string fileResourceName, DirectoryPath rep } var issues = reader.ReadToEnd().DeserializeToIssues(); - return this.CreateReport(issues, repositoryRootPath); + this.CreateReport(issues, repositoryRootPath); } } - public string CreateReport(IEnumerable issues, DirectoryPath repositoryRootPath) + public void CreateReport(IEnumerable issues, DirectoryPath repositoryRootPath) { var generator = - new ConsoleIssueReportGenerator(this.Log, this.ConsoleIssueReportFormatSettings); + new ConsoleIssueReportGenerator( + this.Console, + this.Log, + this.ConsoleIssueReportFormatSettings); var createIssueReportSettings = new CreateIssueReportSettings(repositoryRootPath, string.Empty); _ = generator.Initialize(createIssueReportSettings); _ = generator.CreateReport(issues); - // TODO Return console output - return string.Empty; } public void TestReportCreation(Action settings) @@ -52,20 +56,17 @@ public void TestReportCreation(Action settings settings(this.ConsoleIssueReportFormatSettings); // When - var result = - this.CreateReport( - [ - IssueBuilder - .NewIssue("Message Foo", "ProviderType Foo", "ProviderName Foo") - .InFile(@"src\Cake.Issues.Reporting.Generic.Tests\Foo.cs", 10) - .OfRule("Rule Foo") - .WithPriority(IssuePriority.Warning) - .Create(), - ], - @"c:\Source\Cake.Issues.Reporting.Console"); + this.CreateReport( + [ + IssueBuilder + .NewIssue("Message Foo", "ProviderType Foo", "ProviderName Foo") + .InFile(@"src\Cake.Issues.Reporting.Generic.Tests\Foo.cs", 10) + .OfRule("Rule Foo") + .WithPriority(IssuePriority.Warning) + .Create(), + ], + @"c:\Source\Cake.Issues.Reporting.Console"); // Then - // Currently only checks if generation failed or not without checking actual output. - _ = result.ShouldNotBeNull(); } } diff --git a/src/Cake.Issues.Reporting.Console.Tests/ConsoleIssueReportGeneratorTests.cs b/src/Cake.Issues.Reporting.Console.Tests/ConsoleIssueReportGeneratorTests.cs index c1616b45b..56f25c76b 100644 --- a/src/Cake.Issues.Reporting.Console.Tests/ConsoleIssueReportGeneratorTests.cs +++ b/src/Cake.Issues.Reporting.Console.Tests/ConsoleIssueReportGeneratorTests.cs @@ -1,19 +1,48 @@ namespace Cake.Issues.Reporting.Console.Tests; +using System.Text.RegularExpressions; +using Cake.Core.Diagnostics; +using Spectre.Console; +using Spectre.Console.Testing; using Xunit.Abstractions; -public sealed class ConsoleIssueReportGeneratorTests +public sealed partial class ConsoleIssueReportGeneratorTests { public sealed class TheCtor { + [Fact] + public void Should_Throw_If_Console_Is_Null() + { + // Given + IAnsiConsole console = null; + var log = new FakeLog(); + var settings = new ConsoleIssueReportFormatSettings(); + + // When + var result = Record.Exception(() => + new ConsoleIssueReportGenerator( + console, + log, + settings)); + + // Then + result.IsArgumentNullException("console"); + } + [Fact] public void Should_Throw_If_Log_Is_Null() { - // Given / When + // Given + var console = new TestConsole(); + ICakeLog log = null; + var settings = new ConsoleIssueReportFormatSettings(); + + // When var result = Record.Exception(() => new ConsoleIssueReportGenerator( - null, - new ConsoleIssueReportFormatSettings())); + console, + log, + settings)); // Then result.IsArgumentNullException("log"); @@ -22,18 +51,24 @@ public void Should_Throw_If_Log_Is_Null() [Fact] public void Should_Throw_If_Settings_Are_Null() { - // Given / When + // Given + var console = new TestConsole(); + var log = new FakeLog(); + ConsoleIssueReportFormatSettings settings = null; + + // When var result = Record.Exception(() => new ConsoleIssueReportGenerator( - new FakeLog(), - null)); + console, + log, + settings)); // Then result.IsArgumentNullException("settings"); } } - public sealed class TheInternalCreateReportMethod + public sealed partial class TheInternalCreateReportMethod { public static IEnumerable ReportFormatSettingsCombinations => from b1 in boolArray @@ -68,7 +103,7 @@ public void Should_Generate_Report( }; // When - _ = fixture.CreateReportForTestfile( + fixture.CreateReportForTestfile( "Testfiles.issues.json", @"c:\Source\Cake.Issues.Reporting.Console"); @@ -98,15 +133,21 @@ public void Should_Generate_Report_With_No_Issues( }; // When - _ = fixture.CreateReport( + fixture.CreateReport( [], @"c:\Source\Cake.Issues.Reporting.Console"); // Then } - public sealed class WithShowDiagnosticsEnabled(ITestOutputHelper output) + public sealed partial class WithShowDiagnosticsEnabled(ITestOutputHelper output) { + // (?<=┌─\[) — positive lookbehind to assert the match is preceded by ┌─[ + // [^\]]+ — matches one or more characters that are not a closing bracket ] + // (?=\]) — positive lookahead to assert the match is followed by ] + [GeneratedRegex(@"(?<=┌─\[)[^\]]+(?=\])")] + private static partial Regex DiagnosticRegEx(); + [Fact] public void Should_Filter_Issues_Without_FilePath() { @@ -128,7 +169,7 @@ public void Should_Filter_Issues_Without_FilePath() }; // When - _ = fixture.CreateReport(issues, @"c:\Source\Cake.Issues.Reporting.Console"); + fixture.CreateReport(issues, @"c:\Source\Cake.Issues.Reporting.Console"); // Then fixture.Log.Entries.ShouldContain(x => x.Message == "1 issue(s) were filtered because they either don't belong to a file or the file does not exist."); @@ -155,7 +196,7 @@ public void Should_Filter_Issues_Where_File_Does_Not_Exist() }; // When - _ = fixture.CreateReport(issues, @"c:\Source\Cake.Issues.Reporting.Console"); + fixture.CreateReport(issues, @"c:\Source\Cake.Issues.Reporting.Console"); // Then fixture.Log.Entries.ShouldContain(x => x.Message == "1 issue(s) were filtered because they either don't belong to a file or the file does not exist."); @@ -187,7 +228,7 @@ public void Should_Not_Filter_Issues_With_Existing_File() }; // When - _ = fixture.CreateReport(issues, directory); + fixture.CreateReport(issues, directory); // Then fixture.Log.Entries.ShouldContain(x => x.Message == "0 issue(s) were filtered because they either don't belong to a file or the file does not exist."); @@ -218,7 +259,7 @@ public void Should_Work_Without_Priority() .Create(), }; // When - _ = fixture.CreateReport(issues, directory); + fixture.CreateReport(issues, directory); // Then } @@ -249,10 +290,49 @@ public void Should_Work_With_Priority() .Create(), }; // When - _ = fixture.CreateReport(issues, directory); + fixture.CreateReport(issues, directory); // Then } + + [Fact] + public Task Should_Work_With_Issue_On_End_Of_Line() + { + // Given + using var tempSourceFile = new TemporarySourceFile("Testfiles.TestFile.txt"); + var filePath = tempSourceFile.FilePath; + output.WriteLine($"File path: {filePath}"); + var directory = Path.GetDirectoryName(filePath)!; + var fileName = Path.GetFileName(filePath); + var fixture = new ConsoleIssueReportFixture + { + ConsoleIssueReportFormatSettings = + { + ShowDiagnostics = true + } + }; + var issues = + new List + { + IssueBuilder + .NewIssue("Message Foo", "ProviderType Foo", "ProviderName Foo") + .InFile(fileName, 1, 57) + .Create(), + }; + // When + fixture.CreateReport(issues, directory); + + // Then + // Add a scrubber that replaces the dynamic ID in the output + var settings = new VerifySettings(); + settings.AddScrubber(builder => + { + var updated = DiagnosticRegEx().Replace(builder.ToString(), ""); + + _ = builder.Clear().Append(updated); + }); + return Verify(fixture.Console.Output, settings); + } } } } diff --git a/src/Cake.Issues.Reporting.Console.Tests/TheInternalCreateReportMethod.WithShowDiagnosticsEnabled.Should_Work_With_Issue_On_End_Of_Line.verified.txt b/src/Cake.Issues.Reporting.Console.Tests/TheInternalCreateReportMethod.WithShowDiagnosticsEnabled.Should_Work_With_Issue_On_End_Of_Line.verified.txt new file mode 100644 index 000000000..b9cc1c6c9 --- /dev/null +++ b/src/Cake.Issues.Reporting.Console.Tests/TheInternalCreateReportMethod.WithShowDiagnosticsEnabled.Should_Work_With_Issue_On_End_Of_Line.verified.txt @@ -0,0 +1,8 @@ + + ┌─[] + │ + 1 │ namespace Cake.Issues.Reporting.Console.Tests.Testfiles; + · ┬ + · ╰ Message Foo + │ + └─ \ No newline at end of file diff --git a/src/Cake.Issues.Reporting.Console.Tests/packages.lock.json b/src/Cake.Issues.Reporting.Console.Tests/packages.lock.json index 5daefdce1..ef5c038d6 100644 --- a/src/Cake.Issues.Reporting.Console.Tests/packages.lock.json +++ b/src/Cake.Issues.Reporting.Console.Tests/packages.lock.json @@ -44,6 +44,30 @@ "EmptyFiles": "4.4.0" } }, + "Spectre.Console.Testing": { + "type": "Direct", + "requested": "[0.50.0, )", + "resolved": "0.50.0", + "contentHash": "/RjFt98nw8D580F5jXSF6sF8jyuFxaaHfaarr5ix6tqci7U/lTvd4TIAsud4yy69UuIh2KiO2AsHiEpc8nvJ+A==", + "dependencies": { + "Spectre.Console": "0.50.0", + "Spectre.Console.Cli": "0.50.0" + } + }, + "Verify.Xunit": { + "type": "Direct", + "requested": "[30.18.0, )", + "resolved": "30.18.0", + "contentHash": "Fnm1yhSZRjg+aGgdPfT+jaF4+63i5LGGXDq4g/cTyAXBLDEjFD/wkWmNcnNxVicHRUBbC/RrYWa4hfXuCWOqpw==", + "dependencies": { + "Argon": "0.32.0", + "DiffEngine": "16.6.0", + "SimpleInfoName": "3.1.2", + "Verify": "30.18.0", + "xunit.abstractions": "2.0.3", + "xunit.extensibility.execution": "2.9.3" + } + }, "xunit": { "type": "Direct", "requested": "[2.9.3, )", @@ -71,19 +95,24 @@ "xunit.extensibility.execution": "2.4.0" } }, + "Argon": { + "type": "Transitive", + "resolved": "0.32.0", + "contentHash": "sW2tLL6cllHx5Q305AkXmGN4gWSGk8Ij2xH6kje1dy5k0Ef3V9g/qE6Y0GH5du515k3coiR9PY8EvZ5SIl9jOw==" + }, "DiffEngine": { "type": "Transitive", - "resolved": "11.3.0", - "contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==", + "resolved": "16.6.0", + "contentHash": "xkQvgH9qPpguXye0GVZJUd3/HBJfdIvNQud753L/XBmW3oY8o4MyEuzimBZ95pmWyYQekDv4ZWo+cgkO32+TLA==", "dependencies": { - "EmptyFiles": "4.4.0", - "System.Management": "6.0.1" + "EmptyFiles": "8.13.0", + "System.Management": "8.0.0" } }, "EmptyFiles": { "type": "Transitive", - "resolved": "4.4.0", - "contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==" + "resolved": "8.13.0", + "contentHash": "eO5tyJez50QHC2/nOq3sNFoVRy0OBAZx0YWo2qysuUuU/Rluez0bjDQeoVYJ77145+KRGJrQfUhOUTP5kHENwA==" }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", @@ -131,10 +160,23 @@ "System.Security.Principal.Windows": "5.0.0" } }, + "SimpleInfoName": { + "type": "Transitive", + "resolved": "3.1.2", + "contentHash": "/OoEZQxSW6DeTJ9nfrg8BLCOCWpxBiWHV4NkG3t+Xpe8tvzm7yCwKwxkhpauMl3fg9OjlIjJMKX61H6VavLkrw==" + }, + "Spectre.Console.Cli": { + "type": "Transitive", + "resolved": "0.50.0", + "contentHash": "PrctsupqHEbyoP/iRL30awUWB0Z91i21myKl70qcPwoH6marEPkPyaPgzc37xFm9QSx+/jVBQGUH7tsn0mWGNQ==", + "dependencies": { + "Spectre.Console": "0.50.0" + } + }, "System.CodeDom": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" + "resolved": "8.0.0", + "contentHash": "WTlRjL6KWIMr/pAaq3rYqh0TJlzpouaQ/W1eelssHgtlwHAH25jXTkUphTYx9HaIIf7XA6qs/0+YhtLEQRkJ+Q==" }, "System.Collections.Immutable": { "type": "Transitive", @@ -143,10 +185,10 @@ }, "System.Management": { "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", + "resolved": "8.0.0", + "contentHash": "jrK22i5LRzxZCfGb+tGmke2VH7oE0DvcDlJ1HAKYU8cPmD8XnpUT0bYn2Gy98GEhGjtfbR/sxKTVb+dE770pfA==", "dependencies": { - "System.CodeDom": "6.0.0" + "System.CodeDom": "8.0.0" } }, "System.Memory": { @@ -181,6 +223,16 @@ "resolved": "2.5.51", "contentHash": "g/Aug7PVWaenlJ0QUyt/mEetngkQNsMCuNeRVXbcJED1nZS7JcK+GTU4kz3jcQ7bFuKfi8PF4ExXH7XSFNuSLQ==" }, + "Verify": { + "type": "Transitive", + "resolved": "30.18.0", + "contentHash": "dzXqPfdxXY6dbormksMWl4t+prs15SzlrmHtQpFlZiT4CMOWbdk6uQ64l78rcCLjlLdZfKnSO+rbZOLuKXgo1g==", + "dependencies": { + "Argon": "0.32.0", + "DiffEngine": "16.6.0", + "SimpleInfoName": "3.1.2" + } + }, "xunit.abstractions": { "type": "Transitive", "resolved": "2.0.3", @@ -344,6 +396,30 @@ "EmptyFiles": "4.4.0" } }, + "Spectre.Console.Testing": { + "type": "Direct", + "requested": "[0.50.0, )", + "resolved": "0.50.0", + "contentHash": "/RjFt98nw8D580F5jXSF6sF8jyuFxaaHfaarr5ix6tqci7U/lTvd4TIAsud4yy69UuIh2KiO2AsHiEpc8nvJ+A==", + "dependencies": { + "Spectre.Console": "0.50.0", + "Spectre.Console.Cli": "0.50.0" + } + }, + "Verify.Xunit": { + "type": "Direct", + "requested": "[30.18.0, )", + "resolved": "30.18.0", + "contentHash": "Fnm1yhSZRjg+aGgdPfT+jaF4+63i5LGGXDq4g/cTyAXBLDEjFD/wkWmNcnNxVicHRUBbC/RrYWa4hfXuCWOqpw==", + "dependencies": { + "Argon": "0.32.0", + "DiffEngine": "16.6.0", + "SimpleInfoName": "3.1.2", + "Verify": "30.18.0", + "xunit.abstractions": "2.0.3", + "xunit.extensibility.execution": "2.9.3" + } + }, "xunit": { "type": "Direct", "requested": "[2.9.3, )", @@ -371,19 +447,24 @@ "xunit.extensibility.execution": "2.4.0" } }, + "Argon": { + "type": "Transitive", + "resolved": "0.32.0", + "contentHash": "sW2tLL6cllHx5Q305AkXmGN4gWSGk8Ij2xH6kje1dy5k0Ef3V9g/qE6Y0GH5du515k3coiR9PY8EvZ5SIl9jOw==" + }, "DiffEngine": { "type": "Transitive", - "resolved": "11.3.0", - "contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==", + "resolved": "16.6.0", + "contentHash": "xkQvgH9qPpguXye0GVZJUd3/HBJfdIvNQud753L/XBmW3oY8o4MyEuzimBZ95pmWyYQekDv4ZWo+cgkO32+TLA==", "dependencies": { - "EmptyFiles": "4.4.0", - "System.Management": "6.0.1" + "EmptyFiles": "8.13.0", + "System.Management": "8.0.0" } }, "EmptyFiles": { "type": "Transitive", - "resolved": "4.4.0", - "contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==" + "resolved": "8.13.0", + "contentHash": "eO5tyJez50QHC2/nOq3sNFoVRy0OBAZx0YWo2qysuUuU/Rluez0bjDQeoVYJ77145+KRGJrQfUhOUTP5kHENwA==" }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", @@ -431,10 +512,23 @@ "System.Security.Principal.Windows": "5.0.0" } }, + "SimpleInfoName": { + "type": "Transitive", + "resolved": "3.1.2", + "contentHash": "/OoEZQxSW6DeTJ9nfrg8BLCOCWpxBiWHV4NkG3t+Xpe8tvzm7yCwKwxkhpauMl3fg9OjlIjJMKX61H6VavLkrw==" + }, + "Spectre.Console.Cli": { + "type": "Transitive", + "resolved": "0.50.0", + "contentHash": "PrctsupqHEbyoP/iRL30awUWB0Z91i21myKl70qcPwoH6marEPkPyaPgzc37xFm9QSx+/jVBQGUH7tsn0mWGNQ==", + "dependencies": { + "Spectre.Console": "0.50.0" + } + }, "System.CodeDom": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" + "resolved": "8.0.0", + "contentHash": "WTlRjL6KWIMr/pAaq3rYqh0TJlzpouaQ/W1eelssHgtlwHAH25jXTkUphTYx9HaIIf7XA6qs/0+YhtLEQRkJ+Q==" }, "System.Collections.Immutable": { "type": "Transitive", @@ -443,10 +537,10 @@ }, "System.Management": { "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", + "resolved": "8.0.0", + "contentHash": "jrK22i5LRzxZCfGb+tGmke2VH7oE0DvcDlJ1HAKYU8cPmD8XnpUT0bYn2Gy98GEhGjtfbR/sxKTVb+dE770pfA==", "dependencies": { - "System.CodeDom": "6.0.0" + "System.CodeDom": "8.0.0" } }, "System.Memory": { @@ -481,6 +575,16 @@ "resolved": "2.5.51", "contentHash": "g/Aug7PVWaenlJ0QUyt/mEetngkQNsMCuNeRVXbcJED1nZS7JcK+GTU4kz3jcQ7bFuKfi8PF4ExXH7XSFNuSLQ==" }, + "Verify": { + "type": "Transitive", + "resolved": "30.18.0", + "contentHash": "dzXqPfdxXY6dbormksMWl4t+prs15SzlrmHtQpFlZiT4CMOWbdk6uQ64l78rcCLjlLdZfKnSO+rbZOLuKXgo1g==", + "dependencies": { + "Argon": "0.32.0", + "DiffEngine": "16.6.0", + "SimpleInfoName": "3.1.2" + } + }, "xunit.abstractions": { "type": "Transitive", "resolved": "2.0.3", diff --git a/src/Cake.Issues.Reporting.Console/ConsoleIssueReportFormatAliases.cs b/src/Cake.Issues.Reporting.Console/ConsoleIssueReportFormatAliases.cs index 0fc022f84..38d841b49 100644 --- a/src/Cake.Issues.Reporting.Console/ConsoleIssueReportFormatAliases.cs +++ b/src/Cake.Issues.Reporting.Console/ConsoleIssueReportFormatAliases.cs @@ -3,6 +3,7 @@ using Cake.Core; using Cake.Core.Annotations; using Cake.Issues.Reporting; +using Spectre.Console; /// /// Contains functionality to report issues to the console. @@ -70,6 +71,6 @@ public static IIssueReportFormat ConsoleIssueReportFormat( context.NotNull(); settings.NotNull(); - return new ConsoleIssueReportGenerator(context.Log, settings); + return new ConsoleIssueReportGenerator(AnsiConsole.Console, context.Log, settings); } } diff --git a/src/Cake.Issues.Reporting.Console/ConsoleIssueReportGenerator.cs b/src/Cake.Issues.Reporting.Console/ConsoleIssueReportGenerator.cs index b902d84fa..2d305428d 100644 --- a/src/Cake.Issues.Reporting.Console/ConsoleIssueReportGenerator.cs +++ b/src/Cake.Issues.Reporting.Console/ConsoleIssueReportGenerator.cs @@ -13,18 +13,25 @@ /// internal class ConsoleIssueReportGenerator : IssueReportFormat { + private readonly IAnsiConsole console; private readonly ConsoleIssueReportFormatSettings consoleIssueReportFormatSettings; /// /// Initializes a new instance of the class. /// + /// The console where output should be written to. /// The Cake log context. /// Settings for reporting the issues. - public ConsoleIssueReportGenerator(ICakeLog log, ConsoleIssueReportFormatSettings settings) + public ConsoleIssueReportGenerator( + IAnsiConsole console, + ICakeLog log, + ConsoleIssueReportFormatSettings settings) : base(log) { + console.NotNull(); settings.NotNull(); + this.console = console; this.consoleIssueReportFormatSettings = settings; } @@ -81,7 +88,7 @@ protected override FilePath InternalCreateReport(IEnumerable issues) } report.Render( - AnsiConsole.Console, + this.console, new ReportSettings { Compact = this.consoleIssueReportFormatSettings.Compact, @@ -104,15 +111,15 @@ private void PrintSummary(IList issues) { if (!issues.Any()) { - AnsiConsole.WriteLine("No issues"); + this.console.WriteLine("No issues"); return; } - AnsiConsole.WriteLine(); - AnsiConsole.WriteLine(); + this.console.WriteLine(); + this.console.WriteLine(); var rule = new Rule("Summary").Centered(); - AnsiConsole.Write(rule); - AnsiConsole.WriteLine(); + this.console.Write(rule); + this.console.WriteLine(); var providerChart = new BarChart(); @@ -162,18 +169,18 @@ private void PrintSummary(IList issues) if (this.consoleIssueReportFormatSettings.ShowProviderSummary) { - AnsiConsole.Write(new Markup("[bold]Issues per provider & run[/]").Centered()); - AnsiConsole.WriteLine(); - AnsiConsole.WriteLine(); - AnsiConsole.Write(providerChart); - AnsiConsole.WriteLine(); + this.console.Write(new Markup("[bold]Issues per provider & run[/]").Centered()); + this.console.WriteLine(); + this.console.WriteLine(); + this.console.Write(providerChart); + this.console.WriteLine(); } if (this.consoleIssueReportFormatSettings.ShowPrioritySummary) { - AnsiConsole.Write(new Markup("[bold]Issues per priority[/]").Centered()); - AnsiConsole.WriteLine(); - AnsiConsole.Write(priorityTable); + this.console.Write(new Markup("[bold]Issues per priority[/]").Centered()); + this.console.WriteLine(); + this.console.Write(priorityTable); } } } \ No newline at end of file From 420c6d7ef9787fbd92b504f3a2a57b911236ecf8 Mon Sep 17 00:00:00 2001 From: Pascal Berger Date: Wed, 24 Sep 2025 00:20:21 +0200 Subject: [PATCH 02/10] Fix column position validation to prevent Errata "label column cannot start at the end of the line" errors --- .../ConsoleIssueReportGenerator.cs | 4 +- .../IssueDiagnostic.cs | 60 ++++++++++++++++--- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/Cake.Issues.Reporting.Console/ConsoleIssueReportGenerator.cs b/src/Cake.Issues.Reporting.Console/ConsoleIssueReportGenerator.cs index 2d305428d..388339c3f 100644 --- a/src/Cake.Issues.Reporting.Console/ConsoleIssueReportGenerator.cs +++ b/src/Cake.Issues.Reporting.Console/ConsoleIssueReportGenerator.cs @@ -76,14 +76,14 @@ protected override FilePath InternalCreateReport(IEnumerable issues) { foreach (var issueGroup in diagnosticIssues.GroupBy(x => x.RuleId)) { - _ = report.AddDiagnostic(new IssueDiagnostic(issueGroup)); + _ = report.AddDiagnostic(new IssueDiagnostic(issueGroup, this.Settings.RepositoryRoot)); } } else { foreach (var issue in diagnosticIssues) { - _ = report.AddDiagnostic(new IssueDiagnostic(issue)); + _ = report.AddDiagnostic(new IssueDiagnostic(issue, this.Settings.RepositoryRoot)); } } diff --git a/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs b/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs index a5de115c5..737a8725e 100644 --- a/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs +++ b/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs @@ -2,7 +2,9 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using Cake.Core.IO; using Errata; using Spectre.Console; @@ -12,13 +14,15 @@ internal sealed class IssueDiagnostic : Diagnostic { private readonly IEnumerable issues; + private readonly DirectoryPath repositoryRoot; /// /// Initializes a new instance of the class. /// /// Issue which the diagnostic should describe. - public IssueDiagnostic(IIssue issue) - : this([issue]) + /// Root directory of the repository. + public IssueDiagnostic(IIssue issue, DirectoryPath repositoryRoot = null) + : this([issue], repositoryRoot) { } @@ -26,11 +30,13 @@ public IssueDiagnostic(IIssue issue) /// Initializes a new instance of the class. /// /// Issues which the diagnostic should describe. - public IssueDiagnostic(IEnumerable issues) + /// Root directory of the repository. + public IssueDiagnostic(IEnumerable issues, DirectoryPath repositoryRoot = null) : base(issues.First().RuleId) { this.issues = issues; + this.repositoryRoot = repositoryRoot; var firstIssue = this.issues.First(); @@ -79,7 +85,7 @@ public IssueDiagnostic(IEnumerable issues) /// /// Issue for which the location should be returned. /// Location for the diagnostic. - private static (Location Location, int Lenght) GetLocation(IIssue issue) + private (Location Location, int Lenght) GetLocation(IIssue issue) { // Errata currently doesn't support file or line level diagnostics. if (!issue.Line.HasValue || !issue.Column.HasValue) @@ -87,12 +93,52 @@ private static (Location Location, int Lenght) GetLocation(IIssue issue) return default; } - var location = new Location(issue.Line.Value, issue.Column.Value); + var line = issue.Line.Value; + var column = issue.Column.Value; + + // Try to validate column position against actual file content if possible + if (this.repositoryRoot != null && issue.AffectedFileRelativePath != null) + { + try + { + var fullPath = this.repositoryRoot.CombineWithFilePath(issue.AffectedFileRelativePath).FullPath; + if (File.Exists(fullPath)) + { + var fileContent = File.ReadAllText(fullPath); + var lines = fileContent.Split(['\r', '\n'], StringSplitOptions.None); + + if (line > 0 && line <= lines.Length) + { + var lineContent = lines[line - 1]; // Convert to 0-based indexing + var lineLength = lineContent.Length; + + // If column is beyond the end of the line, adjust it to the last valid position + if (column > lineLength) + { + column = Math.Max(1, lineLength); // Position at end of line content, minimum column 1 + } + } + } + } + catch + { + // If file reading fails, proceed with original column position + // This ensures we don't break functionality when files are not accessible + } + } + + var location = new Location(line, column); var length = 0; if (issue.EndColumn.HasValue) { - length = issue.EndColumn.Value - issue.Column.Value; + length = issue.EndColumn.Value - column; + + // Ensure length is non-negative + if (length < 0) + { + length = 0; + } } return (location, length); @@ -113,7 +159,7 @@ private void CreateLabels() foreach (var issue in this.issues) { - var (location, length) = GetLocation(issue); + var (location, length) = this.GetLocation(issue); var label = new Label( issue.AffectedFileRelativePath.FullPath, From 2d522571255fce00668341d66571862a551b174a Mon Sep 17 00:00:00 2001 From: Pascal Berger Date: Wed, 24 Sep 2025 00:43:12 +0200 Subject: [PATCH 03/10] Improve logging --- .../IssueDiagnosticTests.cs | 64 +++++++++++++++++-- .../ConsoleIssueReportGenerator.cs | 12 +++- .../IssueDiagnostic.cs | 23 +++++-- 3 files changed, 86 insertions(+), 13 deletions(-) diff --git a/src/Cake.Issues.Reporting.Console.Tests/IssueDiagnosticTests.cs b/src/Cake.Issues.Reporting.Console.Tests/IssueDiagnosticTests.cs index 38b19d361..5de8b1200 100644 --- a/src/Cake.Issues.Reporting.Console.Tests/IssueDiagnosticTests.cs +++ b/src/Cake.Issues.Reporting.Console.Tests/IssueDiagnosticTests.cs @@ -1,16 +1,64 @@ namespace Cake.Issues.Reporting.Console.Tests; using System; +using Cake.Core.Diagnostics; +using Cake.Core.IO; using Xunit; public sealed class IssueDiagnosticTests { - public sealed class TheConstructor + public sealed class TheCtor { + [Fact] + public void Should_Throw_If_Log_Is_Null() + { + // Given + ICakeLog log = null; + var repositoryRoot = @"/repo"; + var issue = IssueBuilder + .NewIssue("Test message", "TestProvider", "TestProviderName") + .InFile("test.cs", 10, 5) + .InProject("TestProject.csproj", "TestProject") + .OfRule("TestRule") + .WithPriority(IssuePriority.Warning) + .Create(); + + + // When + var result = Record.Exception(() => new IssueDiagnostic(log, repositoryRoot, issue)); + + // Then + result.IsArgumentNullException("log"); + } + + [Fact] + public void Should_Throw_If_RepositoryRoot_Is_Null() + { + // Given + var log = new FakeLog(); + DirectoryPath repositoryRoot = null; + var issue = IssueBuilder + .NewIssue("Test message", "TestProvider", "TestProviderName") + .InFile("test.cs", 10, 5) + .InProject("TestProject.csproj", "TestProject") + .OfRule("TestRule") + .WithPriority(IssuePriority.Warning) + .Create(); + + + // When + var result = Record.Exception(() => new IssueDiagnostic(log, repositoryRoot, issue)); + + // Then + result.IsArgumentNullException("repositoryRoot"); + } + [Fact] public void Should_Include_Project_Information_In_Note_When_Available() { // Given + var log = new FakeLog(); + var repositoryRoot = @"/repo"; var issue = IssueBuilder .NewIssue("Test message", "TestProvider", "TestProviderName") .InFile("test.cs", 10, 5) @@ -20,7 +68,7 @@ public void Should_Include_Project_Information_In_Note_When_Available() .Create(); // When - var diagnostic = new IssueDiagnostic(issue); + var diagnostic = new IssueDiagnostic(log, repositoryRoot, issue); // Then diagnostic.Note.ShouldContain("Project: TestProject.csproj"); @@ -30,6 +78,8 @@ public void Should_Include_Project_Information_In_Note_When_Available() public void Should_Include_Project_Name_When_No_Project_File_Path() { // Given + var log = new FakeLog(); + var repositoryRoot = @"/repo"; var issue = IssueBuilder .NewIssue("Test message", "TestProvider", "TestProviderName") .InFile("test.cs", 10, 5) @@ -39,7 +89,7 @@ public void Should_Include_Project_Name_When_No_Project_File_Path() .Create(); // When - var diagnostic = new IssueDiagnostic(issue); + var diagnostic = new IssueDiagnostic(log, repositoryRoot, issue); // Then diagnostic.Note.ShouldContain("Project: TestProject"); @@ -49,6 +99,8 @@ public void Should_Include_Project_Name_When_No_Project_File_Path() public void Should_Include_Both_Project_And_Rule_Info_When_Available() { // Given + var log = new FakeLog(); + var repositoryRoot = @"/repo"; var issue = IssueBuilder .NewIssue("Test message", "TestProvider", "TestProviderName") .InFile("test.cs", 10, 5) @@ -58,7 +110,7 @@ public void Should_Include_Both_Project_And_Rule_Info_When_Available() .Create(); // When - var diagnostic = new IssueDiagnostic(issue); + var diagnostic = new IssueDiagnostic(log, repositoryRoot, issue); // Then diagnostic.Note.ShouldContain("Project: TestProject.csproj"); @@ -69,6 +121,8 @@ public void Should_Include_Both_Project_And_Rule_Info_When_Available() public void Should_Not_Include_Project_When_Not_Available() { // Given + var log = new FakeLog(); + var repositoryRoot = @"/repo"; var issue = IssueBuilder .NewIssue("Test message", "TestProvider", "TestProviderName") .InFile("test.cs", 10, 5) @@ -77,7 +131,7 @@ public void Should_Not_Include_Project_When_Not_Available() .Create(); // When - var diagnostic = new IssueDiagnostic(issue); + var diagnostic = new IssueDiagnostic(log, repositoryRoot, issue); // Then if (diagnostic.Note != null) diff --git a/src/Cake.Issues.Reporting.Console/ConsoleIssueReportGenerator.cs b/src/Cake.Issues.Reporting.Console/ConsoleIssueReportGenerator.cs index 388339c3f..de4af8087 100644 --- a/src/Cake.Issues.Reporting.Console/ConsoleIssueReportGenerator.cs +++ b/src/Cake.Issues.Reporting.Console/ConsoleIssueReportGenerator.cs @@ -76,14 +76,22 @@ protected override FilePath InternalCreateReport(IEnumerable issues) { foreach (var issueGroup in diagnosticIssues.GroupBy(x => x.RuleId)) { - _ = report.AddDiagnostic(new IssueDiagnostic(issueGroup, this.Settings.RepositoryRoot)); + _ = report.AddDiagnostic( + new IssueDiagnostic( + this.Log, + this.Settings.RepositoryRoot, + issueGroup)); } } else { foreach (var issue in diagnosticIssues) { - _ = report.AddDiagnostic(new IssueDiagnostic(issue, this.Settings.RepositoryRoot)); + _ = report.AddDiagnostic( + new IssueDiagnostic( + this.Log, + this.Settings.RepositoryRoot, + issue)); } } diff --git a/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs b/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs index 737a8725e..e8f501086 100644 --- a/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs +++ b/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using Cake.Core.Diagnostics; using Cake.Core.IO; using Errata; using Spectre.Console; @@ -13,30 +14,37 @@ /// internal sealed class IssueDiagnostic : Diagnostic { + private readonly ICakeLog log; private readonly IEnumerable issues; private readonly DirectoryPath repositoryRoot; /// /// Initializes a new instance of the class. /// - /// Issue which the diagnostic should describe. + /// The Cake log. /// Root directory of the repository. - public IssueDiagnostic(IIssue issue, DirectoryPath repositoryRoot = null) - : this([issue], repositoryRoot) + /// Issue which the diagnostic should describe. + public IssueDiagnostic(ICakeLog log, DirectoryPath repositoryRoot, IIssue issue) + : this(log, repositoryRoot, [issue]) { } /// /// Initializes a new instance of the class. /// - /// Issues which the diagnostic should describe. + /// The Cake log. /// Root directory of the repository. - public IssueDiagnostic(IEnumerable issues, DirectoryPath repositoryRoot = null) + /// Issues which the diagnostic should describe. + public IssueDiagnostic(ICakeLog log, DirectoryPath repositoryRoot, IEnumerable issues) : base(issues.First().RuleId) { - this.issues = issues; + log.NotNull(); + repositoryRoot.NotNull(); + + this.log = log; this.repositoryRoot = repositoryRoot; + this.issues = issues; var firstIssue = this.issues.First(); @@ -124,6 +132,9 @@ public IssueDiagnostic(IEnumerable issues, DirectoryPath repositoryRoot { // If file reading fails, proceed with original column position // This ensures we don't break functionality when files are not accessible + this.log.Verbose( + "Could not read file '{0}' to validate issue location is in a valid range.", + issue.AffectedFileRelativePath); } } From 55059d5fdafd6363969bd9ce0aeddea20bc8593f Mon Sep 17 00:00:00 2001 From: Pascal Berger Date: Wed, 24 Sep 2025 00:45:45 +0200 Subject: [PATCH 04/10] Improve line ending handling --- src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs b/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs index e8f501086..29888fb46 100644 --- a/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs +++ b/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs @@ -113,9 +113,17 @@ public IssueDiagnostic(ICakeLog log, DirectoryPath repositoryRoot, IEnumerable(); + using (var reader = new StringReader(fileContent)) + { + string? currentLine; + while ((currentLine = reader.ReadLine()) != null) + { + lines.Add(currentLine); + } + } - if (line > 0 && line <= lines.Length) + if (line > 0 && line <= lines.Count) { var lineContent = lines[line - 1]; // Convert to 0-based indexing var lineLength = lineContent.Length; From 5444e1e31ddcb8104a1695fd267faf457cdf77aa Mon Sep 17 00:00:00 2001 From: Pascal Berger Date: Wed, 24 Sep 2025 00:45:53 +0200 Subject: [PATCH 05/10] Improve comment --- .../ConsoleIssueReportGeneratorTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cake.Issues.Reporting.Console.Tests/ConsoleIssueReportGeneratorTests.cs b/src/Cake.Issues.Reporting.Console.Tests/ConsoleIssueReportGeneratorTests.cs index 56f25c76b..54b261aeb 100644 --- a/src/Cake.Issues.Reporting.Console.Tests/ConsoleIssueReportGeneratorTests.cs +++ b/src/Cake.Issues.Reporting.Console.Tests/ConsoleIssueReportGeneratorTests.cs @@ -316,7 +316,7 @@ public Task Should_Work_With_Issue_On_End_Of_Line() { IssueBuilder .NewIssue("Message Foo", "ProviderType Foo", "ProviderName Foo") - .InFile(fileName, 1, 57) + .InFile(fileName, 1, 57) // Position after the last character on line 1 .Create(), }; // When From ac62fa77340eb1ca4cfbeac08851373f66ac7828 Mon Sep 17 00:00:00 2001 From: Pascal Berger Date: Wed, 24 Sep 2025 00:55:00 +0200 Subject: [PATCH 06/10] Improve file reading --- .../IssueDiagnostic.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs b/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs index 29888fb46..f20b49d0f 100644 --- a/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs +++ b/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs @@ -112,20 +112,21 @@ public IssueDiagnostic(ICakeLog log, DirectoryPath repositoryRoot, IEnumerable(); - using (var reader = new StringReader(fileContent)) + // Read the required line from the file + string lineContent = null; + var currentLineNumber = 0; + foreach (var fileLine in File.ReadLines(fullPath)) { - string? currentLine; - while ((currentLine = reader.ReadLine()) != null) + currentLineNumber++; + if (currentLineNumber == line) { - lines.Add(currentLine); + lineContent = fileLine; + break; } } - if (line > 0 && line <= lines.Count) + if (lineContent != null) { - var lineContent = lines[line - 1]; // Convert to 0-based indexing var lineLength = lineContent.Length; // If column is beyond the end of the line, adjust it to the last valid position From 188199f9ccc98894a5cb4d720a4ce9cd40bef058 Mon Sep 17 00:00:00 2001 From: Pascal Berger Date: Wed, 24 Sep 2025 01:00:13 +0200 Subject: [PATCH 07/10] Improve exception handling --- src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs b/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs index f20b49d0f..36023485c 100644 --- a/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs +++ b/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Security; using Cake.Core.Diagnostics; using Cake.Core.IO; using Errata; @@ -137,7 +138,7 @@ public IssueDiagnostic(ICakeLog log, DirectoryPath repositoryRoot, IEnumerable Date: Wed, 24 Sep 2025 01:04:28 +0200 Subject: [PATCH 08/10] Improve performance --- src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs b/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs index 36023485c..93283dc3c 100644 --- a/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs +++ b/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs @@ -114,17 +114,7 @@ public IssueDiagnostic(ICakeLog log, DirectoryPath repositoryRoot, IEnumerable Date: Wed, 24 Sep 2025 01:12:32 +0200 Subject: [PATCH 09/10] Improve performance --- .../IssueDiagnostic.cs | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs b/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs index 93283dc3c..7865906e2 100644 --- a/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs +++ b/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs @@ -113,8 +113,22 @@ public IssueDiagnostic(ICakeLog log, DirectoryPath repositoryRoot, IEnumerable Date: Wed, 24 Sep 2025 01:16:12 +0200 Subject: [PATCH 10/10] Handle more exceptions --- src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs b/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs index 7865906e2..6d8e4ff9c 100644 --- a/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs +++ b/src/Cake.Issues.Reporting.Console/IssueDiagnostic.cs @@ -142,7 +142,11 @@ public IssueDiagnostic(ICakeLog log, DirectoryPath repositoryRoot, IEnumerable