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,