From 65de2a5021300a079f554db6191322ced221830f Mon Sep 17 00:00:00 2001 From: Gordon Fontenot Date: Tue, 16 Dec 2025 14:44:39 -0600 Subject: [PATCH] Add all failures for a given test to the JUnit report Previously, we were only ever parsing the first failure for a test and passing that to the JUnit representation. However, a test might contain multiple failures and this is fully supported by the JUnit spec. To fix the issue, we'll collect all failures from the XCResult file and pass them to the JUnit result a list. This will prevent us from losing important diagnostic information from the test result. --- Sources/xcresultparser/JunitXML.swift | 15 +-- .../XcresultparserTests.swift | 98 +++++++++++++++++++ 2 files changed, 107 insertions(+), 6 deletions(-) diff --git a/Sources/xcresultparser/JunitXML.swift b/Sources/xcresultparser/JunitXML.swift index 56362b9..049197a 100644 --- a/Sources/xcresultparser/JunitXML.swift +++ b/Sources/xcresultparser/JunitXML.swift @@ -293,10 +293,13 @@ public struct JunitXML: XmlSerializable { nodeNames: nodeNames ) if test.isFailed { - if let summary = test.failureSummary(in: failureSummaries) { - testcase.addChild(summary.failureXML(projectRoot: projectRoot)) - } else { + let summaries = test.failureSummaries(in: failureSummaries) + if summaries.isEmpty { testcase.addChild(failureWithoutSummary) + } else { + for summary in summaries { + testcase.addChild(summary.failureXML(projectRoot: projectRoot)) + } } } else if test.isSkipped { testcase.addChild(skippedWithoutSummary) @@ -321,7 +324,7 @@ extension XMLElement { } } -private extension ActionTestMetadata { +extension ActionTestMetadata { func xmlNode( classname: String, numFormatter: NumberFormatter, @@ -348,8 +351,8 @@ private extension ActionTestMetadata { return testcase } - func failureSummary(in summaries: [TestFailureIssueSummary]) -> TestFailureIssueSummary? { - return summaries.first { summary in + func failureSummaries(in summaries: [TestFailureIssueSummary]) -> [TestFailureIssueSummary] { + return summaries.filter { summary in return summary.testCaseName == identifier?.replacingOccurrences(of: "/", with: ".") || summary.testCaseName == "-[\(identifier?.replacingOccurrences(of: "/", with: " ") ?? "")]" } diff --git a/Tests/XcresultparserTests/XcresultparserTests.swift b/Tests/XcresultparserTests/XcresultparserTests.swift index c0ac026..3a5beb3 100644 --- a/Tests/XcresultparserTests/XcresultparserTests.swift +++ b/Tests/XcresultparserTests/XcresultparserTests.swift @@ -1,5 +1,6 @@ import Foundation @testable import XcresultparserLib +import XCResultKit import Testing @MainActor @@ -499,6 +500,73 @@ struct XcresultparserTests { try assertXmlTestReportsAreEqual(expectedFileName: "junit_repeated", actual: junitXML) } + @Test + func testFailureSummariesReturnsAllMatchingFailures() throws { + let testMetadata = try makeTestMetadata() + + let matchingFailure1 = try makeFailureSummary( + testCaseName: "TestClass.testMethod", + message: "First assertion failed" + ) + let matchingFailure2 = try makeFailureSummary( + testCaseName: "TestClass.testMethod", + message: "Second assertion failed" + ) + let nonMatchingFailure = try makeFailureSummary( + testCaseName: "OtherClass.otherMethod", + message: "Unrelated failure" + ) + + let result = testMetadata.failureSummaries(in: [matchingFailure1, nonMatchingFailure, matchingFailure2]) + + #expect(result.count == 2) + #expect(result[0].message == "First assertion failed") + #expect(result[1].message == "Second assertion failed") + } + + @Test + func testFailureSummariesWithBracketNotation() throws { + let testMetadata = try makeTestMetadata() + + // Objective-C bracket notation: -[TestClass testMethod] + let bracketFailure1 = try makeFailureSummary( + testCaseName: "-[TestClass testMethod]", + message: "Bracket notation failure 1" + ) + let bracketFailure2 = try makeFailureSummary( + testCaseName: "-[TestClass testMethod]", + message: "Bracket notation failure 2" + ) + + let result = testMetadata.failureSummaries(in: [bracketFailure1, bracketFailure2]) + + #expect(result.count == 2) + #expect(result[0].message == "Bracket notation failure 1") + #expect(result[1].message == "Bracket notation failure 2") + } + + @Test + func testFailureSummariesReturnsEmptyForNoMatches() throws { + let testMetadata = try makeTestMetadata() + let nonMatchingFailure = try makeFailureSummary( + testCaseName: "OtherClass.otherMethod", + message: "Unrelated failure" + ) + + let result = testMetadata.failureSummaries(in: [nonMatchingFailure]) + + #expect(result.isEmpty) + } + + @Test + func testFailureSummariesWithEmptyArray() throws { + let testMetadata = try makeTestMetadata() + + let result = testMetadata.failureSummaries(in: []) + + #expect(result.isEmpty) + } + @Test func testCleanCodeWarnings() throws { let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! @@ -650,6 +718,36 @@ struct XcresultparserTests { // MARK: helper functions + private func makeTestMetadata( + identifier: String = "TestClass/testMethod", + name: String = "testMethod", + status: String = "Failure", + duration: String = "0.5" + ) throws -> ActionTestMetadata { + let json: [String: AnyObject] = [ + "_type": ["_name": "ActionTestMetadata"] as AnyObject, + "identifier": ["_type": ["_name": "String"], "_value": identifier] as AnyObject, + "name": ["_type": ["_name": "String"], "_value": name] as AnyObject, + "testStatus": ["_type": ["_name": "String"], "_value": status] as AnyObject, + "duration": ["_type": ["_name": "Double"], "_value": duration] as AnyObject + ] + return try #require(ActionTestMetadata(json)) + } + + private func makeFailureSummary( + testCaseName: String, + message: String = "Assertion failed", + issueType: String = "Assertion Failure" + ) throws -> TestFailureIssueSummary { + let json: [String: AnyObject] = [ + "_type": ["_name": "TestFailureIssueSummary"] as AnyObject, + "testCaseName": ["_type": ["_name": "String"], "_value": testCaseName] as AnyObject, + "issueType": ["_type": ["_name": "String"], "_value": issueType] as AnyObject, + "message": ["_type": ["_name": "String"], "_value": message] as AnyObject + ] + return try #require(TestFailureIssueSummary(json)) + } + func assertXmlTestReportsAreEqual( expectedFileName: String, actual: XmlSerializable,