From 0277a74c585315d8ea43a98d5f16eb3b4b4bedd6 Mon Sep 17 00:00:00 2001 From: Chris Peterson Date: Wed, 27 May 2026 18:00:05 -0700 Subject: [PATCH] fix: emit file and line attributes on JUnit testcase JUnit XML elements omitted file and line, so GitLab's Unit test reports UI rendered the Filename column as "null". VSTest exposes the source path and line number via TestCase.CodeFilePath / LineNumber and TestResultInfo already carried them through; the JUnit serializer just wasn't reading them. The Jenkins JUnit XSD supports both attributes, so this is an additive change. file is emitted when CodeFilePath is non-empty; line is emitted when LineNumber > 0. Resolves #235 --- .../JunitXmlSerializer.cs | 10 ++++ .../JUnitXmlTestSerializerTests.cs | 53 +++++++++++++++++-- test/assets/JUnit.xsd | 2 + 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/src/JUnit.Xml.TestLogger/JunitXmlSerializer.cs b/src/JUnit.Xml.TestLogger/JunitXmlSerializer.cs index 835049d..4389b3a 100644 --- a/src/JUnit.Xml.TestLogger/JunitXmlSerializer.cs +++ b/src/JUnit.Xml.TestLogger/JunitXmlSerializer.cs @@ -359,6 +359,16 @@ private XElement CreateTestCaseElement(TestResultInfo result) // seconds should be low enough it won't interfere with anyone monitoring test duration. testcaseElement.SetAttributeValue("time", Math.Max(0.0000001f, result.Duration.TotalSeconds).ToString("0.0000000", CultureInfo.InvariantCulture)); + if (!string.IsNullOrEmpty(result.CodeFilePath)) + { + testcaseElement.SetAttributeValue("file", result.CodeFilePath); + } + + if (result.LineNumber > 0) + { + testcaseElement.SetAttributeValue("line", result.LineNumber); + } + if (result.Outcome == TestOutcome.Failed) { var failureBodySB = new StringBuilder(); diff --git a/test/JUnit.Xml.TestLogger.UnitTests/JUnitXmlTestSerializerTests.cs b/test/JUnit.Xml.TestLogger.UnitTests/JUnitXmlTestSerializerTests.cs index c2076b7..9bc6e25 100644 --- a/test/JUnit.Xml.TestLogger.UnitTests/JUnitXmlTestSerializerTests.cs +++ b/test/JUnit.Xml.TestLogger.UnitTests/JUnitXmlTestSerializerTests.cs @@ -224,6 +224,40 @@ public void TestSuiteShouldIncludeSystemErrElementWhenContentExists() Assert.IsTrue(systemErrElement.Value.Contains("Error - Error message")); } + [TestMethod] + public void TestCaseShouldIncludeFileAndLineAttributesWhenSourceIsPresent() + { + var serializer = new JunitXmlSerializer(); + var result = CreateTestResultInfo(); + + var testCase = SerializeAndGetTestCaseElement(serializer, result); + + Assert.AreEqual(TestCsPath, testCase.Attribute("file")?.Value); + Assert.AreEqual("42", testCase.Attribute("line")?.Value); + } + + [TestMethod] + public void TestCaseShouldOmitFileAttributeWhenCodeFilePathIsMissing() + { + var serializer = new JunitXmlSerializer(); + var result = CreateTestResultInfo(codeFilePath: null); + + var testCase = SerializeAndGetTestCaseElement(serializer, result); + + Assert.IsNull(testCase.Attribute("file")); + } + + [TestMethod] + public void TestCaseShouldOmitLineAttributeWhenLineNumberIsMissing() + { + var serializer = new JunitXmlSerializer(); + var result = CreateTestResultInfo(lineNumber: 0); + + var testCase = SerializeAndGetTestCaseElement(serializer, result); + + Assert.IsNull(testCase.Attribute("line")); + } + private static LoggerConfiguration CreateTestLoggerConfiguration() { return new LoggerConfiguration(new Dictionary @@ -237,7 +271,11 @@ private static TestRunConfiguration CreateTestRunConfiguration() return new TestRunConfiguration { StartTime = DateTime.Now }; } - private static TestResultInfo CreateTestResultInfo(TestOutcome outcome = TestOutcome.Passed, List messages = null) + private static TestResultInfo CreateTestResultInfo( + TestOutcome outcome = TestOutcome.Passed, + List messages = null, + string codeFilePath = TestCsPath, + int lineNumber = 42) { return new TestResultInfo( TestNamespace, @@ -248,8 +286,8 @@ private static TestResultInfo CreateTestResultInfo(TestOutcome outcome = TestOut TestDisplayName, TestDisplayName, TestDllPath, - TestCsPath, - 42, + codeFilePath, + lineNumber, DateTime.Now, DateTime.Now.AddSeconds(1), TimeSpan.FromSeconds(1), @@ -262,7 +300,7 @@ private static TestResultInfo CreateTestResultInfo(TestOutcome outcome = TestOut null); } - private static string SerializeAndExtractElementContent(JunitXmlSerializer serializer, TestResultInfo result, string elementName) + private static XElement SerializeAndGetTestCaseElement(JunitXmlSerializer serializer, TestResultInfo result) { var xml = serializer.Serialize( CreateTestLoggerConfiguration(), @@ -272,6 +310,13 @@ private static string SerializeAndExtractElementContent(JunitXmlSerializer seria var doc = XDocument.Parse(xml); var testCaseElement = doc.XPathSelectElement("//testcase"); + Assert.IsNotNull(testCaseElement, "testcase element not found"); + return testCaseElement; + } + + private static string SerializeAndExtractElementContent(JunitXmlSerializer serializer, TestResultInfo result, string elementName) + { + var testCaseElement = SerializeAndGetTestCaseElement(serializer, result); var targetElement = testCaseElement.Element(elementName); Assert.IsNotNull(targetElement, $"Element '{elementName}' not found in testcase"); diff --git a/test/assets/JUnit.xsd b/test/assets/JUnit.xsd index 89c9e0c..ddca757 100644 --- a/test/assets/JUnit.xsd +++ b/test/assets/JUnit.xsd @@ -82,6 +82,8 @@ Modifications to original xsd for this repo: + +