
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
///
@@ -62,7 +62,7 @@ public static IIssueProvider JUnitIssuesFromFilePath(
/// Content of the JUnit log file.
/// Instance of a provider for issues reported in JUnit XML format.
///
- /// Report issues reported by kubeconform:
+ /// Read issues the content of JUnit XML file:
///
///
- /// Gets an instance of a provider for issues reported in JUnit XML format using log file content.
- ///
- ///
- ///
- ///
///
/// The Cake log context.
/// Settings for the issue provider.
-public class JUnitIssuesProvider(ICakeLog log, JUnitIssuesSettings issueProviderSettings)
+internal class JUnitIssuesProvider(ICakeLog log, JUnitIssuesSettings issueProviderSettings)
: BaseConfigurableIssueProvider(log, issueProviderSettings)
{
- ///
- /// Gets the name of the JUnit issue provider.
- /// This name can be used to identify issues based on the property.
- ///
- public static string ProviderTypeName => typeof(JUnitIssuesProvider).FullName;
-
///
public override string ProviderName => "JUnit";
@@ -204,7 +198,7 @@ private IIssue ProcessTestFailure(XElement failureElement, string className, str
}
var issueBuilder = IssueBuilder
- .NewIssue(fullMessage, ProviderTypeName, this.ProviderName)
+ .NewIssue(fullMessage, typeof(JUnitIssuesProvider).FullName, this.ProviderName)
.WithPriority(priority);
if (!string.IsNullOrEmpty(type))
diff --git a/src/Cake.Issues.JUnit/JUnitIssuesSettings.cs b/src/Cake.Issues.JUnit/JUnitIssuesSettings.cs
index fa49c3f87..baa1d7635 100644
--- a/src/Cake.Issues.JUnit/JUnitIssuesSettings.cs
+++ b/src/Cake.Issues.JUnit/JUnitIssuesSettings.cs
@@ -1,10 +1,9 @@
namespace Cake.Issues.JUnit;
-using System.Text;
using Cake.Core.IO;
///
-/// Settings for .
+/// Settings for .
///
public class JUnitIssuesSettings : IssueProviderSettings
{
@@ -27,40 +26,4 @@ public JUnitIssuesSettings(byte[] logFileContent)
: base(logFileContent)
{
}
-
- ///
- /// Returns a new instance of the class from a log file on disk.
- ///
- /// Path to the JUnit log file.
- /// Instance of the class.
- public static JUnitIssuesSettings FromFilePath(FilePath logFilePath)
- {
- logFilePath.NotNull();
-
- return new JUnitIssuesSettings(logFilePath);
- }
-
- ///
- /// Returns a new instance of the class from log file content.
- ///
- /// Content of the JUnit log file.
- /// Instance of the class.
- public static JUnitIssuesSettings FromContent(string logFileContent)
- {
- logFileContent.NotNullOrWhiteSpace();
-
- return new JUnitIssuesSettings(Encoding.UTF8.GetBytes(logFileContent));
- }
-
- ///
- /// Returns a new instance of the class from log file content.
- ///
- /// Content of the JUnit log file.
- /// Instance of the class.
- public static JUnitIssuesSettings FromContent(byte[] logFileContent)
- {
- logFileContent.NotNull();
-
- return new JUnitIssuesSettings(logFileContent);
- }
}
\ No newline at end of file
From bf5f899afc81ef2bb165b64c4268cb3dadf897e0 Mon Sep 17 00:00:00 2001
From: Pascal Berger
Date: Sun, 3 Aug 2025 10:54:14 +0200
Subject: [PATCH 17/35] Fix line and column reporting
---
.../JUnitIssuesProviderTests.cs | 40 +++++++++++--------
src/Cake.Issues.JUnit/JUnitIssuesProvider.cs | 2 +-
2 files changed, 24 insertions(+), 18 deletions(-)
diff --git a/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderTests.cs b/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderTests.cs
index 7728b5326..8a0405c55 100644
--- a/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderTests.cs
+++ b/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderTests.cs
@@ -42,23 +42,29 @@ public void Should_Read_Issues_Correct_For_CppLint()
// Then
issues.Count.ShouldBe(2);
- var issue1 = issues[0];
- issue1.ProviderType.ShouldBe("Cake.Issues.JUnit.JUnitIssuesProvider");
- issue1.ProviderName.ShouldBe("JUnit");
- issue1.MessageText.ShouldBe("Lines should be <= 80 characters long\nsrc/example.cpp:15: Lines should be <= 80 characters long [whitespace/line_length] [2]");
- issue1.Priority.ShouldBe((int)IssuePriority.Error);
- issue1.Rule().ShouldBe("warning");
- issue1.AffectedFileRelativePath.ShouldBe("src/example.cpp");
- issue1.Line.ShouldBe(15);
-
- var issue2 = issues[1];
- issue2.ProviderType.ShouldBe("Cake.Issues.JUnit.JUnitIssuesProvider");
- issue2.ProviderName.ShouldBe("JUnit");
- issue2.MessageText.ShouldBe("Include order issue\nsrc/example.cpp:5: #includes are not properly sorted [build/include_order] [4]");
- issue2.Priority.ShouldBe((int)IssuePriority.Error);
- issue2.Rule().ShouldBe("warning");
- issue2.AffectedFileRelativePath.ShouldBe("src/example.cpp");
- issue2.Line.ShouldBe(5);
+ var issue = issues[0];
+ IssueChecker.Check(
+ issue,
+ IssueBuilder.NewIssue(
+ "Lines should be <= 80 characters long\nsrc/example.cpp:15: Lines should be <= 80 characters long [whitespace/line_length] [2]",
+ "Cake.Issues.JUnit.JUnitIssuesProvider",
+ "JUnit")
+ .InFile(@"src/example.cpp", 15)
+ .OfRule("warning")
+ .WithPriority(IssuePriority.Error)
+ .Create());
+
+ issue = issues[1];
+ IssueChecker.Check(
+ issue,
+ IssueBuilder.NewIssue(
+ "Include order issue\nsrc/example.cpp:5: #includes are not properly sorted [build/include_order] [4]",
+ "Cake.Issues.JUnit.JUnitIssuesProvider",
+ "JUnit")
+ .InFile(@"src/example.cpp", 5)
+ .OfRule("warning")
+ .WithPriority(IssuePriority.Error)
+ .Create());
}
[Fact]
diff --git a/src/Cake.Issues.JUnit/JUnitIssuesProvider.cs b/src/Cake.Issues.JUnit/JUnitIssuesProvider.cs
index e8064fc8d..51415f50d 100644
--- a/src/Cake.Issues.JUnit/JUnitIssuesProvider.cs
+++ b/src/Cake.Issues.JUnit/JUnitIssuesProvider.cs
@@ -216,7 +216,7 @@ private IIssue ProcessTestFailure(XElement failureElement, string className, str
if (fileInfo.HasValue)
{
var (filePath, line, column) = fileInfo.Value;
- issueBuilder = issueBuilder.InFile(filePath, line, line, column, column);
+ issueBuilder = issueBuilder.InFile(filePath, line, column);
}
return issueBuilder.Create();
From 0357b07192f485aa0bea8766db1c2ab17916c112 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 3 Aug 2025 09:12:35 +0000
Subject: [PATCH 18/35] Add comprehensive cpplint-style JUnit test cases and
improve file path extraction
Co-authored-by: pascalberger <2190718+pascalberger@users.noreply.github.com>
---
.../JUnitIssuesProviderTests.cs | 256 +++++++++++++++---
.../Testfiles/cpplint-mixed-error-failure.xml | 9 +
.../Testfiles/cpplint-multiple-errors.xml | 7 +
.../Testfiles/cpplint-multiple-failures.xml | 10 +
.../Testfiles/cpplint-passed.xml | 5 +
.../Testfiles/cpplint-single-error.xml | 6 +
.../Testfiles/cpplint-xml-escaping.xml | 9 +
src/Cake.Issues.JUnit/JUnitIssuesProvider.cs | 36 ++-
8 files changed, 294 insertions(+), 44 deletions(-)
create mode 100644 src/Cake.Issues.JUnit.Tests/Testfiles/cpplint-mixed-error-failure.xml
create mode 100644 src/Cake.Issues.JUnit.Tests/Testfiles/cpplint-multiple-errors.xml
create mode 100644 src/Cake.Issues.JUnit.Tests/Testfiles/cpplint-multiple-failures.xml
create mode 100644 src/Cake.Issues.JUnit.Tests/Testfiles/cpplint-passed.xml
create mode 100644 src/Cake.Issues.JUnit.Tests/Testfiles/cpplint-single-error.xml
create mode 100644 src/Cake.Issues.JUnit.Tests/Testfiles/cpplint-xml-escaping.xml
diff --git a/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderTests.cs b/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderTests.cs
index 8a0405c55..81fc00e4d 100644
--- a/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderTests.cs
+++ b/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderTests.cs
@@ -79,25 +79,27 @@ public void Should_Read_Issues_Correct_For_Kubeconform()
// Then
issues.Count.ShouldBe(2);
- var issue1 = issues[0];
- issue1.ProviderType.ShouldBe("Cake.Issues.JUnit.JUnitIssuesProvider");
- issue1.ProviderName.ShouldBe("JUnit");
- issue1.MessageText.ShouldBe("Invalid resource definition\ndeployment.yaml:10:15: error validating data: ValidationError(Deployment.spec.template.spec.containers[0].image): invalid value: \"\", expected non-empty string");
- issue1.Priority.ShouldBe((int)IssuePriority.Error);
- issue1.Rule().ShouldBe("ValidationError");
- issue1.AffectedFileRelativePath.ShouldBe("deployment.yaml");
- issue1.Line.ShouldBe(10);
- issue1.Column.ShouldBe(15);
-
- var issue2 = issues[1];
- issue2.ProviderType.ShouldBe("Cake.Issues.JUnit.JUnitIssuesProvider");
- issue2.ProviderName.ShouldBe("JUnit");
- issue2.MessageText.ShouldBe("Port configuration invalid\nservice.yaml:8:5: Port 8080 is already in use by another service");
- issue2.Priority.ShouldBe((int)IssuePriority.Error);
- issue2.Rule().ShouldBe("ConfigError");
- issue2.AffectedFileRelativePath.ShouldBe("service.yaml");
- issue2.Line.ShouldBe(8);
- issue2.Column.ShouldBe(5);
+ IssueChecker.Check(
+ issues[0],
+ IssueBuilder.NewIssue(
+ "Invalid resource definition\ndeployment.yaml:10:15: error validating data: ValidationError(Deployment.spec.template.spec.containers[0].image): invalid value: \"\", expected non-empty string",
+ "Cake.Issues.JUnit.JUnitIssuesProvider",
+ "JUnit")
+ .InFile(@"deployment.yaml", 10, 15)
+ .OfRule("ValidationError")
+ .WithPriority(IssuePriority.Error)
+ .Create());
+
+ IssueChecker.Check(
+ issues[1],
+ IssueBuilder.NewIssue(
+ "Port configuration invalid\nservice.yaml:8:5: Port 8080 is already in use by another service",
+ "Cake.Issues.JUnit.JUnitIssuesProvider",
+ "JUnit")
+ .InFile(@"service.yaml", 8, 5)
+ .OfRule("ConfigError")
+ .WithPriority(IssuePriority.Error)
+ .Create());
}
[Fact]
@@ -112,24 +114,27 @@ public void Should_Read_Issues_Correct_For_HtmlHint()
// Then
issues.Count.ShouldBe(2);
- var issue1 = issues[0];
- issue1.ProviderType.ShouldBe("Cake.Issues.JUnit.JUnitIssuesProvider");
- issue1.ProviderName.ShouldBe("JUnit");
- issue1.MessageText.ShouldBe("Tagname must be lowercase\nindex.html(12,5): Tagname 'DIV' must be lowercase");
- issue1.Priority.ShouldBe((int)IssuePriority.Error);
- issue1.Rule().ShouldBe("error");
- issue1.AffectedFileRelativePath.ShouldBe("index.html");
- issue1.Line.ShouldBe(12);
- issue1.Column.ShouldBe(5);
-
- var issue2 = issues[1];
- issue2.ProviderType.ShouldBe("Cake.Issues.JUnit.JUnitIssuesProvider");
- issue2.ProviderName.ShouldBe("JUnit");
- issue2.MessageText.ShouldBe("Attribute value must be in double quotes\nabout.html line 8: The value of attribute 'class' must be in double quotes.");
- issue2.Priority.ShouldBe((int)IssuePriority.Error);
- issue2.Rule().ShouldBe("warning");
- issue2.AffectedFileRelativePath.ShouldBe("about.html");
- issue2.Line.ShouldBe(8);
+ IssueChecker.Check(
+ issues[0],
+ IssueBuilder.NewIssue(
+ "Tagname must be lowercase\nindex.html(12,5): Tagname 'DIV' must be lowercase",
+ "Cake.Issues.JUnit.JUnitIssuesProvider",
+ "JUnit")
+ .InFile(@"index.html", 12, 5)
+ .OfRule("error")
+ .WithPriority(IssuePriority.Error)
+ .Create());
+
+ IssueChecker.Check(
+ issues[1],
+ IssueBuilder.NewIssue(
+ "Attribute value must be in double quotes\nabout.html line 8: The value of attribute 'class' must be in double quotes.",
+ "Cake.Issues.JUnit.JUnitIssuesProvider",
+ "JUnit")
+ .InFile(@"about.html", 8)
+ .OfRule("warning")
+ .WithPriority(IssuePriority.Error)
+ .Create());
}
[Fact]
@@ -144,12 +149,15 @@ public void Should_Read_Issues_Correct_For_CommitLint()
// Then
issues.Count.ShouldBe(1);
- var issue = issues[0];
- issue.ProviderType.ShouldBe("Cake.Issues.JUnit.JUnitIssuesProvider");
- issue.ProviderName.ShouldBe("JUnit");
- issue.MessageText.ShouldBe("Type must be one of the allowed values\ncommit-2: type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test]");
- issue.Priority.ShouldBe((int)IssuePriority.Error);
- issue.Rule().ShouldBe("error");
+ IssueChecker.Check(
+ issues[0],
+ IssueBuilder.NewIssue(
+ "Type must be one of the allowed values\ncommit-2: type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test]",
+ "Cake.Issues.JUnit.JUnitIssuesProvider",
+ "JUnit")
+ .OfRule("error")
+ .WithPriority(IssuePriority.Error)
+ .Create());
}
[Fact]
@@ -185,5 +193,167 @@ public void Should_Handle_Invalid_XML()
Should.Throw(() => fixture.ReadIssues().ToList())
.Message.ShouldContain("Failed to parse JUnit XML");
}
+
+ [Fact]
+ public void Should_Handle_CppLint_Passed_Test()
+ {
+ // Given
+ var fixture = new JUnitIssuesProviderFixture("cpplint-passed.xml");
+
+ // When
+ var issues = fixture.ReadIssues().ToList();
+
+ // Then
+ issues.Count.ShouldBe(0);
+ }
+
+ [Fact]
+ public void Should_Handle_CppLint_Single_Error()
+ {
+ // Given
+ var fixture = new JUnitIssuesProviderFixture("cpplint-single-error.xml");
+
+ // When
+ var issues = fixture.ReadIssues().ToList();
+
+ // Then
+ issues.Count.ShouldBe(1);
+
+ IssueChecker.Check(
+ issues[0],
+ IssueBuilder.NewIssue(
+ "ErrMsg1",
+ "Cake.Issues.JUnit.JUnitIssuesProvider",
+ "JUnit")
+ .OfRule("errors")
+ .WithPriority(IssuePriority.Error)
+ .Create());
+ }
+
+ [Fact]
+ public void Should_Handle_CppLint_Multiple_Errors()
+ {
+ // Given
+ var fixture = new JUnitIssuesProviderFixture("cpplint-multiple-errors.xml");
+
+ // When
+ var issues = fixture.ReadIssues().ToList();
+
+ // Then
+ issues.Count.ShouldBe(1);
+
+ IssueChecker.Check(
+ issues[0],
+ IssueBuilder.NewIssue(
+ "ErrMsg1\nErrMsg2",
+ "Cake.Issues.JUnit.JUnitIssuesProvider",
+ "JUnit")
+ .OfRule("errors")
+ .WithPriority(IssuePriority.Error)
+ .Create());
+ }
+
+ [Fact]
+ public void Should_Handle_CppLint_Mixed_Error_And_Failure()
+ {
+ // Given
+ var fixture = new JUnitIssuesProviderFixture("cpplint-mixed-error-failure.xml");
+
+ // When
+ var issues = fixture.ReadIssues().ToList();
+
+ // Then
+ issues.Count.ShouldBe(2);
+
+ IssueChecker.Check(
+ issues[0],
+ IssueBuilder.NewIssue(
+ "ErrMsg",
+ "Cake.Issues.JUnit.JUnitIssuesProvider",
+ "JUnit")
+ .OfRule("errors")
+ .WithPriority(IssuePriority.Error)
+ .Create());
+
+ IssueChecker.Check(
+ issues[1],
+ IssueBuilder.NewIssue(
+ "5: FailMsg [category/subcategory] [3]",
+ "Cake.Issues.JUnit.JUnitIssuesProvider",
+ "JUnit")
+ .InFile("File", 5)
+ .OfRule("File")
+ .WithPriority(IssuePriority.Error)
+ .Create());
+ }
+
+ [Fact]
+ public void Should_Handle_CppLint_Multiple_Failures_Grouped_By_File()
+ {
+ // Given
+ var fixture = new JUnitIssuesProviderFixture("cpplint-multiple-failures.xml");
+
+ // When
+ var issues = fixture.ReadIssues().ToList();
+
+ // Then
+ issues.Count.ShouldBe(2);
+
+ IssueChecker.Check(
+ issues[0],
+ IssueBuilder.NewIssue(
+ "5: FailMsg1 [category/subcategory] [3]\n19: FailMsg3 [category/subcategory] [3]",
+ "Cake.Issues.JUnit.JUnitIssuesProvider",
+ "JUnit")
+ .InFile("File1", 5)
+ .OfRule("File1")
+ .WithPriority(IssuePriority.Error)
+ .Create());
+
+ IssueChecker.Check(
+ issues[1],
+ IssueBuilder.NewIssue(
+ "99: FailMsg2 [category/subcategory] [3]",
+ "Cake.Issues.JUnit.JUnitIssuesProvider",
+ "JUnit")
+ .InFile("File2", 99)
+ .OfRule("File2")
+ .WithPriority(IssuePriority.Error)
+ .Create());
+ }
+
+ [Fact]
+ public void Should_Handle_CppLint_XML_Escaping()
+ {
+ // Given
+ var fixture = new JUnitIssuesProviderFixture("cpplint-xml-escaping.xml");
+
+ // When
+ var issues = fixture.ReadIssues().ToList();
+
+ // Then
+ issues.Count.ShouldBe(2);
+
+ IssueChecker.Check(
+ issues[0],
+ IssueBuilder.NewIssue(
+ "&",
+ "Cake.Issues.JUnit.JUnitIssuesProvider",
+ "JUnit")
+ .OfRule("errors")
+ .WithPriority(IssuePriority.Error)
+ .Create());
+
+ IssueChecker.Check(
+ issues[1],
+ IssueBuilder.NewIssue(
+ "5: & [category/subcategory] [3]",
+ "Cake.Issues.JUnit.JUnitIssuesProvider",
+ "JUnit")
+ .InFile("File1", 5)
+ .OfRule("File1")
+ .WithPriority(IssuePriority.Error)
+ .Create());
+ }
}
}
\ No newline at end of file
diff --git a/src/Cake.Issues.JUnit.Tests/Testfiles/cpplint-mixed-error-failure.xml b/src/Cake.Issues.JUnit.Tests/Testfiles/cpplint-mixed-error-failure.xml
new file mode 100644
index 000000000..bcd7fbc62
--- /dev/null
+++ b/src/Cake.Issues.JUnit.Tests/Testfiles/cpplint-mixed-error-failure.xml
@@ -0,0 +1,9 @@
+
+
+
+ ErrMsg
+
+
+ 5: FailMsg [category/subcategory] [3]
+
+
\ No newline at end of file
diff --git a/src/Cake.Issues.JUnit.Tests/Testfiles/cpplint-multiple-errors.xml b/src/Cake.Issues.JUnit.Tests/Testfiles/cpplint-multiple-errors.xml
new file mode 100644
index 000000000..a7bdca34e
--- /dev/null
+++ b/src/Cake.Issues.JUnit.Tests/Testfiles/cpplint-multiple-errors.xml
@@ -0,0 +1,7 @@
+
+
+
+ ErrMsg1
+ErrMsg2
+
+
\ No newline at end of file
diff --git a/src/Cake.Issues.JUnit.Tests/Testfiles/cpplint-multiple-failures.xml b/src/Cake.Issues.JUnit.Tests/Testfiles/cpplint-multiple-failures.xml
new file mode 100644
index 000000000..718a45ae5
--- /dev/null
+++ b/src/Cake.Issues.JUnit.Tests/Testfiles/cpplint-multiple-failures.xml
@@ -0,0 +1,10 @@
+
+
+
+ 5: FailMsg1 [category/subcategory] [3]
+19: FailMsg3 [category/subcategory] [3]
+
+
+ 99: FailMsg2 [category/subcategory] [3]
+
+
\ No newline at end of file
diff --git a/src/Cake.Issues.JUnit.Tests/Testfiles/cpplint-passed.xml b/src/Cake.Issues.JUnit.Tests/Testfiles/cpplint-passed.xml
new file mode 100644
index 000000000..d45c77584
--- /dev/null
+++ b/src/Cake.Issues.JUnit.Tests/Testfiles/cpplint-passed.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Cake.Issues.JUnit.Tests/Testfiles/cpplint-single-error.xml b/src/Cake.Issues.JUnit.Tests/Testfiles/cpplint-single-error.xml
new file mode 100644
index 000000000..21d95e7df
--- /dev/null
+++ b/src/Cake.Issues.JUnit.Tests/Testfiles/cpplint-single-error.xml
@@ -0,0 +1,6 @@
+
+
+
+ ErrMsg1
+
+
\ No newline at end of file
diff --git a/src/Cake.Issues.JUnit.Tests/Testfiles/cpplint-xml-escaping.xml b/src/Cake.Issues.JUnit.Tests/Testfiles/cpplint-xml-escaping.xml
new file mode 100644
index 000000000..49769bc8c
--- /dev/null
+++ b/src/Cake.Issues.JUnit.Tests/Testfiles/cpplint-xml-escaping.xml
@@ -0,0 +1,9 @@
+
+
+
+ &</error>
+
+
+ 5: &</failure> [category/subcategory] [3]
+
+
\ No newline at end of file
diff --git a/src/Cake.Issues.JUnit/JUnitIssuesProvider.cs b/src/Cake.Issues.JUnit/JUnitIssuesProvider.cs
index 51415f50d..b971ee72b 100644
--- a/src/Cake.Issues.JUnit/JUnitIssuesProvider.cs
+++ b/src/Cake.Issues.JUnit/JUnitIssuesProvider.cs
@@ -210,9 +210,43 @@ private IIssue ProcessTestFailure(XElement failureElement, string className, str
issueBuilder = issueBuilder.OfRule(testName);
}
- // Try to extract file information from the message or content
+ // Try to extract file information from the message or content first
var fileInfo = ExtractFileInfoFromOutput(fullMessage) ?? ExtractFileInfoFromClassName(className);
+ // For cpplint-style output, if we don't have file info and the test name looks like a file name,
+ // use the test name as the file name and try to extract line info from the message
+ if (!fileInfo.HasValue && !string.IsNullOrEmpty(testName) && testName != "errors")
+ {
+ // Check if the message contains line info in cpplint format like "5: FailMsg [category/subcategory] [3]"
+ // This is a strong indicator that it's a cpplint-style failure where the test name is the file name
+ var lineMatch = Regex.Match(
+ fullMessage,
+ @"^(\d+):\s*.*\[.*\].*\[.*\]",
+ RegexOptions.Multiline);
+ if (lineMatch.Success && int.TryParse(lineMatch.Groups[1].Value, out var lineNum))
+ {
+ fileInfo = (testName, lineNum, null);
+ }
+
+ // Also check for simple line number pattern without the category/subcategory format
+ else
+ {
+ var simpleLineMatch = Regex.Match(
+ fullMessage,
+ @"^(\d+):",
+ RegexOptions.Multiline);
+ if (simpleLineMatch.Success &&
+ int.TryParse(simpleLineMatch.Groups[1].Value, out var simpleLineNum))
+ {
+ // Only treat as file if the test name doesn't contain hyphens (which are common in rule names)
+ if (!testName.Contains('-') && !testName.Contains('_'))
+ {
+ fileInfo = (testName, simpleLineNum, null);
+ }
+ }
+ }
+ }
+
if (fileInfo.HasValue)
{
var (filePath, line, column) = fileInfo.Value;
From 746694ef0c2eae7e86b6fcd7b56c184fcad7c612 Mon Sep 17 00:00:00 2001
From: Pascal Berger
Date: Sun, 3 Aug 2025 11:16:58 +0200
Subject: [PATCH 19/35] Cleanup tests
---
.../JUnitIssuesProviderTests.cs | 240 +++++++++---------
1 file changed, 120 insertions(+), 120 deletions(-)
diff --git a/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderTests.cs b/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderTests.cs
index 81fc00e4d..d8284a975 100644
--- a/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderTests.cs
+++ b/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderTests.cs
@@ -68,45 +68,69 @@ public void Should_Read_Issues_Correct_For_CppLint()
}
[Fact]
- public void Should_Read_Issues_Correct_For_Kubeconform()
+ public void Should_Handle_CppLint_Passed_Test()
{
// Given
- var fixture = new JUnitIssuesProviderFixture("kubeconform.xml");
+ var fixture = new JUnitIssuesProviderFixture("cpplint-passed.xml");
// When
var issues = fixture.ReadIssues().ToList();
// Then
- issues.Count.ShouldBe(2);
+ issues.ShouldBeEmpty();
+ }
+
+ [Fact]
+ public void Should_Handle_CppLint_Single_Error()
+ {
+ // Given
+ var fixture = new JUnitIssuesProviderFixture("cpplint-single-error.xml");
+
+ // When
+ var issues = fixture.ReadIssues().ToList();
+
+ // Then
+ var issue = issues.ShouldHaveSingleItem();
IssueChecker.Check(
- issues[0],
+ issue,
IssueBuilder.NewIssue(
- "Invalid resource definition\ndeployment.yaml:10:15: error validating data: ValidationError(Deployment.spec.template.spec.containers[0].image): invalid value: \"\", expected non-empty string",
+ "ErrMsg1",
"Cake.Issues.JUnit.JUnitIssuesProvider",
"JUnit")
- .InFile(@"deployment.yaml", 10, 15)
- .OfRule("ValidationError")
+ .OfRule("errors")
.WithPriority(IssuePriority.Error)
.Create());
+ }
+
+ [Fact]
+ public void Should_Handle_CppLint_Multiple_Errors()
+ {
+ // Given
+ var fixture = new JUnitIssuesProviderFixture("cpplint-multiple-errors.xml");
+
+ // When
+ var issues = fixture.ReadIssues().ToList();
+
+ // Then
+ var issue = issues.ShouldHaveSingleItem();
IssueChecker.Check(
- issues[1],
+ issue,
IssueBuilder.NewIssue(
- "Port configuration invalid\nservice.yaml:8:5: Port 8080 is already in use by another service",
+ "ErrMsg1\nErrMsg2",
"Cake.Issues.JUnit.JUnitIssuesProvider",
"JUnit")
- .InFile(@"service.yaml", 8, 5)
- .OfRule("ConfigError")
+ .OfRule("errors")
.WithPriority(IssuePriority.Error)
.Create());
}
[Fact]
- public void Should_Read_Issues_Correct_For_HtmlHint()
+ public void Should_Handle_CppLint_Mixed_Error_And_Failure()
{
// Given
- var fixture = new JUnitIssuesProviderFixture("htmlhint.xml");
+ var fixture = new JUnitIssuesProviderFixture("cpplint-mixed-error-failure.xml");
// When
var issues = fixture.ReadIssues().ToList();
@@ -117,147 +141,99 @@ public void Should_Read_Issues_Correct_For_HtmlHint()
IssueChecker.Check(
issues[0],
IssueBuilder.NewIssue(
- "Tagname must be lowercase\nindex.html(12,5): Tagname 'DIV' must be lowercase",
+ "ErrMsg",
"Cake.Issues.JUnit.JUnitIssuesProvider",
"JUnit")
- .InFile(@"index.html", 12, 5)
- .OfRule("error")
+ .OfRule("errors")
.WithPriority(IssuePriority.Error)
.Create());
IssueChecker.Check(
issues[1],
IssueBuilder.NewIssue(
- "Attribute value must be in double quotes\nabout.html line 8: The value of attribute 'class' must be in double quotes.",
+ "5: FailMsg [category/subcategory] [3]",
"Cake.Issues.JUnit.JUnitIssuesProvider",
"JUnit")
- .InFile(@"about.html", 8)
- .OfRule("warning")
+ .InFile("File", 5)
+ .OfRule("File")
.WithPriority(IssuePriority.Error)
.Create());
}
[Fact]
- public void Should_Read_Issues_Correct_For_CommitLint()
+ public void Should_Handle_CppLint_Multiple_Failures_Grouped_By_File()
{
// Given
- var fixture = new JUnitIssuesProviderFixture("commitlint.xml");
+ var fixture = new JUnitIssuesProviderFixture("cpplint-multiple-failures.xml");
// When
var issues = fixture.ReadIssues().ToList();
// Then
- issues.Count.ShouldBe(1);
+ issues.Count.ShouldBe(2);
IssueChecker.Check(
issues[0],
IssueBuilder.NewIssue(
- "Type must be one of the allowed values\ncommit-2: type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test]",
+ "5: FailMsg1 [category/subcategory] [3]\n19: FailMsg3 [category/subcategory] [3]",
"Cake.Issues.JUnit.JUnitIssuesProvider",
"JUnit")
- .OfRule("error")
+ .InFile("File1", 5)
+ .OfRule("File1")
.WithPriority(IssuePriority.Error)
.Create());
- }
-
- [Fact]
- public void Should_Handle_Empty_TestSuite()
- {
- // Given
- var junitContent = @"
-
- ";
- var fixture = new JUnitIssuesProviderFixture("empty.xml");
- fixture.SetFileContent(junitContent);
-
- // When
- var issues = fixture.ReadIssues().ToList();
- // Then
- issues.Count.ShouldBe(0);
- }
-
- [Fact]
- public void Should_Handle_Invalid_XML()
- {
- // Given
- var junitContent = @"
-
-
-
- ";
- var fixture = new JUnitIssuesProviderFixture("invalid.xml");
- fixture.SetFileContent(junitContent);
-
- // When / Then
- Should.Throw(() => fixture.ReadIssues().ToList())
- .Message.ShouldContain("Failed to parse JUnit XML");
- }
-
- [Fact]
- public void Should_Handle_CppLint_Passed_Test()
- {
- // Given
- var fixture = new JUnitIssuesProviderFixture("cpplint-passed.xml");
-
- // When
- var issues = fixture.ReadIssues().ToList();
-
- // Then
- issues.Count.ShouldBe(0);
+ IssueChecker.Check(
+ issues[1],
+ IssueBuilder.NewIssue(
+ "99: FailMsg2 [category/subcategory] [3]",
+ "Cake.Issues.JUnit.JUnitIssuesProvider",
+ "JUnit")
+ .InFile("File2", 99)
+ .OfRule("File2")
+ .WithPriority(IssuePriority.Error)
+ .Create());
}
[Fact]
- public void Should_Handle_CppLint_Single_Error()
+ public void Should_Handle_CppLint_XML_Escaping()
{
// Given
- var fixture = new JUnitIssuesProviderFixture("cpplint-single-error.xml");
+ var fixture = new JUnitIssuesProviderFixture("cpplint-xml-escaping.xml");
// When
var issues = fixture.ReadIssues().ToList();
// Then
- issues.Count.ShouldBe(1);
+ issues.Count.ShouldBe(2);
IssueChecker.Check(
issues[0],
IssueBuilder.NewIssue(
- "ErrMsg1",
+ "&",
"Cake.Issues.JUnit.JUnitIssuesProvider",
"JUnit")
.OfRule("errors")
.WithPriority(IssuePriority.Error)
.Create());
- }
-
- [Fact]
- public void Should_Handle_CppLint_Multiple_Errors()
- {
- // Given
- var fixture = new JUnitIssuesProviderFixture("cpplint-multiple-errors.xml");
-
- // When
- var issues = fixture.ReadIssues().ToList();
-
- // Then
- issues.Count.ShouldBe(1);
IssueChecker.Check(
- issues[0],
+ issues[1],
IssueBuilder.NewIssue(
- "ErrMsg1\nErrMsg2",
+ "5: & [category/subcategory] [3]",
"Cake.Issues.JUnit.JUnitIssuesProvider",
"JUnit")
- .OfRule("errors")
+ .InFile("File1", 5)
+ .OfRule("File1")
.WithPriority(IssuePriority.Error)
.Create());
}
[Fact]
- public void Should_Handle_CppLint_Mixed_Error_And_Failure()
+ public void Should_Read_Issues_Correct_For_Kubeconform()
{
// Given
- var fixture = new JUnitIssuesProviderFixture("cpplint-mixed-error-failure.xml");
+ var fixture = new JUnitIssuesProviderFixture("kubeconform.xml");
// When
var issues = fixture.ReadIssues().ToList();
@@ -268,30 +244,31 @@ public void Should_Handle_CppLint_Mixed_Error_And_Failure()
IssueChecker.Check(
issues[0],
IssueBuilder.NewIssue(
- "ErrMsg",
+ "Invalid resource definition\ndeployment.yaml:10:15: error validating data: ValidationError(Deployment.spec.template.spec.containers[0].image): invalid value: \"\", expected non-empty string",
"Cake.Issues.JUnit.JUnitIssuesProvider",
"JUnit")
- .OfRule("errors")
+ .InFile(@"deployment.yaml", 10, 15)
+ .OfRule("ValidationError")
.WithPriority(IssuePriority.Error)
.Create());
IssueChecker.Check(
issues[1],
IssueBuilder.NewIssue(
- "5: FailMsg [category/subcategory] [3]",
+ "Port configuration invalid\nservice.yaml:8:5: Port 8080 is already in use by another service",
"Cake.Issues.JUnit.JUnitIssuesProvider",
"JUnit")
- .InFile("File", 5)
- .OfRule("File")
+ .InFile(@"service.yaml", 8, 5)
+ .OfRule("ConfigError")
.WithPriority(IssuePriority.Error)
.Create());
}
[Fact]
- public void Should_Handle_CppLint_Multiple_Failures_Grouped_By_File()
+ public void Should_Read_Issues_Correct_For_HtmlHint()
{
// Given
- var fixture = new JUnitIssuesProviderFixture("cpplint-multiple-failures.xml");
+ var fixture = new JUnitIssuesProviderFixture("htmlhint.xml");
// When
var issues = fixture.ReadIssues().ToList();
@@ -302,58 +279,81 @@ public void Should_Handle_CppLint_Multiple_Failures_Grouped_By_File()
IssueChecker.Check(
issues[0],
IssueBuilder.NewIssue(
- "5: FailMsg1 [category/subcategory] [3]\n19: FailMsg3 [category/subcategory] [3]",
+ "Tagname must be lowercase\nindex.html(12,5): Tagname 'DIV' must be lowercase",
"Cake.Issues.JUnit.JUnitIssuesProvider",
"JUnit")
- .InFile("File1", 5)
- .OfRule("File1")
+ .InFile(@"index.html", 12, 5)
+ .OfRule("error")
.WithPriority(IssuePriority.Error)
.Create());
IssueChecker.Check(
issues[1],
IssueBuilder.NewIssue(
- "99: FailMsg2 [category/subcategory] [3]",
+ "Attribute value must be in double quotes\nabout.html line 8: The value of attribute 'class' must be in double quotes.",
"Cake.Issues.JUnit.JUnitIssuesProvider",
"JUnit")
- .InFile("File2", 99)
- .OfRule("File2")
+ .InFile(@"about.html", 8)
+ .OfRule("warning")
.WithPriority(IssuePriority.Error)
.Create());
}
[Fact]
- public void Should_Handle_CppLint_XML_Escaping()
+ public void Should_Read_Issues_Correct_For_CommitLint()
{
// Given
- var fixture = new JUnitIssuesProviderFixture("cpplint-xml-escaping.xml");
+ var fixture = new JUnitIssuesProviderFixture("commitlint.xml");
// When
var issues = fixture.ReadIssues().ToList();
// Then
- issues.Count.ShouldBe(2);
+ issues.Count.ShouldBe(1);
IssueChecker.Check(
issues[0],
IssueBuilder.NewIssue(
- "&",
+ "Type must be one of the allowed values\ncommit-2: type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test]",
"Cake.Issues.JUnit.JUnitIssuesProvider",
"JUnit")
- .OfRule("errors")
+ .OfRule("error")
.WithPriority(IssuePriority.Error)
.Create());
+ }
- IssueChecker.Check(
- issues[1],
- IssueBuilder.NewIssue(
- "5: & [category/subcategory] [3]",
- "Cake.Issues.JUnit.JUnitIssuesProvider",
- "JUnit")
- .InFile("File1", 5)
- .OfRule("File1")
- .WithPriority(IssuePriority.Error)
- .Create());
+ [Fact]
+ public void Should_Handle_Empty_TestSuite()
+ {
+ // Given
+ var junitContent = @"
+
+ ";
+ var fixture = new JUnitIssuesProviderFixture("empty.xml");
+ fixture.SetFileContent(junitContent);
+
+ // When
+ var issues = fixture.ReadIssues().ToList();
+
+ // Then
+ issues.Count.ShouldBe(0);
+ }
+
+ [Fact]
+ public void Should_Handle_Invalid_XML()
+ {
+ // Given
+ var junitContent = @"
+
+
+
+ ";
+ var fixture = new JUnitIssuesProviderFixture("invalid.xml");
+ fixture.SetFileContent(junitContent);
+
+ // When / Then
+ Should.Throw(() => fixture.ReadIssues().ToList())
+ .Message.ShouldContain("Failed to parse JUnit XML");
}
}
}
\ No newline at end of file
From dbdd83205b3ee55943325bc09ee0db7f5e75eba4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 3 Aug 2025 09:28:21 +0000
Subject: [PATCH 20/35] Replace commitlint test XML with official documentation
format and enhance nested testsuite support
Co-authored-by: pascalberger <2190718+pascalberger@users.noreply.github.com>
---
.../JUnitIssuesProviderTests.cs | 2 +-
.../Testfiles/commitlint.xml | 16 ++--
src/Cake.Issues.JUnit/JUnitIssuesProvider.cs | 78 ++++++++++++-------
3 files changed, 59 insertions(+), 37 deletions(-)
diff --git a/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderTests.cs b/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderTests.cs
index d8284a975..7a5523241 100644
--- a/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderTests.cs
+++ b/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderTests.cs
@@ -314,7 +314,7 @@ public void Should_Read_Issues_Correct_For_CommitLint()
IssueChecker.Check(
issues[0],
IssueBuilder.NewIssue(
- "Type must be one of the allowed values\ncommit-2: type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test]",
+ "type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test] (type-enum)\n\nfoo: bar",
"Cake.Issues.JUnit.JUnitIssuesProvider",
"JUnit")
.OfRule("error")
diff --git a/src/Cake.Issues.JUnit.Tests/Testfiles/commitlint.xml b/src/Cake.Issues.JUnit.Tests/Testfiles/commitlint.xml
index 7fd192b15..781d11850 100644
--- a/src/Cake.Issues.JUnit.Tests/Testfiles/commitlint.xml
+++ b/src/Cake.Issues.JUnit.Tests/Testfiles/commitlint.xml
@@ -1,10 +1,10 @@
-
-
-
-
-commit-2: type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test]
-
-
-
+
+
+
+ type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test] (type-enum)
+
+foo: bar
+
+
\ No newline at end of file
diff --git a/src/Cake.Issues.JUnit/JUnitIssuesProvider.cs b/src/Cake.Issues.JUnit/JUnitIssuesProvider.cs
index b971ee72b..02f99d955 100644
--- a/src/Cake.Issues.JUnit/JUnitIssuesProvider.cs
+++ b/src/Cake.Issues.JUnit/JUnitIssuesProvider.cs
@@ -41,34 +41,7 @@ protected override IEnumerable InternalReadIssues()
continue;
}
- var suiteName = testSuite.Attribute("name")?.Value ?? string.Empty;
-
- foreach (var testCase in testSuite.Elements("testcase"))
- {
- var className = testCase.Attribute("classname")?.Value ?? string.Empty;
- var testName = testCase.Attribute("name")?.Value ?? string.Empty;
- var time = testCase.Attribute("time")?.Value;
-
- // Process failures
- foreach (var failure in testCase.Elements("failure"))
- {
- var issue = this.ProcessTestFailure(failure, className, testName, IssuePriority.Error);
- if (issue != null)
- {
- result.Add(issue);
- }
- }
-
- // Process errors
- foreach (var error in testCase.Elements("error"))
- {
- var issue = this.ProcessTestFailure(error, className, testName, IssuePriority.Error);
- if (issue != null)
- {
- result.Add(issue);
- }
- }
- }
+ this.ProcessTestSuite(testSuite, result);
}
}
catch (Exception ex)
@@ -79,6 +52,55 @@ protected override IEnumerable InternalReadIssues()
return result;
}
+ ///
+ /// Recursively processes a testsuite element and its nested testsuites and testcases.
+ ///
+ /// The testsuite element to process.
+ /// The list to add found issues to.
+ private void ProcessTestSuite(XElement testSuite, List result)
+ {
+ if (testSuite == null)
+ {
+ return;
+ }
+
+ var suiteName = testSuite.Attribute("name")?.Value ?? string.Empty;
+
+ // Process direct testcase children
+ foreach (var testCase in testSuite.Elements("testcase"))
+ {
+ var className = testCase.Attribute("classname")?.Value ?? string.Empty;
+ var testName = testCase.Attribute("name")?.Value ?? string.Empty;
+ var time = testCase.Attribute("time")?.Value;
+
+ // Process failures
+ foreach (var failure in testCase.Elements("failure"))
+ {
+ var issue = this.ProcessTestFailure(failure, className, testName, IssuePriority.Error);
+ if (issue != null)
+ {
+ result.Add(issue);
+ }
+ }
+
+ // Process errors
+ foreach (var error in testCase.Elements("error"))
+ {
+ var issue = this.ProcessTestFailure(error, className, testName, IssuePriority.Error);
+ if (issue != null)
+ {
+ result.Add(issue);
+ }
+ }
+ }
+
+ // Recursively process nested testsuite elements
+ foreach (var nestedTestSuite in testSuite.Elements("testsuite"))
+ {
+ this.ProcessTestSuite(nestedTestSuite, result);
+ }
+ }
+
///
/// Tries to extract file path and line information from output text.
///
From 40511a6115a26e1e1cb962b7030f1952219aa37a Mon Sep 17 00:00:00 2001
From: Pascal Berger
Date: Sun, 3 Aug 2025 12:33:21 +0200
Subject: [PATCH 21/35] Test cleanup
---
.../JUnitIssuesProviderFixture.cs | 12 ------------
.../JUnitIssuesProviderTests.cs | 16 +++-------------
src/Cake.Issues.JUnit.Tests/Testfiles/empty.xml | 3 ++-
3 files changed, 5 insertions(+), 26 deletions(-)
diff --git a/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderFixture.cs b/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderFixture.cs
index 981a47070..c26a916f2 100644
--- a/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderFixture.cs
+++ b/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderFixture.cs
@@ -1,7 +1,5 @@
namespace Cake.Issues.JUnit.Tests;
-using System.Text;
-
internal class JUnitIssuesProviderFixture
: BaseConfigurableIssueProviderFixture
{
@@ -18,14 +16,4 @@ public JUnitIssuesProviderFixture(string fileResourceName, string repositoryRoot
}
protected override string FileResourceNamespace => "Cake.Issues.JUnit.Tests.Testfiles.";
-
- ///
- /// Sets the content of the log file.
- ///
- /// Content to set.
- public void SetFileContent(string content)
- {
- content.NotNullOrWhiteSpace();
- this.LogFileContent = Encoding.UTF8.GetBytes(content);
- }
}
\ No newline at end of file
diff --git a/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderTests.cs b/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderTests.cs
index 7a5523241..9443f613e 100644
--- a/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderTests.cs
+++ b/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderTests.cs
@@ -309,10 +309,10 @@ public void Should_Read_Issues_Correct_For_CommitLint()
var issues = fixture.ReadIssues().ToList();
// Then
- issues.Count.ShouldBe(1);
+ var issue = issues.ShouldHaveSingleItem();
IssueChecker.Check(
- issues[0],
+ issue,
IssueBuilder.NewIssue(
"type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test] (type-enum)\n\nfoo: bar",
"Cake.Issues.JUnit.JUnitIssuesProvider",
@@ -326,30 +326,20 @@ public void Should_Read_Issues_Correct_For_CommitLint()
public void Should_Handle_Empty_TestSuite()
{
// Given
- var junitContent = @"
-
- ";
var fixture = new JUnitIssuesProviderFixture("empty.xml");
- fixture.SetFileContent(junitContent);
// When
var issues = fixture.ReadIssues().ToList();
// Then
- issues.Count.ShouldBe(0);
+ issues.ShouldBeEmpty();
}
[Fact]
public void Should_Handle_Invalid_XML()
{
// Given
- var junitContent = @"
-
-
-
- ";
var fixture = new JUnitIssuesProviderFixture("invalid.xml");
- fixture.SetFileContent(junitContent);
// When / Then
Should.Throw(() => fixture.ReadIssues().ToList())
diff --git a/src/Cake.Issues.JUnit.Tests/Testfiles/empty.xml b/src/Cake.Issues.JUnit.Tests/Testfiles/empty.xml
index 4c4449fd3..e96bc4a25 100644
--- a/src/Cake.Issues.JUnit.Tests/Testfiles/empty.xml
+++ b/src/Cake.Issues.JUnit.Tests/Testfiles/empty.xml
@@ -1,2 +1,3 @@
-
\ No newline at end of file
+
+
\ No newline at end of file
From 949019642de93f0064d6e71981815b8d18b54393 Mon Sep 17 00:00:00 2001
From: Pascal Berger
Date: Sun, 3 Aug 2025 12:41:49 +0200
Subject: [PATCH 22/35] Update list of supported tools
---
.../input/documentation/assets/tables/supported-tools-cpp.csv | 2 ++
docs/input/documentation/supported-tools.md | 4 ++++
2 files changed, 6 insertions(+)
create mode 100644 docs/input/documentation/assets/tables/supported-tools-cpp.csv
diff --git a/docs/input/documentation/assets/tables/supported-tools-cpp.csv b/docs/input/documentation/assets/tables/supported-tools-cpp.csv
new file mode 100644
index 000000000..fdf09d6e7
--- /dev/null
+++ b/docs/input/documentation/assets/tables/supported-tools-cpp.csv
@@ -0,0 +1,2 @@
+"Tool","Tool Version","Format","Issue Provider","Supported Since"
+"[cpplint](https://github.com/cpplint/cpplint){target='_blank'} :material-alert-decagram:",,"[junit](https://github.com/cpplint/cpplint){target='_blank'}","[Cake.Issues.JUnit](issue-providers/junit/index.md)",5.7.0
diff --git a/docs/input/documentation/supported-tools.md b/docs/input/documentation/supported-tools.md
index b32ea204f..29bacd316 100644
--- a/docs/input/documentation/supported-tools.md
+++ b/docs/input/documentation/supported-tools.md
@@ -37,6 +37,10 @@ This pages lists tools known to be working with Cake Issues (1)
{{ read_csv('assets/tables/supported-tools-copypaste.csv',keep_default_na=False) }}
+## C++
+
+{{ read_csv('assets/tables/supported-tools-cpp.csv',keep_default_na=False) }}
+
## CSS, SCSS, Sass
{{ read_csv('assets/tables/supported-tools-css.csv',keep_default_na=False) }}
From e0a62560b9c8c7d15249024ccc4c321f3cde66f8 Mon Sep 17 00:00:00 2001
From: Pascal Berger
Date: Sun, 3 Aug 2025 12:48:30 +0200
Subject: [PATCH 23/35] Fix linting issues
---
src/Cake.Issues.JUnit/JUnitIssuesProvider.cs | 96 ++++++++++----------
1 file changed, 47 insertions(+), 49 deletions(-)
diff --git a/src/Cake.Issues.JUnit/JUnitIssuesProvider.cs b/src/Cake.Issues.JUnit/JUnitIssuesProvider.cs
index 02f99d955..d8ef5833a 100644
--- a/src/Cake.Issues.JUnit/JUnitIssuesProvider.cs
+++ b/src/Cake.Issues.JUnit/JUnitIssuesProvider.cs
@@ -52,55 +52,6 @@ protected override IEnumerable InternalReadIssues()
return result;
}
- ///
- /// Recursively processes a testsuite element and its nested testsuites and testcases.
- ///
- /// The testsuite element to process.
- /// The list to add found issues to.
- private void ProcessTestSuite(XElement testSuite, List result)
- {
- if (testSuite == null)
- {
- return;
- }
-
- var suiteName = testSuite.Attribute("name")?.Value ?? string.Empty;
-
- // Process direct testcase children
- foreach (var testCase in testSuite.Elements("testcase"))
- {
- var className = testCase.Attribute("classname")?.Value ?? string.Empty;
- var testName = testCase.Attribute("name")?.Value ?? string.Empty;
- var time = testCase.Attribute("time")?.Value;
-
- // Process failures
- foreach (var failure in testCase.Elements("failure"))
- {
- var issue = this.ProcessTestFailure(failure, className, testName, IssuePriority.Error);
- if (issue != null)
- {
- result.Add(issue);
- }
- }
-
- // Process errors
- foreach (var error in testCase.Elements("error"))
- {
- var issue = this.ProcessTestFailure(error, className, testName, IssuePriority.Error);
- if (issue != null)
- {
- result.Add(issue);
- }
- }
- }
-
- // Recursively process nested testsuite elements
- foreach (var nestedTestSuite in testSuite.Elements("testsuite"))
- {
- this.ProcessTestSuite(nestedTestSuite, result);
- }
- }
-
///
/// Tries to extract file path and line information from output text.
///
@@ -195,6 +146,53 @@ private static (string FilePath, int? Line, int? Column)? ExtractFileInfoFromCla
return null;
}
+ ///
+ /// Recursively processes a testsuite element and its nested testsuites and testcases.
+ ///
+ /// The testsuite element to process.
+ /// The list to add found issues to.
+ private void ProcessTestSuite(XElement testSuite, List result)
+ {
+ if (testSuite == null)
+ {
+ return;
+ }
+
+ // Process direct testcase children
+ foreach (var testCase in testSuite.Elements("testcase"))
+ {
+ var className = testCase.Attribute("classname")?.Value ?? string.Empty;
+ var testName = testCase.Attribute("name")?.Value ?? string.Empty;
+ var time = testCase.Attribute("time")?.Value;
+
+ // Process failures
+ foreach (var failure in testCase.Elements("failure"))
+ {
+ var issue = this.ProcessTestFailure(failure, className, testName, IssuePriority.Error);
+ if (issue != null)
+ {
+ result.Add(issue);
+ }
+ }
+
+ // Process errors
+ foreach (var error in testCase.Elements("error"))
+ {
+ var issue = this.ProcessTestFailure(error, className, testName, IssuePriority.Error);
+ if (issue != null)
+ {
+ result.Add(issue);
+ }
+ }
+ }
+
+ // Recursively process nested testsuite elements
+ foreach (var nestedTestSuite in testSuite.Elements("testsuite"))
+ {
+ this.ProcessTestSuite(nestedTestSuite, result);
+ }
+ }
+
///
/// Processes a test failure or error element and creates an issue.
///
From e3f49a07e08efd0f522522da86e50eaf880e4077 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 4 Aug 2025 13:45:03 +0000
Subject: [PATCH 24/35] Add markdownlint-cli2 JUnit support and update
supported tools list
Co-authored-by: pascalberger <2190718+pascalberger@users.noreply.github.com>
---
.../tables/supported-tools-markdown.csv | 3 +-
.../JUnitIssuesProviderTests.cs | 68 +++++++++++++++++
.../Testfiles/markdownlint-cli2.xml | 28 +++++++
src/Cake.Issues.JUnit/JUnitIssuesProvider.cs | 73 ++++++++++++++++++-
4 files changed, 168 insertions(+), 4 deletions(-)
create mode 100644 src/Cake.Issues.JUnit.Tests/Testfiles/markdownlint-cli2.xml
diff --git a/docs/input/documentation/assets/tables/supported-tools-markdown.csv b/docs/input/documentation/assets/tables/supported-tools-markdown.csv
index 9f64c7f04..e72b5a27c 100644
--- a/docs/input/documentation/assets/tables/supported-tools-markdown.csv
+++ b/docs/input/documentation/assets/tables/supported-tools-markdown.csv
@@ -5,4 +5,5 @@
"[markdownlint-cli](https://github.com/igorshubovych/markdownlint-cli){target='_blank'}",>= 0.9.0,"Default","[Cake.Issues.Markdownlint](issue-providers/markdownlint/index.md)",0.3.0
"[markdownlint-cli](https://github.com/igorshubovych/markdownlint-cli){target='_blank'}",>= 0.19.0,"Default","[Cake.Issues.Markdownlint](issue-providers/markdownlint/index.md)",0.8.1
"[markdownlint-cli](https://github.com/igorshubovych/markdownlint-cli){target='_blank'}",>= 0.22.0,"Default","[Cake.Issues.Markdownlint](issue-providers/markdownlint/index.md)",0.8.2
-"[markdownlint-cli](https://github.com/igorshubovych/markdownlint-cli){target='_blank'}",,"[json](https://github.com/igorshubovych/markdownlint-cli?tab=readme-ov-file#usage){target='_blank'}","[Cake.Issues.Markdownlint](issue-providers/markdownlint/index.md)",1.1.0
\ No newline at end of file
+"[markdownlint-cli](https://github.com/igorshubovych/markdownlint-cli){target='_blank'}",,"[json](https://github.com/igorshubovych/markdownlint-cli?tab=readme-ov-file#usage){target='_blank'}","[Cake.Issues.Markdownlint](issue-providers/markdownlint/index.md)",1.1.0
+"[markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2){target='_blank'}",,"[JUnit](https://github.com/DavidAnson/markdownlint-cli2#markdownlint-cli2-formatter-junit){target='_blank'}","[Cake.Issues.JUnit](issue-providers/junit/index.md)",5.8.0
\ No newline at end of file
diff --git a/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderTests.cs b/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderTests.cs
index 9443f613e..1488a138d 100644
--- a/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderTests.cs
+++ b/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderTests.cs
@@ -322,6 +322,74 @@ public void Should_Read_Issues_Correct_For_CommitLint()
.Create());
}
+ [Fact]
+ public void Should_Read_Issues_Correct_For_MarkdownlintCli2()
+ {
+ // Given
+ var fixture = new JUnitIssuesProviderFixture("markdownlint-cli2.xml");
+
+ // When
+ var issues = fixture.ReadIssues().ToList();
+
+ // Then
+ issues.Count.ShouldBe(5);
+
+ IssueChecker.Check(
+ issues[0],
+ IssueBuilder.NewIssue(
+ "Trailing spaces\nLine 3, Column 10, Expected: 0 or 2; Actual: 1",
+ "Cake.Issues.JUnit.JUnitIssuesProvider",
+ "JUnit")
+ .InFile(@"viewme.md", 3, 10)
+ .OfRule("MD009/no-trailing-spaces")
+ .WithPriority(IssuePriority.Error)
+ .Create());
+
+ IssueChecker.Check(
+ issues[1],
+ IssueBuilder.NewIssue(
+ "Multiple consecutive blank lines\nLine 5, Expected: 1; Actual: 2",
+ "Cake.Issues.JUnit.JUnitIssuesProvider",
+ "JUnit")
+ .InFile(@"viewme.md", 5)
+ .OfRule("MD012/no-multiple-blanks")
+ .WithPriority(IssuePriority.Error)
+ .Create());
+
+ IssueChecker.Check(
+ issues[2],
+ IssueBuilder.NewIssue(
+ "Multiple top-level headings in the same document\nLine 6, Context: \"# Description\"",
+ "Cake.Issues.JUnit.JUnitIssuesProvider",
+ "JUnit")
+ .InFile(@"viewme.md", 6)
+ .OfRule("MD025/single-title/single-h1")
+ .WithPriority(IssuePriority.Error)
+ .Create());
+
+ IssueChecker.Check(
+ issues[3],
+ IssueBuilder.NewIssue(
+ "Multiple spaces after hash on atx style heading\nLine 12, Column 1, Context: \"## Summary\"",
+ "Cake.Issues.JUnit.JUnitIssuesProvider",
+ "JUnit")
+ .InFile(@"viewme.md", 12, 1)
+ .OfRule("MD019/no-multiple-space-atx")
+ .WithPriority(IssuePriority.Error)
+ .Create());
+
+ IssueChecker.Check(
+ issues[4],
+ IssueBuilder.NewIssue(
+ "Files should end with a single newline character\nLine 14, Column 14",
+ "Cake.Issues.JUnit.JUnitIssuesProvider",
+ "JUnit")
+ .InFile(@"viewme.md", 14, 14)
+ .OfRule("MD047/single-trailing-newline")
+ .WithPriority(IssuePriority.Error)
+ .Create());
+ }
+
[Fact]
public void Should_Handle_Empty_TestSuite()
{
diff --git a/src/Cake.Issues.JUnit.Tests/Testfiles/markdownlint-cli2.xml b/src/Cake.Issues.JUnit.Tests/Testfiles/markdownlint-cli2.xml
new file mode 100644
index 000000000..4559d7e50
--- /dev/null
+++ b/src/Cake.Issues.JUnit.Tests/Testfiles/markdownlint-cli2.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Cake.Issues.JUnit/JUnitIssuesProvider.cs b/src/Cake.Issues.JUnit/JUnitIssuesProvider.cs
index d8ef5833a..2b4dbd27c 100644
--- a/src/Cake.Issues.JUnit/JUnitIssuesProvider.cs
+++ b/src/Cake.Issues.JUnit/JUnitIssuesProvider.cs
@@ -70,13 +70,14 @@ private static (string FilePath, int? Line, int? Column)? ExtractFileInfoFromOut
// file.txt line 123: message
// /path/to/file.txt:123: message
// file.txt:123 message
+ // Line 3, Column 10 (markdownlint-cli2 format)
var patterns = new[]
{
@"([^\s:]+):(\d+):(\d+)", // file:line:column
@"([^\s:]+):(\d+)", // file:line
@"([^\s\(\)]+)\((\d+),(\d+)\)", // file(line,column)
@"([^\s\(\)]+)\((\d+)\)", // file(line)
- @"([^\s]+)\s+line\s+(\d+)", // file line 123
+ @"^([^\s]+)\s+line\s+(\d+)", // file line 123 (must start at beginning of line)
@"File:\s*([^\s]+)", // File: path
};
@@ -113,6 +114,53 @@ private static (string FilePath, int? Line, int? Column)? ExtractFileInfoFromOut
return null;
}
+ ///
+ /// Tries to extract line and column information from markdownlint-cli2 format text.
+ ///
+ /// The output text to parse.
+ /// Line and column information if found, null otherwise.
+ private static (int? Line, int? Column)? ExtractLineColumnFromMarkdownlintCli2(string output)
+ {
+ if (string.IsNullOrEmpty(output))
+ {
+ return null;
+ }
+
+ // Patterns for markdownlint-cli2 format:
+ // Line 3, Column 10, Expected: 0 or 2; Actual: 1
+ // Line 5, Expected: 1; Actual: 2
+ // Line 6, Context: "# Description"
+ var patterns = new[]
+ {
+ @"Line\s+(\d+),\s+Column\s+(\d+)", // Line 3, Column 10
+ @"Line\s+(\d+)", // Line 5
+ };
+
+ foreach (var pattern in patterns)
+ {
+ var match = Regex.Match(output, pattern, RegexOptions.IgnoreCase);
+ if (match.Success)
+ {
+ int? line = null;
+ int? column = null;
+
+ if (int.TryParse(match.Groups[1].Value, out var lineNum))
+ {
+ line = lineNum;
+ }
+
+ if (match.Groups.Count > 2 && int.TryParse(match.Groups[2].Value, out var colNum))
+ {
+ column = colNum;
+ }
+
+ return (line, column);
+ }
+ }
+
+ return null;
+ }
+
///
/// Tries to extract file path from a class name.
///
@@ -230,8 +278,27 @@ private IIssue ProcessTestFailure(XElement failureElement, string className, str
issueBuilder = issueBuilder.OfRule(testName);
}
- // Try to extract file information from the message or content first
- var fileInfo = ExtractFileInfoFromOutput(fullMessage) ?? ExtractFileInfoFromClassName(className);
+ // Initialize fileInfo variable
+ (string FilePath, int? Line, int? Column)? fileInfo = null;
+
+ // For markdownlint-cli2 style output, check if the content contains the specific format
+ // and use the class name as file path in that case
+ if (!string.IsNullOrEmpty(className) && !string.IsNullOrEmpty(content))
+ {
+ var lineColumnInfo = ExtractLineColumnFromMarkdownlintCli2(content);
+ if (lineColumnInfo.HasValue)
+ {
+ // This looks like markdownlint-cli2 format, use class name as file path
+ var (line, column) = lineColumnInfo.Value;
+ fileInfo = (className, line, column);
+ }
+ }
+
+ // If we don't have file info yet, try to extract from the message or content
+ if (!fileInfo.HasValue)
+ {
+ fileInfo = ExtractFileInfoFromOutput(fullMessage) ?? ExtractFileInfoFromClassName(className);
+ }
// For cpplint-style output, if we don't have file info and the test name looks like a file name,
// use the test name as the file name and try to extract line info from the message
From 90176c52aafdbee133f98684dd6ef0302e7bdea9 Mon Sep 17 00:00:00 2001
From: Pascal Berger
Date: Mon, 4 Aug 2025 21:04:44 +0200
Subject: [PATCH 25/35] Add more tools to list
---
.../documentation/assets/tables/supported-tools-cpp.csv | 2 +-
.../documentation/assets/tables/supported-tools-git.csv | 2 ++
.../documentation/assets/tables/supported-tools-html.csv | 2 ++
.../assets/tables/supported-tools-kubernetes.csv | 1 +
.../assets/tables/supported-tools-markdown.csv | 2 +-
docs/input/documentation/supported-tools.md | 8 ++++++++
6 files changed, 15 insertions(+), 2 deletions(-)
create mode 100644 docs/input/documentation/assets/tables/supported-tools-git.csv
create mode 100644 docs/input/documentation/assets/tables/supported-tools-html.csv
diff --git a/docs/input/documentation/assets/tables/supported-tools-cpp.csv b/docs/input/documentation/assets/tables/supported-tools-cpp.csv
index fdf09d6e7..3c033fb7b 100644
--- a/docs/input/documentation/assets/tables/supported-tools-cpp.csv
+++ b/docs/input/documentation/assets/tables/supported-tools-cpp.csv
@@ -1,2 +1,2 @@
"Tool","Tool Version","Format","Issue Provider","Supported Since"
-"[cpplint](https://github.com/cpplint/cpplint){target='_blank'} :material-alert-decagram:",,"[junit](https://github.com/cpplint/cpplint){target='_blank'}","[Cake.Issues.JUnit](issue-providers/junit/index.md)",5.7.0
+"[cpplint](https://github.com/cpplint/cpplint){target='_blank'} :material-alert-decagram:",,"[junit](https://github.com/cpplint/cpplint){target='_blank'}","[Cake.Issues.JUnit](issue-providers/junit/index.md)",5.8.0
diff --git a/docs/input/documentation/assets/tables/supported-tools-git.csv b/docs/input/documentation/assets/tables/supported-tools-git.csv
new file mode 100644
index 000000000..93e396cac
--- /dev/null
+++ b/docs/input/documentation/assets/tables/supported-tools-git.csv
@@ -0,0 +1,2 @@
+"Tool","Tool Version","Format","Issue Provider","Supported Since"
+"[commitlint](https://commitlint.js.org/){target='_blank'} :material-alert-decagram:",,"[junit](https://www.npmjs.com/package/commitlint-format-junit){target='_blank'}","[Cake.Issues.JUnit](issue-providers/junit/index.md)",5.8.0
\ No newline at end of file
diff --git a/docs/input/documentation/assets/tables/supported-tools-html.csv b/docs/input/documentation/assets/tables/supported-tools-html.csv
new file mode 100644
index 000000000..50e555f3e
--- /dev/null
+++ b/docs/input/documentation/assets/tables/supported-tools-html.csv
@@ -0,0 +1,2 @@
+"Tool","Tool Version","Format","Issue Provider","Supported Since"
+"[HtmlHint](https://htmlhint.com/){target='_blank'} :material-alert-decagram:",,"[junit](https://htmlhint.com/usage/options/#format){target='_blank'}","[Cake.Issues.JUnit](issue-providers/junit/index.md)",5.8.0
\ No newline at end of file
diff --git a/docs/input/documentation/assets/tables/supported-tools-kubernetes.csv b/docs/input/documentation/assets/tables/supported-tools-kubernetes.csv
index bcbfbbaa3..d6f24e4cd 100644
--- a/docs/input/documentation/assets/tables/supported-tools-kubernetes.csv
+++ b/docs/input/documentation/assets/tables/supported-tools-kubernetes.csv
@@ -2,4 +2,5 @@
"[checkov](https://www.checkov.io/){target='_blank'}",,"[SARIF](https://www.checkov.io/2.Basics/CLI%20Command%20Reference.html){target='_blank'}","[Cake.Issues.Sarif](issue-providers/sarif/index.md)",4.2.0
"[kics](https://kics.io/){target='_blank'}",,"[sarif](https://github.com/Checkmarx/kics/blob/master/docs/results.md#sarif){target='_blank'}","[Cake.Issues.Sarif](issue-providers/sarif/index.md)",4.2.0
"[Kubeconform](https://github.com/yannh/kubeconform){target='_blank'} :material-alert-decagram:",,"[tap](https://github.com/yannh/kubeconform?tab=readme-ov-file#usage){target='_blank'}","[Cake.Issues.Tap](issue-providers/tap/index.md)",4.2.0
+"[Kubeconform](https://github.com/yannh/kubeconform){target='_blank'} :material-alert-decagram:",,"[junit](https://github.com/yannh/kubeconform?tab=readme-ov-file#usage){target='_blank'}","[Cake.Issues.JUnit](issue-providers/junit/index.md)",5.8.0
"[Trivy](https://trivy.dev/){target='_blank'}",,"[SARIF](https://trivy.dev/v0.58/docs/configuration/reporting/#sarif){target='_blank'}","[Cake.Issues.Sarif](issue-providers/sarif/index.md)",4.2.0
\ No newline at end of file
diff --git a/docs/input/documentation/assets/tables/supported-tools-markdown.csv b/docs/input/documentation/assets/tables/supported-tools-markdown.csv
index e72b5a27c..ac03f4434 100644
--- a/docs/input/documentation/assets/tables/supported-tools-markdown.csv
+++ b/docs/input/documentation/assets/tables/supported-tools-markdown.csv
@@ -6,4 +6,4 @@
"[markdownlint-cli](https://github.com/igorshubovych/markdownlint-cli){target='_blank'}",>= 0.19.0,"Default","[Cake.Issues.Markdownlint](issue-providers/markdownlint/index.md)",0.8.1
"[markdownlint-cli](https://github.com/igorshubovych/markdownlint-cli){target='_blank'}",>= 0.22.0,"Default","[Cake.Issues.Markdownlint](issue-providers/markdownlint/index.md)",0.8.2
"[markdownlint-cli](https://github.com/igorshubovych/markdownlint-cli){target='_blank'}",,"[json](https://github.com/igorshubovych/markdownlint-cli?tab=readme-ov-file#usage){target='_blank'}","[Cake.Issues.Markdownlint](issue-providers/markdownlint/index.md)",1.1.0
-"[markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2){target='_blank'}",,"[JUnit](https://github.com/DavidAnson/markdownlint-cli2#markdownlint-cli2-formatter-junit){target='_blank'}","[Cake.Issues.JUnit](issue-providers/junit/index.md)",5.8.0
\ No newline at end of file
+"[markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2){target='_blank'} :material-alert-decagram:",,"[JUnit](https://github.com/DavidAnson/markdownlint-cli2#markdownlint-cli2-formatter-junit){target='_blank'}","[Cake.Issues.JUnit](issue-providers/junit/index.md)",5.8.0
\ No newline at end of file
diff --git a/docs/input/documentation/supported-tools.md b/docs/input/documentation/supported-tools.md
index 29bacd316..6f3b1950f 100644
--- a/docs/input/documentation/supported-tools.md
+++ b/docs/input/documentation/supported-tools.md
@@ -49,6 +49,10 @@ This pages lists tools known to be working with Cake Issues (1)
{{ read_csv('assets/tables/supported-tools-docker.csv',keep_default_na=False) }}
+## Git
+
+{{ read_csv('assets/tables/supported-tools-git.csv',keep_default_na=False) }}
+
## GitHub Actions
{{ read_csv('assets/tables/supported-tools-github-actions.csv',keep_default_na=False) }}
@@ -65,6 +69,10 @@ This pages lists tools known to be working with Cake Issues (1)
{{ read_csv('assets/tables/supported-tools-helm.csv',keep_default_na=False) }}
+## HTML
+
+{{ read_csv('assets/tables/supported-tools-html.csv',keep_default_na=False) }}
+
## Java
{{ read_csv('assets/tables/supported-tools-java.csv',keep_default_na=False) }}
From 13a8f706e0f9bd53164e2a20217bcd2f652e04f1 Mon Sep 17 00:00:00 2001
From: Pascal Berger
Date: Mon, 4 Aug 2025 21:10:58 +0200
Subject: [PATCH 26/35] Update commitlint testfile to match example from
website
---
.../Testfiles/commitlint.xml | 22 +++++++++++--------
1 file changed, 13 insertions(+), 9 deletions(-)
diff --git a/src/Cake.Issues.JUnit.Tests/Testfiles/commitlint.xml b/src/Cake.Issues.JUnit.Tests/Testfiles/commitlint.xml
index 781d11850..eace4439c 100644
--- a/src/Cake.Issues.JUnit.Tests/Testfiles/commitlint.xml
+++ b/src/Cake.Issues.JUnit.Tests/Testfiles/commitlint.xml
@@ -1,10 +1,14 @@
-
-
-
-
- type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test] (type-enum)
+
+
+
+
+
+
+ type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test] (type-enum)
-foo: bar
-
-
-
\ No newline at end of file
+ foo: bar
+
+
+
+
+
\ No newline at end of file
From 232aca2b999745a2eaf8d5b2653d03e8cf5c1388 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 4 Aug 2025 19:24:31 +0000
Subject: [PATCH 27/35] Fix XML whitespace handling in JUnit parser for
commitlint test with tabs
Co-authored-by: pascalberger <2190718+pascalberger@users.noreply.github.com>
---
src/Cake.Issues.JUnit/JUnitIssuesProvider.cs | 37 +++++++++++++++++++-
1 file changed, 36 insertions(+), 1 deletion(-)
diff --git a/src/Cake.Issues.JUnit/JUnitIssuesProvider.cs b/src/Cake.Issues.JUnit/JUnitIssuesProvider.cs
index 2b4dbd27c..02d8a33d6 100644
--- a/src/Cake.Issues.JUnit/JUnitIssuesProvider.cs
+++ b/src/Cake.Issues.JUnit/JUnitIssuesProvider.cs
@@ -241,6 +241,41 @@ private void ProcessTestSuite(XElement testSuite, List result)
}
}
+ ///
+ /// Normalizes XML content by removing XML formatting indentation while preserving intentional structure.
+ ///
+ /// The XML content to normalize.
+ /// The normalized content.
+ private string NormalizeXmlContent(string content)
+ {
+ if (string.IsNullOrEmpty(content))
+ {
+ return string.Empty;
+ }
+
+ // Split by lines, trim each line to remove XML indentation, then rejoin
+ var lines = content.Split(new[] { '\r', '\n' }, StringSplitOptions.None);
+ var normalizedLines = new List();
+
+ foreach (var line in lines)
+ {
+ // Trim leading and trailing whitespace (including tabs) from each line
+ var trimmedLine = line.Trim();
+ normalizedLines.Add(trimmedLine);
+ }
+
+ // Join lines back together and clean up multiple consecutive empty lines
+ var result = string.Join("\n", normalizedLines);
+
+ // Remove leading and trailing empty lines
+ result = result.Trim('\n');
+
+ // Normalize multiple consecutive newlines to double newlines maximum
+ result = Regex.Replace(result, @"\n{3,}", "\n\n");
+
+ return result;
+ }
+
///
/// Processes a test failure or error element and creates an issue.
///
@@ -253,7 +288,7 @@ private IIssue ProcessTestFailure(XElement failureElement, string className, str
{
var message = failureElement.Attribute("message")?.Value ?? string.Empty;
var type = failureElement.Attribute("type")?.Value ?? string.Empty;
- var content = failureElement.Value?.Trim() ?? string.Empty;
+ var content = this.NormalizeXmlContent(failureElement.Value) ?? string.Empty;
// Combine message and content for full description
var fullMessage = string.IsNullOrEmpty(message) ? content :
From 6d0dc5db6ca15ccfb5543ac5e41e42bf100f96a4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 4 Aug 2025 19:55:08 +0000
Subject: [PATCH 28/35] Refactor JUnit provider to BaseMultiFormatIssueProvider
pattern with optimized formats
Co-authored-by: pascalberger <2190718+pascalberger@users.noreply.github.com>
---
.../JUnitIssuesProviderFixture.cs | 7 +-
.../JUnitIssuesProviderTests.cs | 48 +--
.../JUnitIssuesSettingsTests.cs | 7 +-
.../Testfiles/htmlhint.xml | 26 +-
.../BaseJUnitLogFileFormat.cs | 38 ++
.../JUnitIssuesAliases.IssueProvider.cs | 131 ++++++
.../JUnitIssuesAliases.LogFileFormats.cs | 59 +++
src/Cake.Issues.JUnit/JUnitIssuesAliases.cs | 109 +----
src/Cake.Issues.JUnit/JUnitIssuesProvider.cs | 373 +-----------------
src/Cake.Issues.JUnit/JUnitIssuesSettings.cs | 18 +-
.../LogFileFormat/CppLintLogFileFormat.cs | 222 +++++++++++
.../GenericJUnitLogFileFormat.cs | 288 ++++++++++++++
.../MarkdownlintCli2LogFileFormat.cs | 246 ++++++++++++
13 files changed, 1046 insertions(+), 526 deletions(-)
create mode 100644 src/Cake.Issues.JUnit/BaseJUnitLogFileFormat.cs
create mode 100644 src/Cake.Issues.JUnit/JUnitIssuesAliases.IssueProvider.cs
create mode 100644 src/Cake.Issues.JUnit/JUnitIssuesAliases.LogFileFormats.cs
create mode 100644 src/Cake.Issues.JUnit/LogFileFormat/CppLintLogFileFormat.cs
create mode 100644 src/Cake.Issues.JUnit/LogFileFormat/GenericJUnitLogFileFormat.cs
create mode 100644 src/Cake.Issues.JUnit/LogFileFormat/MarkdownlintCli2LogFileFormat.cs
diff --git a/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderFixture.cs b/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderFixture.cs
index c26a916f2..d8578d523 100644
--- a/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderFixture.cs
+++ b/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderFixture.cs
@@ -1,7 +1,10 @@
namespace Cake.Issues.JUnit.Tests;
-internal class JUnitIssuesProviderFixture
- : BaseConfigurableIssueProviderFixture
+using Cake.Issues.Testing;
+
+internal class JUnitIssuesProviderFixture
+ : BaseMultiFormatIssueProviderFixture
+ where T : BaseJUnitLogFileFormat
{
public JUnitIssuesProviderFixture(string fileResourceName)
: this(fileResourceName, @"c:\Source\Cake.Issues")
diff --git a/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderTests.cs b/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderTests.cs
index 1488a138d..80ebe1eb1 100644
--- a/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderTests.cs
+++ b/src/Cake.Issues.JUnit.Tests/JUnitIssuesProviderTests.cs
@@ -1,5 +1,7 @@
namespace Cake.Issues.JUnit.Tests;
+using Cake.Issues.JUnit.LogFileFormat;
+
public sealed class JUnitIssuesProviderTests
{
public sealed class TheCtor
@@ -11,7 +13,7 @@ public void Should_Throw_If_Log_Is_Null()
var result = Record.Exception(() =>
new JUnitIssuesProvider(
null,
- new JUnitIssuesSettings("Foo".ToByteArray())));
+ new JUnitIssuesSettings("Foo".ToByteArray(), new GenericJUnitLogFileFormat(new FakeLog()))));
// Then
result.IsArgumentNullException("log");
@@ -34,7 +36,7 @@ public sealed class TheReadIssuesMethod
public void Should_Read_Issues_Correct_For_CppLint()
{
// Given
- var fixture = new JUnitIssuesProviderFixture("cpplint.xml");
+ var fixture = new JUnitIssuesProviderFixture("cpplint.xml");
// When
var issues = fixture.ReadIssues().ToList();
@@ -71,7 +73,7 @@ public void Should_Read_Issues_Correct_For_CppLint()
public void Should_Handle_CppLint_Passed_Test()
{
// Given
- var fixture = new JUnitIssuesProviderFixture("cpplint-passed.xml");
+ var fixture = new JUnitIssuesProviderFixture("cpplint-passed.xml");
// When
var issues = fixture.ReadIssues().ToList();
@@ -84,7 +86,7 @@ public void Should_Handle_CppLint_Passed_Test()
public void Should_Handle_CppLint_Single_Error()
{
// Given
- var fixture = new JUnitIssuesProviderFixture("cpplint-single-error.xml");
+ var fixture = new JUnitIssuesProviderFixture("cpplint-single-error.xml");
// When
var issues = fixture.ReadIssues().ToList();
@@ -107,7 +109,7 @@ public void Should_Handle_CppLint_Single_Error()
public void Should_Handle_CppLint_Multiple_Errors()
{
// Given
- var fixture = new JUnitIssuesProviderFixture("cpplint-multiple-errors.xml");
+ var fixture = new JUnitIssuesProviderFixture("cpplint-multiple-errors.xml");
// When
var issues = fixture.ReadIssues().ToList();
@@ -130,7 +132,7 @@ public void Should_Handle_CppLint_Multiple_Errors()
public void Should_Handle_CppLint_Mixed_Error_And_Failure()
{
// Given
- var fixture = new JUnitIssuesProviderFixture("cpplint-mixed-error-failure.xml");
+ var fixture = new JUnitIssuesProviderFixture("cpplint-mixed-error-failure.xml");
// When
var issues = fixture.ReadIssues().ToList();
@@ -164,7 +166,7 @@ public void Should_Handle_CppLint_Mixed_Error_And_Failure()
public void Should_Handle_CppLint_Multiple_Failures_Grouped_By_File()
{
// Given
- var fixture = new JUnitIssuesProviderFixture("cpplint-multiple-failures.xml");
+ var fixture = new JUnitIssuesProviderFixture("cpplint-multiple-failures.xml");
// When
var issues = fixture.ReadIssues().ToList();
@@ -199,7 +201,7 @@ public void Should_Handle_CppLint_Multiple_Failures_Grouped_By_File()
public void Should_Handle_CppLint_XML_Escaping()
{
// Given
- var fixture = new JUnitIssuesProviderFixture("cpplint-xml-escaping.xml");
+ var fixture = new JUnitIssuesProviderFixture("cpplint-xml-escaping.xml");
// When
var issues = fixture.ReadIssues().ToList();
@@ -233,7 +235,7 @@ public void Should_Handle_CppLint_XML_Escaping()
public void Should_Read_Issues_Correct_For_Kubeconform()
{
// Given
- var fixture = new JUnitIssuesProviderFixture("kubeconform.xml");
+ var fixture = new JUnitIssuesProviderFixture("kubeconform.xml");
// When
var issues = fixture.ReadIssues().ToList();
@@ -268,33 +270,21 @@ public void Should_Read_Issues_Correct_For_Kubeconform()
public void Should_Read_Issues_Correct_For_HtmlHint()
{
// Given
- var fixture = new JUnitIssuesProviderFixture("htmlhint.xml");
+ var fixture = new JUnitIssuesProviderFixture("htmlhint.xml");
// When
var issues = fixture.ReadIssues().ToList();
// Then
- issues.Count.ShouldBe(2);
+ issues.Count.ShouldBe(1);
IssueChecker.Check(
issues[0],
IssueBuilder.NewIssue(
- "Tagname must be lowercase\nindex.html(12,5): Tagname 'DIV' must be lowercase",
- "Cake.Issues.JUnit.JUnitIssuesProvider",
- "JUnit")
- .InFile(@"index.html", 12, 5)
- .OfRule("error")
- .WithPriority(IssuePriority.Error)
- .Create());
-
- IssueChecker.Check(
- issues[1],
- IssueBuilder.NewIssue(
- "Attribute value must be in double quotes\nabout.html line 8: The value of attribute 'class' must be in double quotes.",
+ "Found 6 errors\nL2 |\n^ An lang attribute must be present on elements. (html-lang-require)\nL16 |
\n^ An alt attribute must be present on
elements. (alt-require)\nL14 |\n^ must be present in tag. (main-require)\nL3 |\n^ must be present in tag. (meta-charset-require)\nL3 |\n^ must be present in tag. (meta-description-require)\nL3 |\n^ must be present in tag. (meta-viewport-require)",
"Cake.Issues.JUnit.JUnitIssuesProvider",
"JUnit")
- .InFile(@"about.html", 8)
- .OfRule("warning")
+ .OfRule(@"C:\temp\htmlhint\test.html")
.WithPriority(IssuePriority.Error)
.Create());
}
@@ -303,7 +293,7 @@ public void Should_Read_Issues_Correct_For_HtmlHint()
public void Should_Read_Issues_Correct_For_CommitLint()
{
// Given
- var fixture = new JUnitIssuesProviderFixture("commitlint.xml");
+ var fixture = new JUnitIssuesProviderFixture("commitlint.xml");
// When
var issues = fixture.ReadIssues().ToList();
@@ -326,7 +316,7 @@ public void Should_Read_Issues_Correct_For_CommitLint()
public void Should_Read_Issues_Correct_For_MarkdownlintCli2()
{
// Given
- var fixture = new JUnitIssuesProviderFixture("markdownlint-cli2.xml");
+ var fixture = new JUnitIssuesProviderFixture("markdownlint-cli2.xml");
// When
var issues = fixture.ReadIssues().ToList();
@@ -394,7 +384,7 @@ public void Should_Read_Issues_Correct_For_MarkdownlintCli2()
public void Should_Handle_Empty_TestSuite()
{
// Given
- var fixture = new JUnitIssuesProviderFixture("empty.xml");
+ var fixture = new JUnitIssuesProviderFixture("empty.xml");
// When
var issues = fixture.ReadIssues().ToList();
@@ -407,7 +397,7 @@ public void Should_Handle_Empty_TestSuite()
public void Should_Handle_Invalid_XML()
{
// Given
- var fixture = new JUnitIssuesProviderFixture("invalid.xml");
+ var fixture = new JUnitIssuesProviderFixture("invalid.xml");
// When / Then
Should.Throw(() => fixture.ReadIssues().ToList())
diff --git a/src/Cake.Issues.JUnit.Tests/JUnitIssuesSettingsTests.cs b/src/Cake.Issues.JUnit.Tests/JUnitIssuesSettingsTests.cs
index 6c63780f1..5fe0e9eb8 100644
--- a/src/Cake.Issues.JUnit.Tests/JUnitIssuesSettingsTests.cs
+++ b/src/Cake.Issues.JUnit.Tests/JUnitIssuesSettingsTests.cs
@@ -1,6 +1,7 @@
namespace Cake.Issues.JUnit.Tests;
using Cake.Core.IO;
+using Cake.Issues.JUnit.LogFileFormat;
public sealed class JUnitIssuesSettingsTests
{
@@ -11,7 +12,7 @@ public void Should_Throw_If_LogFilePath_Is_Null()
{
// Given / When
var result = Record.Exception(() =>
- new JUnitIssuesSettings((FilePath)null));
+ new JUnitIssuesSettings((FilePath)null, new GenericJUnitLogFileFormat(new FakeLog())));
// Then
result.IsArgumentNullException("logFilePath");
@@ -22,7 +23,7 @@ public void Should_Throw_If_LogFileContent_Is_Null()
{
// Given / When
var result = Record.Exception(() =>
- new JUnitIssuesSettings((byte[])null));
+ new JUnitIssuesSettings((byte[])null, new GenericJUnitLogFileFormat(new FakeLog())));
// Then
result.IsArgumentNullException("logFileContent");
@@ -35,7 +36,7 @@ public void Should_Set_LogFileContent()
var logFileContent = "foo".ToByteArray();
// When
- var settings = new JUnitIssuesSettings(logFileContent);
+ var settings = new JUnitIssuesSettings(logFileContent, new GenericJUnitLogFileFormat(new FakeLog()));
// Then
settings.LogFileContent.ShouldBe(logFileContent);
diff --git a/src/Cake.Issues.JUnit.Tests/Testfiles/htmlhint.xml b/src/Cake.Issues.JUnit.Tests/Testfiles/htmlhint.xml
index 96fe4d4c9..c6dca1bc9 100644
--- a/src/Cake.Issues.JUnit.Tests/Testfiles/htmlhint.xml
+++ b/src/Cake.Issues.JUnit.Tests/Testfiles/htmlhint.xml
@@ -1,13 +1,17 @@
-
-
-
-index.html(12,5): Tagname 'DIV' must be lowercase
-
-
-
-
-about.html line 8: The value of attribute 'class' must be in double quotes.
-
-
+
+
+
+ ^ An lang attribute must be present on elements. (html-lang-require)
+L16 |
+ ^ An alt attribute must be present on
elements. (alt-require)
+L14 |
+ ^ must be present in tag. (main-require)
+L3 |
+ ^ must be present in tag. (meta-charset-require)
+L3 |
+ ^ must be present in tag. (meta-description-require)
+L3 |
+ ^ must be present in tag. (meta-viewport-require)]]>
+
\ No newline at end of file
diff --git a/src/Cake.Issues.JUnit/BaseJUnitLogFileFormat.cs b/src/Cake.Issues.JUnit/BaseJUnitLogFileFormat.cs
new file mode 100644
index 000000000..44975f750
--- /dev/null
+++ b/src/Cake.Issues.JUnit/BaseJUnitLogFileFormat.cs
@@ -0,0 +1,38 @@
+namespace Cake.Issues.JUnit;
+
+using Cake.Core.Diagnostics;
+using Cake.Core.IO;
+
+///
+/// Base class for all log file formats supported by the JUnit issue provider.
+///
+/// The Cake log instance.
+public abstract class BaseJUnitLogFileFormat(ICakeLog log)
+ : BaseLogFileFormat(log)
+{
+ ///
+ /// Validates a file path.
+ ///
+ /// Full file path.
+ /// Repository settings.
+ /// Tuple containing a value if validation was successful, and file path relative to repository root.
+ protected static (bool Valid, string FilePath) ValidateFilePath(string filePath, IRepositorySettings repositorySettings)
+ {
+ filePath.NotNullOrWhiteSpace();
+ repositorySettings.NotNull();
+
+ if (!new FilePath(filePath).IsRelative)
+ {
+ // Ignore files from outside the repository.
+ if (!filePath.IsInRepository(repositorySettings))
+ {
+ return (false, string.Empty);
+ }
+
+ // Make path relative to repository root.
+ filePath = filePath.NormalizePath().MakeFilePathRelativeToRepositoryRoot(repositorySettings);
+ }
+
+ return (true, filePath);
+ }
+}
\ No newline at end of file
diff --git a/src/Cake.Issues.JUnit/JUnitIssuesAliases.IssueProvider.cs b/src/Cake.Issues.JUnit/JUnitIssuesAliases.IssueProvider.cs
new file mode 100644
index 000000000..0b5e51bb5
--- /dev/null
+++ b/src/Cake.Issues.JUnit/JUnitIssuesAliases.IssueProvider.cs
@@ -0,0 +1,131 @@
+namespace Cake.Issues.JUnit;
+
+using Cake.Core;
+using Cake.Core.Annotations;
+using Cake.Core.IO;
+
+///
+/// Aliases to read issues from JUnit XML files.
+///
+public static partial class JUnitIssuesAliases
+{
+ ///
+ /// Gets the name of the JUnit issue provider.
+ /// This name can be used to identify issues based on the property.
+ ///
+ /// The context.
+ /// Name of the JUnit issue provider.
+ [CakePropertyAlias]
+ [CakeAliasCategory(IssuesAliasConstants.IssueProviderCakeAliasCategory)]
+ public static string JUnitIssuesProviderTypeName(
+ this ICakeContext context)
+ {
+ context.NotNull();
+
+ return JUnitIssuesProvider.ProviderTypeName;
+ }
+
+ ///
+ /// Gets an instance of a provider for issues in a JUnit XML file from disk.
+ ///
+ /// The context.
+ /// Path to the JUnit XML file.
+ /// The log file needs to be in the format as defined by the parameter.
+ /// Format of the provided JUnit XML file.
+ /// Instance of a provider for issues in JUnit XML format.
+ ///
+ /// Read issues from a JUnit XML file:
+ ///
+ ///
+ ///
+ ///
+ [CakeMethodAlias]
+ [CakeAliasCategory(IssuesAliasConstants.IssueProviderCakeAliasCategory)]
+ public static IIssueProvider JUnitIssuesFromFilePath(
+ this ICakeContext context,
+ FilePath logFilePath,
+ BaseJUnitLogFileFormat format)
+ {
+ context.NotNull();
+ logFilePath.NotNull();
+ format.NotNull();
+
+ return context.JUnitIssues(new JUnitIssuesSettings(logFilePath, format));
+ }
+
+ ///
+ /// Gets an instance of a provider for issues in a JUnit XML file from memory.
+ ///
+ /// The context.
+ /// Content of the JUnit XML file.
+ /// The log file needs to be in the format as defined by the parameter.
+ /// Format of the provided JUnit XML file.
+ /// Instance of a provider for issues in JUnit XML format.
+ ///
+ /// Read issues from a JUnit XML file:
+ ///
+ ///
+ ///
+ ///
+ [CakeMethodAlias]
+ [CakeAliasCategory(IssuesAliasConstants.IssueProviderCakeAliasCategory)]
+ public static IIssueProvider JUnitIssuesFromContent(
+ this ICakeContext context,
+ string logFileContent,
+ BaseJUnitLogFileFormat format)
+ {
+ context.NotNull();
+ logFileContent.NotNullOrWhiteSpace();
+ format.NotNull();
+
+ return context.JUnitIssues(new JUnitIssuesSettings(logFileContent.ToByteArray(), format));
+ }
+
+ ///
+ /// Gets an instance of a provider for issues in a JUnit XML file using specified settings.
+ ///
+ /// The context.
+ /// Settings for reading the JUnit XML file.
+ /// Instance of a provider for issues in JUnit XML format.
+ ///
+ /// Read issues from a JUnit XML file:
+ ///
+ ///
+ ///
+ ///
+ [CakeMethodAlias]
+ [CakeAliasCategory(IssuesAliasConstants.IssueProviderCakeAliasCategory)]
+ public static IIssueProvider JUnitIssues(
+ this ICakeContext context,
+ JUnitIssuesSettings settings)
+ {
+ context.NotNull();
+ settings.NotNull();
+
+ return new JUnitIssuesProvider(context.Log, settings);
+ }
+}
\ No newline at end of file
diff --git a/src/Cake.Issues.JUnit/JUnitIssuesAliases.LogFileFormats.cs b/src/Cake.Issues.JUnit/JUnitIssuesAliases.LogFileFormats.cs
new file mode 100644
index 000000000..1476fc309
--- /dev/null
+++ b/src/Cake.Issues.JUnit/JUnitIssuesAliases.LogFileFormats.cs
@@ -0,0 +1,59 @@
+namespace Cake.Issues.JUnit;
+
+using Cake.Core;
+using Cake.Core.Annotations;
+using Cake.Issues.JUnit.LogFileFormat;
+
+///
+/// Aliases for JUnit log file formats.
+///
+public static partial class JUnitIssuesAliases
+{
+ ///
+ /// Gets an instance for the log format for any file compatible with JUnit XML format.
+ /// Does best effort parsing for any JUnit XML format.
+ ///
+ /// The context.
+ /// Instance for the generic JUnit XML format.
+ [CakePropertyAlias]
+ [CakeAliasCategory(IssuesAliasConstants.IssueProviderCakeAliasCategory)]
+ public static BaseJUnitLogFileFormat GenericJUnitLogFileFormat(
+ this ICakeContext context)
+ {
+ context.NotNull();
+
+ return new GenericJUnitLogFileFormat(context.Log);
+ }
+
+ ///
+ /// Gets an instance for the log format for cpplint JUnit XML output.
+ /// Optimized for cpplint's specific JUnit format where test case names represent file names.
+ ///
+ /// The context.
+ /// Instance for the cpplint JUnit XML format.
+ [CakePropertyAlias]
+ [CakeAliasCategory(IssuesAliasConstants.IssueProviderCakeAliasCategory)]
+ public static BaseJUnitLogFileFormat CppLintJUnitLogFileFormat(
+ this ICakeContext context)
+ {
+ context.NotNull();
+
+ return new CppLintLogFileFormat(context.Log);
+ }
+
+ ///
+ /// Gets an instance for the log format for markdownlint-cli2 JUnit XML output.
+ /// Optimized for markdownlint-cli2's specific JUnit format where file paths are in classname attributes.
+ ///
+ /// The context.
+ /// Instance for the markdownlint-cli2 JUnit XML format.
+ [CakePropertyAlias]
+ [CakeAliasCategory(IssuesAliasConstants.IssueProviderCakeAliasCategory)]
+ public static BaseJUnitLogFileFormat MarkdownlintCli2JUnitLogFileFormat(
+ this ICakeContext context)
+ {
+ context.NotNull();
+
+ return new MarkdownlintCli2LogFileFormat(context.Log);
+ }
+}
\ No newline at end of file
diff --git a/src/Cake.Issues.JUnit/JUnitIssuesAliases.cs b/src/Cake.Issues.JUnit/JUnitIssuesAliases.cs
index 554689117..a39aa1040 100644
--- a/src/Cake.Issues.JUnit/JUnitIssuesAliases.cs
+++ b/src/Cake.Issues.JUnit/JUnitIssuesAliases.cs
@@ -1,118 +1,11 @@
namespace Cake.Issues.JUnit;
-using Cake.Core;
using Cake.Core.Annotations;
-using Cake.Core.IO;
///
/// Contains functionality for reading issues from JUnit XML files.
///
[CakeAliasCategory(IssuesAliasConstants.MainCakeAliasCategory)]
-public static class JUnitIssuesAliases
+public static partial class JUnitIssuesAliases
{
- ///
- /// Gets the name of the JUnit issue provider.
- /// This name can be used to identify issues based on the property.
- ///
- /// The context.
- /// Name of the JUnit issue provider.
- [CakePropertyAlias]
- [CakeAliasCategory(IssuesAliasConstants.IssueProviderCakeAliasCategory)]
- public static string JUnitIssuesProviderTypeName(
- this ICakeContext context)
- {
- context.NotNull();
-
- return typeof(JUnitIssuesProvider).FullName;
- }
-
- ///
- /// Gets an instance of a provider for issues reported in JUnit XML format using the log file from disk.
- ///
- /// The context.
- /// Path to the JUnit log file.
- /// Instance of a provider for issues reported in JUnit XML format.
- ///
- /// Read issues from a JUnit XML file:
- ///
- ///
- ///
- ///
- [CakeMethodAlias]
- [CakeAliasCategory(IssuesAliasConstants.IssueProviderCakeAliasCategory)]
- public static IIssueProvider JUnitIssuesFromFilePath(
- this ICakeContext context,
- FilePath logFilePath)
- {
- context.NotNull();
- logFilePath.NotNull();
-
- return context.JUnitIssues(new JUnitIssuesSettings(logFilePath));
- }
-
- ///
- /// Gets an instance of a provider for issues reported in JUnit XML format using log file content.
- ///
- /// The context.
- /// Content of the JUnit log file.
- /// Instance of a provider for issues reported in JUnit XML format.
- ///
- /// Read issues the content of JUnit XML file:
- ///
- ///
- ///
- ///
- [CakeMethodAlias]
- [CakeAliasCategory(IssuesAliasConstants.IssueProviderCakeAliasCategory)]
- public static IIssueProvider JUnitIssuesFromContent(
- this ICakeContext context,
- string logFileContent)
- {
- context.NotNull();
- logFileContent.NotNullOrWhiteSpace();
-
- return context.JUnitIssues(new JUnitIssuesSettings(logFileContent.ToByteArray()));
- }
-
- ///
- /// Gets an instance of a provider for issues reported in JUnit XML format using the specified settings.
- ///
- /// The context.
- /// Settings for reading the JUnit log.
- /// Instance of a provider for issues reported in JUnit XML format.
- ///
- /// Read issues from a JUnit XML file:
- ///
- ///
- ///
- ///
- [CakeMethodAlias]
- [CakeAliasCategory(IssuesAliasConstants.IssueProviderCakeAliasCategory)]
- public static IIssueProvider JUnitIssues(
- this ICakeContext context,
- JUnitIssuesSettings settings)
- {
- context.NotNull();
- settings.NotNull();
-
- return new JUnitIssuesProvider(context.Log, settings);
- }
}
\ No newline at end of file
diff --git a/src/Cake.Issues.JUnit/JUnitIssuesProvider.cs b/src/Cake.Issues.JUnit/JUnitIssuesProvider.cs
index 02d8a33d6..a559edba5 100644
--- a/src/Cake.Issues.JUnit/JUnitIssuesProvider.cs
+++ b/src/Cake.Issues.JUnit/JUnitIssuesProvider.cs
@@ -1,10 +1,5 @@
namespace Cake.Issues.JUnit;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text.RegularExpressions;
-using System.Xml.Linq;
using Cake.Core.Diagnostics;
///
@@ -12,369 +7,15 @@
///
/// The Cake log context.
/// Settings for the issue provider.
-internal class JUnitIssuesProvider(ICakeLog log, JUnitIssuesSettings issueProviderSettings)
- : BaseConfigurableIssueProvider(log, issueProviderSettings)
+public class JUnitIssuesProvider(ICakeLog log, JUnitIssuesSettings issueProviderSettings)
+ : BaseMultiFormatIssueProvider(log, issueProviderSettings)
{
- ///
- public override string ProviderName => "JUnit";
-
- ///
- protected override IEnumerable InternalReadIssues()
- {
- var result = new List();
-
- var logContent = this.IssueProviderSettings.LogFileContent.ToStringUsingEncoding();
-
- try
- {
- var doc = XDocument.Parse(logContent);
-
- // Handle both single testsuite and testsuites root elements
- var testSuites = doc.Root?.Name.LocalName == "testsuites"
- ? doc.Root.Elements("testsuite")
- : new[] { doc.Root }.Where(x => x?.Name.LocalName == "testsuite");
-
- foreach (var testSuite in testSuites)
- {
- if (testSuite == null)
- {
- continue;
- }
-
- this.ProcessTestSuite(testSuite, result);
- }
- }
- catch (Exception ex)
- {
- throw new Exception($"Failed to parse JUnit XML: {ex.Message}", ex);
- }
-
- return result;
- }
-
- ///
- /// Tries to extract file path and line information from output text.
- ///
- /// The output text to parse.
- /// File information if found, null otherwise.
- private static (string FilePath, int? Line, int? Column)? ExtractFileInfoFromOutput(string output)
- {
- if (string.IsNullOrEmpty(output))
- {
- return null;
- }
-
- // Common patterns for file paths and line numbers in linter output:
- // file.txt:123:45: message
- // file.txt(123,45): message
- // file.txt line 123: message
- // /path/to/file.txt:123: message
- // file.txt:123 message
- // Line 3, Column 10 (markdownlint-cli2 format)
- var patterns = new[]
- {
- @"([^\s:]+):(\d+):(\d+)", // file:line:column
- @"([^\s:]+):(\d+)", // file:line
- @"([^\s\(\)]+)\((\d+),(\d+)\)", // file(line,column)
- @"([^\s\(\)]+)\((\d+)\)", // file(line)
- @"^([^\s]+)\s+line\s+(\d+)", // file line 123 (must start at beginning of line)
- @"File:\s*([^\s]+)", // File: path
- };
-
- foreach (var pattern in patterns)
- {
- var match = Regex.Match(output, pattern, RegexOptions.IgnoreCase | RegexOptions.Multiline);
- if (match.Success)
- {
- var filePath = match.Groups[1].Value.Trim();
-
- // Skip if it looks like a URL or doesn't look like a file path
- if (filePath.StartsWith("http", StringComparison.Ordinal) || filePath.StartsWith("www.", StringComparison.Ordinal))
- {
- continue;
- }
-
- int? line = null;
- int? column = null;
-
- if (match.Groups.Count > 2 && int.TryParse(match.Groups[2].Value, out var lineNum))
- {
- line = lineNum;
- }
-
- if (match.Groups.Count > 3 && int.TryParse(match.Groups[3].Value, out var colNum))
- {
- column = colNum;
- }
-
- return (filePath, line, column);
- }
- }
-
- return null;
- }
-
- ///
- /// Tries to extract line and column information from markdownlint-cli2 format text.
- ///
- /// The output text to parse.
- /// Line and column information if found, null otherwise.
- private static (int? Line, int? Column)? ExtractLineColumnFromMarkdownlintCli2(string output)
- {
- if (string.IsNullOrEmpty(output))
- {
- return null;
- }
-
- // Patterns for markdownlint-cli2 format:
- // Line 3, Column 10, Expected: 0 or 2; Actual: 1
- // Line 5, Expected: 1; Actual: 2
- // Line 6, Context: "# Description"
- var patterns = new[]
- {
- @"Line\s+(\d+),\s+Column\s+(\d+)", // Line 3, Column 10
- @"Line\s+(\d+)", // Line 5
- };
-
- foreach (var pattern in patterns)
- {
- var match = Regex.Match(output, pattern, RegexOptions.IgnoreCase);
- if (match.Success)
- {
- int? line = null;
- int? column = null;
-
- if (int.TryParse(match.Groups[1].Value, out var lineNum))
- {
- line = lineNum;
- }
-
- if (match.Groups.Count > 2 && int.TryParse(match.Groups[2].Value, out var colNum))
- {
- column = colNum;
- }
-
- return (line, column);
- }
- }
-
- return null;
- }
-
///
- /// Tries to extract file path from a class name.
+ /// Gets the name of the JUnit issue provider.
+ /// This name can be used to identify issues based on the property.
///
- /// The class name to parse.
- /// File information if the class name looks like a file path, null otherwise.
- private static (string FilePath, int? Line, int? Column)? ExtractFileInfoFromClassName(string className)
- {
- if (string.IsNullOrEmpty(className))
- {
- return null;
- }
+ public static string ProviderTypeName => typeof(JUnitIssuesProvider).FullName;
- // Some tools use file paths as class names
- if (className.Contains('/') || className.Contains('\\'))
- {
- return (className, null, null);
- }
-
- // Convert class names to potential file paths
- if (className.Contains('.'))
- {
- // Java-style package.Class -> package/Class.java (but only if it looks like a real package)
- var parts = className.Split('.');
- if (parts.Length > 1 && parts.All(p => !string.IsNullOrEmpty(p)))
- {
- var potentialPath = string.Join("/", parts) + ".java";
- return (potentialPath, null, null);
- }
- }
-
- return null;
- }
-
- ///
- /// Recursively processes a testsuite element and its nested testsuites and testcases.
- ///
- /// The testsuite element to process.
- /// The list to add found issues to.
- private void ProcessTestSuite(XElement testSuite, List result)
- {
- if (testSuite == null)
- {
- return;
- }
-
- // Process direct testcase children
- foreach (var testCase in testSuite.Elements("testcase"))
- {
- var className = testCase.Attribute("classname")?.Value ?? string.Empty;
- var testName = testCase.Attribute("name")?.Value ?? string.Empty;
- var time = testCase.Attribute("time")?.Value;
-
- // Process failures
- foreach (var failure in testCase.Elements("failure"))
- {
- var issue = this.ProcessTestFailure(failure, className, testName, IssuePriority.Error);
- if (issue != null)
- {
- result.Add(issue);
- }
- }
-
- // Process errors
- foreach (var error in testCase.Elements("error"))
- {
- var issue = this.ProcessTestFailure(error, className, testName, IssuePriority.Error);
- if (issue != null)
- {
- result.Add(issue);
- }
- }
- }
-
- // Recursively process nested testsuite elements
- foreach (var nestedTestSuite in testSuite.Elements("testsuite"))
- {
- this.ProcessTestSuite(nestedTestSuite, result);
- }
- }
-
- ///
- /// Normalizes XML content by removing XML formatting indentation while preserving intentional structure.
- ///
- /// The XML content to normalize.
- /// The normalized content.
- private string NormalizeXmlContent(string content)
- {
- if (string.IsNullOrEmpty(content))
- {
- return string.Empty;
- }
-
- // Split by lines, trim each line to remove XML indentation, then rejoin
- var lines = content.Split(new[] { '\r', '\n' }, StringSplitOptions.None);
- var normalizedLines = new List();
-
- foreach (var line in lines)
- {
- // Trim leading and trailing whitespace (including tabs) from each line
- var trimmedLine = line.Trim();
- normalizedLines.Add(trimmedLine);
- }
-
- // Join lines back together and clean up multiple consecutive empty lines
- var result = string.Join("\n", normalizedLines);
-
- // Remove leading and trailing empty lines
- result = result.Trim('\n');
-
- // Normalize multiple consecutive newlines to double newlines maximum
- result = Regex.Replace(result, @"\n{3,}", "\n\n");
-
- return result;
- }
-
- ///
- /// Processes a test failure or error element and creates an issue.
- ///
- /// The failure or error XML element.
- /// The test class name.
- /// The test name.
- /// The issue priority.
- /// The created issue or null if the failure should be ignored.
- private IIssue ProcessTestFailure(XElement failureElement, string className, string testName, IssuePriority priority)
- {
- var message = failureElement.Attribute("message")?.Value ?? string.Empty;
- var type = failureElement.Attribute("type")?.Value ?? string.Empty;
- var content = this.NormalizeXmlContent(failureElement.Value) ?? string.Empty;
-
- // Combine message and content for full description
- var fullMessage = string.IsNullOrEmpty(message) ? content :
- string.IsNullOrEmpty(content) ? message :
- $"{message}\n{content}";
-
- if (string.IsNullOrEmpty(fullMessage))
- {
- return null;
- }
-
- var issueBuilder = IssueBuilder
- .NewIssue(fullMessage, typeof(JUnitIssuesProvider).FullName, this.ProviderName)
- .WithPriority(priority);
-
- if (!string.IsNullOrEmpty(type))
- {
- issueBuilder = issueBuilder.OfRule(type);
- }
- else if (!string.IsNullOrEmpty(testName))
- {
- issueBuilder = issueBuilder.OfRule(testName);
- }
-
- // Initialize fileInfo variable
- (string FilePath, int? Line, int? Column)? fileInfo = null;
-
- // For markdownlint-cli2 style output, check if the content contains the specific format
- // and use the class name as file path in that case
- if (!string.IsNullOrEmpty(className) && !string.IsNullOrEmpty(content))
- {
- var lineColumnInfo = ExtractLineColumnFromMarkdownlintCli2(content);
- if (lineColumnInfo.HasValue)
- {
- // This looks like markdownlint-cli2 format, use class name as file path
- var (line, column) = lineColumnInfo.Value;
- fileInfo = (className, line, column);
- }
- }
-
- // If we don't have file info yet, try to extract from the message or content
- if (!fileInfo.HasValue)
- {
- fileInfo = ExtractFileInfoFromOutput(fullMessage) ?? ExtractFileInfoFromClassName(className);
- }
-
- // For cpplint-style output, if we don't have file info and the test name looks like a file name,
- // use the test name as the file name and try to extract line info from the message
- if (!fileInfo.HasValue && !string.IsNullOrEmpty(testName) && testName != "errors")
- {
- // Check if the message contains line info in cpplint format like "5: FailMsg [category/subcategory] [3]"
- // This is a strong indicator that it's a cpplint-style failure where the test name is the file name
- var lineMatch = Regex.Match(
- fullMessage,
- @"^(\d+):\s*.*\[.*\].*\[.*\]",
- RegexOptions.Multiline);
- if (lineMatch.Success && int.TryParse(lineMatch.Groups[1].Value, out var lineNum))
- {
- fileInfo = (testName, lineNum, null);
- }
-
- // Also check for simple line number pattern without the category/subcategory format
- else
- {
- var simpleLineMatch = Regex.Match(
- fullMessage,
- @"^(\d+):",
- RegexOptions.Multiline);
- if (simpleLineMatch.Success &&
- int.TryParse(simpleLineMatch.Groups[1].Value, out var simpleLineNum))
- {
- // Only treat as file if the test name doesn't contain hyphens (which are common in rule names)
- if (!testName.Contains('-') && !testName.Contains('_'))
- {
- fileInfo = (testName, simpleLineNum, null);
- }
- }
- }
- }
-
- if (fileInfo.HasValue)
- {
- var (filePath, line, column) = fileInfo.Value;
- issueBuilder = issueBuilder.InFile(filePath, line, column);
- }
-
- return issueBuilder.Create();
- }
+ ///
+ public override string ProviderName => "JUnit";
}
\ No newline at end of file
diff --git a/src/Cake.Issues.JUnit/JUnitIssuesSettings.cs b/src/Cake.Issues.JUnit/JUnitIssuesSettings.cs
index baa1d7635..e56c8440d 100644
--- a/src/Cake.Issues.JUnit/JUnitIssuesSettings.cs
+++ b/src/Cake.Issues.JUnit/JUnitIssuesSettings.cs
@@ -5,15 +5,17 @@
///
/// Settings for .
///
-public class JUnitIssuesSettings : IssueProviderSettings
+public class JUnitIssuesSettings : BaseMultiFormatIssueProviderSettings
{
///
/// Initializes a new instance of the class
/// for reading a JUnit log file on disk.
///
- /// Path to the JUnit log file.
- public JUnitIssuesSettings(FilePath logFilePath)
- : base(logFilePath)
+ /// Path to the JUnit log file.
+ /// The JUnit file needs to be in the format as defined by the parameter.
+ /// Format of the provided JUnit file.
+ public JUnitIssuesSettings(FilePath logFilePath, BaseJUnitLogFileFormat format)
+ : base(logFilePath, format)
{
}
@@ -21,9 +23,11 @@ public JUnitIssuesSettings(FilePath logFilePath)
/// Initializes a new instance of the class
/// for a JUnit log file content in memory.
///