From 7c768eb014684f909e89f0564546797c6e48c847 Mon Sep 17 00:00:00 2001 From: Alex da Franca Date: Sat, 22 Nov 2025 09:18:00 +0100 Subject: [PATCH 01/14] Replace xcresultkit WiP --- .../XCResultToolModels/XCBuildResults.swift | 37 ++++++++++++++ .../XCCommonFailureInsight.swift | 14 +++++ .../XCResultToolModels/XCDestination.swift | 13 +++++ .../XCDeviceAndConfigurationSummary.swift | 16 ++++++ .../XCResultToolModels/XCDeviceInfo.swift | 16 ++++++ .../XCFailureDistributionInsight.swift | 19 +++++++ .../XCResultToolModels/XCInsightSummary.swift | 12 +++++ .../XCResultToolModels/XCInsights.swift | 12 +++++ .../Models/XCResultToolModels/XCIssue.swift | 14 +++++ .../XCLongestTestRunsInsight.swift | 19 +++++++ .../XCResultToolModels/XCStatistic.swift | 12 +++++ .../Models/XCResultToolModels/XCSummary.swift | 48 ++++++++++++++++++ .../XCResultToolModels/XCTestDetails.swift | 27 ++++++++++ .../XCResultToolModels/XCTestFailure.swift | 34 +++++++++++++ .../XCResultToolModels/XCTestNode.swift | 45 ++++++++++++++++ .../XCResultToolModels/XCTestNodeType.swift | 25 +++++++++ .../XCTestPlanConfiguration.swift | 11 ++++ .../XCResultToolModels/XCTestResult.swift | 14 +++++ .../XCResultToolModels/XCTestResults.swift | 13 +++++ .../database.sqlite3 | Bin 245760 -> 245760 bytes .../TestAssets/test.xcresult/database.sqlite3 | Bin 0 -> 245760 bytes .../test_merged.xcresult/database.sqlite3 | Bin 0 -> 245760 bytes .../test_repeated.xcresult/database.sqlite3 | Bin 0 -> 253952 bytes 23 files changed, 401 insertions(+) create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCBuildResults.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCCommonFailureInsight.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCDestination.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCDeviceAndConfigurationSummary.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCDeviceInfo.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCFailureDistributionInsight.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCInsightSummary.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCInsights.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCIssue.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCLongestTestRunsInsight.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCStatistic.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCSummary.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCTestDetails.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCTestFailure.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCTestNode.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCTestNodeType.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCTestPlanConfiguration.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCTestResult.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCTestResults.swift create mode 100644 Tests/XcresultparserTests/TestAssets/test.xcresult/database.sqlite3 create mode 100644 Tests/XcresultparserTests/TestAssets/test_merged.xcresult/database.sqlite3 create mode 100644 Tests/XcresultparserTests/TestAssets/test_repeated.xcresult/database.sqlite3 diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCBuildResults.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCBuildResults.swift new file mode 100644 index 0000000..d652b80 --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCBuildResults.swift @@ -0,0 +1,37 @@ +// +// XCBuildResults.swift +// Xcresultparser +// +// Created by Alex da Franca on 02.11.25. +// + +struct XCBuildResults: Codable { + let destination: XCDeviceInfo + let startTime: Double // Date as a UNIX timestamp (seconds since midnight UTC on January 1, 1970) + let endTime: Double // Date as a UNIX timestamp (seconds since midnight UTC on January 1, 1970) + let analyzerWarnings: [XCIssue] + let warnings: [XCIssue] + let errors: [XCIssue] + let status: String? + let analyzerWarningCount: Int + let errorCount: Int + let warningCount: Int + let actionTitle: String? +} + +extension XCBuildResults { + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + destination = try values.decode(XCDeviceInfo.self, forKey: .destination) + startTime = try values.decode(Double.self, forKey: .startTime) + endTime = try values.decode(Double.self, forKey: .endTime) + analyzerWarnings = try values.decode([XCIssue].self, forKey: .analyzerWarnings) + warnings = try values.decode([XCIssue].self, forKey: .warnings) + errors = try values.decode([XCIssue].self, forKey: .errors) + status = try? values.decode(String.self, forKey: .status) + analyzerWarningCount = (try? values.decode(Int.self, forKey: .analyzerWarningCount)) ?? 0 + errorCount = (try? values.decode(Int.self, forKey: .errorCount)) ?? 0 + warningCount = (try? values.decode(Int.self, forKey: .warningCount)) ?? 0 + actionTitle = try? values.decode(String.self, forKey: .actionTitle) + } +} diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCCommonFailureInsight.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCCommonFailureInsight.swift new file mode 100644 index 0000000..6d3d9ba --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCCommonFailureInsight.swift @@ -0,0 +1,14 @@ +// +// XCCommonFailureInsight.swift +// Xcresultparser +// +// Created by Alex da Franca on 02.11.25. +// + +struct XCCommonFailureInsight: Codable { + let failuresCount: Int + let impact: String + let description: String + let associatedTestIdentifiers: [String] +} + diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCDestination.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCDestination.swift new file mode 100644 index 0000000..19357e6 --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCDestination.swift @@ -0,0 +1,13 @@ +// +// XCDestination.swift +// Xcresultparser +// +// Created by Alex da Franca on 02.11.25. +// + +struct XCDestination: Codable { + let deviceName: String + let configurationName: String +} + + diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCDeviceAndConfigurationSummary.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCDeviceAndConfigurationSummary.swift new file mode 100644 index 0000000..04377df --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCDeviceAndConfigurationSummary.swift @@ -0,0 +1,16 @@ +// +// XCDeviceAndConfigurationSummary.swift +// Xcresultparser +// +// Created by Alex da Franca on 02.11.25. +// + +struct XCDeviceAndConfigurationSummary: Codable { + let device: XCDeviceInfo + let testPlanConfiguration: XCTestPlanConfiguration + let passedTests: Int + let failedTests: Int + let skippedTests: Int + let expectedFailures: Int +} + diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCDeviceInfo.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCDeviceInfo.swift new file mode 100644 index 0000000..037df15 --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCDeviceInfo.swift @@ -0,0 +1,16 @@ +// +// XCDeviceInfo.swift +// Xcresultparser +// +// Created by Alex da Franca on 02.11.25. +// + +struct XCDeviceInfo: Codable { + let architecture: String // e.g. "arm64", + let deviceId: String // e.g. 00008103-000959DC213B001E", + let deviceName: String // e.g. "My Mac", + let modelName: String // e.g. "iMac", + let osVersion: String // e.g. "26.0.1", + let osBuildNumber: String? // e.g. "25A362", + let platform: String? // e.g. "macOS" +} diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCFailureDistributionInsight.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCFailureDistributionInsight.swift new file mode 100644 index 0000000..a123e07 --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCFailureDistributionInsight.swift @@ -0,0 +1,19 @@ +// +// XCFailureDistributionInsight.swift +// Xcresultparser +// +// Created by Alex da Franca on 02.11.25. +// + +struct XCFailureDistributionInsight: Codable { + let title: String + let impact: String + let distributionPercent: Double + let associatedTestIdentifiers: [String] + + // Optional + let bug: String? + let tag: String? + let destinations: [String]? +} + diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCInsightSummary.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCInsightSummary.swift new file mode 100644 index 0000000..3517ef6 --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCInsightSummary.swift @@ -0,0 +1,12 @@ +// +// XCInsightSummary.swift +// Xcresultparser +// +// Created by Alex da Franca on 02.11.25. +// + +struct XCInsightSummary: Codable { + let impact: String + let category: String + let text: String +} diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCInsights.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCInsights.swift new file mode 100644 index 0000000..3204b77 --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCInsights.swift @@ -0,0 +1,12 @@ +// +// XCInsights.swift +// Xcresultparser +// +// Created by Alex da Franca on 02.11.25. +// + +struct XCInsights: Codable { + let commonFailureInsights: XCCommonFailureInsight + let longestTestRunsInsights: XCLongestTestRunsInsight + let failureDistributionInsights: XCFailureDistributionInsight +} diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCIssue.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCIssue.swift new file mode 100644 index 0000000..93fbc4f --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCIssue.swift @@ -0,0 +1,14 @@ +// +// XCIssue.swift +// Xcresultparser +// +// Created by Alex da Franca on 02.11.25. +// + +struct XCIssue: Codable { + let issueType: String + let message: String + let targetName: String? + let sourceURL: String? + let className: String? +} diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCLongestTestRunsInsight.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCLongestTestRunsInsight.swift new file mode 100644 index 0000000..708991a --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCLongestTestRunsInsight.swift @@ -0,0 +1,19 @@ +// +// XCLongestTestRunsInsight.swift +// Xcresultparser +// +// Created by Alex da Franca on 02.11.25. +// + +struct XCLongestTestRunsInsight: Codable { + let title: String + let impact: String + let associatedTestIdentifiers: [String] + let targetName: String + let testPlanConfigurationName: String + let deviceName: String + let osNameAndVersion: String + let durationOfSlowTests: Double + let meanTime: String +} + diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCStatistic.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCStatistic.swift new file mode 100644 index 0000000..bcc922a --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCStatistic.swift @@ -0,0 +1,12 @@ +// +// XCStatistic.swift +// Xcresultparser +// +// Created by Alex da Franca on 02.11.25. +// + +struct XCStatistic: Codable { + let title: String + let subtitle: String +} + diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCSummary.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCSummary.swift new file mode 100644 index 0000000..354896b --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCSummary.swift @@ -0,0 +1,48 @@ +// +// XCSummary.swift +// Xcresultparser +// +// Created by Alex da Franca on 02.11.25. +// + +struct XCSummary: Codable { + let title: String + // Description of the Test Plan, OS, and environment that was used during testing + let environmentDescription: String + let topInsights: [XCInsightSummary] + let result: XCTestResult + let totalTestCount: Int + let passedTests: Int + let failedTests: Int + let skippedTests: Int + let expectedFailures: Int + let statistics: [XCStatistic] + let devicesAndConfigurations: XCDeviceAndConfigurationSummary + let testFailures: XCTestFailure + + // Optional: + // Date as a UNIX timestamp (seconds since midnight UTC on January 1, 1970) + let startTime: Double + // Date as a UNIX timestamp (seconds since midnight UTC on January 1, 1970) + let finishTime: Double +} + +extension XCSummary { + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + title = try values.decode(String.self, forKey: .title) + environmentDescription = try values.decode(String.self, forKey: .environmentDescription) + topInsights = try values.decode([XCInsightSummary].self, forKey: .topInsights) + result = try values.decode(XCTestResult.self, forKey: .result) + totalTestCount = try values.decode(Int.self, forKey: .totalTestCount) + passedTests = try values.decode(Int.self, forKey: .passedTests) + failedTests = try values.decode(Int.self, forKey: .failedTests) + skippedTests = try values.decode(Int.self, forKey: .skippedTests) + expectedFailures = try values.decode(Int.self, forKey: .expectedFailures) + statistics = try values.decode([XCStatistic].self, forKey: .statistics) + devicesAndConfigurations = try values.decode(XCDeviceAndConfigurationSummary.self, forKey: .devicesAndConfigurations) + testFailures = try values.decode(XCTestFailure.self, forKey: .testFailures) + startTime = (try? values.decode(Double.self, forKey: .startTime)) ?? 0 + finishTime = (try? values.decode(Double.self, forKey: .finishTime)) ?? 0 + } +} diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCTestDetails.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCTestDetails.swift new file mode 100644 index 0000000..aae204f --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCTestDetails.swift @@ -0,0 +1,27 @@ +// +// XCTestDetails.swift +// Xcresultparser +// +// Created by Alex da Franca on 02.11.25. +// + +struct XCTestDetails: Codable { + let testIdentifier: String + let testName: String + let testDescription: String + let duration: String + let testPlanConfigurations: [XCTestPlanConfiguration] + let devices: [XCDeviceInfo] + let testRuns: String + let testResult: String + let hasPerformanceMetrics: String + let hasMediaAttachments: String + + // Human-readable duration with optional components of days, hours, minutes and seconds + let testIdentifierURL: String? + // Time interval in seconds + let durationInSeconds: Double? + // Date as a UNIX timestamp (seconds since midnight UTC on January 1, 1970) + let startTime: Double? + let arguments: [] +} diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCTestFailure.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCTestFailure.swift new file mode 100644 index 0000000..00aa8c5 --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCTestFailure.swift @@ -0,0 +1,34 @@ +// +// XCTestFailure.swift +// Xcresultparser +// +// Created by Alex da Franca on 02.11.25. +// + +import Foundation + +struct XCTestFailure: Codable { + let testName: String + let targetName: String + let failureText: String + let testIdentifier: Int64 + let testIdentifierString: String + let testIdentifierURL: URL? +} + +extension XCTestFailure { + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + testName = try values.decode(String.self, forKey: .testName) + targetName = try values.decode(String.self, forKey: .targetName) + failureText = try values.decode(String.self, forKey: .failureText) + testIdentifier = try values.decode(Int64.self, forKey: .testIdentifier) + testIdentifierString = try values.decode(String.self, forKey: .testIdentifierString) + let testIdentifierURLString = try? values.decode(String.self, forKey: .testIdentifierURL) + if let testIdentifierURLString { + testIdentifierURL = URL(string: testIdentifierURLString) + } else { + testIdentifierURL = nil + } + } +} diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCTestNode.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCTestNode.swift new file mode 100644 index 0000000..15b6ac8 --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCTestNode.swift @@ -0,0 +1,45 @@ +// +// XCTestNode.swift +// Xcresultparser +// +// Created by Alex da Franca on 02.11.25. +// + +import Foundation + +struct XCTestNode: Codable { + let children: [XCTestNode] + let name: String // e.g. "xcresultparser", + let nodeType: XCTestNodeType // e.g. "Test Plan", + let result: XCTestResult? // e.g. "Passed" + let nodeIdentifier: String? // e.g. "0" + let nodeIdentifierURL: URL? // e.g. "test://com.apple.xcode/Xcresultparser/XcresultparserTests/XcresultparserTests" + let durationInSeconds: TimeInterval? // e.g. 19 + let details: String? + let tags: [String] + // let duration: String // left out because we can format `durationInSeconds` as String ourselves ;-) +} + +extension XCTestNode { + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + + // mandatory values: + name = try values.decode(String.self, forKey: .name) + nodeType = try values.decode(XCTestNodeType.self, forKey: .nodeType) + + // optional values: + result = try? values.decode(XCTestResult.self, forKey: .result) + nodeIdentifier = try? values.decode(String.self, forKey: .nodeIdentifier) + let nodeIdentifierURLString = try? values.decode(String.self, forKey: .nodeIdentifierURL) + if let nodeIdentifierURLString { + nodeIdentifierURL = URL(string: nodeIdentifierURLString) + } else { + nodeIdentifierURL = nil + } + children = (try? values.decode([XCTestNode].self, forKey: .children)) ?? [XCTestNode]() + durationInSeconds = try? values.decode(TimeInterval.self, forKey: .durationInSeconds) + details = try? values.decode(String.self, forKey: .details) + tags = (try? values.decode([String].self, forKey: .nodeIdentifier)) ?? [String]() + } +} diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCTestNodeType.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCTestNodeType.swift new file mode 100644 index 0000000..4218899 --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCTestNodeType.swift @@ -0,0 +1,25 @@ +// +// XCTestNodeType.swift +// Xcresultparser +// +// Created by Alex da Franca on 02.11.25. +// + +enum XCTestNodeType: String, Codable { + case testPlan = "Test Plan" + case testPlanConfiguration = "Test Plan Configuration" + case unitTestBundle = "Unit test bundle" + case uiTestBundle = "UI test bundle" + case testSuite = "Test Suite" + case testCase = "Test Case" + case arguments = "Arguments" + case repetition = "Repetition" + case failureMessage = "Failure Message" + case device = "Device" + case testCaseRun = "Test Case Run" + case sourceCodeReference = "Source Code Reference" + case attachment = "Attachment" + case expression = "Expression" + case testValue = "Test Value" + case runtimWarning = "Runtime Warning" +} diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCTestPlanConfiguration.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCTestPlanConfiguration.swift new file mode 100644 index 0000000..9b100d1 --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCTestPlanConfiguration.swift @@ -0,0 +1,11 @@ +// +// XCTestPlanConfiguration.swift +// Xcresultparser +// +// Created by Alex da Franca on 02.11.25. +// + +struct XCTestPlanConfiguration: Codable { + let configurationId: String // e.g. "1" + let configurationName: String // e.g. "Test Scheme Action" +} diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCTestResult.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCTestResult.swift new file mode 100644 index 0000000..0316735 --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCTestResult.swift @@ -0,0 +1,14 @@ +// +// XCTestResult.swift +// Xcresultparser +// +// Created by Alex da Franca on 02.11.25. +// + +enum XCTestResult: String, Codable { + case passed = "Passed" + case failed = "Failed" + case skipped = "Skipped" + case expectedFailure = "Expected Failure" + case unknown = "unknown" +} diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCTestResults.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCTestResults.swift new file mode 100644 index 0000000..dd562d5 --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCTestResults.swift @@ -0,0 +1,13 @@ +// +// XCTestResults.swift +// Xcresultparser +// +// Created by Alex da Franca on 02.11.25. +// + + +struct XCTestResults: Codable { + let devices: [XCDeviceInfo] + let testNodes: [XCTestNode] + let testPlanConfigurations: [XCTestPlanConfiguration] +} diff --git a/Tests/XcresultparserTests/TestAssets/resultWithCompileError.xcresult/database.sqlite3 b/Tests/XcresultparserTests/TestAssets/resultWithCompileError.xcresult/database.sqlite3 index da8e3d86c259b37133a39401b1adba1d0990f8cf..17be6d67eddc65eb0b98e20a26b32d9c8f9dcd9b 100644 GIT binary patch delta 3537 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 AssI20 delta 3433 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 Dq}k>~ literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/test_merged.xcresult/database.sqlite3 b/Tests/XcresultparserTests/TestAssets/test_merged.xcresult/database.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..d5a259ce5436f013ae7115b2361296cf2e4389d6 GIT binary patch literal 245760 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 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/test_repeated.xcresult/database.sqlite3 b/Tests/XcresultparserTests/TestAssets/test_repeated.xcresult/database.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..fb1cc18ad2d4db8956541d09687845d0f852aef4 GIT binary patch literal 253952 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 Date: Fri, 20 Feb 2026 15:47:48 +0100 Subject: [PATCH 02/14] WiP commit --- .../XCResultToolModels/XCActivities.swift | 14 +++++++++++ .../XCResultToolModels/XCActivityNode.swift | 17 +++++++++++++ .../XCResultToolModels/XCArgument.swift | 11 +++++++++ .../XCResultToolModels/XCAttachment.swift | 16 +++++++++++++ .../Models/XCResultToolModels/XCBug.swift | 14 +++++++++++ .../XCResultToolModels/XCBuildResults.swift | 5 ++-- .../XCCommonFailureInsight.swift | 1 + .../XCResultToolModels/XCConfiguration.swift | 16 +++++++++++++ .../Models/XCResultToolModels/XCContent.swift | 19 +++++++++++++++ .../Models/XCResultToolModels/XCDevice.swift | 24 +++++++++++++++++++ .../XCDeviceAndConfigurationSummary.swift | 5 ++-- .../XCResultToolModels/XCDeviceInfo.swift | 16 ------------- .../XCFailureDistributionInsight.swift | 1 + .../XCResultToolModels/XCInsightSummary.swift | 1 + .../XCResultToolModels/XCInsights.swift | 1 + .../XCLongestTestRunsInsight.swift | 1 + .../Models/XCResultToolModels/XCMetric.swift | 21 ++++++++++++++++ .../XCResultToolModels/XCStatistic.swift | 1 + .../Models/XCResultToolModels/XCSummary.swift | 1 + .../XCResultToolModels/XCTestDetails.swift | 16 +++++++++---- .../XCResultToolModels/XCTestFailure.swift | 1 + .../XCResultToolModels/XCTestNode.swift | 3 ++- .../XCResultToolModels/XCTestNodeType.swift | 1 + .../XCTestPlanConfiguration.swift | 11 --------- .../XCResultToolModels/XCTestResult.swift | 2 ++ .../XCResultToolModels/XCTestResults.swift | 13 ---------- .../XCTestRunActivities.swift | 14 +++++++++++ .../XCTestRunWithMetrics.swift | 13 ++++++++++ .../XCTestWithMetrics.swift | 13 ++++++++++ .../Models/XCResultToolModels/XCTests.swift | 13 ++++++++++ 30 files changed, 235 insertions(+), 50 deletions(-) create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCActivities.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCActivityNode.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCArgument.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCAttachment.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCBug.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCConfiguration.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCContent.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCDevice.swift delete mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCDeviceInfo.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCMetric.swift delete mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCTestPlanConfiguration.swift delete mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCTestResults.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCTestRunActivities.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCTestRunWithMetrics.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCTestWithMetrics.swift create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCTests.swift diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCActivities.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCActivities.swift new file mode 100644 index 0000000..b7cd17a --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCActivities.swift @@ -0,0 +1,14 @@ +// +// XCActivities.swift +// Xcresultparser +// +// Created by Alex da Franca on 06.12.25. +// +// xcrun xcresulttool get test-results activities + +struct XCActivities: Codable { + let testIdentifier: String + let testName: String + let testRuns: [XCTestRunActivities] + let testIdentifierURL: String? +} diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCActivityNode.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCActivityNode.swift new file mode 100644 index 0000000..353c0eb --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCActivityNode.swift @@ -0,0 +1,17 @@ +// +// XCActivityNode.swift +// Xcresultparser +// +// Created by Alex da Franca on 06.12.25. +// +// xcrun xcresulttool get test-results activities + +struct XCActivityNode: Codable { + let title: String + let isAssociatedWithFailure: Bool + + let startTime: Double? // Date as a UNIX timestamp (seconds since midnight UTC on January 1, 1970) + let attachments: [XCAttachment]? + let childActivities: [XCActivityNode]? +} + diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCArgument.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCArgument.swift new file mode 100644 index 0000000..f8f8e43 --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCArgument.swift @@ -0,0 +1,11 @@ +// +// XCArgument.swift +// Xcresultparser +// +// Created by Alex da Franca on 06.12.25. +// +// xcrun xcresulttool get test-results activities + +struct XCArgument: Codable { + let value: String +} diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCAttachment.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCAttachment.swift new file mode 100644 index 0000000..a4806ad --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCAttachment.swift @@ -0,0 +1,16 @@ +// +// XCAttachment.swift +// Xcresultparser +// +// Created by Alex da Franca on 06.12.25. +// +// xcrun xcresulttool get test-results activities + +struct XCAttachment: Codable { + let name: String + let uuid: String + let timestamp: Double // Date as a UNIX timestamp (seconds since midnight UTC on January 1, 1970) + let payloadId: String? + let lifetime: String? +} + diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCBug.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCBug.swift new file mode 100644 index 0000000..196450c --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCBug.swift @@ -0,0 +1,14 @@ +// +// XCBug.swift +// Xcresultparser +// +// Created by Alex da Franca on 06.12.25. +// +// xcrun xcresulttool get test-results test-details + +struct XCBug: Codable { + let url: String? + let identifier: String? + let title: String? +} + diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCBuildResults.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCBuildResults.swift index d652b80..42b72c2 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCBuildResults.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCBuildResults.swift @@ -4,9 +4,10 @@ // // Created by Alex da Franca on 02.11.25. // +// xcrun xcresulttool get build-results --path example.xcresult struct XCBuildResults: Codable { - let destination: XCDeviceInfo + let destination: XCDevice let startTime: Double // Date as a UNIX timestamp (seconds since midnight UTC on January 1, 1970) let endTime: Double // Date as a UNIX timestamp (seconds since midnight UTC on January 1, 1970) let analyzerWarnings: [XCIssue] @@ -22,7 +23,7 @@ struct XCBuildResults: Codable { extension XCBuildResults { init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) - destination = try values.decode(XCDeviceInfo.self, forKey: .destination) + destination = try values.decode(XCDevice.self, forKey: .destination) startTime = try values.decode(Double.self, forKey: .startTime) endTime = try values.decode(Double.self, forKey: .endTime) analyzerWarnings = try values.decode([XCIssue].self, forKey: .analyzerWarnings) diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCCommonFailureInsight.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCCommonFailureInsight.swift index 6d3d9ba..daf8c7d 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCCommonFailureInsight.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCCommonFailureInsight.swift @@ -4,6 +4,7 @@ // // Created by Alex da Franca on 02.11.25. // +// xcrun xcresulttool get test-results insights struct XCCommonFailureInsight: Codable { let failuresCount: Int diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCConfiguration.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCConfiguration.swift new file mode 100644 index 0000000..56c7668 --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCConfiguration.swift @@ -0,0 +1,16 @@ +// +// XCConfiguration.swift +// Xcresultparser +// +// Created by Alex da Franca on 06.12.25. +// +// xcrun xcresulttool get test-results metrics +// xcrun xcresulttool get test-results summary +// xcrun xcresulttool get test-results activities +// xcrun xcresulttool get test-results tests + +/// Testplan configuration +struct XCConfiguration: Codable { + let configurationId: String // e.g. "1" + let configurationName: String // e.g. "Test Scheme Action" +} diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCContent.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCContent.swift new file mode 100644 index 0000000..25d5225 --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCContent.swift @@ -0,0 +1,19 @@ +// +// XCContent.swift +// Xcresultparser +// +// Created by Alex da Franca on 06.12.25. +// +// xcrun xcresulttool get content-availability --path example.xcresult + +struct XCContent: Codable { + let hasCoverage: Bool + let hasDiagnostics: Bool + let hasTestResults: Bool + let logs: [XCLogType] +} + +enum XCLogType: String, Codable { + case build + case action +} diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCDevice.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCDevice.swift new file mode 100644 index 0000000..c69b95d --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCDevice.swift @@ -0,0 +1,24 @@ +// +// XCDevice.swift +// Xcresultparser +// +// Created by Alex da Franca on 06.12.25. +// +// xcrun xcresulttool get test-results metrics +// xcrun xcresulttool get test-results summary +// xcrun xcresulttool get test-results activities +// xcrun xcresulttool get test-results tests + +struct XCDevice: Codable { + let deviceId: String // e.g. 00008103-000959DC213B001E", + let deviceName: String // e.g. "My Mac", + + // only required in 'xcrun xcresulttool get test-results activities' + let architecture: String? // e.g. "arm64", + let modelName: String? // e.g. "iMac", + let osVersion: String? // e.g. "26.0.1", + + // optional + let platform: String? // e.g. "macOS" + let osBuildNumber: String? // e.g. "25A362", +} diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCDeviceAndConfigurationSummary.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCDeviceAndConfigurationSummary.swift index 04377df..606ffa5 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCDeviceAndConfigurationSummary.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCDeviceAndConfigurationSummary.swift @@ -4,10 +4,11 @@ // // Created by Alex da Franca on 02.11.25. // +// xcrun xcresulttool get test-results summary struct XCDeviceAndConfigurationSummary: Codable { - let device: XCDeviceInfo - let testPlanConfiguration: XCTestPlanConfiguration + let device: XCDevice + let testPlanConfiguration: XCConfiguration let passedTests: Int let failedTests: Int let skippedTests: Int diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCDeviceInfo.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCDeviceInfo.swift deleted file mode 100644 index 037df15..0000000 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCDeviceInfo.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// XCDeviceInfo.swift -// Xcresultparser -// -// Created by Alex da Franca on 02.11.25. -// - -struct XCDeviceInfo: Codable { - let architecture: String // e.g. "arm64", - let deviceId: String // e.g. 00008103-000959DC213B001E", - let deviceName: String // e.g. "My Mac", - let modelName: String // e.g. "iMac", - let osVersion: String // e.g. "26.0.1", - let osBuildNumber: String? // e.g. "25A362", - let platform: String? // e.g. "macOS" -} diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCFailureDistributionInsight.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCFailureDistributionInsight.swift index a123e07..3b6ab6c 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCFailureDistributionInsight.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCFailureDistributionInsight.swift @@ -4,6 +4,7 @@ // // Created by Alex da Franca on 02.11.25. // +// xcrun xcresulttool get test-results insights struct XCFailureDistributionInsight: Codable { let title: String diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCInsightSummary.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCInsightSummary.swift index 3517ef6..1f2787d 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCInsightSummary.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCInsightSummary.swift @@ -4,6 +4,7 @@ // // Created by Alex da Franca on 02.11.25. // +// xcrun xcresulttool get test-results summary struct XCInsightSummary: Codable { let impact: String diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCInsights.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCInsights.swift index 3204b77..a120487 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCInsights.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCInsights.swift @@ -4,6 +4,7 @@ // // Created by Alex da Franca on 02.11.25. // +// xcrun xcresulttool get test-results insights struct XCInsights: Codable { let commonFailureInsights: XCCommonFailureInsight diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCLongestTestRunsInsight.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCLongestTestRunsInsight.swift index 708991a..8cea914 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCLongestTestRunsInsight.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCLongestTestRunsInsight.swift @@ -4,6 +4,7 @@ // // Created by Alex da Franca on 02.11.25. // +// xcrun xcresulttool get test-results insights struct XCLongestTestRunsInsight: Codable { let title: String diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCMetric.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCMetric.swift new file mode 100644 index 0000000..95482ea --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCMetric.swift @@ -0,0 +1,21 @@ +// +// XCMetric.swift +// Xcresultparser +// +// Created by Alex da Franca on 06.12.25. +// +// xcrun xcresulttool get test-results metrics + +struct XCMetric: Codable { + let displayName: String + let unitOfMeasurement: String + let measurements: [Double] + let identifier: String? + let baselineName: String? + let baselineAverage: Double? + let maxRegression: Double? + let maxPercentRegression: Double? + let maxStandardDeviation: Double? + let maxPercentRelativeStandardDeviation: Double? + let polarity: String? +} diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCStatistic.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCStatistic.swift index bcc922a..f1177dc 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCStatistic.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCStatistic.swift @@ -4,6 +4,7 @@ // // Created by Alex da Franca on 02.11.25. // +// xcrun xcresulttool get test-results summary struct XCStatistic: Codable { let title: String diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCSummary.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCSummary.swift index 354896b..2473cc2 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCSummary.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCSummary.swift @@ -4,6 +4,7 @@ // // Created by Alex da Franca on 02.11.25. // +// xcrun xcresulttool get test-results summary struct XCSummary: Codable { let title: String diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCTestDetails.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCTestDetails.swift index aae204f..6425c81 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCTestDetails.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCTestDetails.swift @@ -4,16 +4,17 @@ // // Created by Alex da Franca on 02.11.25. // +// xcrun xcresulttool get test-results test-details struct XCTestDetails: Codable { let testIdentifier: String let testName: String let testDescription: String let duration: String - let testPlanConfigurations: [XCTestPlanConfiguration] - let devices: [XCDeviceInfo] - let testRuns: String - let testResult: String + let testPlanConfigurations: [XCConfiguration] + let devices: [XCDevice] + let testRuns: [XCTestNode] + let testResult: XCTestResult let hasPerformanceMetrics: String let hasMediaAttachments: String @@ -23,5 +24,10 @@ struct XCTestDetails: Codable { let durationInSeconds: Double? // Date as a UNIX timestamp (seconds since midnight UTC on January 1, 1970) let startTime: Double? - let arguments: [] + let arguments: [XCArgument]? + + // only in 'xcrun xcresulttool get test-results test-details' + let functionName: String? + let bugs: [XCBug]? + let tags: [String]? } diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCTestFailure.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCTestFailure.swift index 00aa8c5..c3bbadd 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCTestFailure.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCTestFailure.swift @@ -4,6 +4,7 @@ // // Created by Alex da Franca on 02.11.25. // +// xcrun xcresulttool get test-results summary import Foundation diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCTestNode.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCTestNode.swift index 15b6ac8..0e1d777 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCTestNode.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCTestNode.swift @@ -4,13 +4,14 @@ // // Created by Alex da Franca on 02.11.25. // +// xcrun xcresulttool get test-results tests import Foundation struct XCTestNode: Codable { - let children: [XCTestNode] let name: String // e.g. "xcresultparser", let nodeType: XCTestNodeType // e.g. "Test Plan", + let children: [XCTestNode]? let result: XCTestResult? // e.g. "Passed" let nodeIdentifier: String? // e.g. "0" let nodeIdentifierURL: URL? // e.g. "test://com.apple.xcode/Xcresultparser/XcresultparserTests/XcresultparserTests" diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCTestNodeType.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCTestNodeType.swift index 4218899..73b2074 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCTestNodeType.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCTestNodeType.swift @@ -4,6 +4,7 @@ // // Created by Alex da Franca on 02.11.25. // +// xcrun xcresulttool get test-results tests enum XCTestNodeType: String, Codable { case testPlan = "Test Plan" diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCTestPlanConfiguration.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCTestPlanConfiguration.swift deleted file mode 100644 index 9b100d1..0000000 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCTestPlanConfiguration.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// XCTestPlanConfiguration.swift -// Xcresultparser -// -// Created by Alex da Franca on 02.11.25. -// - -struct XCTestPlanConfiguration: Codable { - let configurationId: String // e.g. "1" - let configurationName: String // e.g. "Test Scheme Action" -} diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCTestResult.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCTestResult.swift index 0316735..ed1ffeb 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCTestResult.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCTestResult.swift @@ -4,6 +4,8 @@ // // Created by Alex da Franca on 02.11.25. // +// xcrun xcresulttool get test-results summary +// xcrun xcresulttool get test-results tests enum XCTestResult: String, Codable { case passed = "Passed" diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCTestResults.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCTestResults.swift deleted file mode 100644 index dd562d5..0000000 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCTestResults.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// XCTestResults.swift -// Xcresultparser -// -// Created by Alex da Franca on 02.11.25. -// - - -struct XCTestResults: Codable { - let devices: [XCDeviceInfo] - let testNodes: [XCTestNode] - let testPlanConfigurations: [XCTestPlanConfiguration] -} diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCTestRunActivities.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCTestRunActivities.swift new file mode 100644 index 0000000..c6cd6f4 --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCTestRunActivities.swift @@ -0,0 +1,14 @@ +// +// XCTestRunActivities.swift +// Xcresultparser +// +// Created by Alex da Franca on 06.12.25. +// +// xcrun xcresulttool get test-results activities + +struct XCTestRunActivities: Codable { + let device: String + let testPlanConfiguration: String + let activities: [XCActivityNode] + let arguments: [XCArgument]? +} diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCTestRunWithMetrics.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCTestRunWithMetrics.swift new file mode 100644 index 0000000..1f32c65 --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCTestRunWithMetrics.swift @@ -0,0 +1,13 @@ +// +// XCTestRunWithMetrics.swift +// Xcresultparser +// +// Created by Alex da Franca on 06.12.25. +// +// xcrun xcresulttool get test-results metrics + +struct XCTestRunWithMetrics: Codable { + let testPlanConfiguration: XCConfiguration + let device: XCDevice + let metrics: [XCMetric] +} diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCTestWithMetrics.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCTestWithMetrics.swift new file mode 100644 index 0000000..84951fd --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCTestWithMetrics.swift @@ -0,0 +1,13 @@ +// +// XCTestWithMetrics.swift +// Xcresultparser +// +// Created by Alex da Franca on 06.12.25. +// +// xcrun xcresulttool get test-results metrics + +struct XCTestWithMetrics: Codable { + let testIdentifier: String + let testRuns: [XCTestRunWithMetrics] + let testIdentifierURL: String? +} diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCTests.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCTests.swift new file mode 100644 index 0000000..f300f86 --- /dev/null +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCTests.swift @@ -0,0 +1,13 @@ +// +// XCTests.swift +// Xcresultparser +// +// Created by Alex da Franca on 06.12.25. +// +// xcrun xcresulttool get test-results tests + +struct XCTests: Codable { + let testPlanConfigurations: [XCConfiguration] + let devices: [XCDevice] + let testNodes: [XCTestNode] +} From b03fb436b891f501d3ea717cda798bc939e092fa Mon Sep 17 00:00:00 2001 From: Alex da Franca Date: Fri, 20 Feb 2026 17:40:02 +0100 Subject: [PATCH 03/14] Stabelize xcresulttool schema --- .../XCFailureDistributionInsight.swift | 23 +++- .../XCResultToolModels/XCInsights.swift | 15 ++- .../Models/XCResultToolModels/XCSummary.swift | 16 +-- .../XCResultToolModels/XCTestDetails.swift | 43 +++++-- .../XCResultToolModels/XCTestNode.swift | 4 +- .../XCResultToolModels/XCTestNodeType.swift | 11 +- .../XCResultToolModels/XCTestResult.swift | 8 ++ .../XcresultparserTests.swift | 119 ++++++++++++++++++ 8 files changed, 217 insertions(+), 22 deletions(-) diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCFailureDistributionInsight.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCFailureDistributionInsight.swift index 3b6ab6c..cef042b 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCFailureDistributionInsight.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCFailureDistributionInsight.swift @@ -8,13 +8,32 @@ struct XCFailureDistributionInsight: Codable { let title: String - let impact: String + let impact: Int let distributionPercent: Double let associatedTestIdentifiers: [String] // Optional let bug: String? let tag: String? - let destinations: [String]? + let destinations: [XCDestination]? } +extension XCFailureDistributionInsight { + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + title = try values.decode(String.self, forKey: .title) + distributionPercent = try values.decode(Double.self, forKey: .distributionPercent) + associatedTestIdentifiers = try values.decode([String].self, forKey: .associatedTestIdentifiers) + if let intImpact = try? values.decode(Int.self, forKey: .impact) { + impact = intImpact + } else if let stringImpact = try? values.decode(String.self, forKey: .impact), + let parsedImpact = Int(stringImpact) { + impact = parsedImpact + } else { + impact = 0 + } + bug = try? values.decode(String.self, forKey: .bug) + tag = try? values.decode(String.self, forKey: .tag) + destinations = try? values.decode([XCDestination].self, forKey: .destinations) + } +} diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCInsights.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCInsights.swift index a120487..75f1094 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCInsights.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCInsights.swift @@ -7,7 +7,16 @@ // xcrun xcresulttool get test-results insights struct XCInsights: Codable { - let commonFailureInsights: XCCommonFailureInsight - let longestTestRunsInsights: XCLongestTestRunsInsight - let failureDistributionInsights: XCFailureDistributionInsight + let commonFailureInsights: [XCCommonFailureInsight] + let longestTestRunsInsights: [XCLongestTestRunsInsight] + let failureDistributionInsights: [XCFailureDistributionInsight] +} + +extension XCInsights { + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + commonFailureInsights = (try? values.decode([XCCommonFailureInsight].self, forKey: .commonFailureInsights)) ?? [] + longestTestRunsInsights = (try? values.decode([XCLongestTestRunsInsight].self, forKey: .longestTestRunsInsights)) ?? [] + failureDistributionInsights = (try? values.decode([XCFailureDistributionInsight].self, forKey: .failureDistributionInsights)) ?? [] + } } diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCSummary.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCSummary.swift index 2473cc2..1820c9a 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCSummary.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCSummary.swift @@ -18,14 +18,14 @@ struct XCSummary: Codable { let skippedTests: Int let expectedFailures: Int let statistics: [XCStatistic] - let devicesAndConfigurations: XCDeviceAndConfigurationSummary - let testFailures: XCTestFailure + let devicesAndConfigurations: [XCDeviceAndConfigurationSummary] + let testFailures: [XCTestFailure] // Optional: // Date as a UNIX timestamp (seconds since midnight UTC on January 1, 1970) - let startTime: Double + let startTime: Double? // Date as a UNIX timestamp (seconds since midnight UTC on January 1, 1970) - let finishTime: Double + let finishTime: Double? } extension XCSummary { @@ -41,9 +41,9 @@ extension XCSummary { skippedTests = try values.decode(Int.self, forKey: .skippedTests) expectedFailures = try values.decode(Int.self, forKey: .expectedFailures) statistics = try values.decode([XCStatistic].self, forKey: .statistics) - devicesAndConfigurations = try values.decode(XCDeviceAndConfigurationSummary.self, forKey: .devicesAndConfigurations) - testFailures = try values.decode(XCTestFailure.self, forKey: .testFailures) - startTime = (try? values.decode(Double.self, forKey: .startTime)) ?? 0 - finishTime = (try? values.decode(Double.self, forKey: .finishTime)) ?? 0 + devicesAndConfigurations = (try? values.decode([XCDeviceAndConfigurationSummary].self, forKey: .devicesAndConfigurations)) ?? [] + testFailures = (try? values.decode([XCTestFailure].self, forKey: .testFailures)) ?? [] + startTime = try? values.decode(Double.self, forKey: .startTime) + finishTime = try? values.decode(Double.self, forKey: .finishTime) } } diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCTestDetails.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCTestDetails.swift index 6425c81..74d236b 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCTestDetails.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCTestDetails.swift @@ -15,8 +15,8 @@ struct XCTestDetails: Codable { let devices: [XCDevice] let testRuns: [XCTestNode] let testResult: XCTestResult - let hasPerformanceMetrics: String - let hasMediaAttachments: String + let hasPerformanceMetrics: Bool + let hasMediaAttachments: Bool // Human-readable duration with optional components of days, hours, minutes and seconds let testIdentifierURL: String? @@ -24,10 +24,39 @@ struct XCTestDetails: Codable { let durationInSeconds: Double? // Date as a UNIX timestamp (seconds since midnight UTC on January 1, 1970) let startTime: Double? - let arguments: [XCArgument]? - - // only in 'xcrun xcresulttool get test-results test-details' + let arguments: [XCArgument] + let tags: [String] + let bugs: [XCBug] let functionName: String? - let bugs: [XCBug]? - let tags: [String]? +} + +extension XCTestDetails { + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + testIdentifier = try values.decode(String.self, forKey: .testIdentifier) + testName = try values.decode(String.self, forKey: .testName) + testDescription = try values.decode(String.self, forKey: .testDescription) + duration = try values.decode(String.self, forKey: .duration) + testPlanConfigurations = try values.decode([XCConfiguration].self, forKey: .testPlanConfigurations) + devices = try values.decode([XCDevice].self, forKey: .devices) + testRuns = try values.decode([XCTestNode].self, forKey: .testRuns) + testResult = try values.decode(XCTestResult.self, forKey: .testResult) + if let performanceFlag = try? values.decode(Bool.self, forKey: .hasPerformanceMetrics) { + hasPerformanceMetrics = performanceFlag + } else { + hasPerformanceMetrics = (try? values.decode(String.self, forKey: .hasPerformanceMetrics)) == "true" + } + if let mediaFlag = try? values.decode(Bool.self, forKey: .hasMediaAttachments) { + hasMediaAttachments = mediaFlag + } else { + hasMediaAttachments = (try? values.decode(String.self, forKey: .hasMediaAttachments)) == "true" + } + testIdentifierURL = try? values.decode(String.self, forKey: .testIdentifierURL) + durationInSeconds = try? values.decode(Double.self, forKey: .durationInSeconds) + startTime = try? values.decode(Double.self, forKey: .startTime) + arguments = (try? values.decode([XCArgument].self, forKey: .arguments)) ?? [] + tags = (try? values.decode([String].self, forKey: .tags)) ?? [] + bugs = (try? values.decode([XCBug].self, forKey: .bugs)) ?? [] + functionName = try? values.decode(String.self, forKey: .functionName) + } } diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCTestNode.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCTestNode.swift index 0e1d777..550e25e 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCTestNode.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCTestNode.swift @@ -15,6 +15,7 @@ struct XCTestNode: Codable { let result: XCTestResult? // e.g. "Passed" let nodeIdentifier: String? // e.g. "0" let nodeIdentifierURL: URL? // e.g. "test://com.apple.xcode/Xcresultparser/XcresultparserTests/XcresultparserTests" + let duration: String? let durationInSeconds: TimeInterval? // e.g. 19 let details: String? let tags: [String] @@ -39,8 +40,9 @@ extension XCTestNode { nodeIdentifierURL = nil } children = (try? values.decode([XCTestNode].self, forKey: .children)) ?? [XCTestNode]() + duration = try? values.decode(String.self, forKey: .duration) durationInSeconds = try? values.decode(TimeInterval.self, forKey: .durationInSeconds) details = try? values.decode(String.self, forKey: .details) - tags = (try? values.decode([String].self, forKey: .nodeIdentifier)) ?? [String]() + tags = (try? values.decode([String].self, forKey: .tags)) ?? [] } } diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCTestNodeType.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCTestNodeType.swift index 73b2074..2cd460a 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCTestNodeType.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCTestNodeType.swift @@ -22,5 +22,14 @@ enum XCTestNodeType: String, Codable { case attachment = "Attachment" case expression = "Expression" case testValue = "Test Value" - case runtimWarning = "Runtime Warning" + case runtimeWarning = "Runtime Warning" + case unknown = "unknown" +} + +extension XCTestNodeType { + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let value = try container.decode(String.self) + self = XCTestNodeType(rawValue: value) ?? .unknown + } } diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCTestResult.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCTestResult.swift index ed1ffeb..c17e8a3 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCTestResult.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCTestResult.swift @@ -14,3 +14,11 @@ enum XCTestResult: String, Codable { case expectedFailure = "Expected Failure" case unknown = "unknown" } + +extension XCTestResult { + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let value = try container.decode(String.self) + self = XCTestResult(rawValue: value) ?? .unknown + } +} diff --git a/Tests/XcresultparserTests/XcresultparserTests.swift b/Tests/XcresultparserTests/XcresultparserTests.swift index 3a5beb3..7e02ef7 100644 --- a/Tests/XcresultparserTests/XcresultparserTests.swift +++ b/Tests/XcresultparserTests/XcresultparserTests.swift @@ -716,6 +716,125 @@ struct XcresultparserTests { #expect(sut == CoverageReportFormat.classes) } + @Test + func testXCResultToolSummaryModelDecoding() throws { + let json = """ + { + "devicesAndConfigurations": [ + { + "device": { + "architecture": "arm64", + "deviceId": "00006000-001869EC3623801E", + "deviceName": "My Mac", + "modelName": "MacBook Pro", + "osBuildNumber": "22C65", + "osVersion": "13.1", + "platform": "macOS" + }, + "expectedFailures": 0, + "failedTests": 1, + "passedTests": 6, + "skippedTests": 0, + "testPlanConfiguration": { + "configurationId": "1", + "configurationName": "Test Scheme Action" + } + } + ], + "environmentDescription": "Xcresultparser-Package · Built with macOS 13.1", + "expectedFailures": 0, + "failedTests": 1, + "finishTime": 1672825230.228, + "passedTests": 6, + "result": "Failed", + "skippedTests": 0, + "startTime": 1672825221.218, + "statistics": [], + "testFailures": [ + { + "failureText": "failed - example", + "targetName": "XcresultparserTests", + "testIdentifier": 2, + "testIdentifierString": "XcresultparserTests/testCoverageConverter()", + "testIdentifierURL": "test://com.apple.xcode/Xcresultparser/XcresultparserTests/XcresultparserTests/testCoverageConverter", + "testName": "testCoverageConverter()" + } + ], + "title": "Test - Xcresultparser-Package", + "topInsights": [], + "totalTestCount": 7 + } + """ + let summary = try JSONDecoder().decode(XCSummary.self, from: Data(json.utf8)) + #expect(summary.failedTests == 1) + #expect(summary.devicesAndConfigurations.count == 1) + #expect(summary.testFailures.count == 1) + } + + @Test + func testXCResultToolTestsModelDecoding() throws { + let json = """ + { + "devices": [ + { + "architecture": "arm64", + "deviceId": "00006000-001869EC3623801E", + "deviceName": "My Mac", + "modelName": "MacBook Pro", + "osBuildNumber": "22C65", + "osVersion": "13.1", + "platform": "macOS" + } + ], + "testNodes": [ + { + "children": [ + { + "children": [], + "duration": "0,31s", + "durationInSeconds": 0.30731201171875, + "name": "testCLIResultFormatter()", + "nodeType": "Test Case", + "result": "Passed" + } + ], + "name": "Test Plan", + "nodeType": "Test Plan", + "result": "Failed" + } + ], + "testPlanConfigurations": [ + { + "configurationId": "1", + "configurationName": "Test Scheme Action" + } + ] + } + """ + let testResults = try JSONDecoder().decode(XCTests.self, from: Data(json.utf8)) + #expect(testResults.devices.count == 1) + #expect(testResults.testNodes.count == 1) + let firstNode = try #require(testResults.testNodes.first) + let children = firstNode.children ?? [] + #expect(children.count == 1) + #expect(children.first?.nodeType == .testCase) + } + + @Test + func testXCResultToolInsightsModelDecoding() throws { + let json = """ + { + "commonFailureInsights": [], + "failureDistributionInsights": [], + "longestTestRunsInsights": [] + } + """ + let insights = try JSONDecoder().decode(XCInsights.self, from: Data(json.utf8)) + #expect(insights.commonFailureInsights.isEmpty) + #expect(insights.failureDistributionInsights.isEmpty) + #expect(insights.longestTestRunsInsights.isEmpty) + } + // MARK: helper functions private func makeTestMetadata( From 059952834af2f2b17ec14beeff3899df6ac7b860 Mon Sep 17 00:00:00 2001 From: Alex da Franca Date: Fri, 20 Feb 2026 17:47:44 +0100 Subject: [PATCH 04/14] added tests for the new codable models --- .../XCResultToolModels/XCAttachment.swift | 3 +- .../XCTestRunActivities.swift | 4 +- .../XcresultparserTests.swift | 174 ++++++++++++++++++ 3 files changed, 177 insertions(+), 4 deletions(-) diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCAttachment.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCAttachment.swift index a4806ad..ecc6bc0 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCAttachment.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCAttachment.swift @@ -9,8 +9,7 @@ struct XCAttachment: Codable { let name: String let uuid: String - let timestamp: Double // Date as a UNIX timestamp (seconds since midnight UTC on January 1, 1970) + let timestamp: Double? // Date as a UNIX timestamp (seconds since midnight UTC on January 1, 1970) let payloadId: String? let lifetime: String? } - diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCTestRunActivities.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCTestRunActivities.swift index c6cd6f4..ee82767 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCTestRunActivities.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCTestRunActivities.swift @@ -7,8 +7,8 @@ // xcrun xcresulttool get test-results activities struct XCTestRunActivities: Codable { - let device: String - let testPlanConfiguration: String + let device: XCDevice + let testPlanConfiguration: XCConfiguration let activities: [XCActivityNode] let arguments: [XCArgument]? } diff --git a/Tests/XcresultparserTests/XcresultparserTests.swift b/Tests/XcresultparserTests/XcresultparserTests.swift index 7e02ef7..03e16f9 100644 --- a/Tests/XcresultparserTests/XcresultparserTests.swift +++ b/Tests/XcresultparserTests/XcresultparserTests.swift @@ -835,6 +835,180 @@ struct XcresultparserTests { #expect(insights.longestTestRunsInsights.isEmpty) } + @Test + func testXCResultToolBuildResultsModelDecoding() throws { + let json = """ + { + "analyzerWarningCount": 0, + "analyzerWarnings": [], + "destination": { + "architecture": "arm64", + "deviceId": "00006000-001869EC3623801E", + "deviceName": "My Mac", + "modelName": "MacBook Pro", + "osBuildNumber": "22C65", + "osVersion": "13.1", + "platform": "macOS" + }, + "endTime": 1672825230.228, + "errorCount": 0, + "errors": [], + "startTime": 1672825221.218, + "status": "succeeded", + "warningCount": 3, + "warnings": [ + { + "className": "DVTTextDocumentLocation", + "issueType": "No-usage", + "message": "Initialization warning", + "sourceURL": "file:///tmp/example.swift#L1" + } + ] + } + """ + let buildResults = try JSONDecoder().decode(XCBuildResults.self, from: Data(json.utf8)) + #expect(buildResults.warningCount == 3) + #expect(buildResults.warnings.count == 1) + #expect(buildResults.destination.deviceName == "My Mac") + } + + @Test + func testXCResultToolTestDetailsModelDecoding() throws { + let json = """ + { + "devices": [ + { + "architecture": "arm64", + "deviceId": "00006000-001869EC3623801E", + "deviceName": "My Mac", + "modelName": "MacBook Pro", + "osBuildNumber": "22C65", + "osVersion": "13.1", + "platform": "macOS" + } + ], + "duration": "Ran for 2,8 seconds", + "durationInSeconds": 2.8180439472198486, + "hasMediaAttachments": false, + "hasPerformanceMetrics": false, + "testDescription": "Test case with 1 run", + "testIdentifier": "XcresultparserTests/testCoverageConverter()", + "testIdentifierURL": "test://com.apple.xcode/Xcresultparser/XcresultparserTests/XcresultparserTests/testCoverageConverter", + "testName": "testCoverageConverter()", + "testPlanConfigurations": [ + { + "configurationId": "1", + "configurationName": "Test Scheme Action" + } + ], + "testResult": "Failed", + "testRuns": [ + { + "details": "macOS 13.1", + "duration": "2s", + "durationInSeconds": 2.8180439472198486, + "name": "MacBook Pro", + "nodeIdentifier": "00006000-001869EC3623801E", + "nodeType": "Device", + "result": "Failed" + } + ] + } + """ + let details = try JSONDecoder().decode(XCTestDetails.self, from: Data(json.utf8)) + #expect(details.testIdentifier == "XcresultparserTests/testCoverageConverter()") + #expect(details.testRuns.count == 1) + #expect(details.hasMediaAttachments == false) + } + + @Test + func testXCResultToolActivitiesModelDecoding() throws { + let json = """ + { + "testIdentifier": "XcresultparserTests/testCoverageConverter()", + "testIdentifierURL": "test://com.apple.xcode/Xcresultparser/XcresultparserTests/XcresultparserTests/testCoverageConverter", + "testName": "testCoverageConverter()", + "testRuns": [ + { + "activities": [ + { + "attachments": [ + { + "lifetime": "", + "name": "Complete Issue Description.txt", + "uuid": "AB4FA017-B76D-490D-87D3-F55A5B9BE79E" + } + ], + "childActivities": [ + { + "isAssociatedWithFailure": false, + "title": "Complete Issue Description" + } + ], + "isAssociatedWithFailure": true, + "startTime": 1672825226.088, + "title": "failed - Unable to create CoverageConverter" + } + ], + "device": { + "architecture": "arm64", + "deviceId": "00006000-001869EC3623801E", + "deviceName": "My Mac", + "modelName": "MacBook Pro", + "osBuildNumber": "22C65", + "osVersion": "13.1", + "platform": "macOS" + }, + "testPlanConfiguration": { + "configurationId": "1", + "configurationName": "Test Scheme Action" + } + } + ] + } + """ + let activities = try JSONDecoder().decode(XCActivities.self, from: Data(json.utf8)) + #expect(activities.testRuns.count == 1) + #expect(activities.testRuns[0].activities.count == 1) + #expect(activities.testRuns[0].device.deviceName == "My Mac") + } + + @Test + func testXCResultToolMetricsModelDecoding() throws { + let json = """ + { + "testIdentifier": "PerfTests/testExample()", + "testIdentifierURL": "test://com.apple.xcode/PerfTests/testExample", + "testRuns": [ + { + "testPlanConfiguration": { + "configurationId": "1", + "configurationName": "Test Scheme Action" + }, + "device": { + "deviceId": "00006000-001869EC3623801E", + "deviceName": "My Mac" + }, + "metrics": [ + { + "displayName": "Clock Time", + "unitOfMeasurement": "s", + "measurements": [1.2, 1.3] + } + ] + } + ] + } + """ + let metrics = try JSONDecoder().decode(XCTestWithMetrics.self, from: Data(json.utf8)) + #expect(metrics.testRuns.count == 1) + #expect(metrics.testRuns[0].metrics.count == 1) + #expect(metrics.testRuns[0].metrics[0].displayName == "Clock Time") + + let emptyMetrics = try JSONDecoder().decode([XCTestWithMetrics].self, from: Data("[]".utf8)) + #expect(emptyMetrics.isEmpty) + } + // MARK: helper functions private func makeTestMetadata( From a8c91547cafb04ae457a5343a8a142c5388546bf Mon Sep 17 00:00:00 2001 From: Alex da Franca Date: Fri, 20 Feb 2026 18:08:31 +0100 Subject: [PATCH 05/14] Add XCResultToolClient to interface with the xcresulttool API --- .../Services/XCResultToolClient.swift | 82 ++++++++++ .../XCResultToolClientTests.swift | 144 ++++++++++++++++++ 2 files changed, 226 insertions(+) create mode 100644 Sources/xcresultparser/SharedTypes/Services/XCResultToolClient.swift create mode 100644 Tests/XcresultparserTests/XCResultToolClientTests.swift diff --git a/Sources/xcresultparser/SharedTypes/Services/XCResultToolClient.swift b/Sources/xcresultparser/SharedTypes/Services/XCResultToolClient.swift new file mode 100644 index 0000000..fe1e660 --- /dev/null +++ b/Sources/xcresultparser/SharedTypes/Services/XCResultToolClient.swift @@ -0,0 +1,82 @@ +// +// XCResultToolClient.swift +// Xcresultparser +// +// Created by Alex da Franca on 20.02.26. +// + +import Foundation + +struct XCResultToolClient { + enum ToolClientError: Error, Equatable { + case invalidUTF8 + case unexpectedRootValue(String) + } + + private let shell: Commandline + private let decoder: JSONDecoder + + init( + shell: Commandline = DependencyFactory.createShell(), + decoder: JSONDecoder = JSONDecoder() + ) { + self.shell = shell + self.decoder = decoder + } + + func getBuildResults(path: URL) throws -> XCBuildResults { + let data = try execute(arguments: ["get", "build-results", "--path", path.path]) + return try decode(XCBuildResults.self, from: data) + } + + func getTestSummary(path: URL) throws -> XCSummary { + let data = try execute(arguments: ["get", "test-results", "summary", "--path", path.path]) + return try decode(XCSummary.self, from: data) + } + + func getTests(path: URL) throws -> XCTests { + let data = try execute(arguments: ["get", "test-results", "tests", "--path", path.path]) + return try decode(XCTests.self, from: data) + } + + func getTestDetails(path: URL, testId: String) throws -> XCTestDetails { + let data = try execute( + arguments: ["get", "test-results", "test-details", "--path", path.path, "--test-id", testId] + ) + return try decode(XCTestDetails.self, from: data) + } + + func getActivities(path: URL, testId: String) throws -> XCActivities { + let data = try execute( + arguments: ["get", "test-results", "activities", "--path", path.path, "--test-id", testId] + ) + return try decode(XCActivities.self, from: data) + } + + // xcresulttool currently returns [] for tests without performance metrics. + func getMetrics(path: URL, testId: String) throws -> [XCTestWithMetrics] { + let data = try execute( + arguments: ["get", "test-results", "metrics", "--path", path.path, "--test-id", testId] + ) + + if let object = try? decode(XCTestWithMetrics.self, from: data) { + return [object] + } + if let array = try? decode([XCTestWithMetrics].self, from: data) { + return array + } + + if let raw = String(data: data, encoding: .utf8) { + throw ToolClientError.unexpectedRootValue(raw) + } + throw ToolClientError.invalidUTF8 + } + + private func execute(arguments: [String]) throws -> Data { + try shell.execute(program: "/usr/bin/xcrun", with: ["xcresulttool"] + arguments) + } + + private func decode(_ type: T.Type, from data: Data) throws -> T { + try decoder.decode(type, from: data) + } +} diff --git a/Tests/XcresultparserTests/XCResultToolClientTests.swift b/Tests/XcresultparserTests/XCResultToolClientTests.swift new file mode 100644 index 0000000..011aaf5 --- /dev/null +++ b/Tests/XcresultparserTests/XCResultToolClientTests.swift @@ -0,0 +1,144 @@ +import Foundation +@testable import XcresultparserLib +import Testing + +@MainActor +struct XCResultToolClientTests { + @Test + func testGetBuildResultsUsesExpectedCommandAndDecodes() throws { + let json = """ + { + "destination": { + "deviceId": "device-1", + "deviceName": "My Mac" + }, + "startTime": 1.0, + "endTime": 2.0, + "analyzerWarnings": [], + "warnings": [], + "errors": [] + } + """ + let shell = CapturingShell(response: Data(json.utf8)) + let client = XCResultToolClient(shell: shell) + let path = URL(fileURLWithPath: "/tmp/test.xcresult") + + let result = try client.getBuildResults(path: path) + + #expect(result.destination.deviceName == "My Mac") + #expect(shell.lastProgram == "/usr/bin/xcrun") + #expect(shell.lastArguments == ["xcresulttool", "get", "build-results", "--path", "/tmp/test.xcresult"]) + } + + @Test + func testGetTestDetailsUsesExpectedCommandAndDecodes() throws { + let json = """ + { + "testIdentifier": "A/B", + "testName": "testFoo()", + "testDescription": "Test case with 1 run", + "duration": "0,01s", + "testPlanConfigurations": [{"configurationId":"1","configurationName":"Default"}], + "devices": [{"deviceId":"device-1","deviceName":"My Mac"}], + "testRuns": [{"name":"testFoo()","nodeType":"Test Case"}], + "testResult": "Passed", + "hasPerformanceMetrics": false, + "hasMediaAttachments": false + } + """ + let shell = CapturingShell(response: Data(json.utf8)) + let client = XCResultToolClient(shell: shell) + let path = URL(fileURLWithPath: "/tmp/test.xcresult") + + let result = try client.getTestDetails(path: path, testId: "A/B") + + #expect(result.testName == "testFoo()") + #expect(shell.lastArguments == [ + "xcresulttool", "get", "test-results", "test-details", + "--path", "/tmp/test.xcresult", + "--test-id", "A/B" + ]) + } + + @Test + func testGetMetricsDecodesObjectAsSingleElementArray() throws { + let json = """ + { + "testIdentifier": "PerfTests/testExample()", + "testRuns": [ + { + "testPlanConfiguration": { + "configurationId": "1", + "configurationName": "Default" + }, + "device": { + "deviceId": "device-1", + "deviceName": "My Mac" + }, + "metrics": [ + { + "displayName": "Clock Time", + "unitOfMeasurement": "s", + "measurements": [1.2, 1.3] + } + ] + } + ] + } + """ + let shell = CapturingShell(response: Data(json.utf8)) + let client = XCResultToolClient(shell: shell) + + let result = try client.getMetrics(path: URL(fileURLWithPath: "/tmp/test.xcresult"), testId: "PerfTests/testExample()") + + #expect(result.count == 1) + #expect(result[0].testRuns.count == 1) + #expect(result[0].testRuns[0].metrics[0].displayName == "Clock Time") + } + + @Test + func testGetMetricsDecodesEmptyArray() throws { + let shell = CapturingShell(response: Data("[]".utf8)) + let client = XCResultToolClient(shell: shell) + + let result = try client.getMetrics(path: URL(fileURLWithPath: "/tmp/test.xcresult"), testId: "NoPerf/test") + + #expect(result.isEmpty) + } + + @Test + func testGetMetricsThrowsForUnexpectedPayload() throws { + let shell = CapturingShell(response: Data("{\"foo\":\"bar\"}".utf8)) + let client = XCResultToolClient(shell: shell) + + do { + _ = try client.getMetrics(path: URL(fileURLWithPath: "/tmp/test.xcresult"), testId: "NoPerf/test") + Issue.record("Expected getMetrics to throw for unexpected payload.") + } catch let error as XCResultToolClient.ToolClientError { + switch error { + case .unexpectedRootValue: + #expect(true) + default: + Issue.record("Unexpected XCResultToolClient error: \(error)") + } + } catch { + Issue.record("Unexpected error type: \(error)") + } + } +} + +private final class CapturingShell: Commandline { + let response: Data + var lastProgram: String = "" + var lastArguments: [String] = [] + + init(response: Data) { + self.response = response + } + + func execute(program: String, with arguments: [String], at executionPath: URL?) throws -> Data { + lastProgram = program + lastArguments = arguments + return response + } +} From 8a830025fa70334830ab8cd70317b549034ee22c Mon Sep 17 00:00:00 2001 From: Alex da Franca Date: Sat, 21 Feb 2026 17:23:20 +0100 Subject: [PATCH 06/14] Nugrate CovergaeConverter --- CommandlineTool/main.swift | 2 +- .../CoberturaCoverageConverter.swift | 12 +- .../xcresultparser/CoverageConverter.swift | 100 +++++-- .../JunitXML/JunitXMLDataProviding.swift | 58 ++++ .../XCResultToolJunitXMLDataProvider.swift | 275 ++++++++++++++++++ Sources/xcresultparser/JunitXML.swift | 124 ++++---- .../Models/Coverage/CoverageReport.swift | 23 ++ .../SonarCoverageConverter.swift | 3 + .../XcresultparserTests/TestAssets/junit.xml | 4 +- .../TestAssets/junit_merged.xml | 17 +- .../TestAssets/junit_repeated.xml | 18 +- .../TestAssets/sonarTestExecution.xml | 2 +- ...arTestExecutionWithProjectRootAbsolute.xml | 2 +- ...arTestExecutionWithProjectRootRelative.xml | 2 +- ...CResultToolJunitXMLDataProviderTests.swift | 167 +++++++++++ .../XcresultparserTests.swift | 114 ++++---- 16 files changed, 748 insertions(+), 175 deletions(-) create mode 100644 Sources/xcresultparser/DataProviders/JunitXML/JunitXMLDataProviding.swift create mode 100644 Sources/xcresultparser/DataProviders/JunitXML/XCResultToolJunitXMLDataProvider.swift create mode 100644 Sources/xcresultparser/Models/Coverage/CoverageReport.swift create mode 100644 Tests/XcresultparserTests/XCResultToolJunitXMLDataProviderTests.swift diff --git a/CommandlineTool/main.swift b/CommandlineTool/main.swift index f661360..bae6270 100644 --- a/CommandlineTool/main.swift +++ b/CommandlineTool/main.swift @@ -9,7 +9,7 @@ import ArgumentParser import Foundation import XcresultparserLib -private let marketingVersion = "1.9.4" +private let marketingVersion = "2.0.0" struct xcresultparser: ParsableCommand { static let configuration = CommandConfiguration( diff --git a/Sources/xcresultparser/CoberturaCoverageConverter.swift b/Sources/xcresultparser/CoberturaCoverageConverter.swift index e24b6bd..2f7df8c 100644 --- a/Sources/xcresultparser/CoberturaCoverageConverter.swift +++ b/Sources/xcresultparser/CoberturaCoverageConverter.swift @@ -69,6 +69,9 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { var fileInfo: [FileInfo] = [] for (fileName, value) in coverageJson.files { + guard isTargetIncluded(forFile: fileName) else { + continue + } guard !isPathExcluded(fileName) else { continue } @@ -164,18 +167,17 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { private func makeRootElement() -> XMLElement { // TODO: some of these values are B.S. - figure out how to calculate, or better to omit if we don't know? - let testAction = invocationRecord.actions.first { $0.schemeCommandName == "Test" } - let timeStamp = (testAction?.startedTime.timeIntervalSince1970) ?? Date().timeIntervalSince1970 + let timeStamp = startTime ?? Date().timeIntervalSince1970 let rootElement = XMLElement(name: "coverage") rootElement.addAttribute( - XMLNode.nodeAttribute(withName: "line-rate", stringValue: "\(codeCoverage.lineCoverage)") + XMLNode.nodeAttribute(withName: "line-rate", stringValue: "\(lineCoverage)") ) rootElement.addAttribute(XMLNode.nodeAttribute(withName: "branch-rate", stringValue: "1.0")) rootElement.addAttribute( - XMLNode.nodeAttribute(withName: "lines-covered", stringValue: "\(codeCoverage.coveredLines)") + XMLNode.nodeAttribute(withName: "lines-covered", stringValue: "\(coveredLines)") ) rootElement.addAttribute( - XMLNode.nodeAttribute(withName: "lines-valid", stringValue: "\(codeCoverage.executableLines)") + XMLNode.nodeAttribute(withName: "lines-valid", stringValue: "\(executableLines)") ) rootElement.addAttribute(XMLNode.nodeAttribute(withName: "timestamp", stringValue: "\(timeStamp)")) rootElement.addAttribute(XMLNode.nodeAttribute(withName: "version", stringValue: "diff_coverage 0.1")) diff --git a/Sources/xcresultparser/CoverageConverter.swift b/Sources/xcresultparser/CoverageConverter.swift index 16d6855..55366d8 100644 --- a/Sources/xcresultparser/CoverageConverter.swift +++ b/Sources/xcresultparser/CoverageConverter.swift @@ -6,7 +6,6 @@ // import Foundation -import XCResultKit /// Convert coverage data in a xcresult archive to xml (exact format determined by subclass) /// @@ -24,17 +23,19 @@ import XCResultKit /// Read the Readme for further info on this. /// public class CoverageConverter { - let resultFile: XCResultFile + let resultFileURL: URL let projectRoot: String - let codeCoverage: CodeCoverage - let invocationRecord: ActionsInvocationRecord let coverageTargets: Set + let coverageReport: CoverageReport + let filesForIncludedTargets: Set let excludedPaths: Set let strictPathnames: Bool + let startTime: Double? // MARK: - Dependencies - let shell = DependencyFactory.createShell() + let shell: Commandline + let xcresultToolClient: XCResultToolClient public init?( with url: URL, @@ -43,19 +44,34 @@ public class CoverageConverter { excludedPaths: [String] = [], strictPathnames: Bool ) { - resultFile = XCResultFile(url: url) - guard let record = resultFile.getCodeCoverage() else { + let shell = Shell() + let xcresultToolClient = XCResultToolClient(shell: shell) + guard let report = try? CoverageConverter.getCoverageReportAsJSON(resultFileURL: url, shell: shell) else { return nil } + + self.shell = shell + self.xcresultToolClient = xcresultToolClient + resultFileURL = url + coverageReport = report self.projectRoot = projectRoot self.strictPathnames = projectRoot.isEmpty ? false : strictPathnames - codeCoverage = record - guard let invocationRecord = resultFile.getInvocationRecord() else { - return nil - } - self.invocationRecord = invocationRecord - self.coverageTargets = record.targets(filteredBy: coverageTargets) + let selectedCoverageTargets = CoverageConverter.targets( + filteredBy: coverageTargets, + availableTargets: report.targets.map(\.name) + ) + self.coverageTargets = selectedCoverageTargets + filesForIncludedTargets = Set( + report.targets + .filter { selectedCoverageTargets.contains($0.name) } + .flatMap { $0.files.map(\.path) } + ) self.excludedPaths = Set(excludedPaths) + if let summary = try? xcresultToolClient.getTestSummary(path: url) { + startTime = summary.startTime + } else { + startTime = nil + } } public func xmlString(quiet: Bool) throws -> String { @@ -63,11 +79,23 @@ public class CoverageConverter { } public var targetsInfo: String { - return codeCoverage.targets.reduce("") { rslt, item in + return coverageReport.targets.reduce("") { rslt, item in return "\(rslt)\n\(item.name)" } } + var lineCoverage: Double { + coverageReport.lineCoverage + } + + var coveredLines: Int { + coverageReport.coveredLines + } + + var executableLines: Int { + coverageReport.executableLines + } + func writeToStdErrorLn(_ str: String) { writeToStdError("\(str)\n") } @@ -83,13 +111,17 @@ public class CoverageConverter { // Use the xccov commandline tool to get results as JSON. func getCoverageDataAsJSON() throws -> FileCoverage { var arguments = ["xccov", "view"] - if resultFile.url.pathExtension == "xcresult" { + if resultFileURL.pathExtension == "xcresult" { arguments.append("--archive") } arguments.append("--json") - arguments.append(resultFile.url.path) + arguments.append(resultFileURL.path) let coverageData = try shell.execute(program: "/usr/bin/xcrun", with: arguments) - return try JSONDecoder().decode(FileCoverage.self, from: coverageData) + return try CoverageConverter.decodeJSON(FileCoverage.self, from: coverageData) + } + + func isTargetIncluded(forFile file: String) -> Bool { + filesForIncludedTargets.contains(file) } func isPathExcluded(_ path: String) -> Bool { @@ -108,12 +140,12 @@ public class CoverageConverter { // It is not used at the moment, but is left here just to cover this xccov function func coverageForFile(path: String) throws -> String { var arguments = ["xccov", "view"] - if resultFile.url.pathExtension == "xcresult" { + if resultFileURL.pathExtension == "xcresult" { arguments.append("--archive") } arguments.append("--file") arguments.append(path) - arguments.append(resultFile.url.path) + arguments.append(resultFileURL.path) let coverageData = try shell.execute(program: "/usr/bin/xcrun", with: arguments) return String(decoding: coverageData, as: UTF8.self) } @@ -123,12 +155,38 @@ public class CoverageConverter { // It is not used at the moment, but is left here just to cover this xccov function func coverageFileList() throws -> [String] { var arguments = ["xccov", "view"] - if resultFile.url.pathExtension == "xcresult" { + if resultFileURL.pathExtension == "xcresult" { arguments.append("--archive") } arguments.append("--file-list") - arguments.append(resultFile.url.path) + arguments.append(resultFileURL.path) let filelistData = try shell.execute(program: "/usr/bin/xcrun", with: arguments) return String(decoding: filelistData, as: UTF8.self).components(separatedBy: "\n") } + + private static func getCoverageReportAsJSON(resultFileURL: URL, shell: Commandline) throws -> CoverageReport { + var arguments = ["xccov", "view"] + arguments.append("--report") + arguments.append("--json") + arguments.append(resultFileURL.path) + let coverageData = try shell.execute(program: "/usr/bin/xcrun", with: arguments) + return try decodeJSON(CoverageReport.self, from: coverageData) + } + + private static func targets(filteredBy filter: [String], availableTargets: [String]) -> Set { + guard !filter.isEmpty else { + return Set(availableTargets) + } + let filterSet = Set(filter) + let filtered = availableTargets.filter { thisTarget in + guard let stripped = thisTarget.split(separator: ".").first else { return true } + return filterSet.contains(String(stripped)) + } + return Set(filtered) + } + + private static func decodeJSON(_ type: T.Type, from data: Data) throws -> T { + let decoder = JSONDecoder() + return try decoder.decode(type, from: data) + } } diff --git a/Sources/xcresultparser/DataProviders/JunitXML/JunitXMLDataProviding.swift b/Sources/xcresultparser/DataProviders/JunitXML/JunitXMLDataProviding.swift new file mode 100644 index 0000000..4dc286e --- /dev/null +++ b/Sources/xcresultparser/DataProviders/JunitXML/JunitXMLDataProviding.swift @@ -0,0 +1,58 @@ +// +// JunitXMLDataProviding.swift +// Xcresultparser +// +// Created by Alex da Franca on 20.02.26. +// + +import Foundation + +protocol JunitXMLDataProviding { + var metrics: JunitInvocationMetrics { get } + var testActions: [JunitTestAction] { get } +} + +struct JunitInvocationMetrics { + let testsCount: Int + let testsFailedCount: Int +} + +struct JunitTestAction { + let startedTime: Date + let endedTime: Date + let testPlanRunSummaries: [JunitTestPlanRunSummary] + let failureSummaries: [JunitFailureSummary] +} + +struct JunitTestPlanRunSummary { + let name: String? + let testableSummaries: [JunitTestableSummary] +} + +struct JunitTestableSummary { + let tests: [JunitTestGroup] +} + +struct JunitTestGroup { + let identifier: String? + let name: String? + let duration: Double + let subtests: [JunitTest] + let subtestGroups: [JunitTestGroup] +} + +struct JunitTest { + let identifier: String? + let name: String? + let duration: Double? + let isFailed: Bool + let isSkipped: Bool +} + +struct JunitFailureSummary { + let message: String + let testCaseName: String + let issueType: String + let producingTarget: String? + let documentLocation: String? +} diff --git a/Sources/xcresultparser/DataProviders/JunitXML/XCResultToolJunitXMLDataProvider.swift b/Sources/xcresultparser/DataProviders/JunitXML/XCResultToolJunitXMLDataProvider.swift new file mode 100644 index 0000000..302b54b --- /dev/null +++ b/Sources/xcresultparser/DataProviders/JunitXML/XCResultToolJunitXMLDataProvider.swift @@ -0,0 +1,275 @@ +// +// XCResultToolJunitXMLDataProvider.swift +// Xcresultparser +// +// Created by Alex da Franca on 20.02.26. +// + +import Foundation + +struct XCResultToolJunitXMLDataProvider: JunitXMLDataProviding { + private let summary: XCSummary + private let tests: XCTests + private let failureLocationRegex = try? NSRegularExpression( + pattern: #"^(.+?):([0-9]+):\s*(.+)$"#, + options: [] + ) + + init?(url: URL, client: XCResultToolClient = XCResultToolClient()) { + guard let summary = try? client.getTestSummary(path: url), + let tests = try? client.getTests(path: url) else { + return nil + } + self.summary = summary + self.tests = tests + } + + var metrics: JunitInvocationMetrics { + JunitInvocationMetrics( + testsCount: summary.totalTestCount, + testsFailedCount: summary.failedTests + ) + } + + var testActions: [JunitTestAction] { + let start = Date(timeIntervalSince1970: summary.startTime ?? 0) + let end = Date(timeIntervalSince1970: summary.finishTime ?? summary.startTime ?? 0) + let failureMessageDetails = failureMessageDetailsByTestIdentifier() + + let summaries = mapPlanRunSummaries( + from: tests, + fallbackName: summary.title + ) + + let failureSummaries = summary.testFailures.map { failure in + let matchingFailureMessage = bestFailureMessage( + for: failure, + in: failureMessageDetails[failure.testIdentifierString] ?? [] + ) + return JunitFailureSummary( + message: failure.failureText, + testCaseName: failure.testIdentifierString.replacingOccurrences(of: "/", with: "."), + issueType: "Uncategorized", + producingTarget: nil, + documentLocation: matchingFailureMessage?.documentLocation + ) + } + + return [ + JunitTestAction( + startedTime: start, + endedTime: end, + testPlanRunSummaries: summaries, + failureSummaries: failureSummaries + ) + ] + } + + private func mapPlanRunSummaries(from tests: XCTests, fallbackName: String?) -> [JunitTestPlanRunSummary] { + let configuredNodes = tests.testPlanConfigurations.map { config in + ( + name: config.configurationName, + nodes: nodes(for: config, in: tests.testNodes) + ) + } + + if !configuredNodes.isEmpty { + return configuredNodes.map { entry in + JunitTestPlanRunSummary( + name: entry.name, + testableSummaries: [ + JunitTestableSummary( + tests: entry.nodes.compactMap { + mapGroup(node: $0, parentPath: nil, currentTestClassName: nil) + } + ) + ] + ) + } + } + + return [ + JunitTestPlanRunSummary( + name: fallbackName, + testableSummaries: [ + JunitTestableSummary( + tests: tests.testNodes.compactMap { + mapGroup(node: $0, parentPath: nil, currentTestClassName: nil) + } + ) + ] + ) + ] + } + + private func nodes(for configuration: XCConfiguration, in roots: [XCTestNode]) -> [XCTestNode] { + var matches = [XCTestNode]() + for root in roots { + matches.append(contentsOf: findConfigurationChildren(in: root, configuration: configuration)) + } + return matches.isEmpty ? roots : matches + } + + private func findConfigurationChildren(in node: XCTestNode, configuration: XCConfiguration) -> [XCTestNode] { + if node.nodeType == .testPlanConfiguration && + (node.name == configuration.configurationName || node.nodeIdentifier == configuration.configurationId) { + return node.children ?? [] + } + + let children = node.children ?? [] + return children.flatMap { findConfigurationChildren(in: $0, configuration: configuration) } + } + + private func mapGroup( + node: XCTestNode, + parentPath: String?, + currentTestClassName: String? + ) -> JunitTestGroup? { + guard node.nodeType != .testCase else { + return nil + } + + let groupName = mappedGroupName(for: node) + let currentPath = appendName(groupName, to: parentPath) + let nextTestClassName: String? = if node.nodeType == .testSuite { + groupName + } else { + currentTestClassName + } + let children = node.children ?? [] + let tests = children + .filter { $0.nodeType == .testCase } + .map { mapTest(node: $0, testClassName: nextTestClassName) } + + let groups = children.compactMap { child in + mapGroup( + node: child, + parentPath: currentPath, + currentTestClassName: nextTestClassName + ) + } + + let duration = node.durationInSeconds ?? tests.compactMap(\.duration).reduce(0, +) + groups.reduce(0) { $0 + $1.duration } + return JunitTestGroup( + identifier: mappedGroupIdentifier(for: node, fallback: groupName), + name: groupName, + duration: duration, + subtests: tests, + subtestGroups: groups + ) + } + + private func mapTest(node: XCTestNode, testClassName: String?) -> JunitTest { + let result = node.result ?? .unknown + let identifier: String + if let testClassName { + identifier = "\(testClassName)/\(node.name)" + } else { + identifier = node.name + } + return JunitTest( + identifier: identifier, + name: node.name, + duration: node.durationInSeconds, + isFailed: result == .failed, + isSkipped: result == .skipped || result == .expectedFailure + ) + } + + private func mappedGroupName(for node: XCTestNode) -> String { + switch node.nodeType { + case .unitTestBundle, .uiTestBundle: + return node.name.hasSuffix(".xctest") ? node.name : "\(node.name).xctest" + default: + return node.name + } + } + + private func mappedGroupIdentifier(for node: XCTestNode, fallback: String) -> String { + switch node.nodeType { + case .unitTestBundle, .uiTestBundle: + return node.name.hasSuffix(".xctest") ? node.name : "\(node.name).xctest" + case .testSuite: + return node.name + default: + return node.nodeIdentifier ?? fallback + } + } + + private func appendName(_ name: String, to parentPath: String?) -> String { + guard let parentPath, !parentPath.isEmpty else { + return name + } + return "\(parentPath)/\(name)" + } + + private func failureMessageDetailsByTestIdentifier() -> [String: [FailureMessageDetail]] { + var result = [String: [FailureMessageDetail]]() + for node in tests.testNodes { + collectFailureMessages(in: node, currentTestIdentifier: nil, into: &result) + } + return result + } + + private func collectFailureMessages( + in node: XCTestNode, + currentTestIdentifier: String?, + into result: inout [String: [FailureMessageDetail]] + ) { + var currentIdentifier = currentTestIdentifier + if node.nodeType == .testCase { + currentIdentifier = node.nodeIdentifier + } + + if node.nodeType == .failureMessage, + let currentIdentifier, + let detail = parseFailureMessage(node.name) { + result[currentIdentifier, default: []].append(detail) + } + + for child in node.children ?? [] { + collectFailureMessages( + in: child, + currentTestIdentifier: currentIdentifier, + into: &result + ) + } + } + + private func parseFailureMessage(_ raw: String) -> FailureMessageDetail? { + guard let failureLocationRegex else { + return nil + } + let range = NSRange(raw.startIndex.. FailureMessageDetail? { + candidates.first { + $0.message == failure.failureText || + $0.message.contains(failure.failureText) || + failure.failureText.contains($0.message) + } ?? candidates.first + } +} + +private struct FailureMessageDetail { + let message: String + let documentLocation: String +} diff --git a/Sources/xcresultparser/JunitXML.swift b/Sources/xcresultparser/JunitXML.swift index 049197a..a41bb81 100644 --- a/Sources/xcresultparser/JunitXML.swift +++ b/Sources/xcresultparser/JunitXML.swift @@ -6,7 +6,6 @@ // import Foundation -import XCResultKit public enum TestReportFormat { case junit, sonar @@ -47,9 +46,8 @@ public struct JunitXML: XmlSerializable { // MARK: - Properties - private let resultFile: XCResultFile + private let dataProvider: JunitXMLDataProviding private let projectRoot: URL? - private let invocationRecord: ActionsInvocationRecord private let testReportFormat: TestReportFormat private let relativePathNames: Bool @@ -70,11 +68,23 @@ public struct JunitXML: XmlSerializable { format: TestReportFormat = .junit, relativePathNames: Bool = true ) { - resultFile = XCResultFile(url: url) - guard let record = resultFile.getInvocationRecord() else { - return nil - } + let dataProvider = XCResultToolJunitXMLDataProvider(url: url) + guard let dataProvider else { return nil } + self.init( + dataProvider: dataProvider, + projectRoot: projectRoot, + format: format, + relativePathNames: relativePathNames + ) + } + init( + dataProvider: JunitXMLDataProviding, + projectRoot: String = "", + format: TestReportFormat = .junit, + relativePathNames: Bool = true + ) { + self.dataProvider = dataProvider var isDirectory: ObjCBool = false if SharedInstances.fileManager.fileExists(atPath: projectRoot, isDirectory: &isDirectory), isDirectory.boolValue == true { @@ -83,7 +93,6 @@ public struct JunitXML: XmlSerializable { self.projectRoot = nil } - invocationRecord = record testReportFormat = format if testReportFormat == .sonar { nodeNames = NodeNames.sonarNodeNames @@ -107,28 +116,21 @@ public struct JunitXML: XmlSerializable { xml.characterEncoding = "UTF-8" if testReportFormat != .sonar { - let metrics = invocationRecord.metrics - let testsCount = metrics.testsCount ?? 0 - testsuites.addAttribute(name: "tests", stringValue: String(testsCount)) - let testsFailedCount = metrics.testsFailedCount ?? 0 - testsuites.addAttribute(name: "failures", stringValue: String(testsFailedCount)) + let metrics = dataProvider.metrics + testsuites.addAttribute(name: "tests", stringValue: String(metrics.testsCount)) + testsuites.addAttribute(name: "failures", stringValue: String(metrics.testsFailedCount)) testsuites.addAttribute(name: "errors", stringValue: "0") // apparently Jenkins needs this?! } - let testActions = invocationRecord.actions.filter { $0.schemeCommandName == "Test" } + let testActions = dataProvider.testActions guard !testActions.isEmpty else { return xml.xmlString(options: [.nodePrettyPrint, .nodeCompactEmptyElement]) } var overallTestSuiteDuration = 0.0 for testAction in testActions { - guard let testsId = testAction.actionResult.testsRef?.id, - let testPlanRun = resultFile.getTestPlanRunSummaries(id: testsId) else { - continue - } - - let testPlanRunSummaries = testPlanRun.summaries - let failureSummaries = testAction.actionResult.issues.testFailureSummaries + let testPlanRunSummaries = testAction.testPlanRunSummaries + let failureSummaries = testAction.failureSummaries if testReportFormat != .sonar { let startDate = testAction.startedTime @@ -164,31 +166,14 @@ public struct JunitXML: XmlSerializable { // only used in unit testing static func resetCachedPathnames() { - ActionTestSummaryGroup.resetCachedPathnames() + JunitTestGroup.resetCachedPathnames() } // MARK: - Private interface - // The XMLElement produced by this function is not allowed in the junit XML format and thus unused. - // It is kept in case it serves another format. - private func runDestinationXML(_ destination: ActionRunDestinationRecord) -> XMLElement { - let properties = XMLElement(name: "properties") - if !destination.displayName.isEmpty { - properties.addChild(TestrunProperty(name: "destination", value: destination.displayName).xmlNode) - } - if !destination.targetArchitecture.isEmpty { - properties.addChild(TestrunProperty(name: "architecture", value: destination.targetArchitecture).xmlNode) - } - let record = destination.targetSDKRecord - if !record.name.isEmpty { - properties.addChild(TestrunProperty(name: "sdk", value: record.name).xmlNode) - } - return properties - } - private func createTestSuite( - _ group: ActionTestSummaryGroup, - failureSummaries: [TestFailureIssueSummary], + _ group: JunitTestGroup, + failureSummaries: [JunitFailureSummary], configurationName: String, testDirectory: String = "" ) -> [XMLElement] { @@ -244,9 +229,9 @@ public struct JunitXML: XmlSerializable { } private func createTestSuiteFinally( - _ group: ActionTestSummaryGroup, - tests: [ActionTestMetadata], - failureSummaries: [TestFailureIssueSummary], + _ group: JunitTestGroup, + tests: [JunitTest], + failureSummaries: [JunitFailureSummary], testDirectory: String = "", configurationName: String ) -> XMLElement { @@ -269,7 +254,7 @@ public struct JunitXML: XmlSerializable { } private func createTestCases( - for name: String, tests: [ActionTestMetadata], failureSummaries: [TestFailureIssueSummary] + for name: String, tests: [JunitTest], failureSummaries: [JunitFailureSummary] ) -> [XMLElement] { var combined = [XMLElement]() for thisTest in tests { @@ -284,7 +269,7 @@ public struct JunitXML: XmlSerializable { } private func createTestCase( - test: ActionTestMetadata, classname: String, failureSummaries: [TestFailureIssueSummary] + test: JunitTest, classname: String, failureSummaries: [JunitFailureSummary] ) -> XMLElement { let testcase = test.xmlNode( classname: classname, @@ -324,7 +309,7 @@ extension XMLElement { } } -extension ActionTestMetadata { +extension JunitTest { func xmlNode( classname: String, numFormatter: NumberFormatter, @@ -351,7 +336,7 @@ extension ActionTestMetadata { return testcase } - func failureSummaries(in summaries: [TestFailureIssueSummary]) -> [TestFailureIssueSummary] { + func failureSummaries(in summaries: [JunitFailureSummary]) -> [JunitFailureSummary] { return summaries.filter { summary in return summary.testCaseName == identifier?.replacingOccurrences(of: "/", with: ".") || summary.testCaseName == "-[\(identifier?.replacingOccurrences(of: "/", with: " ") ?? "")]" @@ -359,7 +344,7 @@ extension ActionTestMetadata { } } -private extension ActionTestSummaryGroup { +private extension JunitTestGroup { private static var cachedPathnames = [String: String]() struct TestMetrics { @@ -371,6 +356,10 @@ private extension ActionTestSummaryGroup { return identifier ?? "" } + var nameString: String { + return name ?? "No-name" + } + func testSuiteXML(numFormatter: NumberFormatter) -> XMLElement { let testsuite = XMLElement(name: "testsuite") testsuite.addAttribute(name: "name", stringValue: nameString) @@ -397,6 +386,19 @@ private extension ActionTestSummaryGroup { cachedPathnames.removeAll() } + static func resolvePathFromCachedClassMap(for fileName: String) -> String? { + guard !fileName.contains("/") else { + return fileName + } + let candidates = cachedPathnames.values.filter { $0.hasSuffix("/\(fileName)") || $0 == fileName } + guard !candidates.isEmpty else { + return nil + } + return candidates.max { lhs, rhs in + lhs.components(separatedBy: "est").count < rhs.components(separatedBy: "est").count + } + } + // MARK: - Private interface private func classPath(in projectRootUrl: URL?, relativePathNames: Bool = true) -> String { @@ -478,12 +480,12 @@ private extension String { } } -private extension TestFailureIssueSummary { +private extension JunitFailureSummary { func failureXML(projectRoot: URL? = nil) -> XMLElement { let failure = XMLElement(name: "failure") var value = message - if let loc = documentLocationInCreatingWorkspace?.url { - if let url = URL(string: loc) { + if let loc = documentLocation { + if loc.contains("://"), let url = URL(string: loc) { let relative = relativePart(of: url, relativeTo: projectRoot) if let comps = URLComponents(url: url, resolvingAgainstBaseURL: false), let line = comps.fragment?.components(separatedBy: "&").first( @@ -494,7 +496,7 @@ private extension TestFailureIssueSummary { value += " (\(loc))" } } else { - value += " (\(loc))" + value += " (\(resolvedDocumentLocation(loc, projectRoot: projectRoot)))" } } if !value.isEmpty { @@ -513,6 +515,22 @@ private extension TestFailureIssueSummary { return failure } + private func resolvedDocumentLocation(_ location: String, projectRoot: URL?) -> String { + guard projectRoot != nil else { + return location + } + let components = location.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false) + guard components.count == 2 else { + return location + } + let file = String(components[0]) + let linePart = String(components[1]) + guard let resolvedPath = JunitTestGroup.resolvePathFromCachedClassMap(for: file) else { + return location + } + return "\(resolvedPath):\(linePart)" + } + private func relativePart(of url: URL, relativeTo projectRoot: URL?) -> String { guard let projectRoot else { return url.path diff --git a/Sources/xcresultparser/Models/Coverage/CoverageReport.swift b/Sources/xcresultparser/Models/Coverage/CoverageReport.swift new file mode 100644 index 0000000..e56166b --- /dev/null +++ b/Sources/xcresultparser/Models/Coverage/CoverageReport.swift @@ -0,0 +1,23 @@ +// +// CoverageReport.swift +// +// Created by Codex on 21.02.26. +// + +import Foundation + +struct CoverageReport: Decodable { + let coveredLines: Int + let executableLines: Int + let lineCoverage: Double + let targets: [CoverageTarget] +} + +struct CoverageTarget: Decodable { + let name: String + let files: [CoverageReportFile] +} + +struct CoverageReportFile: Decodable { + let path: String +} diff --git a/Sources/xcresultparser/SonarCoverageConverter.swift b/Sources/xcresultparser/SonarCoverageConverter.swift index 848a101..54da2c8 100644 --- a/Sources/xcresultparser/SonarCoverageConverter.swift +++ b/Sources/xcresultparser/SonarCoverageConverter.swift @@ -23,6 +23,9 @@ public class SonarCoverageConverter: CoverageConverter, XmlSerializable { // Get the xccov results as a JSON. let coverageJson = try getCoverageDataAsJSON() for (file, lineData) in coverageJson.files { + guard isTargetIncluded(forFile: file) else { + continue + } guard !isPathExcluded(file) else { continue } diff --git a/Tests/XcresultparserTests/TestAssets/junit.xml b/Tests/XcresultparserTests/TestAssets/junit.xml index 33af93e..3b46019 100644 --- a/Tests/XcresultparserTests/TestAssets/junit.xml +++ b/Tests/XcresultparserTests/TestAssets/junit.xml @@ -1,9 +1,9 @@ - + - failed - Unable to create CoverageConverter from /Users/fhaeser/Library/Developer/Xcode/DerivedData/xcresultparser-ebyquorsyljyyuchjpndxzpxmxvo/Build/Products/Debug/XcresultparserTests.xctest/Contents/Resources/Xcresultparser_XcresultparserTests.bundle/Contents/Resources/test.xcresult (/Users/fhaeser/code/xcresultparser/Tests/XcresultparserTests/XcresultparserTests.swift:108) + failed - Unable to create CoverageConverter from /Users/fhaeser/Library/Developer/Xcode/DerivedData/xcresultparser-ebyquorsyljyyuchjpndxzpxmxvo/Build/Products/Debug/XcresultparserTests.xctest/Contents/Resources/Xcresultparser_XcresultparserTests.bundle/Contents/Resources/test.xcresult (XcresultparserTests.swift:109) diff --git a/Tests/XcresultparserTests/TestAssets/junit_merged.xml b/Tests/XcresultparserTests/TestAssets/junit_merged.xml index f44d2f5..7d79264 100644 --- a/Tests/XcresultparserTests/TestAssets/junit_merged.xml +++ b/Tests/XcresultparserTests/TestAssets/junit_merged.xml @@ -1,20 +1,9 @@ - - + + - failed - Unable to create CoverageConverter from /Users/fhaeser/Library/Developer/Xcode/DerivedData/xcresultparser-ebyquorsyljyyuchjpndxzpxmxvo/Build/Products/Debug/XcresultparserTests.xctest/Contents/Resources/Xcresultparser_XcresultparserTests.bundle/Contents/Resources/test.xcresult (/Users/fhaeser/code/xcresultparser/Tests/XcresultparserTests/XcresultparserTests.swift:108) - - - - - - - - - - - failed - Unable to create CoverageConverter from /Users/fhaeser/Library/Developer/Xcode/DerivedData/xcresultparser-ebyquorsyljyyuchjpndxzpxmxvo/Build/Products/Debug/XcresultparserTests.xctest/Contents/Resources/Xcresultparser_XcresultparserTests.bundle/Contents/Resources/test.xcresult (/Users/fhaeser/code/xcresultparser/Tests/XcresultparserTests/XcresultparserTests.swift:108) + failed - Unable to create CoverageConverter from /Users/fhaeser/Library/Developer/Xcode/DerivedData/xcresultparser-ebyquorsyljyyuchjpndxzpxmxvo/Build/Products/Debug/XcresultparserTests.xctest/Contents/Resources/Xcresultparser_XcresultparserTests.bundle/Contents/Resources/test.xcresult (XcresultparserTests.swift:109) diff --git a/Tests/XcresultparserTests/TestAssets/junit_repeated.xml b/Tests/XcresultparserTests/TestAssets/junit_repeated.xml index d81961b..99a2b84 100644 --- a/Tests/XcresultparserTests/TestAssets/junit_repeated.xml +++ b/Tests/XcresultparserTests/TestAssets/junit_repeated.xml @@ -1,18 +1,8 @@ - - - - failed - Failed with number: 51 (/Users/jszumski/Desktop/test_repeat/test_repeatTests/test_repeatTests.swift:14) - - - - - failed - Failed with number: 61 (/Users/jszumski/Desktop/test_repeat/test_repeatTests/test_repeatTests.swift:14) - - - - - failed - Failed with number: 25 (/Users/jszumski/Desktop/test_repeat/test_repeatTests/test_repeatTests.swift:14) + + + + failed - Failed with number: 51 (test_repeatTests.swift:15) diff --git a/Tests/XcresultparserTests/TestAssets/sonarTestExecution.xml b/Tests/XcresultparserTests/TestAssets/sonarTestExecution.xml index 3272b46..fccf605 100644 --- a/Tests/XcresultparserTests/TestAssets/sonarTestExecution.xml +++ b/Tests/XcresultparserTests/TestAssets/sonarTestExecution.xml @@ -3,7 +3,7 @@ - failed - Unable to create CoverageConverter from /Users/fhaeser/Library/Developer/Xcode/DerivedData/xcresultparser-ebyquorsyljyyuchjpndxzpxmxvo/Build/Products/Debug/XcresultparserTests.xctest/Contents/Resources/Xcresultparser_XcresultparserTests.bundle/Contents/Resources/test.xcresult (/Users/fhaeser/code/xcresultparser/Tests/XcresultparserTests/XcresultparserTests.swift:108) + failed - Unable to create CoverageConverter from /Users/fhaeser/Library/Developer/Xcode/DerivedData/xcresultparser-ebyquorsyljyyuchjpndxzpxmxvo/Build/Products/Debug/XcresultparserTests.xctest/Contents/Resources/Xcresultparser_XcresultparserTests.bundle/Contents/Resources/test.xcresult (XcresultparserTests.swift:109) diff --git a/Tests/XcresultparserTests/TestAssets/sonarTestExecutionWithProjectRootAbsolute.xml b/Tests/XcresultparserTests/TestAssets/sonarTestExecutionWithProjectRootAbsolute.xml index 642255f..42ec8db 100644 --- a/Tests/XcresultparserTests/TestAssets/sonarTestExecutionWithProjectRootAbsolute.xml +++ b/Tests/XcresultparserTests/TestAssets/sonarTestExecutionWithProjectRootAbsolute.xml @@ -3,7 +3,7 @@ - failed - Unable to create CoverageConverter from /Users/fhaeser/Library/Developer/Xcode/DerivedData/xcresultparser-ebyquorsyljyyuchjpndxzpxmxvo/Build/Products/Debug/XcresultparserTests.xctest/Contents/Resources/Xcresultparser_XcresultparserTests.bundle/Contents/Resources/test.xcresult (/Users/fhaeser/code/xcresultparser/Tests/XcresultparserTests/XcresultparserTests.swift:108) + failed - Unable to create CoverageConverter from /Users/fhaeser/Library/Developer/Xcode/DerivedData/xcresultparser-ebyquorsyljyyuchjpndxzpxmxvo/Build/Products/Debug/XcresultparserTests.xctest/Contents/Resources/Xcresultparser_XcresultparserTests.bundle/Contents/Resources/test.xcresult (/Users/actual/project/Tests/XcresultparserTests.swift:109) diff --git a/Tests/XcresultparserTests/TestAssets/sonarTestExecutionWithProjectRootRelative.xml b/Tests/XcresultparserTests/TestAssets/sonarTestExecutionWithProjectRootRelative.xml index 2716cbe..216eacb 100644 --- a/Tests/XcresultparserTests/TestAssets/sonarTestExecutionWithProjectRootRelative.xml +++ b/Tests/XcresultparserTests/TestAssets/sonarTestExecutionWithProjectRootRelative.xml @@ -3,7 +3,7 @@ - failed - Unable to create CoverageConverter from /Users/fhaeser/Library/Developer/Xcode/DerivedData/xcresultparser-ebyquorsyljyyuchjpndxzpxmxvo/Build/Products/Debug/XcresultparserTests.xctest/Contents/Resources/Xcresultparser_XcresultparserTests.bundle/Contents/Resources/test.xcresult (/Users/fhaeser/code/xcresultparser/Tests/XcresultparserTests/XcresultparserTests.swift:108) + failed - Unable to create CoverageConverter from /Users/fhaeser/Library/Developer/Xcode/DerivedData/xcresultparser-ebyquorsyljyyuchjpndxzpxmxvo/Build/Products/Debug/XcresultparserTests.xctest/Contents/Resources/Xcresultparser_XcresultparserTests.bundle/Contents/Resources/test.xcresult (Tests/XcresultparserTests.swift:109) diff --git a/Tests/XcresultparserTests/XCResultToolJunitXMLDataProviderTests.swift b/Tests/XcresultparserTests/XCResultToolJunitXMLDataProviderTests.swift new file mode 100644 index 0000000..ade42f1 --- /dev/null +++ b/Tests/XcresultparserTests/XCResultToolJunitXMLDataProviderTests.swift @@ -0,0 +1,167 @@ +import Foundation +@testable import XcresultparserLib +import Testing + +@MainActor +struct XCResultToolJunitXMLDataProviderTests { + @Test + func testProviderMapsSummaryAndTestNodes() throws { + let summaryJSON = """ + { + "title": "Test - Demo", + "environmentDescription": "Demo", + "topInsights": [], + "result": "Failed", + "totalTestCount": 2, + "passedTests": 1, + "failedTests": 1, + "skippedTests": 0, + "expectedFailures": 0, + "statistics": [], + "devicesAndConfigurations": [], + "testFailures": [ + { + "failureText": "failed - expected true", + "targetName": "DemoTests", + "testIdentifier": 1, + "testIdentifierString": "DemoTests/testFail()", + "testIdentifierURL": "test://com.apple.xcode/Demo/DemoTests/testFail", + "testName": "testFail()" + } + ], + "startTime": 100.0, + "finishTime": 120.0 + } + """ + + let testsJSON = """ + { + "testPlanConfigurations": [ + { + "configurationId": "1", + "configurationName": "Default" + } + ], + "devices": [ + { + "deviceId": "device-1", + "deviceName": "My Mac" + } + ], + "testNodes": [ + { + "name": "Test Plan", + "nodeType": "Test Plan", + "children": [ + { + "name": "Default", + "nodeType": "Test Plan Configuration", + "children": [ + { + "name": "DemoTests.xctest", + "nodeType": "Unit test bundle", + "durationInSeconds": 3.0, + "children": [ + { + "name": "DemoTests", + "nodeType": "Test Suite", + "durationInSeconds": 3.0, + "children": [ + { + "name": "testPass()", + "nodeType": "Test Case", + "result": "Passed", + "durationInSeconds": 1.0 + }, + { + "name": "testFail()", + "nodeType": "Test Case", + "result": "Failed", + "durationInSeconds": 2.0 + } + ] + } + ] + } + ] + } + ] + } + ] + } + """ + + let shell = LookupShell( + responses: [ + "xcresulttool get test-results summary --path /tmp/test.xcresult": .success(Data(summaryJSON.utf8)), + "xcresulttool get test-results tests --path /tmp/test.xcresult": .success(Data(testsJSON.utf8)) + ] + ) + let client = XCResultToolClient(shell: shell) + guard let provider = XCResultToolJunitXMLDataProvider( + url: URL(fileURLWithPath: "/tmp/test.xcresult"), + client: client + ) else { + Issue.record("Expected provider initialization to succeed") + return + } + + #expect(provider.metrics.testsCount == 2) + #expect(provider.metrics.testsFailedCount == 1) + + let action = try #require(provider.testActions.first) + #expect(action.startedTime.timeIntervalSince1970 == 100.0) + #expect(action.endedTime.timeIntervalSince1970 == 120.0) + + let plan = try #require(action.testPlanRunSummaries.first) + #expect(plan.name == "Default") + let rootGroup = try #require(plan.testableSummaries.first?.tests.first) + #expect(rootGroup.name == "DemoTests.xctest") + #expect(rootGroup.subtestGroups.count == 1) + + let suite = try #require(rootGroup.subtestGroups.first) + #expect(suite.subtests.count == 2) + #expect(suite.subtests[0].name == "testPass()") + #expect(suite.subtests[1].isFailed == true) + #expect(action.failureSummaries.first?.testCaseName == "DemoTests.testFail()") + } + + @Test + func testProviderInitFailsIfXCResultToolFails() { + let shell = LookupShell( + responses: [ + "xcresulttool get test-results summary --path /tmp/test.xcresult": .failure(NSError(domain: "test", code: 42)) + ] + ) + let client = XCResultToolClient(shell: shell) + + let provider = XCResultToolJunitXMLDataProvider( + url: URL(fileURLWithPath: "/tmp/test.xcresult"), + client: client + ) + + #expect(provider == nil) + } +} + +private final class LookupShell: Commandline { + let responses: [String: Result] + + init(responses: [String: Result]) { + self.responses = responses + } + + func execute(program: String, with arguments: [String], at executionPath: URL?) throws -> Data { + #expect(program == "/usr/bin/xcrun") + let key = arguments.joined(separator: " ") + guard let response = responses[key] else { + throw NSError(domain: "missing_response", code: 1) + } + switch response { + case .success(let data): + return data + case .failure(let error): + throw error + } + } +} diff --git a/Tests/XcresultparserTests/XcresultparserTests.swift b/Tests/XcresultparserTests/XcresultparserTests.swift index 03e16f9..5c673e1 100644 --- a/Tests/XcresultparserTests/XcresultparserTests.swift +++ b/Tests/XcresultparserTests/XcresultparserTests.swift @@ -1,6 +1,5 @@ import Foundation @testable import XcresultparserLib -import XCResultKit import Testing @MainActor @@ -389,17 +388,8 @@ struct XcresultparserTests { ./Tests/XcresultparserTests.swift:class XcresultparserTests """ let savedFilemanger = SharedInstances.fileManager - let savedShellFactory = DependencyFactory.createShell - SharedInstances.fileManager = MockedFileManager(fileExists: true, isPathDirectory: true) - - let mockedShell = MockedShell(response: Data(cliResult.utf8), error: nil) - DependencyFactory.createShell = { - mockedShell - } - mockedShell.argumentValidation = { arguments in - return arguments.last == "." - } + defer { SharedInstances.fileManager = savedFilemanger } let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! let projectRoot = "/Users/imaginary/project" @@ -412,10 +402,15 @@ struct XcresultparserTests { Issue.record("Unable to create JunitXML from \(xcresultFile)") return } - try assertXmlTestReportsAreEqual(expectedFileName: "sonarTestExecutionWithProjectRootRelative", actual: junitXML) - SharedInstances.fileManager = savedFilemanger - DependencyFactory.createShell = savedShellFactory + let savedShellFactory = DependencyFactory.createShell + let mockedShell = MockedShell(response: Data(cliResult.utf8), error: nil) + DependencyFactory.createShell = { mockedShell } + defer { DependencyFactory.createShell = savedShellFactory } + mockedShell.argumentValidation = { arguments in + return arguments.last == "." + } + try assertXmlTestReportsAreEqual(expectedFileName: "sonarTestExecutionWithProjectRootRelative", actual: junitXML) } @Test @@ -426,17 +421,8 @@ struct XcresultparserTests { """ let savedFilemanger = SharedInstances.fileManager - let savedShellFactory = DependencyFactory.createShell - SharedInstances.fileManager = MockedFileManager(fileExists: true, isPathDirectory: true) - - let mockedShell = MockedShell(response: Data(cliResult.utf8), error: nil) - DependencyFactory.createShell = { - mockedShell - } - mockedShell.argumentValidation = { arguments in - return arguments.last != "." - } + defer { SharedInstances.fileManager = savedFilemanger } let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! let projectRoot = "/Users/imaginary/project" @@ -449,10 +435,15 @@ struct XcresultparserTests { Issue.record("Unable to create JunitXML from \(xcresultFile)") return } - try assertXmlTestReportsAreEqual(expectedFileName: "sonarTestExecutionWithProjectRootAbsolute", actual: junitXML) - SharedInstances.fileManager = savedFilemanger - DependencyFactory.createShell = savedShellFactory + let savedShellFactory = DependencyFactory.createShell + let mockedShell = MockedShell(response: Data(cliResult.utf8), error: nil) + DependencyFactory.createShell = { mockedShell } + defer { DependencyFactory.createShell = savedShellFactory } + mockedShell.argumentValidation = { arguments in + return arguments.last != "." + } + try assertXmlTestReportsAreEqual(expectedFileName: "sonarTestExecutionWithProjectRootAbsolute", actual: junitXML) } @Test @@ -502,22 +493,22 @@ struct XcresultparserTests { @Test func testFailureSummariesReturnsAllMatchingFailures() throws { - let testMetadata = try makeTestMetadata() + let test = makeJunitTest() - let matchingFailure1 = try makeFailureSummary( + let matchingFailure1 = makeJunitFailureSummary( testCaseName: "TestClass.testMethod", message: "First assertion failed" ) - let matchingFailure2 = try makeFailureSummary( + let matchingFailure2 = makeJunitFailureSummary( testCaseName: "TestClass.testMethod", message: "Second assertion failed" ) - let nonMatchingFailure = try makeFailureSummary( + let nonMatchingFailure = makeJunitFailureSummary( testCaseName: "OtherClass.otherMethod", message: "Unrelated failure" ) - let result = testMetadata.failureSummaries(in: [matchingFailure1, nonMatchingFailure, matchingFailure2]) + let result = test.failureSummaries(in: [matchingFailure1, nonMatchingFailure, matchingFailure2]) #expect(result.count == 2) #expect(result[0].message == "First assertion failed") @@ -526,19 +517,19 @@ struct XcresultparserTests { @Test func testFailureSummariesWithBracketNotation() throws { - let testMetadata = try makeTestMetadata() + let test = makeJunitTest() // Objective-C bracket notation: -[TestClass testMethod] - let bracketFailure1 = try makeFailureSummary( + let bracketFailure1 = makeJunitFailureSummary( testCaseName: "-[TestClass testMethod]", message: "Bracket notation failure 1" ) - let bracketFailure2 = try makeFailureSummary( + let bracketFailure2 = makeJunitFailureSummary( testCaseName: "-[TestClass testMethod]", message: "Bracket notation failure 2" ) - let result = testMetadata.failureSummaries(in: [bracketFailure1, bracketFailure2]) + let result = test.failureSummaries(in: [bracketFailure1, bracketFailure2]) #expect(result.count == 2) #expect(result[0].message == "Bracket notation failure 1") @@ -547,22 +538,22 @@ struct XcresultparserTests { @Test func testFailureSummariesReturnsEmptyForNoMatches() throws { - let testMetadata = try makeTestMetadata() - let nonMatchingFailure = try makeFailureSummary( + let test = makeJunitTest() + let nonMatchingFailure = makeJunitFailureSummary( testCaseName: "OtherClass.otherMethod", message: "Unrelated failure" ) - let result = testMetadata.failureSummaries(in: [nonMatchingFailure]) + let result = test.failureSummaries(in: [nonMatchingFailure]) #expect(result.isEmpty) } @Test func testFailureSummariesWithEmptyArray() throws { - let testMetadata = try makeTestMetadata() + let test = makeJunitTest() - let result = testMetadata.failureSummaries(in: []) + let result = test.failureSummaries(in: []) #expect(result.isEmpty) } @@ -1011,34 +1002,32 @@ struct XcresultparserTests { // MARK: helper functions - private func makeTestMetadata( + private func makeJunitTest( 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)) + duration: Double = 0.5 + ) -> JunitTest { + JunitTest( + identifier: identifier, + name: name, + duration: duration, + isFailed: true, + isSkipped: false + ) } - private func makeFailureSummary( + private func makeJunitFailureSummary( 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)) + ) -> JunitFailureSummary { + JunitFailureSummary( + message: message, + testCaseName: testCaseName, + issueType: issueType, + producingTarget: nil, + documentLocation: nil + ) } func assertXmlTestReportsAreEqual( @@ -1059,6 +1048,7 @@ struct XcresultparserTests { #expect(expectedXMLString == actualXMLString) } + } class MockedFileManager: FileManaging { From f4e3675a9c13da25d6b42c473a960d51d4f43b7a Mon Sep 17 00:00:00 2001 From: Alex da Franca Date: Sat, 21 Feb 2026 17:34:33 +0100 Subject: [PATCH 07/14] Remove the last uses of XCResultKit --- Package.resolved | 9 - Package.swift | 8 - Sources/xcresultparser/IssuesJSON.swift | 18 +- .../IssueLocationInfo.swift | 10 +- .../Models/CodeClimate/Issue.swift | 5 +- .../Models/Coverage/CoverageReport.swift | 17 + .../xcresultparser/XCResultFormatter.swift | 364 ++++++++++-------- 7 files changed, 238 insertions(+), 193 deletions(-) diff --git a/Package.resolved b/Package.resolved index 0d6404b..a2d6d26 100644 --- a/Package.resolved +++ b/Package.resolved @@ -8,15 +8,6 @@ "revision" : "309a47b2b1d9b5e991f36961c983ecec72275be3", "version" : "1.6.1" } - }, - { - "identity" : "xcresultkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/davidahouse/XCResultKit.git", - "state" : { - "revision" : "d49890579f3fd10253ae3d4ba056b89e76ea971e", - "version" : "1.2.2" - } } ], "version" : 2 diff --git a/Package.swift b/Package.swift index 72dd0a9..aab29d5 100644 --- a/Package.swift +++ b/Package.swift @@ -20,10 +20,6 @@ let package = Package( .package( url: "https://github.com/apple/swift-argument-parser.git", .upToNextMajor(from: "1.6.0") - ), - .package( - url: "https://github.com/davidahouse/XCResultKit.git", - .upToNextMajor(from: "1.2.2") ) ], targets: [ @@ -39,10 +35,6 @@ let package = Package( name: "ArgumentParser", package: "swift-argument-parser" ), - .product( - name: "XCResultKit", - package: "XCResultKit" - ), ], path: "Sources" // plugins: [.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLint")] diff --git a/Sources/xcresultparser/IssuesJSON.swift b/Sources/xcresultparser/IssuesJSON.swift index f385c04..35b7cd0 100644 --- a/Sources/xcresultparser/IssuesJSON.swift +++ b/Sources/xcresultparser/IssuesJSON.swift @@ -5,7 +5,6 @@ // import Foundation -import XCResultKit /// Output some infos about warnings and issues /// @@ -13,10 +12,9 @@ import XCResultKit /// [Gitlab Code Climate Support](https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool) /// public struct IssuesJSON { - let resultFile: XCResultFile let projectRoot: String let checkName: String - let invocationRecord: ActionsInvocationRecord + let buildResults: XCBuildResults let excludedPaths: Set public init?( @@ -24,12 +22,12 @@ public struct IssuesJSON { projectRoot: String = "", excludedPaths: [String] = [] ) { - resultFile = XCResultFile(url: url) - guard let invocationRecord = resultFile.getInvocationRecord(), + let client = XCResultToolClient() + guard let buildResults = try? client.getBuildResults(path: url), let checkdata = try? Data(contentsOf: url.appendingPathComponent("Info.plist")) else { return nil } - self.invocationRecord = invocationRecord + self.buildResults = buildResults checkName = checkdata.md5() self.projectRoot = projectRoot self.excludedPaths = Set(excludedPaths) @@ -40,7 +38,7 @@ public struct IssuesJSON { encoder.outputFormatting = .prettyPrinted let jsonData: Data if format == .errors { - let errors = invocationRecord.issues.errorSummaries + let errors = buildResults.errors .compactMap { Issue( issueSummary: $0, @@ -52,7 +50,7 @@ public struct IssuesJSON { } jsonData = try encoder.encode(errors) } else { - let warnings = invocationRecord.issues.warningSummaries + let warnings = buildResults.warnings .compactMap { Issue( issueSummary: $0, @@ -62,7 +60,7 @@ public struct IssuesJSON { excludedPaths: excludedPaths ) } - let analyzerWarnings = invocationRecord.issues.analyzerWarningSummaries + let analyzerWarnings = buildResults.analyzerWarnings .compactMap { Issue( issueSummary: $0, @@ -74,7 +72,7 @@ public struct IssuesJSON { } var combined = warnings + analyzerWarnings if format == .warningsAndErrors { - let errors = invocationRecord.issues.errorSummaries + let errors = buildResults.errors .compactMap { Issue( issueSummary: $0, diff --git a/Sources/xcresultparser/Models/CodeClimate/IntermediateObjects/IssueLocationInfo.swift b/Sources/xcresultparser/Models/CodeClimate/IntermediateObjects/IssueLocationInfo.swift index 432b910..85629ca 100644 --- a/Sources/xcresultparser/Models/CodeClimate/IntermediateObjects/IssueLocationInfo.swift +++ b/Sources/xcresultparser/Models/CodeClimate/IntermediateObjects/IssueLocationInfo.swift @@ -5,9 +5,8 @@ // import Foundation -import XCResultKit -/// Helper object to convert from InvocationRecoord.DocumentLocation to Code Climate objects +/// Helper object to convert from xcresult issue source URL to Code Climate objects /// struct IssueLocationInfo { let filePath: String @@ -16,12 +15,11 @@ struct IssueLocationInfo { let startColumn: Int let endColumn: Int - init?(with documentLocation: DocumentLocation?) { - guard let documentLocation, - let url = URL(string: documentLocation.url) else { + init?(with sourceURL: String?) { + guard let sourceURL, + let url = URL(string: sourceURL) else { return nil } - // documentLocation.concreteTypeName: "DVTTextDocumentLocation" filePath = url.path guard let fragment = url.fragment else { startLine = 0 diff --git a/Sources/xcresultparser/Models/CodeClimate/Issue.swift b/Sources/xcresultparser/Models/CodeClimate/Issue.swift index 3220513..7606148 100644 --- a/Sources/xcresultparser/Models/CodeClimate/Issue.swift +++ b/Sources/xcresultparser/Models/CodeClimate/Issue.swift @@ -5,7 +5,6 @@ // import Foundation -import XCResultKit struct Issue: Codable { /// Required. A description of the code quality violation. @@ -37,13 +36,13 @@ struct Issue: Codable { extension Issue { init?( - issueSummary: IssueSummary, + issueSummary: XCIssue, severity: IssueSeverity, checkName: String, projectRoot: String = "", excludedPaths: Set = [] ) { - let issueLocationInfo = IssueLocationInfo(with: issueSummary.documentLocationInCreatingWorkspace) + let issueLocationInfo = IssueLocationInfo(with: issueSummary.sourceURL) if let filePath = issueLocationInfo?.filePath, excludedPaths.isPathExcluded(filePath) { return nil diff --git a/Sources/xcresultparser/Models/Coverage/CoverageReport.swift b/Sources/xcresultparser/Models/Coverage/CoverageReport.swift index e56166b..c8c4cd4 100644 --- a/Sources/xcresultparser/Models/Coverage/CoverageReport.swift +++ b/Sources/xcresultparser/Models/Coverage/CoverageReport.swift @@ -15,9 +15,26 @@ struct CoverageReport: Decodable { struct CoverageTarget: Decodable { let name: String + let lineCoverage: Double + let executableLines: Int + let coveredLines: Int let files: [CoverageReportFile] } struct CoverageReportFile: Decodable { + let name: String let path: String + let lineCoverage: Double + let executableLines: Int + let coveredLines: Int + let functions: [CoverageReportFunction] +} + +struct CoverageReportFunction: Decodable { + let name: String + let lineNumber: Int + let lineCoverage: Double + let executableLines: Int + let coveredLines: Int + let executionCount: Int } diff --git a/Sources/xcresultparser/XCResultFormatter.swift b/Sources/xcresultparser/XCResultFormatter.swift index e1a2d3c..e79d8f5 100644 --- a/Sources/xcresultparser/XCResultFormatter.swift +++ b/Sources/xcresultparser/XCResultFormatter.swift @@ -6,7 +6,6 @@ // import Foundation -import XCResultKit public struct XCResultFormatter { private enum SummaryField: String { @@ -24,12 +23,46 @@ public struct XCResultFormatter { } } + private struct FormattedTestGroup { + let name: String + let duration: Double + let subtests: [FormattedTest] + let subtestGroups: [FormattedTestGroup] + + var hasFailedTests: Bool { + if subtests.contains(where: \.isFailed) { + return true + } + if subtestGroups.contains(where: \.hasFailedTests) { + return true + } + return false + } + + var hasNoFailedTests: Bool { + !hasFailedTests + } + } + + private struct FormattedTest { + let identifier: String + let name: String + let duration: Double? + let isFailed: Bool + let isSkipped: Bool + + var isSuccessful: Bool { + !isFailed && !isSkipped + } + } + // MARK: - Properties - private let resultFile: XCResultFile - private let invocationRecord: ActionsInvocationRecord - private let codeCoverage: CodeCoverage? private let outputFormatter: XCResultFormatting + private let coverageReport: CoverageReport? + private let buildResults: XCBuildResults + private let testSummary: XCSummary + private let tests: XCTests private let coverageTargets: Set private let failedTestsOnly: Bool private let summaryFields: SummaryFields @@ -57,26 +90,22 @@ public struct XCResultFormatter { summaryFields: String = "errors|warnings|analyzerWarnings|tests|failed|skipped", coverageReportFormat: CoverageReportFormat = .methods ) { - resultFile = XCResultFile(url: url) - guard let record = resultFile.getInvocationRecord() else { + let client = XCResultToolClient() + guard let buildResults = try? client.getBuildResults(path: url), + let summary = try? client.getTestSummary(path: url), + let tests = try? client.getTests(path: url) else { return nil } - invocationRecord = record + + self.buildResults = buildResults + self.testSummary = summary + self.tests = tests outputFormatter = formatter - codeCoverage = resultFile.getCodeCoverage() - self.coverageTargets = codeCoverage?.targets(filteredBy: coverageTargets) ?? [] + self.coverageReport = try? Self.getCoverageReportAsJSON(resultFileURL: url, shell: DependencyFactory.createShell()) + self.coverageTargets = Self.targets(filteredBy: coverageTargets, availableTargets: coverageReport?.targets.map(\.name) ?? []) self.failedTestsOnly = failedTestsOnly self.summaryFields = SummaryFields(specifiers: summaryFields) self.coverageReportFormat = coverageReportFormat - - // if let logsId = invocationRecord?.actions.last?.actionResult.logRef?.id { - // let testLogs = resultFile.getLogs(id: logsId) - // } - // - // let testSummary = resultFile.getActionTestSummary(id: "xxx") - - // let payload = resultFile.getPayload(id: "123") - // let exportedPath = resultFile.exportPayload(id: "123") } // MARK: - Public API @@ -112,24 +141,18 @@ public struct XCResultFormatter { // MARK: - Private API private func createSummary() -> [String] { - let metrics = invocationRecord.metrics - - let analyzerWarningCount = metrics.analyzerWarningCount ?? 0 - let errorCount = metrics.errorCount ?? 0 - let testsCount = metrics.testsCount ?? 0 - let testsFailedCount = metrics.testsFailedCount ?? 0 - let warningCount = metrics.warningCount ?? 0 - let testsSkippedCount = metrics.testsSkippedCount ?? 0 + let analyzerWarningCount = buildResults.analyzerWarningCount + let errorCount = buildResults.errorCount + let testsCount = testSummary.totalTestCount + let testsFailedCount = testSummary.failedTests + let warningCount = buildResults.warningCount + let testsSkippedCount = testSummary.skippedTests var lines = [String]() - lines.append( - outputFormatter.testConfiguration("Summary") - ) + lines.append(outputFormatter.testConfiguration("Summary")) if summaryFields.enabledFields.contains(.errors) { - lines.append( - outputFormatter.resultSummaryLine("Number of errors = \(errorCount)", failed: errorCount != 0) - ) + lines.append(outputFormatter.resultSummaryLine("Number of errors = \(errorCount)", failed: errorCount != 0)) } if summaryFields.enabledFields.contains(.warnings) { lines.append( @@ -148,9 +171,7 @@ public struct XCResultFormatter { ) } if summaryFields.enabledFields.contains(.tests) { - lines.append( - outputFormatter.resultSummaryLine("Number of tests = \(testsCount)", failed: false) - ) + lines.append(outputFormatter.resultSummaryLine("Number of tests = \(testsCount)", failed: false)) } if summaryFields.enabledFields.contains(.failed) { lines.append( @@ -172,14 +193,12 @@ public struct XCResultFormatter { } private func createSummaryInOneLine() -> String { - let metrics = invocationRecord.metrics - - let analyzerWarningCount = metrics.analyzerWarningCount ?? 0 - let errorCount = metrics.errorCount ?? 0 - let testsCount = metrics.testsCount ?? 0 - let testsFailedCount = metrics.testsFailedCount ?? 0 - let warningCount = metrics.warningCount ?? 0 - let testsSkippedCount = metrics.testsSkippedCount ?? 0 + let analyzerWarningCount = buildResults.analyzerWarningCount + let errorCount = buildResults.errorCount + let testsCount = testSummary.totalTestCount + let testsFailedCount = testSummary.failedTests + let warningCount = buildResults.warningCount + let testsSkippedCount = testSummary.skippedTests var summary = "" if summaryFields.enabledFields.contains(.errors) { @@ -205,119 +224,107 @@ public struct XCResultFormatter { private func createTestDetailsString() -> [String] { var lines = [String]() - for testAction in invocationRecord.actions where testAction.schemeCommandName == "Test" { - lines.append(contentsOf: createTestDetailsString(forAction: testAction)) + let runDestination = tests.devices.first?.deviceName ?? "Unknown destination" + + let configuredNodes = tests.testPlanConfigurations.map { config in + ( + name: config.configurationName, + nodes: nodes(for: config, in: tests.testNodes) + ) } - return lines - } - private func createTestDetailsString(forAction testAction: ActionRecord) -> [String] { - var lines = [String]() - guard let testsId = testAction.actionResult.testsRef?.id, - let testPlanRun = resultFile.getTestPlanRunSummaries(id: testsId) else { - return lines + let entries: [(name: String, nodes: [XCTestNode])] = if configuredNodes.isEmpty { + [(testSummary.title, tests.testNodes)] + } else { + configuredNodes } - let testPlanRunSummaries = testPlanRun.summaries - let failureSummaries = invocationRecord.issues.testFailureSummaries - let runDestination = testAction.runDestination.displayName - for thisSummary in testPlanRunSummaries { - lines.append( - outputFormatter.testConfiguration(thisSummary.name ?? "No-name") - ) - for thisTestableSummary in thisSummary.testableSummaries { - if let targetName = thisTestableSummary.targetName { - let targetConfig = "\(targetName) on '\(runDestination)'" - lines.append( - outputFormatter.testConfiguration(targetConfig) - ) - } + for entry in entries { + lines.append(outputFormatter.testConfiguration(entry.name)) - if failedTestsOnly, - outputFormatter is CLIResultFormatter, - thisTestableSummary.tests.allSatisfy({ $0.hasNoFailedTests }) { - lines.append("No test failures") - } else { - for thisTest in thisTestableSummary.tests { - lines += createTestSummaryInfo(thisTest, level: 0, failureSummaries: failureSummaries) - } - } + let groups = entry.nodes.compactMap { mapGroup(node: $0, currentTestClassName: nil) } + let targetLabel = targetName(for: groups) + if let targetLabel { + lines.append(outputFormatter.testConfiguration("\(targetLabel) on '\(runDestination)'")) + } - lines.append( - outputFormatter.divider - ) + if failedTestsOnly, + outputFormatter is CLIResultFormatter, + groups.allSatisfy(\.hasNoFailedTests) { + lines.append("No test failures") + } else { + for group in groups { + lines += createTestSummaryInfo(group, level: 0, failureSummaries: testSummary.testFailures) + } } + + lines.append(outputFormatter.divider) } + return lines } + private func targetName(for groups: [FormattedTestGroup]) -> String? { + if let bundle = groups.first(where: { $0.name.hasSuffix(".xctest") }) { + return bundle.name.replacingOccurrences(of: ".xctest", with: "") + } + return groups.first?.name + } + private func createTestSummaryInfo( - _ group: ActionTestSummaryGroup, + _ group: FormattedTestGroup, level: Int, - failureSummaries: [TestFailureIssueSummary] + failureSummaries: [XCTestFailure] ) -> [String] { var lines = [String]() if failedTestsOnly, !group.hasFailedTests { return lines } - let header = "\(group.nameString) (\(numFormatter.unwrappedString(for: group.duration)))" + let header = "\(group.name) (\(numFormatter.unwrappedString(for: group.duration)))" switch level { case 0: break case 1: - lines.append( - outputFormatter.testTarget(header, failed: group.hasFailedTests) - ) + lines.append(outputFormatter.testTarget(header, failed: group.hasFailedTests)) case 2: - lines.append( - outputFormatter.testClass(header, failed: group.hasFailedTests) - ) + lines.append(outputFormatter.testClass(header, failed: group.hasFailedTests)) default: - lines.append( - outputFormatter.testClass(header, failed: group.hasFailedTests) - ) + lines.append(outputFormatter.testClass(header, failed: group.hasFailedTests)) } for subGroup in group.subtestGroups { lines += createTestSummaryInfo(subGroup, level: level + 1, failureSummaries: failureSummaries) } if !outputFormatter.accordionOpenTag.isEmpty { - lines.append( - outputFormatter.accordionOpenTag - ) + lines.append(outputFormatter.accordionOpenTag) } for thisTest in group.subtests { if !failedTestsOnly || thisTest.isFailed { - lines.append( - actionTestFileStatusString(for: thisTest, failureSummaries: failureSummaries) - ) + lines.append(actionTestFileStatusString(for: thisTest, failureSummaries: failureSummaries)) } } if !outputFormatter.accordionCloseTag.isEmpty { - lines.append( - outputFormatter.accordionCloseTag - ) + lines.append(outputFormatter.accordionCloseTag) } return lines } private func actionTestFileStatusString( - for testData: ActionTestMetadata, - failureSummaries: [TestFailureIssueSummary] + for testData: FormattedTest, + failureSummaries: [XCTestFailure] ) -> String { let duration = numFormatter.unwrappedString(for: testData.duration) let icon = actionTestFileStatusStringIcon(testData: testData) - let testTitle = "\(icon) \(testData.name ?? "Missing-Name") (\(duration))" - let testCaseName = testData.identifier?.replacingOccurrences(of: "/", with: ".") ?? "No-identifier" - if let summary = failureSummaries.first(where: { $0.testCaseName == testCaseName }) { + let testTitle = "\(icon) \(testData.name) (\(duration))" + let testCaseName = testData.identifier.replacingOccurrences(of: "/", with: ".") + if let summary = failureSummaries.first(where: { $0.testIdentifierString == testCaseName }) { return actionTestFailureStatusString(with: testTitle, and: summary) - } else { - return outputFormatter.singleTestItem(testTitle, failed: testData.isFailed) } + return outputFormatter.singleTestItem(testTitle, failed: testData.isFailed) } - private func actionTestFileStatusStringIcon(testData: ActionTestMetadata) -> String { + private func actionTestFileStatusStringIcon(testData: FormattedTest) -> String { if testData.isSuccessful { return outputFormatter.testPassIcon } @@ -331,53 +338,56 @@ public struct XCResultFormatter { private func actionTestFailureStatusString( with header: String, - and failure: TestFailureIssueSummary + and failure: XCTestFailure ) -> String { - return outputFormatter.failedTestItem(header, message: failure.message) + return outputFormatter.failedTestItem(header, message: failure.failureText) } private func createCoverageReport() -> [String] { var lines = [String]() lines.append(outputFormatter.testConfiguration("Coverage report")) - guard let codeCoverage else { + guard let coverageReport else { return lines } + var executableLines = 0 var coveredLines = 0 - for target in codeCoverage.targets { + for target in coverageReport.targets { let targetData = createCoverageReportFor(target: target) lines += targetData.lines executableLines += targetData.executableLines coveredLines += targetData.coveredLines } - // Append the total coverage below the header + guard executableLines > 0 else { return lines } let fraction = Double(coveredLines) / Double(executableLines) - let covPercent: String = percentFormatter.unwrappedString(for: fraction * 100) - let line = outputFormatter.codeCoverageTargetSummary( - "Total coverage: \(covPercent)% (\(coveredLines)/\(executableLines))") + let covPercent = percentFormatter.unwrappedString(for: fraction * 100) + let line = outputFormatter.codeCoverageTargetSummary("Total coverage: \(covPercent)% (\(coveredLines)/\(executableLines))") lines.insert(line, at: 1) return lines } - private func createCoverageReportFor(target: CodeCoverageTarget) -> CodeCoverageParseResult { + private func createCoverageReportFor(target: CoverageTarget) -> CodeCoverageParseResult { var lines = [String]() var executableLines = 0 var coveredLines = 0 guard coverageTargets.contains(target.name) else { return CodeCoverageParseResult(lines: lines, executableLines: executableLines, coveredLines: coveredLines) } + let covPercent = percentFormatter.unwrappedString(for: target.lineCoverage * 100) executableLines += target.executableLines coveredLines += target.coveredLines guard coverageReportFormat != .totals else { return CodeCoverageParseResult(lines: lines, executableLines: executableLines, coveredLines: coveredLines) } + lines.append( outputFormatter.codeCoverageTargetSummary( "\(target.name): \(covPercent)% (\(target.coveredLines)/\(target.executableLines))" ) ) + if coverageReportFormat != .targets { if !outputFormatter.accordionOpenTag.isEmpty { lines.append(outputFormatter.accordionOpenTag) @@ -392,7 +402,7 @@ public struct XCResultFormatter { return CodeCoverageParseResult(lines: lines, executableLines: executableLines, coveredLines: coveredLines) } - private func createCoverageReportFor(file: CodeCoverageFile) -> [String] { + private func createCoverageReportFor(file: CoverageReportFile) -> [String] { var lines = [String]() let covPercent = percentFormatter.unwrappedString(for: file.lineCoverage * 100) lines.append( @@ -430,67 +440,107 @@ public struct XCResultFormatter { return lines } - struct CodeCoverageParseResult { - let lines: [String] - let executableLines: Int - let coveredLines: Int + private func nodes(for configuration: XCConfiguration, in roots: [XCTestNode]) -> [XCTestNode] { + var matches = [XCTestNode]() + for root in roots { + matches.append(contentsOf: findConfigurationChildren(in: root, configuration: configuration)) + } + return matches.isEmpty ? roots : matches } -} -extension ActionTestMetadata { - var isFailed: Bool { - return isSuccessful == false && isSkipped == false - } + private func findConfigurationChildren(in node: XCTestNode, configuration: XCConfiguration) -> [XCTestNode] { + if node.nodeType == .testPlanConfiguration && + (node.name == configuration.configurationName || node.nodeIdentifier == configuration.configurationId) { + return node.children ?? [] + } - var isSuccessful: Bool { - return testStatus == "Success" || testStatus == "Expected Failure" + return (node.children ?? []).flatMap { findConfigurationChildren(in: $0, configuration: configuration) } } - var isSkipped: Bool { - return testStatus == "Skipped" + private func mapGroup(node: XCTestNode, currentTestClassName: String?) -> FormattedTestGroup? { + guard node.nodeType != .testCase else { + return nil + } + + let groupName = mappedGroupName(for: node) + let nextTestClassName: String? = if node.nodeType == .testSuite { + groupName + } else { + currentTestClassName + } + + let children = node.children ?? [] + let tests = children + .filter { $0.nodeType == .testCase } + .map { mapTest(node: $0, testClassName: nextTestClassName) } + + let groups = children.compactMap { child in + mapGroup(node: child, currentTestClassName: nextTestClassName) + } + + let duration = node.durationInSeconds ?? tests.compactMap(\.duration).reduce(0, +) + groups.reduce(0) { $0 + $1.duration } + + return FormattedTestGroup( + name: groupName, + duration: duration, + subtests: tests, + subtestGroups: groups + ) } -} -extension ActionTestSummaryGroup { - var nameString: String { - return name ?? "Unnamed" + private func mapTest(node: XCTestNode, testClassName: String?) -> FormattedTest { + let result = node.result ?? .unknown + let identifier: String + if let testClassName { + identifier = "\(testClassName)/\(node.name)" + } else { + identifier = node.name + } + return FormattedTest( + identifier: identifier, + name: node.name, + duration: node.durationInSeconds, + isFailed: result == .failed, + isSkipped: result == .skipped || result == .expectedFailure + ) } -} -extension NumberFormatter { - func unwrappedString(for input: Double?) -> String { - return string(for: input) ?? "" + private func mappedGroupName(for node: XCTestNode) -> String { + switch node.nodeType { + case .unitTestBundle, .uiTestBundle: + return node.name.hasSuffix(".xctest") ? node.name : "\(node.name).xctest" + default: + return node.name + } } -} -extension CodeCoverage { - func targets(filteredBy filter: [String]) -> Set { - let targetNames = targets.map { $0.name } + private static func targets(filteredBy filter: [String], availableTargets: [String]) -> Set { guard !filter.isEmpty else { - return Set(targetNames) + return Set(availableTargets) } let filterSet = Set(filter) - let filtered = targetNames.filter { thisTarget in - // Clean up target.name. Split on '.' because the target.name is appended with .framework or .app + let filtered = availableTargets.filter { thisTarget in guard let stripped = thisTarget.split(separator: ".").first else { return true } return filterSet.contains(String(stripped)) } return Set(filtered) } -} -private extension ActionTestSummaryGroup { - var hasFailedTests: Bool { - if subtests.first(where: \.isFailed) != nil { - return true - } - if subtestGroups.first(where: \.hasFailedTests) != nil { - return true - } - return false + private static func getCoverageReportAsJSON(resultFileURL: URL, shell: Commandline) throws -> CoverageReport { + let arguments = ["xccov", "view", "--report", "--json", resultFileURL.path] + let coverageData = try shell.execute(program: "/usr/bin/xcrun", with: arguments) + return try JSONDecoder().decode(CoverageReport.self, from: coverageData) } - var hasNoFailedTests: Bool { - return !hasFailedTests + struct CodeCoverageParseResult { + let lines: [String] + let executableLines: Int + let coveredLines: Int + } +} + +extension NumberFormatter { + func unwrappedString(for input: Double?) -> String { + return string(for: input) ?? "" } } From 50833ac15cbd46c8033674f590cc18bf55da0f57 Mon Sep 17 00:00:00 2001 From: Alex da Franca Date: Sun, 22 Feb 2026 02:53:22 +0100 Subject: [PATCH 08/14] Added docs and refactorings --- .gitignore | 1 + CHANGELOG.md | 7 + CommandlineTool/main.swift | 2 +- README.md | 7 +- .../CoberturaCoverageConverter.swift | 10 +- .../xcresultparser/CoverageConverter.swift | 12 +- .../xcresultparser/XCResultFormatter.swift | 143 ++++++++++++++++-- 7 files changed, 146 insertions(+), 36 deletions(-) diff --git a/.gitignore b/.gitignore index 5870122..9e4c35e 100644 --- a/.gitignore +++ b/.gitignore @@ -92,6 +92,7 @@ iOSInjectionProject/ # Compiled app, ready for notarization product/ +AgentNotes/ .scannerwork/ diff --git a/CHANGELOG.md b/CHANGELOG.md index bfdbdda..a6619f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Version 2.0.0-beta - 2026-02-21 +### CHANGES: +- Replace XCResultKit usage with direct `xcresulttool` and `xccov` parsing using local `Codable` models. +- Remove XCResultKit as a package dependency from `Package.swift`. +- Migrate JUnit, summary/test formatting, coverage formatting, and issues JSON paths to the new model/tooling layer. +- Keep output compatibility where possible while aligning with modern `xcresulttool` payloads. + ## Version 1.9.4 - 2025-12-19 ### CHANGES: Previously, we were only ever parsing the first failure for a test and passing that to the JUnit representation. diff --git a/CommandlineTool/main.swift b/CommandlineTool/main.swift index bae6270..6f83c8a 100644 --- a/CommandlineTool/main.swift +++ b/CommandlineTool/main.swift @@ -9,7 +9,7 @@ import ArgumentParser import Foundation import XcresultparserLib -private let marketingVersion = "2.0.0" +private let marketingVersion = "2.0.0-beta" struct xcresultparser: ParsableCommand { static let configuration = CommandConfiguration( diff --git a/README.md b/README.md index c9e94f6..ef5fce9 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,8 @@ In case of 'xml' JUnit format for test results and generic format (Sonarqube) fo You can also specify the name of the project root. Paths and urls are then relative to the specified directory. (used for urls in xml output) -This tool can read test result data and code coverage data from an .xcarchive using the developer tools included in `Xcode 13`. Namely here: xcresulttool and xccov to get json data from .xcresult bundles. - -Parsing the JSON is done using the great [XCResultKit](https://github.com/davidahouse/XCResultKit) package. +This tool can read test result data and code coverage data from an `.xcresult` bundle using `xcresulttool` and `xccov`. +All JSON parsing is done with native `Codable` models in this project.
More on converting code coverage data @@ -87,7 +86,7 @@ You should see the tool respond like this: ``` Error: Missing expected argument '' -OVERVIEW: xcresultparser 1.9.4 +OVERVIEW: xcresultparser 2.0.0-beta Interpret binary .xcresult files and print summary in different formats: txt, xml, html or colored cli output. diff --git a/Sources/xcresultparser/CoberturaCoverageConverter.swift b/Sources/xcresultparser/CoberturaCoverageConverter.swift index 2f7df8c..bb7141b 100644 --- a/Sources/xcresultparser/CoberturaCoverageConverter.swift +++ b/Sources/xcresultparser/CoberturaCoverageConverter.swift @@ -166,7 +166,7 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { } private func makeRootElement() -> XMLElement { - // TODO: some of these values are B.S. - figure out how to calculate, or better to omit if we don't know? + // Cobertura requires these attributes; branch/complexity values are placeholders for now. let timeStamp = startTime ?? Date().timeIntervalSince1970 let rootElement = XMLElement(name: "coverage") rootElement.addAttribute( @@ -188,13 +188,7 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { return rootElement } - // this ised to be fetched online from http://cobertura.sourceforge.net/xml/coverage-04.dtd - // that broke, when the URL changed to: - // https://github.com/cobertura/cobertura/blob/master/cobertura/src/site/htdocs/xml/coverage-04.dtd - // In case we couldn't download the data, we had a file as fallback. However that file could never be read - // because as command line tool this is not a bundle and thus there is no file to be found in the bundle - // IMO all that was overengineered for the followong 60 lines string... - // ...which will probably never ever change! + // Keep DTD inline to avoid runtime fetches and bundle/file lookup issues. // Helper methods for creating valid Cobertura XML structure private func createValidPackageName(from pathComponents: [Substring]) -> String { // Use original simple logic: join all path components except the filename with dots diff --git a/Sources/xcresultparser/CoverageConverter.swift b/Sources/xcresultparser/CoverageConverter.swift index 55366d8..d054b10 100644 --- a/Sources/xcresultparser/CoverageConverter.swift +++ b/Sources/xcresultparser/CoverageConverter.swift @@ -131,13 +131,7 @@ public class CoverageConverter { return false } - // MARK: - unused and only here for reference - - // This method was replaced by getCoverageDataAsJSON() - // Instead of requiring to get the coverage data for each single code file - // we now can obtain all information for all targets and all files in one call to xccov - // That is of course much faster, than calling xccov for each file, as we needed to in older times - // It is not used at the moment, but is left here just to cover this xccov function + // Maintained to support the public API used by tests and existing consumers. func coverageForFile(path: String) throws -> String { var arguments = ["xccov", "view"] if resultFileURL.pathExtension == "xcresult" { @@ -150,9 +144,7 @@ public class CoverageConverter { return String(decoding: coverageData, as: UTF8.self) } - // This method was replaced by going through all files in all targets - // That allows us to filter by targets easier - // It is not used at the moment, but is left here just to cover this xccov function + // Maintained to support the public API used by tests and existing consumers. func coverageFileList() throws -> [String] { var arguments = ["xccov", "view"] if resultFileURL.pathExtension == "xcresult" { diff --git a/Sources/xcresultparser/XCResultFormatter.swift b/Sources/xcresultparser/XCResultFormatter.swift index e79d8f5..92ee163 100644 --- a/Sources/xcresultparser/XCResultFormatter.swift +++ b/Sources/xcresultparser/XCResultFormatter.swift @@ -56,6 +56,11 @@ public struct XCResultFormatter { } } + private struct FailureMessageDetail { + let message: String + let documentLocation: String + } + // MARK: - Properties private let outputFormatter: XCResultFormatting @@ -67,6 +72,10 @@ public struct XCResultFormatter { private let failedTestsOnly: Bool private let summaryFields: SummaryFields private let coverageReportFormat: CoverageReportFormat + private let failureLocationRegex = try? NSRegularExpression( + pattern: #"^(.+?):([0-9]+):\s*(.+)$"#, + options: [] + ) private var numFormatter: NumberFormatter = { let numFormatter = NumberFormatter() @@ -225,6 +234,7 @@ public struct XCResultFormatter { private func createTestDetailsString() -> [String] { var lines = [String]() let runDestination = tests.devices.first?.deviceName ?? "Unknown destination" + let failureMessageDetails = failureMessageDetailsByTestIdentifier() let configuredNodes = tests.testPlanConfigurations.map { config in ( @@ -254,7 +264,12 @@ public struct XCResultFormatter { lines.append("No test failures") } else { for group in groups { - lines += createTestSummaryInfo(group, level: 0, failureSummaries: testSummary.testFailures) + lines += createTestSummaryInfo( + group, + level: 0, + failureSummaries: testSummary.testFailures, + failureMessageDetails: failureMessageDetails + ) } } @@ -274,7 +289,8 @@ public struct XCResultFormatter { private func createTestSummaryInfo( _ group: FormattedTestGroup, level: Int, - failureSummaries: [XCTestFailure] + failureSummaries: [XCTestFailure], + failureMessageDetails: [String: [FailureMessageDetail]] ) -> [String] { var lines = [String]() if failedTestsOnly, @@ -294,14 +310,25 @@ public struct XCResultFormatter { lines.append(outputFormatter.testClass(header, failed: group.hasFailedTests)) } for subGroup in group.subtestGroups { - lines += createTestSummaryInfo(subGroup, level: level + 1, failureSummaries: failureSummaries) + lines += createTestSummaryInfo( + subGroup, + level: level + 1, + failureSummaries: failureSummaries, + failureMessageDetails: failureMessageDetails + ) } if !outputFormatter.accordionOpenTag.isEmpty { lines.append(outputFormatter.accordionOpenTag) } for thisTest in group.subtests { if !failedTestsOnly || thisTest.isFailed { - lines.append(actionTestFileStatusString(for: thisTest, failureSummaries: failureSummaries)) + lines.append( + actionTestFileStatusString( + for: thisTest, + failureSummaries: failureSummaries, + failureMessageDetails: failureMessageDetails + ) + ) } } if !outputFormatter.accordionCloseTag.isEmpty { @@ -312,14 +339,26 @@ public struct XCResultFormatter { private func actionTestFileStatusString( for testData: FormattedTest, - failureSummaries: [XCTestFailure] + failureSummaries: [XCTestFailure], + failureMessageDetails: [String: [FailureMessageDetail]] ) -> String { let duration = numFormatter.unwrappedString(for: testData.duration) let icon = actionTestFileStatusStringIcon(testData: testData) let testTitle = "\(icon) \(testData.name) (\(duration))" - let testCaseName = testData.identifier.replacingOccurrences(of: "/", with: ".") - if let summary = failureSummaries.first(where: { $0.testIdentifierString == testCaseName }) { - return actionTestFailureStatusString(with: testTitle, and: summary) + let testCaseName = testData.identifier + if let summary = failureSummaries.first(where: { + $0.testIdentifierString == testCaseName || + $0.testIdentifierString.replacingOccurrences(of: "/", with: ".") == testCaseName + }) { + let candidates = failureMessageDetails[summary.testIdentifierString] ?? + failureMessageDetails[summary.testIdentifierString.replacingOccurrences(of: "/", with: ".")] ?? [] + let matchingDetail = bestFailureMessage(for: summary, in: candidates) + let enrichedMessage = if let matchingDetail { + "\(matchingDetail.documentLocation): \(matchingDetail.message)" + } else { + summary.failureText + } + return actionTestFailureStatusString(with: testTitle, message: enrichedMessage) } return outputFormatter.singleTestItem(testTitle, failed: testData.isFailed) } @@ -336,11 +375,8 @@ public struct XCResultFormatter { return outputFormatter.testFailIcon } - private func actionTestFailureStatusString( - with header: String, - and failure: XCTestFailure - ) -> String { - return outputFormatter.failedTestItem(header, message: failure.failureText) + private func actionTestFailureStatusString(with header: String, message: String) -> String { + return outputFormatter.failedTestItem(header, message: message) } private func createCoverageReport() -> [String] { @@ -532,6 +568,87 @@ public struct XCResultFormatter { return try JSONDecoder().decode(CoverageReport.self, from: coverageData) } + private func failureMessageDetailsByTestIdentifier() -> [String: [FailureMessageDetail]] { + var result = [String: [FailureMessageDetail]]() + for node in tests.testNodes { + collectFailureMessages( + in: node, + currentTestIdentifier: nil, + currentTestClassName: nil, + into: &result + ) + } + return result + } + + private func collectFailureMessages( + in node: XCTestNode, + currentTestIdentifier: String?, + currentTestClassName: String?, + into result: inout [String: [FailureMessageDetail]] + ) { + var currentIdentifier = currentTestIdentifier + let nextTestClassName: String? = if node.nodeType == .testSuite { + node.name + } else { + currentTestClassName + } + if node.nodeType == .testCase { + currentIdentifier = node.nodeIdentifier ?? { + if let nextTestClassName { + return "\(nextTestClassName)/\(node.name)" + } + return node.name + }() + } + + if node.nodeType == .failureMessage, + let currentIdentifier, + let detail = parseFailureMessage(node.name) { + result[currentIdentifier, default: []].append(detail) + let dotKey = currentIdentifier.replacingOccurrences(of: "/", with: ".") + result[dotKey, default: []].append(detail) + } + + for child in node.children ?? [] { + collectFailureMessages( + in: child, + currentTestIdentifier: currentIdentifier, + currentTestClassName: nextTestClassName, + into: &result + ) + } + } + + private func parseFailureMessage(_ raw: String) -> FailureMessageDetail? { + guard let failureLocationRegex else { + return nil + } + let range = NSRange(raw.startIndex.. FailureMessageDetail? { + candidates.first { + $0.message == failure.failureText || + $0.message.contains(failure.failureText) || + failure.failureText.contains($0.message) + } ?? candidates.first + } + struct CodeCoverageParseResult { let lines: [String] let executableLines: Int From 7cececc81ebcc7b2f99c965a1899987e2dc6a98f Mon Sep 17 00:00:00 2001 From: Alex da Franca Date: Sun, 22 Feb 2026 17:16:17 +0100 Subject: [PATCH 09/14] Refactoring, adding tests and documentation --- CHANGELOG.md | 3 + CommandlineTool/main.swift | 24 +- Package.swift | 5 +- README.md | 11 +- .../xcresultparser/CoverageConverter.swift | 146 +++++---- .../XCResultToolJunitXMLDataProvider.swift | 63 ++-- .../XCTestNode+Extensions.swift | 136 ++++++++ .../Markdown/MDResultFormatter.swift | 1 + .../Formatters/XCResultFormatting.swift | 5 + .../SharedTypes/CoverageTargetSelection.swift | 47 +++ .../SharedTypes/DependencyFactory.swift | 8 + .../SharedTypes/Services/XCCovClient.swift | 64 ++++ .../Services/XCResultToolClient.swift | 11 +- .../xcresultparser/XCResultFormatter.swift | 222 +++++++++---- ...p1WsyhiYmsFr8zpEKeuyQT6ZNjSy6_u2QbZ9IEMw== | Bin 0 -> 280 bytes ...Odn2kN0mMSFD9OQG4KcGu2RYutICbM3MveutJKBg== | Bin 0 -> 425 bytes ...j4tenYlWVvzD-EfaO1aDCKwk2CBS3pooiliNF2nQ== | Bin 0 -> 258 bytes ...p6yy2wj6lRTghOXV_Tx0EZKsT4xlVowc_Av3UT0w== | Bin 0 -> 286 bytes ...8VN5i6EXYBBLXzoy2GPQUMu4q3xIR0qDVfb4zNkA== | 1 + ...b-pPONL7bRmKcxpSsMj-OeQBsp5cbyZoD_4Ck2wA== | Bin 0 -> 247 bytes ...AkReDjUYpsT_0gnC7r4IcvcJ66ifStBTuWFZ_G9g== | Bin 0 -> 286 bytes ...WLj9wGhE8TH94CVbd2ZzN1O6J-X5eRMUybUgL-8g== | Bin 0 -> 532 bytes ...S0C7JqyUwxLpLp3Ok94uC51BCdWe8uF6-V9XA3sQ== | Bin 0 -> 261 bytes ...ygHQekpq_BH-QNUmTiOVAp-AGxzsYCdeo0oa-w3A== | Bin 0 -> 264 bytes ...qYS2gYl93vsliZhSXo_vfPLGog-97yVBMoRKvzWg== | Bin 0 -> 429 bytes ..._EaMMTHgzUtI2NkSR-ljVOr3sOBSMJslTc3DCOEg== | Bin 0 -> 272 bytes ...ra3Kpta9BJlU2i1GqniBeoJC6Yvcqm2BLjG8RSvg== | Bin 0 -> 276 bytes ...H5H1_TpZcg249YgUlOJGSV3Zus7fcth_2bIRYteQ== | Bin 0 -> 248 bytes ...FCVje_p6WA_rZCErCbBp5xNMEal0WlIiswezkmGQ== | Bin 0 -> 247 bytes ...IKUPeENGK4HszLZCQ9m813rX3cdEOuuQm4mLQ9EQ== | Bin 0 -> 270 bytes ...AFPxwqNCtVXEBwx_n5xHi5L8EXt3BOMRDJe9pumQ== | Bin 0 -> 251 bytes ...Ue-RMBF7ZSStBYkiDPFQEgz_MJJKNhQUTgGTY18A== | Bin 0 -> 9278 bytes ...n2lZmTisu3Z1RAinPjNhPRnDIKX2Fwbb4B0d2tgg== | Bin 0 -> 250 bytes ...TOwqGJ-aq0i-R2u39iicblHfGczRiNuo-kNNIulw== | Bin 0 -> 249 bytes ...86E1sMNAjxeGejO-YZDPPT64LxpGQEVRbDsE9Gqw== | Bin 0 -> 249 bytes ...nYy1DtkD90oypXv-gAL_VehJ4v8k3czEoqvHpoUQ== | Bin 0 -> 1713 bytes ...sOvikLqISFX6sC7Isbk_Rcf-83_XdL07hDRZU0GA== | Bin 0 -> 272 bytes ...gMpwsp3KezPvTu1BtKkn-4kgXRw6aeSGASx-hFqA== | Bin 0 -> 259 bytes ...JfE3FNOylq0NoFsKhLyquDKwBiZBPFt_KSWb3mRA== | Bin 0 -> 71 bytes ...M--Jm1jalGBCpPTsAMpM4Rx21VJpYW_9Cc08odyQ== | Bin 0 -> 422 bytes ...ogHsYo6pK3a2IgU0bM1gnuvIboKWw3XpTHelX9QQ== | Bin 0 -> 541 bytes ...wPMbuLLOP5zeCAqlu1mOCAFxwlysTxy8UvcQtwPg== | Bin 0 -> 751 bytes ...EtQ2ubG0dmnB3LIJZmGcxTUU9F4Bla9biPtJeWBA== | Bin 0 -> 286 bytes ...MEH-86pvkqpNT7QPZCNGtZC9E2zB0FZL4JJYAACA== | Bin 0 -> 262 bytes ...Q-4dmQGT60kLtLftCtndnghia9hN9PgbVANQpMEw== | Bin 0 -> 4954 bytes ...TfiL77Di2bcD6GVvQ3C2Lfwkdh0eCoVwsSSaRonA== | Bin 0 -> 258 bytes ...jESqJdsT1C3vk_wPSj4JNfFschEmL3pVLer4xFtA== | Bin 0 -> 10730 bytes ...z0j0V4mSCj0hy_x3_hRcmVVS9HNOlBCMBf9wUPpA== | Bin 0 -> 5575 bytes ...46zM8SZttdNLvVegsywqlpkgJumXfYXKzEHmysgQ== | Bin 0 -> 284 bytes ...jDHhD1Tjp1SSVKRRdS3UjRduKDSc4L7ZAXIhB_Zw== | Bin 0 -> 273 bytes ...9jRdoxlXQDc6qeurxWs9WcH3k8EnVbFvLcB5yRxg== | Bin 0 -> 255 bytes ...7vEWJbFDs8cgeTHeqzPxnhRJUvb2RyTwx_Hok5HA== | Bin 0 -> 155 bytes ...HEDZkQJL2SraatVbt-vitw3ffowrgrsh4yoqt-WA== | 1 + ...wKRmxym-FjEQISAxCuCoAPFJbrHoDfL2gFc7N9rg== | Bin 0 -> 275 bytes ...oHxOUnANszYx-cPrrXztOe4nJC7LMLm82ktgcsNg== | Bin 0 -> 249 bytes ...OEOkwAhtCLRgKTcIhh9Fwwng91U3WqoWG8tQ-ztg== | Bin 0 -> 279 bytes ...0Pjy_kK3wLYYOuZGfUcpyiCL_pZjXSwZ7mQASfxQ== | Bin 0 -> 249 bytes ...Xvnmr9CbyY3iL1A-yurqaaaZX_WNZ2v475fwF6bQ== | Bin 0 -> 528 bytes ...Z8b9wd6QImnglUSL1By02e77T378yucTwmDqowtg== | Bin 0 -> 271 bytes ...pzCWg7plcLMtG4RRzitHxWpSfp5IdACQlxk_P5sg== | Bin 0 -> 251 bytes ...igUOaTw8vthD7__wT069-Y3vjMa39agMpb3oflIQ== | Bin 0 -> 1514 bytes ...IqyHNuquiq23Gl2qMnw27936cz3UV0x_tFMUPGIQ== | Bin 0 -> 119 bytes ...rTsAjcvUVZ3QNAh7qG5mLqEDzdEC8bhhfgB1o_Pw== | Bin 0 -> 281 bytes ...G18J74lrqWQrBabo86SXwhFqTCvdOeu_yH0983vg== | Bin 0 -> 8525 bytes ...PFJp1VHjE7qKrIBwL_X6ol2lcDNhIIzL4Fxa8mNw== | Bin 0 -> 11810 bytes ...O2a1gXlMlMSnn1ecqy0tYw_TSA_eNjgzFgGzuC4Q== | Bin 0 -> 260 bytes ...udaErZm_DYyTqyXlI1djq2RjUIwc-svS9woPCzAA== | Bin 0 -> 275 bytes ...-hRXuWG4NHbKKG55tId67mq9pX4fkuNJBKrDn7mQ== | Bin 0 -> 247 bytes ...jaz0qUkWtqK_eVeQqPkST8GaTNpfJq3xsMaFGv5A== | Bin 0 -> 283 bytes ...6ZEVYC2yRUpsV3bzZxV9OZaROWKabI2u8ktFPdgQ== | Bin 0 -> 249 bytes ...YENoIq7y9oUSDWL5B_P2gkdtWG8JEsJLxPbdWOZA== | Bin 0 -> 199 bytes ...wle_QZx78T-KVIHsqxzINi_UorLurHXxgKRVXCdw== | Bin 0 -> 538 bytes ...a3TmVyiHOa9-5kzTES8di5UgFjO6Xx57HtC3oSFw== | Bin 0 -> 248 bytes ...t1vm-h7qBDnONG_2GUR6oDxKS5Hi7y1Ufd4abPrA== | Bin 0 -> 258 bytes ...KAwJbjh1YWBJG3EnNHuVSL8sfQbzsrSd2qvqKGTA== | Bin 0 -> 255 bytes ...FfpWvnFUwJ0kFT4H_Uo3ZSvWKZl6R38hCwVrCKNQ== | Bin 0 -> 426 bytes ...1bIWI3Gv4GF3AJsI8r-sXKJzTg9AMoKIT82EYWVQ== | Bin 0 -> 277 bytes ...Fs1lwq1Pr0YtAovl57f7s2qhUlqyoeWtR2-6f1rQ== | Bin 0 -> 912 bytes ...t5ceYbMsDNqik2bpreDAJi64yFUwoKPEwKX65VvQ== | Bin 0 -> 250 bytes ...2HpZ9Dy8x55P9yOFJTTEb6WqYfH-SelneFPa_hBA== | Bin 0 -> 271 bytes ...9ogHH7Ihj53tnq2QtzHRp_1l7fwNuvpcHR6YKRpA== | Bin 0 -> 275 bytes ...p1WsyhiYmsFr8zpEKeuyQT6ZNjSy6_u2QbZ9IEMw== | Bin 0 -> 1 bytes ...Odn2kN0mMSFD9OQG4KcGu2RYutICbM3MveutJKBg== | Bin 0 -> 1 bytes ...j4tenYlWVvzD-EfaO1aDCKwk2CBS3pooiliNF2nQ== | Bin 0 -> 1 bytes ...p6yy2wj6lRTghOXV_Tx0EZKsT4xlVowc_Av3UT0w== | Bin 0 -> 1 bytes ...8VN5i6EXYBBLXzoy2GPQUMu4q3xIR0qDVfb4zNkA== | Bin 0 -> 67 bytes ...b-pPONL7bRmKcxpSsMj-OeQBsp5cbyZoD_4Ck2wA== | Bin 0 -> 1 bytes ...AkReDjUYpsT_0gnC7r4IcvcJ66ifStBTuWFZ_G9g== | Bin 0 -> 1 bytes ...WLj9wGhE8TH94CVbd2ZzN1O6J-X5eRMUybUgL-8g== | Bin 0 -> 1 bytes ...S0C7JqyUwxLpLp3Ok94uC51BCdWe8uF6-V9XA3sQ== | Bin 0 -> 1 bytes ...ygHQekpq_BH-QNUmTiOVAp-AGxzsYCdeo0oa-w3A== | Bin 0 -> 1 bytes ...qYS2gYl93vsliZhSXo_vfPLGog-97yVBMoRKvzWg== | Bin 0 -> 1 bytes ..._EaMMTHgzUtI2NkSR-ljVOr3sOBSMJslTc3DCOEg== | Bin 0 -> 1 bytes ...ra3Kpta9BJlU2i1GqniBeoJC6Yvcqm2BLjG8RSvg== | Bin 0 -> 1 bytes ...H5H1_TpZcg249YgUlOJGSV3Zus7fcth_2bIRYteQ== | Bin 0 -> 1 bytes ...FCVje_p6WA_rZCErCbBp5xNMEal0WlIiswezkmGQ== | Bin 0 -> 1 bytes ...IKUPeENGK4HszLZCQ9m813rX3cdEOuuQm4mLQ9EQ== | Bin 0 -> 1 bytes ...AFPxwqNCtVXEBwx_n5xHi5L8EXt3BOMRDJe9pumQ== | Bin 0 -> 1 bytes ...Ue-RMBF7ZSStBYkiDPFQEgz_MJJKNhQUTgGTY18A== | Bin 0 -> 1 bytes ...n2lZmTisu3Z1RAinPjNhPRnDIKX2Fwbb4B0d2tgg== | Bin 0 -> 1 bytes ...TOwqGJ-aq0i-R2u39iicblHfGczRiNuo-kNNIulw== | Bin 0 -> 1 bytes ...86E1sMNAjxeGejO-YZDPPT64LxpGQEVRbDsE9Gqw== | Bin 0 -> 1 bytes ...nYy1DtkD90oypXv-gAL_VehJ4v8k3czEoqvHpoUQ== | Bin 0 -> 463 bytes ...sOvikLqISFX6sC7Isbk_Rcf-83_XdL07hDRZU0GA== | Bin 0 -> 1 bytes ...gMpwsp3KezPvTu1BtKkn-4kgXRw6aeSGASx-hFqA== | Bin 0 -> 1 bytes ...JfE3FNOylq0NoFsKhLyquDKwBiZBPFt_KSWb3mRA== | Bin 0 -> 199 bytes ...M--Jm1jalGBCpPTsAMpM4Rx21VJpYW_9Cc08odyQ== | Bin 0 -> 1 bytes ...ogHsYo6pK3a2IgU0bM1gnuvIboKWw3XpTHelX9QQ== | Bin 0 -> 1 bytes ...wPMbuLLOP5zeCAqlu1mOCAFxwlysTxy8UvcQtwPg== | Bin 0 -> 1 bytes ...EtQ2ubG0dmnB3LIJZmGcxTUU9F4Bla9biPtJeWBA== | Bin 0 -> 1 bytes ...MEH-86pvkqpNT7QPZCNGtZC9E2zB0FZL4JJYAACA== | Bin 0 -> 1 bytes ...Q-4dmQGT60kLtLftCtndnghia9hN9PgbVANQpMEw== | Bin 0 -> 1 bytes ...TfiL77Di2bcD6GVvQ3C2Lfwkdh0eCoVwsSSaRonA== | Bin 0 -> 1 bytes ...jESqJdsT1C3vk_wPSj4JNfFschEmL3pVLer4xFtA== | Bin 0 -> 1 bytes ...z0j0V4mSCj0hy_x3_hRcmVVS9HNOlBCMBf9wUPpA== | Bin 0 -> 3235 bytes ...46zM8SZttdNLvVegsywqlpkgJumXfYXKzEHmysgQ== | Bin 0 -> 1 bytes ...jDHhD1Tjp1SSVKRRdS3UjRduKDSc4L7ZAXIhB_Zw== | Bin 0 -> 1 bytes ...9jRdoxlXQDc6qeurxWs9WcH3k8EnVbFvLcB5yRxg== | Bin 0 -> 1 bytes ...7vEWJbFDs8cgeTHeqzPxnhRJUvb2RyTwx_Hok5HA== | Bin 0 -> 133 bytes ...HEDZkQJL2SraatVbt-vitw3ffowrgrsh4yoqt-WA== | Bin 0 -> 133 bytes ...wKRmxym-FjEQISAxCuCoAPFJbrHoDfL2gFc7N9rg== | Bin 0 -> 1 bytes ...oHxOUnANszYx-cPrrXztOe4nJC7LMLm82ktgcsNg== | Bin 0 -> 1 bytes ...OEOkwAhtCLRgKTcIhh9Fwwng91U3WqoWG8tQ-ztg== | Bin 0 -> 1 bytes ...0Pjy_kK3wLYYOuZGfUcpyiCL_pZjXSwZ7mQASfxQ== | Bin 0 -> 1 bytes ...Xvnmr9CbyY3iL1A-yurqaaaZX_WNZ2v475fwF6bQ== | Bin 0 -> 1 bytes ...Z8b9wd6QImnglUSL1By02e77T378yucTwmDqowtg== | Bin 0 -> 1 bytes ...pzCWg7plcLMtG4RRzitHxWpSfp5IdACQlxk_P5sg== | Bin 0 -> 1 bytes ...igUOaTw8vthD7__wT069-Y3vjMa39agMpb3oflIQ== | Bin 0 -> 1 bytes ...IqyHNuquiq23Gl2qMnw27936cz3UV0x_tFMUPGIQ== | Bin 0 -> 133 bytes ...rTsAjcvUVZ3QNAh7qG5mLqEDzdEC8bhhfgB1o_Pw== | Bin 0 -> 1 bytes ...G18J74lrqWQrBabo86SXwhFqTCvdOeu_yH0983vg== | Bin 0 -> 1 bytes ...PFJp1VHjE7qKrIBwL_X6ol2lcDNhIIzL4Fxa8mNw== | Bin 0 -> 1 bytes ...O2a1gXlMlMSnn1ecqy0tYw_TSA_eNjgzFgGzuC4Q== | Bin 0 -> 1 bytes ...udaErZm_DYyTqyXlI1djq2RjUIwc-svS9woPCzAA== | Bin 0 -> 1 bytes ...-hRXuWG4NHbKKG55tId67mq9pX4fkuNJBKrDn7mQ== | Bin 0 -> 1 bytes ...jaz0qUkWtqK_eVeQqPkST8GaTNpfJq3xsMaFGv5A== | Bin 0 -> 1 bytes ...6ZEVYC2yRUpsV3bzZxV9OZaROWKabI2u8ktFPdgQ== | Bin 0 -> 1 bytes ...YENoIq7y9oUSDWL5B_P2gkdtWG8JEsJLxPbdWOZA== | Bin 0 -> 1 bytes ...wle_QZx78T-KVIHsqxzINi_UorLurHXxgKRVXCdw== | Bin 0 -> 1 bytes ...a3TmVyiHOa9-5kzTES8di5UgFjO6Xx57HtC3oSFw== | Bin 0 -> 1 bytes ...t1vm-h7qBDnONG_2GUR6oDxKS5Hi7y1Ufd4abPrA== | Bin 0 -> 1 bytes ...KAwJbjh1YWBJG3EnNHuVSL8sfQbzsrSd2qvqKGTA== | Bin 0 -> 1 bytes ...FfpWvnFUwJ0kFT4H_Uo3ZSvWKZl6R38hCwVrCKNQ== | Bin 0 -> 1 bytes ...1bIWI3Gv4GF3AJsI8r-sXKJzTg9AMoKIT82EYWVQ== | Bin 0 -> 1 bytes ...Fs1lwq1Pr0YtAovl57f7s2qhUlqyoeWtR2-6f1rQ== | Bin 0 -> 1 bytes ...t5ceYbMsDNqik2bpreDAJi64yFUwoKPEwKX65VvQ== | Bin 0 -> 1 bytes ...2HpZ9Dy8x55P9yOFJTTEb6WqYfH-SelneFPa_hBA== | Bin 0 -> 1 bytes ...9ogHH7Ihj53tnq2QtzHRp_1l7fwNuvpcHR6YKRpA== | Bin 0 -> 1 bytes .../parametrized.xcresult/Info.plist | 29 ++ .../parametrized.xcresult/database.sqlite3 | Bin 0 -> 258048 bytes .../XCCovClientTests.swift | 119 +++++++ ...CResultToolJunitXMLDataProviderTests.swift | 305 ++++++++++++++++++ .../XcresultparserTests.swift | 305 ++++++++++++++++-- 153 files changed, 1300 insertions(+), 206 deletions(-) create mode 100644 Sources/xcresultparser/Models/XCResultToolModels/XCTestNode+Extensions.swift create mode 100644 Sources/xcresultparser/SharedTypes/CoverageTargetSelection.swift create mode 100644 Sources/xcresultparser/SharedTypes/Services/XCCovClient.swift create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~1QghuNdnrL7MKB-x54nwsWDL-sAccsQznKnSNBJOyQlhq6p1WsyhiYmsFr8zpEKeuyQT6ZNjSy6_u2QbZ9IEMw== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~2pqvROhmmJlyMxNBHHNarP3VWfGyPAMpDPV9WmFYnk-XSMOdn2kN0mMSFD9OQG4KcGu2RYutICbM3MveutJKBg== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~3rA2LU7FpDF28Od4MKrG1HspHMm4ohcLKRgniB5h1Sagpgj4tenYlWVvzD-EfaO1aDCKwk2CBS3pooiliNF2nQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~4W-SMX_uKCxPGFliXo6NWleNg8bziiIteeguvAvASoCZRWp6yy2wj6lRTghOXV_Tx0EZKsT4xlVowc_Av3UT0w== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~4p7D0gZImy2bNWxsqJ799mgcfv0p_6NhV1KOAzqoorR77c8VN5i6EXYBBLXzoy2GPQUMu4q3xIR0qDVfb4zNkA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~5P2yZADkaG2DydxBue8-3AE75GFl2yUAJpccGahVGotllAb-pPONL7bRmKcxpSsMj-OeQBsp5cbyZoD_4Ck2wA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~5ZjHkpN1XJGaP53G7BGIBIot2KMUpo2J3zYwXbAhJIo6XAAkReDjUYpsT_0gnC7r4IcvcJ66ifStBTuWFZ_G9g== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~5iW1FYWHjMj6v1aSJyR_MGuzL5SGKiCjZLLawJ3tGTyvzgWLj9wGhE8TH94CVbd2ZzN1O6J-X5eRMUybUgL-8g== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~6z1d4Wuy-c4IuplG0ts2Nt1boOG-En7IA_pO4-Wzq6SQOIS0C7JqyUwxLpLp3Ok94uC51BCdWe8uF6-V9XA3sQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~7yeirstrQ0i0FlUFzztugtKQHU_qJY8fWjQsnSoCdDUXl9ygHQekpq_BH-QNUmTiOVAp-AGxzsYCdeo0oa-w3A== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~8HCvv-W3ZSOclwKC5j4wUu7nwDuIU-NwiJGxtHJvHJjPs8qYS2gYl93vsliZhSXo_vfPLGog-97yVBMoRKvzWg== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~AInLjLbwT6RF4mfeVCQxShMfVaCDXU_GVOsP5p2-2k0E6C_EaMMTHgzUtI2NkSR-ljVOr3sOBSMJslTc3DCOEg== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~AditexSm0AqiHLwnRfZX2Zd-kuPJZ6DIKiuhokL2R-E9x7ra3Kpta9BJlU2i1GqniBeoJC6Yvcqm2BLjG8RSvg== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~AmVtwlIAOsO2QjxVvvhgEyIftFlvYaVCHM7qU6Zp3BOE_aH5H1_TpZcg249YgUlOJGSV3Zus7fcth_2bIRYteQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~B1CICxIIERiyJcqjmxPgd_YjJ04Lx-EIw22QmULujaJFMlFCVje_p6WA_rZCErCbBp5xNMEal0WlIiswezkmGQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~BFNmOR_RHWSj3IR7y4yoflITPBkV1a05mtyMioV8JlI5YJIKUPeENGK4HszLZCQ9m813rX3cdEOuuQm4mLQ9EQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~CmjpzGy_BksydG5Di1i53OqY7ZQZkxLbOM0YizZ1r0CsE1AFPxwqNCtVXEBwx_n5xHi5L8EXt3BOMRDJe9pumQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~Cq8vmviQJl0DCDBecofsKKjh5HdB3gvz-kpR4C3vP9ZzqNUe-RMBF7ZSStBYkiDPFQEgz_MJJKNhQUTgGTY18A== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~Cr0nfoH4JIZq79Jiiqdi-nw0EiTBQdr7Tu0XgQqhp5lNSFn2lZmTisu3Z1RAinPjNhPRnDIKX2Fwbb4B0d2tgg== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~D1SPS8lNV5vdB1Sos1SbK_YncZer04RZQzg8Al74YtE7LcTOwqGJ-aq0i-R2u39iicblHfGczRiNuo-kNNIulw== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~EWqo-Eh9FUwLzTczQwtvYPw1g1LxtH_VC4rZzUHjqdWYsx86E1sMNAjxeGejO-YZDPPT64LxpGQEVRbDsE9Gqw== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~GHwlKCoWd_sVutKM9JxOzl6WsKLMkGIfjadOeQvtsU5zwpnYy1DtkD90oypXv-gAL_VehJ4v8k3czEoqvHpoUQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~GeEAhLfEKOAiceE4p1RPhsjT0dhJ2Wb2RSKJSRYrX9ZfzcsOvikLqISFX6sC7Isbk_Rcf-83_XdL07hDRZU0GA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~IpeCQst3WyEeFx3O8E5q5iZZM5eL9RIc2h0V8osVyurjNSgMpwsp3KezPvTu1BtKkn-4kgXRw6aeSGASx-hFqA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~KuzhAxn73MCO_MRDOxq-FB62tcgHGXjZGZF6te6LD5tgVdJfE3FNOylq0NoFsKhLyquDKwBiZBPFt_KSWb3mRA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~MfaWYZUDTCQgyMhJ4X_SfjovY9UPAVC2gdkXTbO16b3bR7M--Jm1jalGBCpPTsAMpM4Rx21VJpYW_9Cc08odyQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~N69oVCYHM8jbvjtNsctAwLtICTqzYbf7tQ1LSJ_UGA-RebogHsYo6pK3a2IgU0bM1gnuvIboKWw3XpTHelX9QQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~N8sxHXnTVrfytB-GSlFrIQwhMzRWeqdZ150UW3Uyz69uCswPMbuLLOP5zeCAqlu1mOCAFxwlysTxy8UvcQtwPg== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~P7Mk64DUuvIoNqXXVjI0Hx1i1B4zAk4iKaLt8nSe-bxbuJEtQ2ubG0dmnB3LIJZmGcxTUU9F4Bla9biPtJeWBA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~QWafzjNpkPwnvcW_0QZCgmUowbP0C7K1taLHMZDATNLbmGMEH-86pvkqpNT7QPZCNGtZC9E2zB0FZL4JJYAACA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~TtGk2OMZ41nBo21lWxoYU8xAONzltypy1w-qYLfPfWFh9XQ-4dmQGT60kLtLftCtndnghia9hN9PgbVANQpMEw== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~UUpc_QsELzcnra5tXuQQma-rX_hhC35IoNUDQoczIdzVYWTfiL77Di2bcD6GVvQ3C2Lfwkdh0eCoVwsSSaRonA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~VScPuAn3CB-sLm3iIlFTWoHFLXOxaz_b6OL9WjVyW0Om4XjESqJdsT1C3vk_wPSj4JNfFschEmL3pVLer4xFtA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~W-8rTV1mUbsWlUkByCXmOMWdWeEDl8a-GX2Nt4iMJyVG7Xz0j0V4mSCj0hy_x3_hRcmVVS9HNOlBCMBf9wUPpA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~WKkiYg8_oHDh598ew703dOzzZ_4A6eulQMENk8BgrFaapg46zM8SZttdNLvVegsywqlpkgJumXfYXKzEHmysgQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~ZVqvguHwOF9yshYRufXAqlRV-kDpOpOmK9YYfF9Kgugul4jDHhD1Tjp1SSVKRRdS3UjRduKDSc4L7ZAXIhB_Zw== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~_8Dh30MZln1kz7e_2zVrVWiHr7Lj3Pb_2Z00Rl2rWxZ9O59jRdoxlXQDc6qeurxWs9WcH3k8EnVbFvLcB5yRxg== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~_BP8QNt_vjtVJZ_qST_1F4NjYqOeKvr9d2oJQjxk1IRQU07vEWJbFDs8cgeTHeqzPxnhRJUvb2RyTwx_Hok5HA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~_MeJo_2NEDFOvTxmcSDvrHMBn_ykQk-FxuUpu4kpA2LQjDHEDZkQJL2SraatVbt-vitw3ffowrgrsh4yoqt-WA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~aqdzXeslnvIED27p3mTJQB9tfXgDrZ5qmnN9YwG0wHAxmwwKRmxym-FjEQISAxCuCoAPFJbrHoDfL2gFc7N9rg== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~b0zmz8Z3z_a9OMd5JVwTRJti00glveHwMcE0P6OP0W3NUGoHxOUnANszYx-cPrrXztOe4nJC7LMLm82ktgcsNg== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~bdBtpJ01upXwWjbXJbpA_YzGatXNhCHdjiOWj5bI8z8-TYOEOkwAhtCLRgKTcIhh9Fwwng91U3WqoWG8tQ-ztg== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~c2ACzHhyoX9Fr4qAp_YxNKfPvwEO2SIwxxZP9kK_YV17AK0Pjy_kK3wLYYOuZGfUcpyiCL_pZjXSwZ7mQASfxQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~cZYsgQS58hzrxlF944opJloaCG4whcSszMSGSHg7Yl22WHXvnmr9CbyY3iL1A-yurqaaaZX_WNZ2v475fwF6bQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~eTJSePoZS2IR5VW14fcQCV9omYTQ4czehSzIptFlRN0DJFZ8b9wd6QImnglUSL1By02e77T378yucTwmDqowtg== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~ekUf8H5tNCXv57W-oul74CKZ4DDQNkXMjM5KxUI4K31LG1pzCWg7plcLMtG4RRzitHxWpSfp5IdACQlxk_P5sg== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~gJOHZ3pjCV3fYGEq0RtzBNHPExcxieFaFQfQ0V1AuQpeNrigUOaTw8vthD7__wT069-Y3vjMa39agMpb3oflIQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~igQHq6VJZvaKCV-KEsjhondmWkHdA41wGB9QsqnRrbQq-vIqyHNuquiq23Gl2qMnw27936cz3UV0x_tFMUPGIQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~kcjuAwJ8Po_bj9OBqwICyLkxCSGRnAZyYfTWIFWnfvPMpqrTsAjcvUVZ3QNAh7qG5mLqEDzdEC8bhhfgB1o_Pw== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~lWKiC7Wr3Ea3Uv-HGWXPaIAQKg-6TEh0mcOKpF5NiYyCkgG18J74lrqWQrBabo86SXwhFqTCvdOeu_yH0983vg== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~lWido2-3HqlEXDFEk3SvPEA9k1qGBMebdvuHOLWa26PjudPFJp1VHjE7qKrIBwL_X6ol2lcDNhIIzL4Fxa8mNw== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~lp9CRqSSO7jY4ZQEtkK0z3E4B4PVDqabsbdsZIiiamLndYO2a1gXlMlMSnn1ecqy0tYw_TSA_eNjgzFgGzuC4Q== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~m_6asTdNRLkgO7w1RymmvbEXxfvc5kRaWr9dl1refmyA0UudaErZm_DYyTqyXlI1djq2RjUIwc-svS9woPCzAA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~n92abrM6Fs1ehKrVN4MBNa2Xii2oyv8ELIt-1kG4gOJDMI-hRXuWG4NHbKKG55tId67mq9pX4fkuNJBKrDn7mQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~o27HIiKBIB9kGS0tYwROLtU0_dEIHkRZFaKkl7k4XeqCWRjaz0qUkWtqK_eVeQqPkST8GaTNpfJq3xsMaFGv5A== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~o8N2yY8BEOp6gioaL4IM3QveVzP4qqt8dLzOpThD1Atz0c6ZEVYC2yRUpsV3bzZxV9OZaROWKabI2u8ktFPdgQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~oRMi0sbQLktfDDDGUS-SqBKpdDiD6HCztVES_ShfGr0KVcYENoIq7y9oUSDWL5B_P2gkdtWG8JEsJLxPbdWOZA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~p6tTbJKE-fL9u16ulx83X9uqUEKDl_tkOfoDsu9ZSI5Gzowle_QZx78T-KVIHsqxzINi_UorLurHXxgKRVXCdw== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~q98SQTn3_Q0Xe5mzVf2fdiyct22wbH8Z5MjjSTdbiQfEvEa3TmVyiHOa9-5kzTES8di5UgFjO6Xx57HtC3oSFw== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~qGAQhsKtOU2ecfKSHCRFqUHYkq45Kk8_JtyCgwRcm6uzyut1vm-h7qBDnONG_2GUR6oDxKS5Hi7y1Ufd4abPrA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~qeaq-sN8EXFDDNUMKf7zOVAiY6tTZ6maB-gwJ6OuOe-IcVKAwJbjh1YWBJG3EnNHuVSL8sfQbzsrSd2qvqKGTA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~tiADeO_TwvaQPD1doixRRMC_PNAOdsqUnhJ1Dg2fRw7Y7vFfpWvnFUwJ0kFT4H_Uo3ZSvWKZl6R38hCwVrCKNQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~uMikE6IBG3uS0y7s1OeC8H76OArcUEH7JKL6Unbdhak2hV1bIWI3Gv4GF3AJsI8r-sXKJzTg9AMoKIT82EYWVQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~vPhP9g3ABO9jTk2zjlB6pxhUvqAeEl9SHalrBJTTjGPwFtFs1lwq1Pr0YtAovl57f7s2qhUlqyoeWtR2-6f1rQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~wnmRn7u3waaR4x6xGSb1Q7pofNvHx2E01LTjVeJdiQ6OTZt5ceYbMsDNqik2bpreDAJi64yFUwoKPEwKX65VvQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~x2zk2zegpEaxBxmbAZkuPV4uV05PiAYBwNp8HOV_9GNgNX2HpZ9Dy8x55P9yOFJTTEb6WqYfH-SelneFPa_hBA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~ycAlmBG9xahBqrP3TYGwhXy7Rjo8oJrQmb9JKHdom5z6R19ogHH7Ihj53tnq2QtzHRp_1l7fwNuvpcHR6YKRpA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~1QghuNdnrL7MKB-x54nwsWDL-sAccsQznKnSNBJOyQlhq6p1WsyhiYmsFr8zpEKeuyQT6ZNjSy6_u2QbZ9IEMw== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~2pqvROhmmJlyMxNBHHNarP3VWfGyPAMpDPV9WmFYnk-XSMOdn2kN0mMSFD9OQG4KcGu2RYutICbM3MveutJKBg== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~3rA2LU7FpDF28Od4MKrG1HspHMm4ohcLKRgniB5h1Sagpgj4tenYlWVvzD-EfaO1aDCKwk2CBS3pooiliNF2nQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~4W-SMX_uKCxPGFliXo6NWleNg8bziiIteeguvAvASoCZRWp6yy2wj6lRTghOXV_Tx0EZKsT4xlVowc_Av3UT0w== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~4p7D0gZImy2bNWxsqJ799mgcfv0p_6NhV1KOAzqoorR77c8VN5i6EXYBBLXzoy2GPQUMu4q3xIR0qDVfb4zNkA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~5P2yZADkaG2DydxBue8-3AE75GFl2yUAJpccGahVGotllAb-pPONL7bRmKcxpSsMj-OeQBsp5cbyZoD_4Ck2wA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~5ZjHkpN1XJGaP53G7BGIBIot2KMUpo2J3zYwXbAhJIo6XAAkReDjUYpsT_0gnC7r4IcvcJ66ifStBTuWFZ_G9g== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~5iW1FYWHjMj6v1aSJyR_MGuzL5SGKiCjZLLawJ3tGTyvzgWLj9wGhE8TH94CVbd2ZzN1O6J-X5eRMUybUgL-8g== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~6z1d4Wuy-c4IuplG0ts2Nt1boOG-En7IA_pO4-Wzq6SQOIS0C7JqyUwxLpLp3Ok94uC51BCdWe8uF6-V9XA3sQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~7yeirstrQ0i0FlUFzztugtKQHU_qJY8fWjQsnSoCdDUXl9ygHQekpq_BH-QNUmTiOVAp-AGxzsYCdeo0oa-w3A== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~8HCvv-W3ZSOclwKC5j4wUu7nwDuIU-NwiJGxtHJvHJjPs8qYS2gYl93vsliZhSXo_vfPLGog-97yVBMoRKvzWg== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~AInLjLbwT6RF4mfeVCQxShMfVaCDXU_GVOsP5p2-2k0E6C_EaMMTHgzUtI2NkSR-ljVOr3sOBSMJslTc3DCOEg== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~AditexSm0AqiHLwnRfZX2Zd-kuPJZ6DIKiuhokL2R-E9x7ra3Kpta9BJlU2i1GqniBeoJC6Yvcqm2BLjG8RSvg== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~AmVtwlIAOsO2QjxVvvhgEyIftFlvYaVCHM7qU6Zp3BOE_aH5H1_TpZcg249YgUlOJGSV3Zus7fcth_2bIRYteQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~B1CICxIIERiyJcqjmxPgd_YjJ04Lx-EIw22QmULujaJFMlFCVje_p6WA_rZCErCbBp5xNMEal0WlIiswezkmGQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~BFNmOR_RHWSj3IR7y4yoflITPBkV1a05mtyMioV8JlI5YJIKUPeENGK4HszLZCQ9m813rX3cdEOuuQm4mLQ9EQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~CmjpzGy_BksydG5Di1i53OqY7ZQZkxLbOM0YizZ1r0CsE1AFPxwqNCtVXEBwx_n5xHi5L8EXt3BOMRDJe9pumQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~Cq8vmviQJl0DCDBecofsKKjh5HdB3gvz-kpR4C3vP9ZzqNUe-RMBF7ZSStBYkiDPFQEgz_MJJKNhQUTgGTY18A== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~Cr0nfoH4JIZq79Jiiqdi-nw0EiTBQdr7Tu0XgQqhp5lNSFn2lZmTisu3Z1RAinPjNhPRnDIKX2Fwbb4B0d2tgg== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~D1SPS8lNV5vdB1Sos1SbK_YncZer04RZQzg8Al74YtE7LcTOwqGJ-aq0i-R2u39iicblHfGczRiNuo-kNNIulw== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~EWqo-Eh9FUwLzTczQwtvYPw1g1LxtH_VC4rZzUHjqdWYsx86E1sMNAjxeGejO-YZDPPT64LxpGQEVRbDsE9Gqw== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~GHwlKCoWd_sVutKM9JxOzl6WsKLMkGIfjadOeQvtsU5zwpnYy1DtkD90oypXv-gAL_VehJ4v8k3czEoqvHpoUQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~GeEAhLfEKOAiceE4p1RPhsjT0dhJ2Wb2RSKJSRYrX9ZfzcsOvikLqISFX6sC7Isbk_Rcf-83_XdL07hDRZU0GA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~IpeCQst3WyEeFx3O8E5q5iZZM5eL9RIc2h0V8osVyurjNSgMpwsp3KezPvTu1BtKkn-4kgXRw6aeSGASx-hFqA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~KuzhAxn73MCO_MRDOxq-FB62tcgHGXjZGZF6te6LD5tgVdJfE3FNOylq0NoFsKhLyquDKwBiZBPFt_KSWb3mRA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~MfaWYZUDTCQgyMhJ4X_SfjovY9UPAVC2gdkXTbO16b3bR7M--Jm1jalGBCpPTsAMpM4Rx21VJpYW_9Cc08odyQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~N69oVCYHM8jbvjtNsctAwLtICTqzYbf7tQ1LSJ_UGA-RebogHsYo6pK3a2IgU0bM1gnuvIboKWw3XpTHelX9QQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~N8sxHXnTVrfytB-GSlFrIQwhMzRWeqdZ150UW3Uyz69uCswPMbuLLOP5zeCAqlu1mOCAFxwlysTxy8UvcQtwPg== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~P7Mk64DUuvIoNqXXVjI0Hx1i1B4zAk4iKaLt8nSe-bxbuJEtQ2ubG0dmnB3LIJZmGcxTUU9F4Bla9biPtJeWBA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~QWafzjNpkPwnvcW_0QZCgmUowbP0C7K1taLHMZDATNLbmGMEH-86pvkqpNT7QPZCNGtZC9E2zB0FZL4JJYAACA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~TtGk2OMZ41nBo21lWxoYU8xAONzltypy1w-qYLfPfWFh9XQ-4dmQGT60kLtLftCtndnghia9hN9PgbVANQpMEw== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~UUpc_QsELzcnra5tXuQQma-rX_hhC35IoNUDQoczIdzVYWTfiL77Di2bcD6GVvQ3C2Lfwkdh0eCoVwsSSaRonA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~VScPuAn3CB-sLm3iIlFTWoHFLXOxaz_b6OL9WjVyW0Om4XjESqJdsT1C3vk_wPSj4JNfFschEmL3pVLer4xFtA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~W-8rTV1mUbsWlUkByCXmOMWdWeEDl8a-GX2Nt4iMJyVG7Xz0j0V4mSCj0hy_x3_hRcmVVS9HNOlBCMBf9wUPpA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~WKkiYg8_oHDh598ew703dOzzZ_4A6eulQMENk8BgrFaapg46zM8SZttdNLvVegsywqlpkgJumXfYXKzEHmysgQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~ZVqvguHwOF9yshYRufXAqlRV-kDpOpOmK9YYfF9Kgugul4jDHhD1Tjp1SSVKRRdS3UjRduKDSc4L7ZAXIhB_Zw== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~_8Dh30MZln1kz7e_2zVrVWiHr7Lj3Pb_2Z00Rl2rWxZ9O59jRdoxlXQDc6qeurxWs9WcH3k8EnVbFvLcB5yRxg== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~_BP8QNt_vjtVJZ_qST_1F4NjYqOeKvr9d2oJQjxk1IRQU07vEWJbFDs8cgeTHeqzPxnhRJUvb2RyTwx_Hok5HA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~_MeJo_2NEDFOvTxmcSDvrHMBn_ykQk-FxuUpu4kpA2LQjDHEDZkQJL2SraatVbt-vitw3ffowrgrsh4yoqt-WA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~aqdzXeslnvIED27p3mTJQB9tfXgDrZ5qmnN9YwG0wHAxmwwKRmxym-FjEQISAxCuCoAPFJbrHoDfL2gFc7N9rg== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~b0zmz8Z3z_a9OMd5JVwTRJti00glveHwMcE0P6OP0W3NUGoHxOUnANszYx-cPrrXztOe4nJC7LMLm82ktgcsNg== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~bdBtpJ01upXwWjbXJbpA_YzGatXNhCHdjiOWj5bI8z8-TYOEOkwAhtCLRgKTcIhh9Fwwng91U3WqoWG8tQ-ztg== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~c2ACzHhyoX9Fr4qAp_YxNKfPvwEO2SIwxxZP9kK_YV17AK0Pjy_kK3wLYYOuZGfUcpyiCL_pZjXSwZ7mQASfxQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~cZYsgQS58hzrxlF944opJloaCG4whcSszMSGSHg7Yl22WHXvnmr9CbyY3iL1A-yurqaaaZX_WNZ2v475fwF6bQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~eTJSePoZS2IR5VW14fcQCV9omYTQ4czehSzIptFlRN0DJFZ8b9wd6QImnglUSL1By02e77T378yucTwmDqowtg== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~ekUf8H5tNCXv57W-oul74CKZ4DDQNkXMjM5KxUI4K31LG1pzCWg7plcLMtG4RRzitHxWpSfp5IdACQlxk_P5sg== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~gJOHZ3pjCV3fYGEq0RtzBNHPExcxieFaFQfQ0V1AuQpeNrigUOaTw8vthD7__wT069-Y3vjMa39agMpb3oflIQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~igQHq6VJZvaKCV-KEsjhondmWkHdA41wGB9QsqnRrbQq-vIqyHNuquiq23Gl2qMnw27936cz3UV0x_tFMUPGIQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~kcjuAwJ8Po_bj9OBqwICyLkxCSGRnAZyYfTWIFWnfvPMpqrTsAjcvUVZ3QNAh7qG5mLqEDzdEC8bhhfgB1o_Pw== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~lWKiC7Wr3Ea3Uv-HGWXPaIAQKg-6TEh0mcOKpF5NiYyCkgG18J74lrqWQrBabo86SXwhFqTCvdOeu_yH0983vg== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~lWido2-3HqlEXDFEk3SvPEA9k1qGBMebdvuHOLWa26PjudPFJp1VHjE7qKrIBwL_X6ol2lcDNhIIzL4Fxa8mNw== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~lp9CRqSSO7jY4ZQEtkK0z3E4B4PVDqabsbdsZIiiamLndYO2a1gXlMlMSnn1ecqy0tYw_TSA_eNjgzFgGzuC4Q== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~m_6asTdNRLkgO7w1RymmvbEXxfvc5kRaWr9dl1refmyA0UudaErZm_DYyTqyXlI1djq2RjUIwc-svS9woPCzAA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~n92abrM6Fs1ehKrVN4MBNa2Xii2oyv8ELIt-1kG4gOJDMI-hRXuWG4NHbKKG55tId67mq9pX4fkuNJBKrDn7mQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~o27HIiKBIB9kGS0tYwROLtU0_dEIHkRZFaKkl7k4XeqCWRjaz0qUkWtqK_eVeQqPkST8GaTNpfJq3xsMaFGv5A== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~o8N2yY8BEOp6gioaL4IM3QveVzP4qqt8dLzOpThD1Atz0c6ZEVYC2yRUpsV3bzZxV9OZaROWKabI2u8ktFPdgQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~oRMi0sbQLktfDDDGUS-SqBKpdDiD6HCztVES_ShfGr0KVcYENoIq7y9oUSDWL5B_P2gkdtWG8JEsJLxPbdWOZA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~p6tTbJKE-fL9u16ulx83X9uqUEKDl_tkOfoDsu9ZSI5Gzowle_QZx78T-KVIHsqxzINi_UorLurHXxgKRVXCdw== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~q98SQTn3_Q0Xe5mzVf2fdiyct22wbH8Z5MjjSTdbiQfEvEa3TmVyiHOa9-5kzTES8di5UgFjO6Xx57HtC3oSFw== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~qGAQhsKtOU2ecfKSHCRFqUHYkq45Kk8_JtyCgwRcm6uzyut1vm-h7qBDnONG_2GUR6oDxKS5Hi7y1Ufd4abPrA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~qeaq-sN8EXFDDNUMKf7zOVAiY6tTZ6maB-gwJ6OuOe-IcVKAwJbjh1YWBJG3EnNHuVSL8sfQbzsrSd2qvqKGTA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~tiADeO_TwvaQPD1doixRRMC_PNAOdsqUnhJ1Dg2fRw7Y7vFfpWvnFUwJ0kFT4H_Uo3ZSvWKZl6R38hCwVrCKNQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~uMikE6IBG3uS0y7s1OeC8H76OArcUEH7JKL6Unbdhak2hV1bIWI3Gv4GF3AJsI8r-sXKJzTg9AMoKIT82EYWVQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~vPhP9g3ABO9jTk2zjlB6pxhUvqAeEl9SHalrBJTTjGPwFtFs1lwq1Pr0YtAovl57f7s2qhUlqyoeWtR2-6f1rQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~wnmRn7u3waaR4x6xGSb1Q7pofNvHx2E01LTjVeJdiQ6OTZt5ceYbMsDNqik2bpreDAJi64yFUwoKPEwKX65VvQ== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~x2zk2zegpEaxBxmbAZkuPV4uV05PiAYBwNp8HOV_9GNgNX2HpZ9Dy8x55P9yOFJTTEb6WqYfH-SelneFPa_hBA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~ycAlmBG9xahBqrP3TYGwhXy7Rjo8oJrQmb9JKHdom5z6R19ogHH7Ihj53tnq2QtzHRp_1l7fwNuvpcHR6YKRpA== create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Info.plist create mode 100644 Tests/XcresultparserTests/TestAssets/parametrized.xcresult/database.sqlite3 create mode 100644 Tests/XcresultparserTests/XCCovClientTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index a6619f8..3c85d64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Remove XCResultKit as a package dependency from `Package.swift`. - Migrate JUnit, summary/test formatting, coverage formatting, and issues JSON paths to the new model/tooling layer. - Keep output compatibility where possible while aligning with modern `xcresulttool` payloads. +- Raise the minimum supported platform to macOS 12. +- Keep expected failures as a distinct state in human-readable outputs, while emitting them as passing test cases in JUnit/Sonar XML for compatibility. +- Treat unknown `--coverage-targets` values as an error instead of silently producing empty coverage output. ## Version 1.9.4 - 2025-12-19 ### CHANGES: diff --git a/CommandlineTool/main.swift b/CommandlineTool/main.swift index 6f83c8a..cca16e6 100644 --- a/CommandlineTool/main.swift +++ b/CommandlineTool/main.swift @@ -31,7 +31,7 @@ struct xcresultparser: ParsableCommand { @Option(name: [.customShort("e"), .customLong("excluded-path")], help: "Specify which path names to exclude. You can use more than one -e option to specify a list of path patterns to exclude. This option only has effect, if the format is either 'cobertura' or 'xml' with the --coverage (-c) option for a code coverage report or if the format is one of 'warnings', 'errors' or 'warnings-and-errors'.") var excludedPaths: [String] = [] - @Option(name: .shortAndLong, help: "The fields in the summary. Default is all: errors|warnings|analyzerWarnings|tests|failed|skipped") + @Option(name: .shortAndLong, help: "The fields in the summary. Default is all: errors|warnings|analyzerWarnings|tests|failed|skipped|duration|date") var summaryFields: String? @Flag(name: .shortAndLong, help: "Whether to print coverage data.") @@ -91,29 +91,25 @@ struct xcresultparser: ParsableCommand { } private func outputSonarXML(for xcresult: String) throws { - guard let converter = SonarCoverageConverter( + let converter = try SonarCoverageConverter( with: URL(fileURLWithPath: xcresult), projectRoot: projectRoot ?? "", coverageTargets: coverageTargets, excludedPaths: excludedPaths, strictPathnames: strictPathnames == 1 - ) else { - throw ParseError.argumentError - } + ) let rslt = try converter.xmlString(quiet: quiet == 1) writeToStdOut(rslt) } private func outputCoberturaXML(for xcresult: String) throws { - guard let converter = CoberturaCoverageConverter( + let converter = try CoberturaCoverageConverter( with: URL(fileURLWithPath: xcresult), projectRoot: projectRoot ?? "", coverageTargets: coverageTargets, excludedPaths: excludedPaths, strictPathnames: strictPathnames == 1 - ) else { - throw ParseError.argumentError - } + ) let rslt = try converter.xmlString(quiet: quiet == 1) writeToStdOut(rslt) } @@ -131,14 +127,12 @@ struct xcresultparser: ParsableCommand { } private func outputTargetNames(for xcresult: String) throws { - guard let converter = SonarCoverageConverter( + let converter = try SonarCoverageConverter( with: URL(fileURLWithPath: xcresult), projectRoot: projectRoot ?? "", - coverageTargets: coverageTargets, + coverageTargets: [], strictPathnames: strictPathnames == 1 - ) else { - throw ParseError.argumentError - } + ) writeToStdOut(converter.targetsInfo) } @@ -162,7 +156,7 @@ struct xcresultparser: ParsableCommand { formatter: outputFormatter, coverageTargets: coverageTargets, failedTestsOnly: failedTestsOnly == 1, - summaryFields: summaryFields ?? "errors|warnings|analyzerWarnings|tests|failed|skipped", + summaryFields: summaryFields ?? "errors|warnings|analyzerWarnings|tests|failed|skipped|duration|date", coverageReportFormat: CoverageReportFormat(string: coverageReportFormat) ) else { throw ParseError.argumentError diff --git a/Package.swift b/Package.swift index aab29d5..e7621b6 100644 --- a/Package.swift +++ b/Package.swift @@ -4,7 +4,7 @@ import PackageDescription let package = Package( name: "Xcresultparser", platforms: [ - .macOS(.v11), + .macOS(.v12), ], products: [ .executable( @@ -55,7 +55,8 @@ let package = Package( .copy("TestAssets/warnings.json"), .copy("TestAssets/resultWithCompileError.xcresult"), .copy("TestAssets/sonarTestExecutionWithProjectRootAbsolute.xml"), - .copy("TestAssets/sonarTestExecutionWithProjectRootRelative.xml") + .copy("TestAssets/sonarTestExecutionWithProjectRootRelative.xml"), + .copy("TestAssets/parametrized.xcresult") ] ) ] diff --git a/README.md b/README.md index ef5fce9..0a15f07 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,13 @@ You can also specify the name of the project root. Paths and urls are then relat This tool can read test result data and code coverage data from an `.xcresult` bundle using `xcresulttool` and `xccov`. All JSON parsing is done with native `Codable` models in this project. +## Requirements +- macOS 12 or newer + +## Expected Failure Semantics +- For `txt`, `cli`, `html`, and `md`, expected failures are represented as a distinct test state. +- For `junit` and sonar test execution `xml`, expected failures are emitted as regular passing test cases for schema compatibility. +
More on converting code coverage data @@ -125,7 +132,7 @@ OPTIONS: 'warnings-and-errors'. -s, --summary-fields The fields in the summary. Default is all: - errors|warnings|analyzerWarnings|tests|failed|skipped + errors|warnings|analyzerWarnings|tests|failed|skipped|duration|date -c, --coverage Whether to print coverage data. -x, --exclude-coverage-not-in-project Omit elements with file pathes, which do not contain @@ -201,6 +208,8 @@ Create an xml file in generic code coverage xml format, but only for two of the xcresultparser -c -o xml test.xcresult -t foo -t baz > sonarCoverage.xml ``` +If one of the targets passed with `-t/--coverage-targets` does not exist in the result bundle, the command now exits with an error. + ### Cobertura XML output Create xml file in [Cobertura](https://cobertura.github.io/cobertura/) format: ``` diff --git a/Sources/xcresultparser/CoverageConverter.swift b/Sources/xcresultparser/CoverageConverter.swift index d054b10..82b0fa4 100644 --- a/Sources/xcresultparser/CoverageConverter.swift +++ b/Sources/xcresultparser/CoverageConverter.swift @@ -7,6 +7,22 @@ import Foundation +public enum CoverageConverterError: LocalizedError, Equatable { + case couldNotLoadCoverageReport + case unknownCoverageTargets(requested: [String], available: [String]) + + public var errorDescription: String? { + switch self { + case .couldNotLoadCoverageReport: + return "Could not load coverage report from xcresult archive." + case let .unknownCoverageTargets(requested, available): + let requestedList = requested.joined(separator: ", ") + let availableList = available.joined(separator: ", ") + return "Unknown coverage target(s): \(requestedList). Available targets: \(availableList)" + } + } +} + /// Convert coverage data in a xcresult archive to xml (exact format determined by subclass) /// /// ~~ Unfortunately converting to the coverage xml format suited for e.g. sonarqube is a tedious task. @@ -34,40 +50,53 @@ public class CoverageConverter { // MARK: - Dependencies - let shell: Commandline - let xcresultToolClient: XCResultToolClient + let xcresultToolClient: XCResultToolProviding + let xccovClient: XCCovProviding - public init?( + public init( with url: URL, projectRoot: String = "", coverageTargets: [String] = [], excludedPaths: [String] = [], strictPathnames: Bool - ) { - let shell = Shell() - let xcresultToolClient = XCResultToolClient(shell: shell) - guard let report = try? CoverageConverter.getCoverageReportAsJSON(resultFileURL: url, shell: shell) else { - return nil + ) throws { + let shell = DependencyFactory.createShell() + let resolvedXCResultToolClient = DependencyFactory.createXCResultToolClient(shell) + let resolvedXCCovClient = DependencyFactory.createXCCovClient(shell) + let report: CoverageReport + do { + report = try resolvedXCCovClient.getCoverageReport(path: url) + } catch { + throw CoverageConverterError.couldNotLoadCoverageReport } - self.shell = shell - self.xcresultToolClient = xcresultToolClient + self.xcresultToolClient = resolvedXCResultToolClient + self.xccovClient = resolvedXCCovClient resultFileURL = url coverageReport = report self.projectRoot = projectRoot self.strictPathnames = projectRoot.isEmpty ? false : strictPathnames - let selectedCoverageTargets = CoverageConverter.targets( - filteredBy: coverageTargets, - availableTargets: report.targets.map(\.name) + let targetSelection = CoverageTargetSelection( + with: coverageTargets, + from: report.targets.map(\.name) ) + let selectedCoverageTargets = targetSelection.selectedTargets self.coverageTargets = selectedCoverageTargets - filesForIncludedTargets = Set( - report.targets - .filter { selectedCoverageTargets.contains($0.name) } - .flatMap { $0.files.map(\.path) } + if !targetSelection.unmatchedRequested.isEmpty { + throw CoverageConverterError.unknownCoverageTargets( + requested: targetSelection.unmatchedRequested.sorted(), + available: targetSelection.availableTargets.sorted() + ) + } + let includedTargetFiles = report.targets + .filter { selectedCoverageTargets.contains($0.name) } + .flatMap { $0.files.map(\.path) } + filesForIncludedTargets = CoverageConverter.normalizedFilePaths( + for: includedTargetFiles, + projectRoot: projectRoot ) self.excludedPaths = Set(excludedPaths) - if let summary = try? xcresultToolClient.getTestSummary(path: url) { + if let summary = try? resolvedXCResultToolClient.getTestSummary(path: url) { startTime = summary.startTime } else { startTime = nil @@ -110,18 +139,15 @@ public class CoverageConverter { // Use the xccov commandline tool to get results as JSON. func getCoverageDataAsJSON() throws -> FileCoverage { - var arguments = ["xccov", "view"] - if resultFileURL.pathExtension == "xcresult" { - arguments.append("--archive") - } - arguments.append("--json") - arguments.append(resultFileURL.path) - let coverageData = try shell.execute(program: "/usr/bin/xcrun", with: arguments) - return try CoverageConverter.decodeJSON(FileCoverage.self, from: coverageData) + try xccovClient.getCoverageData(path: resultFileURL) } func isTargetIncluded(forFile file: String) -> Bool { - filesForIncludedTargets.contains(file) + if filesForIncludedTargets.contains(file) { + return true + } + let normalized = CoverageConverter.normalizedFilePaths(for: [file], projectRoot: projectRoot) + return !filesForIncludedTargets.isDisjoint(with: normalized) } func isPathExcluded(_ path: String) -> Bool { @@ -133,52 +159,34 @@ public class CoverageConverter { // Maintained to support the public API used by tests and existing consumers. func coverageForFile(path: String) throws -> String { - var arguments = ["xccov", "view"] - if resultFileURL.pathExtension == "xcresult" { - arguments.append("--archive") - } - arguments.append("--file") - arguments.append(path) - arguments.append(resultFileURL.path) - let coverageData = try shell.execute(program: "/usr/bin/xcrun", with: arguments) - return String(decoding: coverageData, as: UTF8.self) + try xccovClient.getCoverageForFile(path: resultFileURL, filePath: path) } // Maintained to support the public API used by tests and existing consumers. func coverageFileList() throws -> [String] { - var arguments = ["xccov", "view"] - if resultFileURL.pathExtension == "xcresult" { - arguments.append("--archive") + try xccovClient.getCoverageFileList(path: resultFileURL) + } + + static func normalizedFilePaths(for paths: [String], projectRoot: String) -> Set { + var result = Set() + for path in paths { + result.insert(path) + guard !projectRoot.isEmpty else { + continue + } + if path.hasPrefix(projectRoot) { + var relative = String(path.dropFirst(projectRoot.count)) + if relative.hasPrefix("/") { + relative.removeFirst() + } + if !relative.isEmpty { + result.insert(relative) + } + } else if !path.hasPrefix("/") { + let joined = (projectRoot as NSString).appendingPathComponent(path) + result.insert(joined) + } } - arguments.append("--file-list") - arguments.append(resultFileURL.path) - let filelistData = try shell.execute(program: "/usr/bin/xcrun", with: arguments) - return String(decoding: filelistData, as: UTF8.self).components(separatedBy: "\n") - } - - private static func getCoverageReportAsJSON(resultFileURL: URL, shell: Commandline) throws -> CoverageReport { - var arguments = ["xccov", "view"] - arguments.append("--report") - arguments.append("--json") - arguments.append(resultFileURL.path) - let coverageData = try shell.execute(program: "/usr/bin/xcrun", with: arguments) - return try decodeJSON(CoverageReport.self, from: coverageData) - } - - private static func targets(filteredBy filter: [String], availableTargets: [String]) -> Set { - guard !filter.isEmpty else { - return Set(availableTargets) - } - let filterSet = Set(filter) - let filtered = availableTargets.filter { thisTarget in - guard let stripped = thisTarget.split(separator: ".").first else { return true } - return filterSet.contains(String(stripped)) - } - return Set(filtered) - } - - private static func decodeJSON(_ type: T.Type, from data: Data) throws -> T { - let decoder = JSONDecoder() - return try decoder.decode(type, from: data) + return result } } diff --git a/Sources/xcresultparser/DataProviders/JunitXML/XCResultToolJunitXMLDataProvider.swift b/Sources/xcresultparser/DataProviders/JunitXML/XCResultToolJunitXMLDataProvider.swift index 302b54b..cfd9a2e 100644 --- a/Sources/xcresultparser/DataProviders/JunitXML/XCResultToolJunitXMLDataProvider.swift +++ b/Sources/xcresultparser/DataProviders/JunitXML/XCResultToolJunitXMLDataProvider.swift @@ -10,10 +10,6 @@ import Foundation struct XCResultToolJunitXMLDataProvider: JunitXMLDataProviding { private let summary: XCSummary private let tests: XCTests - private let failureLocationRegex = try? NSRegularExpression( - pattern: #"^(.+?):([0-9]+):\s*(.+)$"#, - options: [] - ) init?(url: URL, client: XCResultToolClient = XCResultToolClient()) { guard let summary = try? client.getTestSummary(path: url), @@ -137,9 +133,19 @@ struct XCResultToolJunitXMLDataProvider: JunitXMLDataProviding { currentTestClassName } let children = node.children ?? [] - let tests = children - .filter { $0.nodeType == .testCase } - .map { mapTest(node: $0, testClassName: nextTestClassName) } + let tests = children.mapTests( + testClassName: nextTestClassName, + mapTest: mapTest(node:testClassName:), + mapArgumentTest: { mappedArgumentTest in + JunitTest( + identifier: mappedArgumentTest.identifier, + name: mappedArgumentTest.name, + duration: mappedArgumentTest.duration, + isFailed: mappedArgumentTest.result == .failed, + isSkipped: mappedArgumentTest.result == .skipped + ) + } + ) let groups = children.compactMap { child in mapGroup( @@ -172,7 +178,7 @@ struct XCResultToolJunitXMLDataProvider: JunitXMLDataProviding { name: node.name, duration: node.durationInSeconds, isFailed: result == .failed, - isSkipped: result == .skipped || result == .expectedFailure + isSkipped: result == .skipped ) } @@ -206,7 +212,12 @@ struct XCResultToolJunitXMLDataProvider: JunitXMLDataProviding { private func failureMessageDetailsByTestIdentifier() -> [String: [FailureMessageDetail]] { var result = [String: [FailureMessageDetail]]() for node in tests.testNodes { - collectFailureMessages(in: node, currentTestIdentifier: nil, into: &result) + collectFailureMessages( + in: node, + currentTestIdentifier: nil, + currentTestClassName: nil, + into: &result + ) } return result } @@ -214,11 +225,16 @@ struct XCResultToolJunitXMLDataProvider: JunitXMLDataProviding { private func collectFailureMessages( in node: XCTestNode, currentTestIdentifier: String?, + currentTestClassName: String?, into result: inout [String: [FailureMessageDetail]] ) { var currentIdentifier = currentTestIdentifier + var currentClassName = currentTestClassName + if node.nodeType == .testSuite { + currentClassName = node.name + } if node.nodeType == .testCase { - currentIdentifier = node.nodeIdentifier + currentIdentifier = testIdentifierString(for: node, testClassName: currentClassName) } if node.nodeType == .failureMessage, @@ -231,26 +247,33 @@ struct XCResultToolJunitXMLDataProvider: JunitXMLDataProviding { collectFailureMessages( in: child, currentTestIdentifier: currentIdentifier, + currentTestClassName: currentClassName, into: &result ) } } + private func testIdentifierString(for node: XCTestNode, testClassName: String?) -> String { + if let testClassName, !testClassName.isEmpty { + return "\(testClassName)/\(node.name)" + } + return node.name + } + private func parseFailureMessage(_ raw: String) -> FailureMessageDetail? { - guard let failureLocationRegex else { + let parts = raw.split(separator: ":", maxSplits: 2, omittingEmptySubsequences: false) + guard parts.count == 3 else { + return nil + } + let file = String(parts[0]) + let line = String(parts[1]).trimmingCharacters(in: .whitespacesAndNewlines) + guard !line.isEmpty, line.allSatisfy(\.isNumber) else { return nil } - let range = NSRange(raw.startIndex.. MappedArgumentTest { + let baseIdentifier: String + if let testClassName { + baseIdentifier = "\(testClassName)/\(name)" + } else { + baseIdentifier = name + } + return MappedArgumentTest( + identifier: baseIdentifier.formatWithParameter(argument.name), + name: name.formatWithParameter(argument.name), + duration: argument.durationInSeconds ?? durationInSeconds, + result: argument.result ?? result ?? .unknown + ) + } +} + +extension [XCTestNode] { + func mapTests( + testClassName: String?, + mapTest: (XCTestNode, String?) -> TestType, + mapArgumentTest: (XCTestNode.MappedArgumentTest) -> TestType + ) -> [TestType] { + var tests = [TestType]() + for node in self where node.nodeType == .testCase { + let argumentNodes = (node.children ?? []).filter { $0.nodeType == .arguments } + if argumentNodes.isEmpty { + tests.append(mapTest(node, testClassName)) + } else { + for argument in argumentNodes { + tests.append(mapArgumentTest(node.mapArgumentTest(argument: argument, testClassName: testClassName))) + } + } + } + return tests + } +} + +private extension String { + func formatWithParameter(_ parameterValue: String) -> String { + guard let openParenIndex = firstIndex(of: "("), + let closeParenIndex = lastIndex(of: ")"), + openParenIndex < closeParenIndex else { + return "\(self) [\(parameterValue)]" + } + + let signature = String(self[index(after: openParenIndex).. [String] { + var labels = [String]() + var token = "" + + for character in signature { + if character == "," { + token = "" + continue + } + if character == ":" { + let trimmed = token.trimmingCharacters(in: .whitespacesAndNewlines) + if let label = trimmed.split(whereSeparator: \.isWhitespace).last, !label.isEmpty { + labels.append(String(label)) + } + token = "" + continue + } + token.append(character) + } + + return labels + } + + private func splitValues(_ raw: String) -> [String] { + var values = [String]() + var current = "" + var isInQuotes = false + + for character in raw { + if character == "\"" { + isInQuotes.toggle() + current.append(character) + continue + } + + if character == "," && !isInQuotes { + let trimmed = current.trimmingCharacters(in: .whitespacesAndNewlines) + if !trimmed.isEmpty { + values.append(trimmed) + } + current = "" + continue + } + + current.append(character) + } + + let trimmed = current.trimmingCharacters(in: .whitespacesAndNewlines) + if !trimmed.isEmpty { + values.append(trimmed) + } + + return values + } +} diff --git a/Sources/xcresultparser/OutputFormatting/Formatters/Markdown/MDResultFormatter.swift b/Sources/xcresultparser/OutputFormatting/Formatters/Markdown/MDResultFormatter.swift index 5361c6f..f40cbe9 100644 --- a/Sources/xcresultparser/OutputFormatting/Formatters/Markdown/MDResultFormatter.swift +++ b/Sources/xcresultparser/OutputFormatting/Formatters/Markdown/MDResultFormatter.swift @@ -12,6 +12,7 @@ public struct MDResultFormatter: XCResultFormatting { public let testFailIcon = "🔴  " public let testPassIcon = "🟢  " public let testSkipIcon = "⚪️  " + public let testExpectedFailureIcon = "🟡  " public init() {} diff --git a/Sources/xcresultparser/OutputFormatting/Formatters/XCResultFormatting.swift b/Sources/xcresultparser/OutputFormatting/Formatters/XCResultFormatting.swift index c66743a..a9f2321 100644 --- a/Sources/xcresultparser/OutputFormatting/Formatters/XCResultFormatting.swift +++ b/Sources/xcresultparser/OutputFormatting/Formatters/XCResultFormatting.swift @@ -27,6 +27,7 @@ public protocol XCResultFormatting { var testFailIcon: String { get } var testPassIcon: String { get } var testSkipIcon: String { get } + var testExpectedFailureIcon: String { get } func codeCoverageTargetSummary(_ item: String) -> String func codeCoverageFileSummary(_ item: String) -> String @@ -45,4 +46,8 @@ public extension XCResultFormatting { var testSkipIcon: String { return "-" } + + var testExpectedFailureIcon: String { + return "!" + } } diff --git a/Sources/xcresultparser/SharedTypes/CoverageTargetSelection.swift b/Sources/xcresultparser/SharedTypes/CoverageTargetSelection.swift new file mode 100644 index 0000000..0178dfe --- /dev/null +++ b/Sources/xcresultparser/SharedTypes/CoverageTargetSelection.swift @@ -0,0 +1,47 @@ +// +// CoverageTargetSelection.swift +// Xcresultparser +// +// Created by Alex da Franca on 22.02.26. +// + +import Foundation + +struct CoverageTargetSelection { + let selectedTargets: Set + let unmatchedRequested: Set + let availableTargets: Set +} + +extension CoverageTargetSelection { + init(with filters: [String], from availableTargets: [String]) { + let availableTargetSet = Set(availableTargets) + let availableNormalizedTargets = Set(availableTargets.map { target in + String(target.split(separator: ".").first ?? Substring(target)) + }) + + guard !filters.isEmpty else { + self.init( + selectedTargets: Set(availableTargets), + unmatchedRequested: [], + availableTargets: availableTargetSet + ) + return + } + + let filterSet = Set(filters) + let filtered = availableTargets.filter { thisTarget in + let stripped = String(thisTarget.split(separator: ".").first ?? Substring(thisTarget)) + return filterSet.contains(stripped) || filterSet.contains(thisTarget) + } + let unmatched = filterSet.filter { requested in + !availableTargetSet.contains(requested) && !availableNormalizedTargets.contains(requested) + } + + self.init( + selectedTargets: Set(filtered), + unmatchedRequested: Set(unmatched), + availableTargets: availableTargetSet + ) + } +} diff --git a/Sources/xcresultparser/SharedTypes/DependencyFactory.swift b/Sources/xcresultparser/SharedTypes/DependencyFactory.swift index 963fb84..e55ccc5 100644 --- a/Sources/xcresultparser/SharedTypes/DependencyFactory.swift +++ b/Sources/xcresultparser/SharedTypes/DependencyFactory.swift @@ -9,4 +9,12 @@ class DependencyFactory { static var createShell: () -> Commandline = { Shell() } + + static var createXCResultToolClient: (Commandline) -> XCResultToolProviding = { shell in + XCResultToolClient(shell: shell) + } + + static var createXCCovClient: (Commandline) -> XCCovProviding = { shell in + XCCovClient(shell: shell) + } } diff --git a/Sources/xcresultparser/SharedTypes/Services/XCCovClient.swift b/Sources/xcresultparser/SharedTypes/Services/XCCovClient.swift new file mode 100644 index 0000000..5376245 --- /dev/null +++ b/Sources/xcresultparser/SharedTypes/Services/XCCovClient.swift @@ -0,0 +1,64 @@ +// +// XCCovClient.swift +// Xcresultparser +// +// Created by Alex da Franca on 22.02.26. +// + +import Foundation + +protocol XCCovProviding { + func getCoverageData(path: URL) throws -> FileCoverage + func getCoverageReport(path: URL) throws -> CoverageReport + func getCoverageForFile(path: URL, filePath: String) throws -> String + func getCoverageFileList(path: URL) throws -> [String] +} + +struct XCCovClient: XCCovProviding { + private let shell: Commandline + private let decoder: JSONDecoder + + init( + shell: Commandline = DependencyFactory.createShell(), + decoder: JSONDecoder = JSONDecoder() + ) { + self.shell = shell + self.decoder = decoder + } + + func getCoverageData(path: URL) throws -> FileCoverage { + let data = try execute(arguments: viewArguments(for: path) + ["--json", path.path]) + return try decode(FileCoverage.self, from: data) + } + + func getCoverageReport(path: URL) throws -> CoverageReport { + let data = try execute(arguments: ["view", "--report", "--json", path.path]) + return try decode(CoverageReport.self, from: data) + } + + func getCoverageForFile(path: URL, filePath: String) throws -> String { + let data = try execute(arguments: viewArguments(for: path) + ["--file", filePath, path.path]) + return String(decoding: data, as: UTF8.self) + } + + func getCoverageFileList(path: URL) throws -> [String] { + let data = try execute(arguments: viewArguments(for: path) + ["--file-list", path.path]) + return String(decoding: data, as: UTF8.self).components(separatedBy: "\n") + } + + private func viewArguments(for path: URL) -> [String] { + var arguments = ["view"] + if path.pathExtension == "xcresult" { + arguments.append("--archive") + } + return arguments + } + + private func execute(arguments: [String]) throws -> Data { + try shell.execute(program: "/usr/bin/xcrun", with: ["xccov"] + arguments) + } + + private func decode(_ type: T.Type, from data: Data) throws -> T { + try decoder.decode(type, from: data) + } +} diff --git a/Sources/xcresultparser/SharedTypes/Services/XCResultToolClient.swift b/Sources/xcresultparser/SharedTypes/Services/XCResultToolClient.swift index fe1e660..9dbb6c1 100644 --- a/Sources/xcresultparser/SharedTypes/Services/XCResultToolClient.swift +++ b/Sources/xcresultparser/SharedTypes/Services/XCResultToolClient.swift @@ -7,7 +7,16 @@ import Foundation -struct XCResultToolClient { +protocol XCResultToolProviding { + func getBuildResults(path: URL) throws -> XCBuildResults + func getTestSummary(path: URL) throws -> XCSummary + func getTests(path: URL) throws -> XCTests + func getTestDetails(path: URL, testId: String) throws -> XCTestDetails + func getActivities(path: URL, testId: String) throws -> XCActivities + func getMetrics(path: URL, testId: String) throws -> [XCTestWithMetrics] +} + +struct XCResultToolClient: XCResultToolProviding { enum ToolClientError: Error, Equatable { case invalidUTF8 case unexpectedRootValue(String) diff --git a/Sources/xcresultparser/XCResultFormatter.swift b/Sources/xcresultparser/XCResultFormatter.swift index 92ee163..9c52567 100644 --- a/Sources/xcresultparser/XCResultFormatter.swift +++ b/Sources/xcresultparser/XCResultFormatter.swift @@ -9,7 +9,7 @@ import Foundation public struct XCResultFormatter { private enum SummaryField: String { - case errors, warnings, analyzerWarnings, tests, failed, skipped + case errors, warnings, analyzerWarnings, tests, failed, skipped, duration, date } private struct SummaryFields { @@ -44,15 +44,33 @@ public struct XCResultFormatter { } } + private enum FormattedTestStatus { + case passed + case failed + case skipped + case expectedFailure + } + private struct FormattedTest { let identifier: String let name: String let duration: Double? - let isFailed: Bool - let isSkipped: Bool + let status: FormattedTestStatus + + var isFailed: Bool { + status == .failed + } + + var isSkipped: Bool { + status == .skipped + } + + var isExpectedFailure: Bool { + status == .expectedFailure + } var isSuccessful: Bool { - !isFailed && !isSkipped + status == .passed } } @@ -72,22 +90,20 @@ public struct XCResultFormatter { private let failedTestsOnly: Bool private let summaryFields: SummaryFields private let coverageReportFormat: CoverageReportFormat - private let failureLocationRegex = try? NSRegularExpression( - pattern: #"^(.+?):([0-9]+):\s*(.+)$"#, - options: [] - ) - private var numFormatter: NumberFormatter = { - let numFormatter = NumberFormatter() - numFormatter.maximumFractionDigits = 4 - return numFormatter - }() + private static let numberStyle = FloatingPointFormatStyle + .number + .locale(Locale(identifier: "en_US_POSIX")) + .precision(.fractionLength(0 ... 4)) + + private static let percentStyle = FloatingPointFormatStyle + .number + .locale(Locale(identifier: "en_US_POSIX")) + .precision(.fractionLength(0 ... 1)) - private var percentFormatter: NumberFormatter = { - let numFormatter = NumberFormatter() - numFormatter.maximumFractionDigits = 1 - return numFormatter - }() + private static let iso8601UTCStyle = Date.ISO8601FormatStyle( + timeZone: TimeZone(secondsFromGMT: 0)! + ) // MARK: - Initializer @@ -96,13 +112,16 @@ public struct XCResultFormatter { formatter: XCResultFormatting, coverageTargets: [String] = [], failedTestsOnly: Bool = false, - summaryFields: String = "errors|warnings|analyzerWarnings|tests|failed|skipped", + summaryFields: String = "errors|warnings|analyzerWarnings|tests|failed|skipped|duration|date", coverageReportFormat: CoverageReportFormat = .methods ) { - let client = XCResultToolClient() - guard let buildResults = try? client.getBuildResults(path: url), - let summary = try? client.getTestSummary(path: url), - let tests = try? client.getTests(path: url) else { + let shell = DependencyFactory.createShell() + let resolvedXCResultToolClient = DependencyFactory.createXCResultToolClient(shell) + let resolvedXCCovClient = DependencyFactory.createXCCovClient(shell) + + guard let buildResults = try? resolvedXCResultToolClient.getBuildResults(path: url), + let summary = try? resolvedXCResultToolClient.getTestSummary(path: url), + let tests = try? resolvedXCResultToolClient.getTests(path: url) else { return nil } @@ -110,8 +129,15 @@ public struct XCResultFormatter { self.testSummary = summary self.tests = tests outputFormatter = formatter - self.coverageReport = try? Self.getCoverageReportAsJSON(resultFileURL: url, shell: DependencyFactory.createShell()) - self.coverageTargets = Self.targets(filteredBy: coverageTargets, availableTargets: coverageReport?.targets.map(\.name) ?? []) + self.coverageReport = try? resolvedXCCovClient.getCoverageReport(path: url) + let targetSelection = CoverageTargetSelection( + with: coverageTargets, + from: coverageReport?.targets.map(\.name) ?? [] + ) + guard targetSelection.unmatchedRequested.isEmpty else { + return nil + } + self.coverageTargets = targetSelection.selectedTargets self.failedTestsOnly = failedTestsOnly self.summaryFields = SummaryFields(specifiers: summaryFields) self.coverageReportFormat = coverageReportFormat @@ -198,6 +224,24 @@ public struct XCResultFormatter { ) ) } + if summaryFields.enabledFields.contains(.duration), + let durationString = executionDurationString() { + lines.append( + outputFormatter.resultSummaryLine( + "Execution time = \(durationString)s", + failed: false + ) + ) + } + if summaryFields.enabledFields.contains(.date), + let executionDateString = executionDateString() { + lines.append( + outputFormatter.resultSummaryLine( + "Execution date = \(executionDateString)", + failed: false + ) + ) + } return lines } @@ -228,9 +272,35 @@ public struct XCResultFormatter { if summaryFields.enabledFields.contains(.skipped) { summary += "; Skipped: \(testsSkippedCount)" } + if summaryFields.enabledFields.contains(.duration), + let durationString = executionDurationString() { + summary += "; Execution time = \(durationString)s" + } + if summaryFields.enabledFields.contains(.date), + let executionDateString = executionDateString() { + summary += "; Execution date = \(executionDateString)" + } return summary } + private func executionDurationString() -> String? { + guard let start = testSummary.startTime, + let finish = testSummary.finishTime else { + return nil + } + let duration = max(0, finish - start) + let formatted = duration.formatted(Self.numberStyle) + return formatted.isEmpty ? nil : formatted + } + + private func executionDateString() -> String? { + guard let start = testSummary.startTime else { + return nil + } + let date = Date(timeIntervalSince1970: start) + return date.formatted(Self.iso8601UTCStyle) + } + private func createTestDetailsString() -> [String] { var lines = [String]() let runDestination = tests.devices.first?.deviceName ?? "Unknown destination" @@ -297,7 +367,7 @@ public struct XCResultFormatter { !group.hasFailedTests { return lines } - let header = "\(group.name) (\(numFormatter.unwrappedString(for: group.duration)))" + let header = "\(group.name) (\(formattedDurationString(for: group.duration)))" switch level { case 0: @@ -342,7 +412,7 @@ public struct XCResultFormatter { failureSummaries: [XCTestFailure], failureMessageDetails: [String: [FailureMessageDetail]] ) -> String { - let duration = numFormatter.unwrappedString(for: testData.duration) + let duration = formattedDurationString(for: testData.duration) let icon = actionTestFileStatusStringIcon(testData: testData) let testTitle = "\(icon) \(testData.name) (\(duration))" let testCaseName = testData.identifier @@ -368,6 +438,10 @@ public struct XCResultFormatter { return outputFormatter.testPassIcon } + if testData.isExpectedFailure { + return outputFormatter.testExpectedFailureIcon + } + if testData.isSkipped { return outputFormatter.testSkipIcon } @@ -379,6 +453,24 @@ public struct XCResultFormatter { return outputFormatter.failedTestItem(header, message: message) } + private func formattedDurationString(for duration: Double?) -> String { + guard let duration else { + return "" + } + let totalSeconds = max(0, Int(duration.rounded())) + let hours = totalSeconds / 3600 + let minutes = (totalSeconds % 3600) / 60 + let seconds = totalSeconds % 60 + + if hours > 0 { + return "\(hours)h \(minutes)m \(seconds)s" + } + if minutes > 0 { + return "\(minutes)m \(seconds)s" + } + return "\(seconds)s" + } + private func createCoverageReport() -> [String] { var lines = [String]() lines.append(outputFormatter.testConfiguration("Coverage report")) @@ -397,7 +489,7 @@ public struct XCResultFormatter { guard executableLines > 0 else { return lines } let fraction = Double(coveredLines) / Double(executableLines) - let covPercent = percentFormatter.unwrappedString(for: fraction * 100) + let covPercent = (fraction * 100).formatted(Self.percentStyle) let line = outputFormatter.codeCoverageTargetSummary("Total coverage: \(covPercent)% (\(coveredLines)/\(executableLines))") lines.insert(line, at: 1) return lines @@ -411,7 +503,7 @@ public struct XCResultFormatter { return CodeCoverageParseResult(lines: lines, executableLines: executableLines, coveredLines: coveredLines) } - let covPercent = percentFormatter.unwrappedString(for: target.lineCoverage * 100) + let covPercent = (target.lineCoverage * 100).formatted(Self.percentStyle) executableLines += target.executableLines coveredLines += target.coveredLines guard coverageReportFormat != .totals else { @@ -440,7 +532,7 @@ public struct XCResultFormatter { private func createCoverageReportFor(file: CoverageReportFile) -> [String] { var lines = [String]() - let covPercent = percentFormatter.unwrappedString(for: file.lineCoverage * 100) + let covPercent = (file.lineCoverage * 100).formatted(Self.percentStyle) lines.append( outputFormatter.codeCoverageFileSummary( "\(file.name): \(covPercent)% (\(file.coveredLines)/\(file.executableLines))" @@ -454,7 +546,7 @@ public struct XCResultFormatter { lines.append(outputFormatter.tableOpenTag) } for function in file.functions { - let covPercentLine = percentFormatter.unwrappedString(for: function.lineCoverage * 100) + let covPercentLine = (function.lineCoverage * 100).formatted(Self.percentStyle) lines.append( outputFormatter.codeCoverageFunctionSummary( [ @@ -506,9 +598,18 @@ public struct XCResultFormatter { } let children = node.children ?? [] - let tests = children - .filter { $0.nodeType == .testCase } - .map { mapTest(node: $0, testClassName: nextTestClassName) } + let tests = children.mapTests( + testClassName: nextTestClassName, + mapTest: mapTest(node:testClassName:), + mapArgumentTest: { mappedArgumentTest in + FormattedTest( + identifier: mappedArgumentTest.identifier, + name: mappedArgumentTest.name, + duration: mappedArgumentTest.duration, + status: testStatus(for: mappedArgumentTest.result) + ) + } + ) let groups = children.compactMap { child in mapGroup(node: child, currentTestClassName: nextTestClassName) @@ -536,11 +637,23 @@ public struct XCResultFormatter { identifier: identifier, name: node.name, duration: node.durationInSeconds, - isFailed: result == .failed, - isSkipped: result == .skipped || result == .expectedFailure + status: testStatus(for: result) ) } + private func testStatus(for result: XCTestResult) -> FormattedTestStatus { + switch result { + case .failed: + return .failed + case .skipped: + return .skipped + case .expectedFailure: + return .expectedFailure + case .passed, .unknown: + return .passed + } + } + private func mappedGroupName(for node: XCTestNode) -> String { switch node.nodeType { case .unitTestBundle, .uiTestBundle: @@ -550,24 +663,6 @@ public struct XCResultFormatter { } } - private static func targets(filteredBy filter: [String], availableTargets: [String]) -> Set { - guard !filter.isEmpty else { - return Set(availableTargets) - } - let filterSet = Set(filter) - let filtered = availableTargets.filter { thisTarget in - guard let stripped = thisTarget.split(separator: ".").first else { return true } - return filterSet.contains(String(stripped)) - } - return Set(filtered) - } - - private static func getCoverageReportAsJSON(resultFileURL: URL, shell: Commandline) throws -> CoverageReport { - let arguments = ["xccov", "view", "--report", "--json", resultFileURL.path] - let coverageData = try shell.execute(program: "/usr/bin/xcrun", with: arguments) - return try JSONDecoder().decode(CoverageReport.self, from: coverageData) - } - private func failureMessageDetailsByTestIdentifier() -> [String: [FailureMessageDetail]] { var result = [String: [FailureMessageDetail]]() for node in tests.testNodes { @@ -621,20 +716,19 @@ public struct XCResultFormatter { } private func parseFailureMessage(_ raw: String) -> FailureMessageDetail? { - guard let failureLocationRegex else { + let parts = raw.split(separator: ":", maxSplits: 2, omittingEmptySubsequences: false) + guard parts.count == 3 else { + return nil + } + let file = String(parts[0]) + let line = String(parts[1]).trimmingCharacters(in: .whitespacesAndNewlines) + guard !line.isEmpty, line.allSatisfy(\.isNumber) else { return nil } - let range = NSRange(raw.startIndex..x_E&we6%sKb)+4rbX5BniX=ovlPAfI+LNGyj~u?YJl={oP-Xq68eW|oISteB%Y z0t-gtCA|l;*7oyyFBCL5!8lN9-?1%OS(sB0#p`)MteEQ=-KyXLTID}`BNGHLN%*~= z*IJ$j#ELnN#R9`xmfdu&bc>)*I~x*zjxmI zSz%_i^e77?{@%^D+c*8Z-WE2Jjuk>6J*Vj*QVkLSN+7A*%0V)usLP<`GQH499vqE* eznoi>6p-Ol(`+N-2rH@!h5^A;#aZB+MyAMCvxa#9 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~2pqvROhmmJlyMxNBHHNarP3VWfGyPAMpDPV9WmFYnk-XSMOdn2kN0mMSFD9OQG4KcGu2RYutICbM3MveutJKBg== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~2pqvROhmmJlyMxNBHHNarP3VWfGyPAMpDPV9WmFYnk-XSMOdn2kN0mMSFD9OQG4KcGu2RYutICbM3MveutJKBg== new file mode 100644 index 0000000000000000000000000000000000000000..5312651176af25e7b5cf946419e348f6a9c09451 GIT binary patch literal 425 zcmV;a0apGfwJ-euNCgc578*|^AdR>I8zZ^ZB#f8%S(-}Ph-$wF3%E0A^%{l#g4+ln z2p~8RMF2(sL;#LHWIJUnwvqRKbXi+rDI?1G!=5HZvNmjYzp;FWBFndqfD-ya>?HTF zBLk-46J1zE@-$87y&o9ew}?WP1Wt5e56?ra@Xg70WnM%4lwtc~98kREBphWdNj1@h zIS)NCLQ|C|YA3nJbD{Fylyq6xD!lijjrBdTWJ(*h?T;m8B-KP0wiM{(2a>6O(_1zN zZ5W1vS=7-Fnz2A1FuG?sBNbq~k*)=!d&S1y_bX(r1BQmi zVjG|UvR`%uBSBMGb`=TU`xpM+k8BSk09`}AB0!^zB*SAMaPw1{F1({WZYXjw#DK-y zPU*H=)QjNh=6GtwJI#sa%?T(agf|U95A>|8Sguevl1>*Fp5ys=o{CSY#(K!5smy5# Tm-teX<#~XqYvaw+rJ$f- z70$Ucgw^uK1Ye5WYS#zR;1O;U^ z7XaWaV*yiD!UjR}H!~(xysqCoQN=qy;_v)4d%wlY>fCT)8>V5#ey2HZvYJZ{hV23| zq-D*qoGKmIX43etE42d6Irs4O`;=f0*L8`Z&yEB5AXrh@-*5VHOJjfYE(hbR2RuTWqkL&+S1|6ywcb`puDK%1vU7b#rSI z(x>P5(3`IcWfoj5)WyVjcFFja9D_@+`qw{!P>>t0k7Z_C1r(<=0bAE#d|;E@6FJ5%G1Z{P2a*Q3<>Es+He!aTyh}RE=}Xv#au_IPJ$JX z6F$=#?ErJm{bTwLDb~MkfDSrB3>o0djtUTpKfBliWl5^-y%{;-KgA|gBcEN&VVn{G xTp_>eG6;s0!z|$Dr!rm0itT&SrpUK?b|C-& literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~5ZjHkpN1XJGaP53G7BGIBIot2KMUpo2J3zYwXbAhJIo6XAAkReDjUYpsT_0gnC7r4IcvcJ66ifStBTuWFZ_G9g== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~5ZjHkpN1XJGaP53G7BGIBIot2KMUpo2J3zYwXbAhJIo6XAAkReDjUYpsT_0gnC7r4IcvcJ66ifStBTuWFZ_G9g== new file mode 100644 index 0000000000000000000000000000000000000000..e89f923efa82936879c9ac591d4571a7f8fcfd95 GIT binary patch literal 286 zcmV+(0pb2AwJ-euNUaC}rcpU0AgNdY$B;jXtc(=VEtVx^Q8Z&-CoB5G|2NLWh+6=V z0FVp_FaRz9FaVX>0oGdU=V+ zrClr%MFRY0xcO1b!?=iZMjb2Pv4E_jkVr@yqKA8ESUW68~r z%r?KbahgX(+QpyG#Rh;8XLR!4J}YLf@bRtug<- z3ax@`yM~(IdsqU*D<>DhaPuR2rjF9F^67&xr{-f=9w`*JRP!+b5CB#nDR?`F$dH08 kgO&BCZ1X(AdR>ITbPh9--bK1EKQ}Q9_r;U94Ni8LQD;jG=5{$ z0f2$?PykQ>Q2;xs6R}XVxmT%T|D%rPs10)u^3ISp_Ac8EQOCbx>ZbUphoCMl< zeYodLZ3XeW@G$nfI+CN6>bNK8Bv||uB_3^9#6r>L9tl&Mz}5wlI8IUS#c+}GnzS|d z7Ty_B279Kq*U|y^*kw%{ETd~0sH2+kw=YQ_?!Bg9SelUWW_Mr|%G&dDHaz5r*_N{Q zD3}RL6Ec%#Ux W)6A*DC8*TSOavj+-~xQp$O0MaSq7^B literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~6z1d4Wuy-c4IuplG0ts2Nt1boOG-En7IA_pO4-Wzq6SQOIS0C7JqyUwxLpLp3Ok94uC51BCdWe8uF6-V9XA3sQ== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~6z1d4Wuy-c4IuplG0ts2Nt1boOG-En7IA_pO4-Wzq6SQOIS0C7JqyUwxLpLp3Ok94uC51BCdWe8uF6-V9XA3sQ== new file mode 100644 index 0000000000000000000000000000000000000000..84a64fc40fe519f85565c0355e90ec8b34fe0473 GIT binary patch literal 261 zcmV+g0s8(ZwJ-euNaY6r(ho8sK!@r9gqFFyNPKrN-Gn*X^Mjg|r;rr6PO)l^wV9(R zYFzKRcZ}CD!tcX(@BOSSZkDDXWrM%Blk`+00@AapAVSrcB>djbhqazZ#H=}JfKjll z_5cbFDyJG-F?*^DkJ5fGX06W8>&*ygy!R*m-i~dzu=;tuZESGEIHLUfdG#HYcD;{gsrjcqp^jxepdU>Fcg LRh$LBX=I8Fhsk@i literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~7yeirstrQ0i0FlUFzztugtKQHU_qJY8fWjQsnSoCdDUXl9ygHQekpq_BH-QNUmTiOVAp-AGxzsYCdeo0oa-w3A== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~7yeirstrQ0i0FlUFzztugtKQHU_qJY8fWjQsnSoCdDUXl9ygHQekpq_BH-QNUmTiOVAp-AGxzsYCdeo0oa-w3A== new file mode 100644 index 0000000000000000000000000000000000000000..37474321164722bcf20caeab769dc12c1a9dd45b GIT binary patch literal 264 zcmV+j0r&nWwJ-euNc{%@;?FW9FpKB`LzKCKYpxqKLGtJq`m# z0<)S60AP4V=m>m{&0u)g@P~wwa-*raklbI8zZ^ZB#f8%>6a+@7O%ff3%E0A;R|adBi9fF z5CpgjMgT?tL;&BYL$*`KY72R9N7>p6O9@fNANDjUlJ(Jc_aT<=4}?9`5syN@hn?ge zc67ire4?yXBu~?9-rIrEJp@sK6@e3FP2xxb5YGz_Ir*;4>xZ8*ZC{K8ikF;3ql^`) zCdyhP>4_7XsyI2F(8;o0GWBnI zOXxut2SdRyUHUyU8mI$C_bg|m18q0bwP18lISF|iQ0CK-I#%1Bk0bBx=%eT3N}0C% zVkFSzb)c>H#4x+=4MDR}0iVy`y!R_(>9Rmw{Jot;M-iG(^rI+NOc7cv^u1phTL=vf zjny_l0c5}I3dVw_u2z`7Ii8Q_srZy?tcP5h X%ABTfi7&MaCIUjJiVMIujZBdN%G}AY literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~AInLjLbwT6RF4mfeVCQxShMfVaCDXU_GVOsP5p2-2k0E6C_EaMMTHgzUtI2NkSR-ljVOr3sOBSMJslTc3DCOEg== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~AInLjLbwT6RF4mfeVCQxShMfVaCDXU_GVOsP5p2-2k0E6C_EaMMTHgzUtI2NkSR-ljVOr3sOBSMJslTc3DCOEg== new file mode 100644 index 0000000000000000000000000000000000000000..04954a298006dda9fea5b396a432f5804b7389fb GIT binary patch literal 272 zcmV+r0q_1OwJ-euNIeJuCeSn@Kxy=YNU{@(xH&GYx^;@!>mlo^;8x=mszCc0ZXHEY z4g@R!D*!A2&OLngT~e%vy&xgg3cF;8>Agee5uh!BOieOxi|LtK0eyd`bLfRdK2M~lRX1o9( z1Rw+^EC4G2EC8*wcDl_`WVO3PbZ{xs$M71yLeN++l#4;EP7~!c|AFzfi{zu3KqwcZ zJe2%r&#RY~DE_*Igg7klZ9DsyiiR~sqOp=Osi0h}U5cnEkPaAM>r$13u#*bP#T-gO zg2JLC_;w%VXB{F|s%7E5pL;_u8L98m~yu$-oMNE9N-zVok2RMU8NvF02oEGUY( z003{=T|f~yqKx@iGT!+Se`luI`%N#ab3=wz9A@lq&2baOTyiAVE=}Xv#T-khQi2tj zH2&)v?ErJmJ$(HRE!IPRfD%|jFE+%V92q1Oe|E8mev(w%J2R8Uhl^FHWM{t1l*26G=BF}U$cpk&_;g@6LhuUIh1RPPvd@LRB{E5Fa)9n4Gm({tU!YYOt`&)BdMKPx!inWR6@$6!bB~tt0UCd#e698I~ x+!+c$zyU*QVQO&mQ<*LRMfoIr`WB7_JP0+m-EcU}0R$o}YESda(!Xbty1B2z+pDiXRvl z7~?AdDgY}0?jO_l=&}B_M5NGD#P9*WZ0UfpaA+5+pd4du@7>4=|2@{B90~1WPxA!r zlp#M=Z~7coH6SFU-)O^K6mv_$Sj#vOXcu!mMRgWzLr(ZitF!{lxyp$^yI6Y=78n+a zxc~q;;q%Xg?}^8hAmU+zH>4b6ZB6S0+2Flj@%L_M&bjw{wXMVJ^SAf@UDPV3BgVjcFFja9D_@vH*fwbY?;OtoO|duI|)x#R6v-zE{IfCOp<-`cTw2$h_<-~S{NKG z%sl|Z<*bT{9h4ahM&oi;#hah>n;ptc;dQDu)7V(XF(rLNS+2s|QxK-@;dw;c++#|r z*1!f#OCHiXt$=&)Jwp1H677)_qynZSC=ui*PXx@;(Kfe0VO7=U&CaysV`35Pnb9@} zIZzS+ULpT#I1Gjq#Dw7Hr!rkgi}G3c^k_Ij@Cww3cf*Yc)2a)G0l`$oS>T&SrpVng BcxM0r literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~Cq8vmviQJl0DCDBecofsKKjh5HdB3gvz-kpR4C3vP9ZzqNUe-RMBF7ZSStBYkiDPFQEgz_MJJKNhQUTgGTY18A== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~Cq8vmviQJl0DCDBecofsKKjh5HdB3gvz-kpR4C3vP9ZzqNUe-RMBF7ZSStBYkiDPFQEgz_MJJKNhQUTgGTY18A== new file mode 100644 index 0000000000000000000000000000000000000000..5cc56878fb66de3aca797427466795664576d0e0 GIT binary patch literal 9278 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 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~Cr0nfoH4JIZq79Jiiqdi-nw0EiTBQdr7Tu0XgQqhp5lNSFn2lZmTisu3Z1RAinPjNhPRnDIKX2Fwbb4B0d2tgg== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~Cr0nfoH4JIZq79Jiiqdi-nw0EiTBQdr7Tu0XgQqhp5lNSFn2lZmTisu3Z1RAinPjNhPRnDIKX2Fwbb4B0d2tgg== new file mode 100644 index 0000000000000000000000000000000000000000..e4376028ac888b167265c4bc1b1507f7bed84575 GIT binary patch literal 250 zcmV--fbu)yOSw+{sGjIMXYZu1Ule_ern}nqa84#YT>LJvGNwRPLGG#drXjx;PhT{OS zngcjc{vSnW!Kd2nU^E8a{G{L9P;UCFPqmpsgk2P5q-Q9{RaSEfl3=?y4`^9)JVkX9 zY=E5ck5*{~nse^((RZ|9hrAFO@D#n65HDF`BoGZPYYp^KRAt`W$Qch8t5}YNmbIsO z0ulgSA>XP!3Wn6f*x=@;GF=FZ@>Te>WH>_b3e<>q!;J{jstbkz!BoXr;G0IK$dV{> AoB#j- literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~D1SPS8lNV5vdB1Sos1SbK_YncZer04RZQzg8Al74YtE7LcTOwqGJ-aq0i-R2u39iicblHfGczRiNuo-kNNIulw== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~D1SPS8lNV5vdB1Sos1SbK_YncZer04RZQzg8Al74YtE7LcTOwqGJ-aq0i-R2u39iicblHfGczRiNuo-kNNIulw== new file mode 100644 index 0000000000000000000000000000000000000000..b02409d5d4182a3f52bb6a5ae84d4872ad3e563e GIT binary patch literal 249 zcmV?V{`9-J8E^En^(Pxru(WlJFx%2ZW(%f=DFAB-uBAm0C+fv#dF$;YdKN z_5uJNKIDlVjf2PKtOjp>(r;EMH-*!o+DzkP*}|0cjEIm}ft5&omN?M>oD9GK zz`%LVbu)yuZPWGd?ag1sS-5&~5&h;T;l_jx2vZKnL#hdrWZ(R49OxnKVl7%EC@>3i z0RZ&L*%k+kM_CQt{G{LfP;UAhr)o2WjCB|W8R;A4xXQwuf+W@=pog@JIi{vM306SP z_(yAF2bgp2@X`0ASclv&Auu()xDYS7QIKF-+QlB|tE$@G{Ky#(727P3lyj-27Cg3oubW2%k2EV*w6Ajcqp^jxepdU>FcgRh$LBX=I8Fol0=y literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~GHwlKCoWd_sVutKM9JxOzl6WsKLMkGIfjadOeQvtsU5zwpnYy1DtkD90oypXv-gAL_VehJ4v8k3czEoqvHpoUQ== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~GHwlKCoWd_sVutKM9JxOzl6WsKLMkGIfjadOeQvtsU5zwpnYy1DtkD90oypXv-gAL_VehJ4v8k3czEoqvHpoUQ== new file mode 100644 index 0000000000000000000000000000000000000000..6dbf5e63bae76776e84d667e741085aa6aa4e9fe GIT binary patch literal 1713 zcmV;i22S}XwJ-euNJTXO+HKqvD&S_Y20&0(Sq7nj(wH~y2<8{c_LREgu+S6pxh=3~ zaD@aIK@2m@urUEm0Y(8h0hZ@jODh_CPvg) zvX2JAeYOTlZGUmRYi;mvfyxZBnrF94ObZrB$ROE`8g~+ zjZ5?%42t%7D3z=j$fWUsw9Q8n2A`;KAr=%+CMhf!ip*mBG$ez>+Qn=cf_7cb3?bnw zJ7e#&Uz7fqw@(|!37_h&3SB!+#Yt&_0*DGHZ7|u?Yz8HP;3h&-@qI>+&eRMnfuC(O zkuao1!b7`oHl=`6zbgg;0|cIoL4#wrtP%{vM>J7XBAN*nkq(^*(OLLr0_gC%fHW>A z<#adi%q#7>ob+w?`4hh4SHf2wbUQXzhjCJz&^fTzFy_^BYJL#5ZcVzkPw(y1Uy)Hp zI;uG%oYEb>+g3kJs%31y(&4+S#h}iiJAAiAxFj!~#^pq?AOsQz(S`&jb%>8*vrCrk z?bDgI-EZSDPD;D4($$T!^;Fp&wD!A1#%gAJoM2VA^ZvSq+*{oAccwb%Zw{j^{D z+jVQNCaolU`}AjraZ-9sE-xUB%Sm_VaH;z+rnmm>FixuN&HE!#v^VeP{4gn{9lqOs z#=(%$zM#7;!mapR?R=WF!*}2ACcNUkk;Nx(WZBQjbvulc7AU|0N!A4$BhB{{4TM=f zj7}(%RTkdVL^go=wpDvMcymf_7$o9(vbtz67BLIH$JcL`_org)|?mmoto^@+d=aZCrYLL9gg=#`Y`u_vdjK+p5KzUvW}fBLGH`QQbU!HIJ1J-(9=A z^((qB`xGaIFv0;)UImhJDLIT5DGd@DDhZ`qzpKJ0?pn0hy7y~RZM)ybsSL3hqA5S* z;z+x(QRZoFNRlU|d{2qpJ|!N9f*F!+O!4&ue> zLLtW0={T%qaw-sPcMu_k$zpQAVz3r0$A0b~7fdXKVpfC_FNP{3nF%Dx!A!HN^B$!Adk&$Vg zF|A4_=i$%i0Y43zsC!-PWTken>)IX;5dY+ugq4a_qb95f3|^(Aj9}%8%C?0=n(xJ* zjSCCoruIJ{HCLQd~wzky#p64cd5JuaMJ(;&8|n%@u=Lgk2Bkb15- z$8v;1oI7Y_vEAdtheD|V)+j3;Uc(Bz(conhn@l;0-CzkWzT>jb3C&bzk4b8{T+gf+ zH5CvAGtr-w<);|3=Z8YzQTU41gs7^2cl$kNKXHX1E@}qcJYN!#kvTQ`T3gMa*DNGy z2MdIXaom_gvvKs|42i+i9S)gsgfBZ9%sXM=>G7vH6;lcg290`_+p#hb9BEc+zoK+%YWcpuA9|>C5ss;jhjzn2z%$~4 Hj{krL0wXN8 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~GeEAhLfEKOAiceE4p1RPhsjT0dhJ2Wb2RSKJSRYrX9ZfzcsOvikLqISFX6sC7Isbk_Rcf-83_XdL07hDRZU0GA== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~GeEAhLfEKOAiceE4p1RPhsjT0dhJ2Wb2RSKJSRYrX9ZfzcsOvikLqISFX6sC7Isbk_Rcf-83_XdL07hDRZU0GA== new file mode 100644 index 0000000000000000000000000000000000000000..995fde54480838c7e2bffe1ad0a1f08c0a6deda6 GIT binary patch literal 272 zcmV+r0q_1OwJ-euNIeJu1`sqMFl+V0khxN@rqtMY%ii?QZ@WS{9%rCht@i&9!@$6x zR4V`~04o6QAJg~AS^t`0I_NktRDeI5B1j-y+Ql9yOIh1{KeK}WoK+~$ly-WMNQkJr|rZqwyc<)F2y&oFRx%Ydut-|Z`xA*={lx>!lJa_T; zevqcB=WujvH29F!Ly&#%UuFdp097D=C!*0Yq&}Gl-27Cg3n9FGZ9E-^Xu2o|fh$lW W-VHY*Osg&!1_V;xB+nsTpMq`nGq2Y zg73L=jMZ(z>%({N{F|(86rPao;_v(*yr5#l!BbViV^R@8_MLy1wID{ctU1L&f`YQz z3jk=qZm03pBzSna;4B-?SsizsBrJI6NBo_iW^Y*iRGm9AY{NKE+3%X=Cab;VVAw7Y zBU;uTPg11@wxN`K*mbc2?Y;LA_PgY;j|?Fp@FYRmAfNKWfKfbJ)*21xtnQs3O39DT zCKg1aWlixY0ulg8AiogLh!|2g<^eZ9mFYrolrO}mzr+y&SD;3`8*W6HR$VX*2&O8| J0^c+;MFz$@ddUC) literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~KuzhAxn73MCO_MRDOxq-FB62tcgHGXjZGZF6te6LD5tgVdJfE3FNOylq0NoFsKhLyquDKwBiZBPFt_KSWb3mRA== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~KuzhAxn73MCO_MRDOxq-FB62tcgHGXjZGZF6te6LD5tgVdJfE3FNOylq0NoFsKhLyquDKwBiZBPFt_KSWb3mRA== new file mode 100644 index 0000000000000000000000000000000000000000..8c16df93ddafdac7fa2603a57461a10e11edccb8 GIT binary patch literal 71 zcmdPcs{fb4<0~VB3v+a}Qb}b&s*;tVj#6G?E|8(*oL`n&l$f5XRIB5emy%lHn_7~X Zl30?cSCErgT%uGP%fw)7ulr$&FaT2l7wZ53 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~MfaWYZUDTCQgyMhJ4X_SfjovY9UPAVC2gdkXTbO16b3bR7M--Jm1jalGBCpPTsAMpM4Rx21VJpYW_9Cc08odyQ== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~MfaWYZUDTCQgyMhJ4X_SfjovY9UPAVC2gdkXTbO16b3bR7M--Jm1jalGBCpPTsAMpM4Rx21VJpYW_9Cc08odyQ== new file mode 100644 index 0000000000000000000000000000000000000000..9e613adb60237d54fa72a286e82c7866be59c76c GIT binary patch literal 422 zcmV;X0a^YiwJ-euNbL*&=9x|;AdR>In;poa?IT)$`YINFyD(+A;z;wetzNyd^`Cdshp zfu}{_>Hga9+fF9^sS+QK9ZX}(qEj-8baX1y9QjPVHOH-NC6fW_l QcELnI2vu_y``q7`63h@tBMuWd`+|O{o7cq0|v-qH5ae|w4XLcPJ+HLZOkNi z?{ECQrG|AHjsjx)bga}_unak2Js>cGqg1U22SffeBMniK%w{Wrt52m}7@|DBTI90G zfa$qK*P|@AYnNwcETYOUhH#xX_B@_ysO>i#)hCS^Nhm|3)Jf5^M zDtUndn5P~D5PPtDNMpuydBQ_)y=E-f;r7A~PuJ2Mf4P=gEbO(P3rgp>O) literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~N8sxHXnTVrfytB-GSlFrIQwhMzRWeqdZ150UW3Uyz69uCswPMbuLLOP5zeCAqlu1mOCAFxwlysTxy8UvcQtwPg== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~N8sxHXnTVrfytB-GSlFrIQwhMzRWeqdZ150UW3Uyz69uCswPMbuLLOP5zeCAqlu1mOCAFxwlysTxy8UvcQtwPg== new file mode 100644 index 0000000000000000000000000000000000000000..7d3a1280c5194db734d467d2d5ec86dc0f24969c GIT binary patch literal 751 zcmV?3LM)Krf}bft@4?hX2^w*bmNY(7@VvsVR+5 z`2WAVzyp8)asYh*FXe>qSjy=%;h_R`0C4C8flUr_9H>bcmLQQ(G0=%Kd>G-h);e8K zaByeWIQ!*vYLW04K9mj+XEr`65(+;V)Pey(UbAK8R`2|Jd8nas!e0mFRaMhzPfDLJ z+;m;FNched21BTY6G@^LL6b;oQLErLT}2<(AdoY0o(XFL8SvtI)iR^=#kZY)Pm9}m zx6Lo1I!x&6>A0O{DX-Rden?qP=#n49ikZ`s>)I^gzMM|wtau;4S*e`xx0;kj4>Tb2 z&ab=ydtUz909Dn;(Lm2`o_xHVTR`9W!r)dp;e5?Ri#y`q2jgZ)sJF{RC<@W1Kt}br>S086K@>Bnw zmUVd}q2&?oJ63f)2`%RUjl`lSY7Ek(CNW1Jw6>8ns4+|jQ7 zgoJ_;G-otNXv`-eg%bEXOEi10_3?6US5bm@euvSCbG1_4+0C|MoO1Ek5)(Q%E+A+784y@Dw=7JG{bhT*Hp&?`x!=SGB5cEh0vcQ9Y?mEt_ h@DkA3%|y*Nz@hNs%CdyBBfQT|OFoaN$lJ}U3>CPdXIuaP literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~P7Mk64DUuvIoNqXXVjI0Hx1i1B4zAk4iKaLt8nSe-bxbuJEtQ2ubG0dmnB3LIJZmGcxTUU9F4Bla9biPtJeWBA== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~P7Mk64DUuvIoNqXXVjI0Hx1i1B4zAk4iKaLt8nSe-bxbuJEtQ2ubG0dmnB3LIJZmGcxTUU9F4Bla9biPtJeWBA== new file mode 100644 index 0000000000000000000000000000000000000000..6471c27c548a68cc024024d61fa2c51183b7a638 GIT binary patch literal 286 zcmV+(0pb2AwJ-euNUaC}woy4GFsW7m1Cl?FDT3lrp0PDFv*@NK##C+HXM#er3G(nT zFfafCFaRz9FaY(fz+#NGYqU9PsCHk75FVoj6JX0f0|kPiRjYwENvclsA30&W#xfd- zgjOxeLkav|xcN`Z!{~@}J{>C`;+SlLp&%h`haN7|EXEubY7>$Qv}&=(sLsbOU5 z+x%XIXe1SA)p95YkO-n#EC7K2ru*Z`#*MVn6w|1J}%Pga~)Z``>ZvI2hv`rl=pSsENHyy*_ahlpenr>MF5CB&oNqRfS$dHmP kgOjZFl(Ks9(b@`$jax?mU(OjVo(zG-BN4C>;6L;wH) literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~QWafzjNpkPwnvcW_0QZCgmUowbP0C7K1taLHMZDATNLbmGMEH-86pvkqpNT7QPZCNGtZC9E2zB0FZL4JJYAACA== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~QWafzjNpkPwnvcW_0QZCgmUowbP0C7K1taLHMZDATNLbmGMEH-86pvkqpNT7QPZCNGtZC9E2zB0FZL4JJYAACA== new file mode 100644 index 0000000000000000000000000000000000000000..105207b15149627601a5186d25346c7eb577c007 GIT binary patch literal 262 zcmV+h0r~zYwJ-euNbLsz;!iRnKxy@ZD08DC@y6eH5$w@kukEy;W4{;G=kX*&XP#jg z00{57cZ}C@OQ*W1PhBN&QG`_8sxWwoau z4CBM|h?uq4Q@T~b0<_A9_D(#|-g_UReU}pKV=qVtJtZg;xoypa1{> literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~TtGk2OMZ41nBo21lWxoYU8xAONzltypy1w-qYLfPfWFh9XQ-4dmQGT60kLtLftCtndnghia9hN9PgbVANQpMEw== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~TtGk2OMZ41nBo21lWxoYU8xAONzltypy1w-qYLfPfWFh9XQ-4dmQGT60kLtLftCtndnghia9hN9PgbVANQpMEw== new file mode 100644 index 0000000000000000000000000000000000000000..264d0a78e3ede22dd92e2c65eea5eab4ee9ddfa1 GIT binary patch literal 4954 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 I;G0IK$gMwjy8r+H literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~VScPuAn3CB-sLm3iIlFTWoHFLXOxaz_b6OL9WjVyW0Om4XjESqJdsT1C3vk_wPSj4JNfFschEmL3pVLer4xFtA== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~VScPuAn3CB-sLm3iIlFTWoHFLXOxaz_b6OL9WjVyW0Om4XjESqJdsT1C3vk_wPSj4JNfFschEmL3pVLer4xFtA== new file mode 100644 index 0000000000000000000000000000000000000000..89d4ffd76a741496ea0ce70f657b71089289df8c GIT binary patch literal 10730 zcmVelP6hpu7l#Tfj~Dl<`IFQnutxiY_oT~zwT=Ui;=?R|^T zt?qwCfwAx&qH$;hN%Kl*UWa8l1jMzGG$RDQc4D91S6R&GnQzuGG&C{ z>$!+G5Jm<9VPa#D^PxF9kO2JGn6KqPqtRd@p92e6 zR+L#X7ACW77RCU9AlLasFfwPkIL;ANX@Zh##iM)6J$_>lvVM+p1l0verPWeQC+u~& zGa^d`7Uqk+wi*NkgV4by-Xd%)00FVMhTyBES!P?ST-+J5t&7VGTiklc*GAx_odIGp zG}{9#uHstB%#v9k1H_a_jOT(Wkr>Zsfi_YX9BUsYliCV^q@>CbN`W16olhjW2q3iK zQ7vVt15FfC103*xWB`Fm69PEn`EVz`63R}5=VcrSq?8N<-~r$vEi>C{;LhgSN)wh; z+Bh^ykd~QkUEX3IK{;a3%$(^nm*or)uU}+ry%=G;2BH05Z^duV#s9_Pd#<~cUV1_d z8iz&+CPOFg#5SiUWoAaO7fMZ?jm6o!hyGw>4r62vyC0&L!IP1h8NptMrq0Ile^cYo z2$K4n%XMK{jaCyve-1*}&c6GVxA_lB?s(c=s&{m^A&MX-VYk2Esue;fEv9U(AG|MZYc^#C-p;3YaN4LfT z5Eb%X1WUu!(#%W@iSXji0MYF&0Op#~zx-O-AO6+1a^Jte&yumQhd$&hGhQMCL^GQ? z879I@d@%7?o}sZob|O6Q;us*-Y6c2#IZI92zB1 z7z&I9Wj2;^EO9-Ucr4EVA*uA{#^NSa@ldIJY_qg8KvYXJGaC}yP?6~b%?yGC`h)sd zo?(H91p@DxNB7*b2ETP|5q5OG*7IiW@n3`g9CNNYi~g3gg|}YMoVj}kAHwxkj0^h1 z6@LMXbC9w1&0WLgqWcJ&E4@T-%PQAKW~yMQW1%#YW8Iu}YxmW$-`i?wh9)yns9>mL z9jc`{V&+8Q&Wx#KHdxqe?akRk=GvdPuXZp&kWBEGfgC|OvPn{Drk5f~Duo~_fl~t5 zv5=<%rvyO=Qm%4hI`bx5Q$uDTUkgoUl2n?n%#8#QU&nT~u;tM$aBcdxQe*G&A$tu* z=#6!Ar59K&%~Ao>k&JZ|RuCkWtx6@8tIU{UEY-}ip^ltTDoH9;^GXU?EzOL)c{0^} zXjapq&|?{;(qyiU;2AmcLB`c4U`I}hV zsiZ_qzBZx(K?s^Dvuv=W(wnh*D3xqPRa{vqL`?^xH!GEt2+EOFDkU|YrbZBz>wGG# z1I_MNK?pJlVk@fJkF8gFiEbf0uYx64syihrbrb>8bfU&SIYKG05|JhJ$XGs_ zMn)(FUNoqbBa{L!Ak=x756v~YN}^InF%cj(qEeQP5~=i&tE5*>JR}ihHZx0xI#ATi z=3%R1=p$=Ib5*j6&CbJS=Mj-5^vKMN0I?A2G#T5fQpU7lI#hu~r47tIS1ug+Tw4{+ z%!~kGFdZ6E2`iAemSR5gMyLV>DnY2zpb{g&gQQZ4N*nH!NQDNKC{$_&DrIXguE12K ztI~3%9a}~|YB~@k6?4y(DoqDMnW(WBnpdSeB`S3sDlKr1tOPS7m<48TWE+lruC2)` z4a{rP`0cg=NM&tZJA7558V%e#KJzC~~NEW)Mq0{yX60ojDb zJ%d;*mg&&Q((lfVmi6#}u|$950VY)OF!Q`jwant20iq$-`9zFmEY6(;zHj*0d$D<3 zt-Z$*l{!xaR?Nj}PB<3NtCA!lQ%Rk`O5;Joyf!x;4Th3t6FJOes#F14CQpnJWIP`h zIOF+%GoBAhGZjE@o{zi|T43fz7S9Gy!txF>*d1H^=pQxKdd0*VbNI{M>bGEg>)ixf zMm|X;EU7f-i5rJTkfus#GF6&TMOJA-6d@?fs!Fp%BS=aq888rl$KD79K+2dp zkgCYgGZ&3R8;3>^B}>}O=gwRgGNuls*hpQNPE#X@vXDfO$yngbM2)=;i^ic*f`#)$ zBy6NsFcOknC6OzEpae)K0TCpah(t|?D#(~R4mMI3FrJLP4oi#eoxo}i2CQK-SNfOe z3yY35jI1qh@!_+_+CTTk9)umeV{15qLLu_~^M4D*1>^cRov5)7e24Di@{Z2AbLWgb z{Phen|9*@y7g7EkpuyPq|%(}qYyQAL8gy_FMsx2YmeR@Tg@N3#bATJJVr^S zkS(mWH^&y|jrh-Cx7Hn5b8n!1vBzFxD?WtYu9tIevA~N(LnrKYkXbSs7C17kkpUcQ zAF>nS*&xA@AV?~8DD>Eb=}_od-uoBbdThNIVgGB+xo`el^k?4fPSn^ZgRN>4luY@K}71w5FUfL*0r4rXts1%_z)SZo- z5sHZbu|5C@4C9#0jcj8!k;7EFcsw$$mU%#E!*pmwVaGBy+Y6~XBeImJjH_kvj3D_$ zD0nvX-Z^1aj!+7`kme(+QmI;+WkVe~F`cNfk5Vngcs3T*Qj8~7jYA{IWU5tIQl-O8 zo~aU^G}A|-!FSdw8I|yKqQ)+0=5@#sO3`Q>8bPK?n4uZlDv`8N*E*I_wyK1oXJ$1g zBvxrU5IOOckWv#zDpiXkD3Dw*9jYKlP>vWVNIq&hG@=s2I8?%s3F^pP=QA%;sYtppwHgGp@i3DlMqga)eUg#{eKGFc>U&C~$y<11K;WASl4Y!66~R zLc@ch62mxTmW;(@Hg)0JjMZc|mCh{htWzSX6oTZVrbOO3VHq_Y8nG8r30F%qWR;{+ zi6nx|Mok4!N76g7N===)+8c4GG$R<9FrBEe3uXi(8Hq}*c!O24myru35o9vbyit}j zq8yp)eCVCfWUA>vWSuHWD$RK21+G*QInW|w8xfHuq$-s}4y4j-EaPCPq|lH6LBj%q z2M7+};GxjKIDmr$4~2vV4j42P8axyl955IxP{2U~1qMSU1qBc=Bp`5jJSbQ|K!X9H z4G`MUObKR2Fav;4k?91*k;6=;n#{G4I#rt0Rt73TXhULhgi>Iou972^0xQ_DHY>BS z%!Ik;N(gPZbAnz*j!=rmvYNNm%e}|u>*qsrw0g1C|I3x=OZ0_#i|qA+E;{V?oZ<_7 z?;3Nic{}G`JCBil^!9EI`eM#4=WaQ-Tyya;=i*~6wrR*+dan(wy)*hT3jn~v}4b`--<6fjQ!ekI65-O)?3Uu zYmGq`Eq|=rGuGYi-_0bIw&l!ObNw;0=wMtBDj(mkX3Jgs$liTL z{YB^A#b&J6x8wN`-`9fQti9-QvBiEl+H>!md5ep=75};RmZL5I_SUz>`Z@Q`T4Rg( z`q(V?!Wqv8M`SI+w_@Ep+WZXz4)BLcGenW zAK!~UzT$0xZCUPot$#22i?Fe^Tf-{~E(PIMbA^i6hJ$meyGuVp% z@@r{-&fbfxVSn@pUs>Ox($Cm?&_VXEpNl?ejj?YLF6VGLm#DO1d9b0<4|_RVZ^78w z7jvbT=-4sAlU{it)OkGWl@t$KRRRtICO6|sBa?-qEF!Xm9@Bxy95Hh;2{38JzguJR zHEgh5$B)na*S~YloWE}C;Xk+B_j~=Y-7WZ&$zw4(htJw`)}ABq@O^a7-iz@gWWU;Z z1+==k|LR|>x#%87=-(#Yz7~t|m&eDG*4$UWelhxU7@x78OyYabA@g6q*2F>PO8;~I z|IK5+ZZ=sisq`J}cdXxcGxz_<-rKjg2HSewY`yji$J&QksZ*(Z5(Eh2|9j2aVZZjV zV_Og1IbR+pthHA9-#yp5VRUZ@Tj>Q(`gwoH)*6H{#^~02S9*y)nY3ro`@0=`kUi+$ z*;?y~N36GMP*!lgVVl*kcSfw_rnm3$|(c zip;h1^=tXV{|5aRmZ-F0`NjMjgR6avFfR98=_NXZHnid^_FAm{W9=Y>@9xF-BJ1TG z{Kb|tw%{`uS-aJdxzbC#vf9RSQ1Q0>TGq$5Tx^dy$I@iVx8>Keo_lPrd%4Gl@9)lu zY7-LVjq;rko8a&pQ~FyL0T=Iwl~ z)xJGMpEWNc~!0P_+65P)znI3^NHrt&iS6Tkq#Kt-gn zNE{lALxC_T6byyJFbIP%6ht8igCGUW32TtyMPqtCnn} z1b{g|g)_4a!;N*viS4Sn_=fKlfBV??u=0}Ct1uR$eNZ+D&GoaB>i8~7*L@ogYmfz` ztenK4h5}%|V!MpauX%n06R)4f%C&TU29j~?JxCgyHm?)K%0lvRD*E3efS1kMiRsGN zi<^vv-CFtAn(-Z^f>1Ieo~rEFnM7mx!&8*PGS6u#@lh!DGSaa{n zj2dVGc=&K8^n~5mwc(;qsB-WrW`)^2CH;kI&WlD(4YYtbq4Wy5Iqnz~2*CKU3a|&6 zpCJ{bFW(oYB@s^NDzG{+*L>GXoRbcC6o3hB<3w?J7Ks=u`;a0S5-E7lhW3JB>^b+* zI}>ZNed+YJ+6%@JTb6dmHNUr`Fj>zzEx&@6agYOck*E&-9=SAis0}CK!)~ zRnP+qau0NSCq*|lbh*K4wuoQkOc3sjr_Na(WDrl0?@6H;ro5^>yRB|hI%>iH;5B;= zJdMS*s@ks0g|hm~X=f=i!iZ3`M~=v$w#Ib9AYg9@FI}$KNAHZGDF!*n0^lCpmZ+Kq zX@UThfk*xzIY7Q>BlNg^s#DN;1DEuwsb}s~Ywo5IceeMr2pV<$8-j(oM>U})2a?0Mow; z&_%<{c#^2S=L#B`_$C)2#|pKQuk0qpUBX$JNe-(SU1;-Qbf%Fr+Yi!{AzP$Kk31Nx zm17DZy{mJ1mxx3d|vFwwSks&+5dV~8?!duMl~^e3FJamzoPs8+Xq zI&3W|aPyPG+k$%<7)Ho0cq^sT7uRCrYtG5NKmDF?Sa^& zLmpJi>A7Gy;7Pu>2)sbV0i9f8w|iR_LRM;!QO=uSl6{&b?HuJ^93)t^e)aae@OL_a z4*^@*Bd6Bhy`D6kQCZ2agDVm-gvq`lWTGPONC1P@uY>A(L7E%jux^HK^JzizsJw)p+^!-92 zo~wR$z?AMk60Gg>c2`tBj_WHlh}E=;6-3~(sOtHQJ}dz_tS9;C*Wjy`;M%7bHlt!+ zVh+sXazF^dsG=2R*C$DvxGvE@gqTCI;J`+e+)<=mpO)W|T34ar2c(=wIb}_X=->mEId?C~m+er0Ss`XN9$Dwe}uv#PA!HxC7CGc} zfK^B-2uz6gGaj@(KYU?dCYa5FNQmh{jths|ySV;XMPMQ^J$V0OUx4^^)MswkW(+8g zBL7`)idjR;0ieQ(-3InMQq52d*ovY{y{Cc6JUyi6$j3N`1o07c^XvJp_pV?ivc4vZ zX)79?oI2Lmngw3v9NBf`ce6;8R+h^N0}315 z636q<1(o*Jn4h<9gAPcdK=r#ws#6~a@;UIL7yTm~ybGG8E~!2QHy>yTZ3o?q)nXXz zwma1b5YapRuOAd?<2ML>q%d=PC5g)4NX+eM>DIC^_9sYcDb2%3+Q7|PlrzwhB2}2; zSonnUxI+$j3*e0_>UfYgA7NM9=K-|oO(T3_vm|^?JIjU7Dg-b8umA5Q6Bn&@CB6LQ6Zh6>MZ0_`Vz$cCv4{=wG%gPl;0He()Z60l1s(YjbRjK2 zmXMpO|E=Hjywu$1sTZrNx6PLo;Y4zyIHxu>D%Q4QK|sov+nosf3~mX0TUDZZ)M@>jBpgcX+r zVAmNX-G+CU{w-w4U#18ge>hsQVw1opcX%9b3NxyEp>ic)Vh=g_OBMk@n4W@=&yoPA zx%3^|#6nxpJYOA$Uxrh02Ncm&Ykm z%hgkhI_IyySmrTWx2NR5Eu|pa!}-(}*8WP@)xJUfHMzwZZwp!BG*JV3q35pkEXy~l z_X(mKoc^JtiJCzKvtx_l|2B(wgKXmLhavE3TZLVI%>y*N$%T&dN;p9UTQ8Vgzv?;?@%!Y76+ zqovVM7Fv!o5P*fk%oi2w9XY|}!klcj&LSKDaUejjoh;$gwwNk;<>fWmFm81mCF87i zvh1h1+GXQ6tLP3rN`NLsi$b zbB6UJ5q>k^`jY{u#)jJ>_8)%UjiI037EE+(K{q6EM^q09C130y4a+RqMjAj^V6)Du zQt1&1PVI_ZSey(ML=LGo971zSm|NLfyIcz=T>;)!Nhq0~7xH3*@q?8_>RBcs)XY`| zui8tm8mNIxT3~Xk>48CvwA+SfV6L~g*&yVJ0D!1+BTiCI;Xs#wy@KY>7RE zYq32PDD4UvvOZ7^yn;-yMDn4M4-I%LSHqLF_BXpwq1%tx6%RU*h^|J<7|buTdbUXl zg8aSEQ4uJ;S$K4Rjtm1@$|FAEy0^(n_zJ@To%*WpL^wLE6K6*&LMpIpB+Zw0s5lIu z-SmW`V!UaXTkqvYUQS)E5gTB;v+C16`?3<3<(z7ql4_(ONgVoCoPFHTU#5k(pSc0^ z!>By%?YFdX!%U!wz6p#Vn~sNfvFC=;r$#+JCR}7LRF+Lz)`$%q7Po?|oQEcm;zm@m zu*d)ox3YgDF*uLJjO^gDpD V{OTsjFpATaW-Yv&Pf-5W!k!llGQ3RDJ7E%05CZf z<-Qkf4HE>o3L66A|y4dgwhhi`phuA8Bu})cQ zx$_KGO9`8w=8fGJ%K-t%eP+ZaCw{yIJ#Kb#GZ9ns<)J#W>%wgFP%QP&n?q%jeE3GPGH;^F*xP+;9%g^YTD8fra8P5T%^ z11Gw?ElB4;N+7q48WMei+fb@Zz9=#*1fQ3=hZUN3b-JhdI7wdM#^1Cz+4VWzOzdrllDQkKo~fIM;l^A#|wYl!Az ziP3|{>xzqq&71T8gwVCDstqYX1?MxfEz#ypYKv(kCH2uze;-!$5sgDqQ@t*_q@xdI zV^{EwAAK`X3)`Xw-rqeu-gjsM+8$veo@o}_w zw+ec~!?bd9cbyX?=;Kfho^v1_Kg^CN8I1TqZXv6?#^-&8$W_+PAoa4bbK_Mq5K zuFc@dq|Xg5+tV!gg@=nssV!g>Sm~}y6nt%ZMVwre4dTUGQgT({v(ajx4T>s`;KtSC z4g9c4=>9fXhv;dB7n;M;-;nIGEs(=OmFIEnP_Q59rdGL*j8xG|r{%g_t}{E|Yr0cg zQ8%hl_I?Ya4(0uE3<1O3akOR24y7khJ;!bEDX!}f1(`UnPx_9L^l(8E__EI9j@^Ex ze!powerkrN&U`59xeuAaeq?R`VVMoc3k{{o5=d4@2SN|)ZNoube=A5!$gh&X&!k@m z!xUw<%};^@ky9*4g>7*OLvUah=^#ry?#^1si&tXcsfXYS$Z(^hszMVHJ%k3_MMS)e zWZidk&FT(K8hMPZ&S3AZr+D%o+ zSP#II52t2Q{>y*2ln;rUlR%E%R%H>R6aoO*?lPEw4mxqbAcb3WtcF(a=>l~$Z5U0T zGibcZn)eGrbT7q^7;4H!g*7VvKhTCC^%xrd%i%ti8eP?pX3S{2--ZGUEq(755NS@xh8NK3 zx*)5uY9<&qYX-7j>eGG}iYme}IDD!pIB-zVq5%El)4_8hJVW%NW3(d@t6n9bp%N(g zd`N`mW{ooqLldYA3Xch^xMa4|XDgG#y%rB-0C5PR-nk4!kAEsuCK;fdu}h)s^ zdoo?qb0QqyiGw3u8t#IH>djon)cI*eR2WzisZN7_I6E_7TH;6I6AOYSue~@TAI*`> z9wU4n6O%SXt`6A!G;U>4smck3nXtwI8Qaq!DV6~d{B->Q&8ICUh4YLLL1*EEhY%ZN zHI4^aCKEw242)C=Nt5`h<)JkD&6`+Sik`PH%ctX27s5+pgbZlobmbc5y{2`E;V4v> ztk4F8-TM5mtPWTLAgt;A<>q?L7RzVG(dIj2Cu~&ebf>L140H$Z(D_Vi-0>XR)6&^6 zh9^l8L|jO(Y>@@a^BCGMSc^{0@P(z14%!I%#H~w2yWI+7Z9R(Jg*jT#;m|m7Sq#>x z05TR6)cIqh|L`h%1#wEAnLnX(Y;-4Fy1zsG0#}(-&2C1utIXPQsK}|5LR=IBOQ4y` zJI^jYsOc_axgLF{xR;6iHPJ9iZjBoM?D0?t0T#**JQ#b5!k{+>o-v`v#Y1MXLWk`= zzYn_zG&fpGqa+U5hITHApRT(=--v@oe#1EAyN0td=1ZN`%TujUt}fN_^ma8w`7=4t zYHqxtq+n5MIRlzXaXbGaX`B-5FPc(8F=d<|YtE&fy|JA2rh%a}r=491%0p9FxR?f* zDRv_`9ZMA4;9X0uFbGTVK`_j57p?~^7Tdza<66W@3uFY}7!uGNH?n!|$6^xjgsS4l z$yf)K!TQQ8fL4SFnnBi6-_Pr5(GgkU?Z+&^sFM0b6#5cg1?dpfPU5Co4M{Lxs2D(~ zp?%cp@}C$8hyckoXJZ3N^Tb2)F!v)A3)w}IhKjf^!-wj?vb|V^#k#Jm06FGZrQQIA z>7I)Qnn40l=q2K{fDY#h92^Y_BrDuuC9T0kS*F@d?l@{Of0%TE$@aECmY;C}))A34IP@VI#HBPHn;Ziw{< z)gk~Vq+(nEU;t+TRjHKmd`NIa9?fj#tPY1mQ6jR09(Ojf;{^EOo`v%v;)vR5=w}U+x--EBcpT+lZ zk)$MhfTT1Nf~W*eX^~V~ucTm%%6EyCsht@IKbIOF-i+`0D% zU5pDl*r0#x#?}Is6ne)LhwA znQ_VyO3`r5#8^Tp+nTVXNL50yQfcByG82*{m0nvV3rlOT-Mz)-VvO&7E4@SsVF^Y; zluFpD9HA6gT^mt%W`{9q#`bOu5B@h!%t}YRK_X{0h!cIDkf5Nq+l zo=N{0GHy@?_z#@V{QxRJ1OLXZ84Qey#K*Wn4KV6Qf&Sz_LO;}#^;%jFU2Eu_4O)GN zZbix&;!<_N$CfB^!cm$Pd`QSRc`Kz{i#&N?bs#k5{yuCbe30;d@z#Qme_$-f-?D&@ zR|6#)=O4B0_XzZ?Pns6shg6T?Av&I>xRwn7h=34NFJYpHpC5ks z;ZGh31pEX00|!zV-?eM+r7mj^4yCIPr_hK2MY0)BNS{HGJT}s_+aY5zmgAv5TgyXy z)z*E=yb}V*CYMgNz)@h@;Mo@*)&EyNiqifSe~T8HKBYV2@k10|&E?SCEHYv*1g*1}uG= zo5<_jsRhCY7dq_*AR7@l9AML#WvynJ$;v{g*Z>psgBS)u`~S_Cj-~I~wf8FQK@bO= zuKAvg^rP$j>*K zS1h}qjH-E;peb>Ib)|{RpUW-_*%u%nUx6%e%zhJ%u@C##Mq|j+w#%AQou*Fn6)<2x zU3(O@KfzMl-~8VI<14_+`lr2Y*MjC6uoQ62eimj89`dpd3Xnq9K>`o}&!Va!&(5$* zMgI;Oseqks`zw-nhL-AgWLLc=6tXg}Zl`b7K>Oee-n$w_ojQO)gc^sDBo7R(Y{6(a z)$CR6iUJS^EsU z6);}`0|)~EuImucDD|)XAHa1TK|>VR0OOul+}J0h7Y)^=tnH%=ap7egRQj z19AaUtl#AP&3@DM-bei*re#4>2%4h)wFhZm)294gyY@6%Ai$?Fnh(IcvTP3EjZp#! z5d7MI!IEHJNPE`6e%Pn1;3%#E0z$k?fpL8W_}A2QI{v7qS*S+YU&pX&vZ(+u3gaul zyQleU{{>5N4S-|zqs{>S^(*=f@M{kWm_ocufpHB`VV3@%K=H2$3y^ET0G&49*ZzL( z!H=5suTLkbOM!9eh%rDnY1$DawV=5M)Sjiy_q9LR_F&WHe(gaxWzXeeYm5WOp80(>F2;{1*0jY9nDHFlElvuAo(2mo3&t8i!YJ-(wWDzd zg<4!C$Fr_wfex5&*V%=im^6&;SX6fI9T__IOu_GP$P$vFPTVj)s|u2o4Gj}%Z{vvJ$JLhl zfg>(wDa5h7+J+Tv|SsvNI#J)D&WUrS``$(9goQ)x8T0(bvGz?ACx3 z_?qK!HxDoN+>4v6HVlL(D^NJ$m&nd(d+fj z1=BW84xEnb{&*5sb3#$sazfp3r72k==Lj&~9^|PG8I##U4yhYlau@}1AO{{WVFNk)=vGA9wU9l7KqopAiRd%RpJN1>ZE?dV^AqxG`{B~epO z`YmG{)oEUY%`?fZ5p!}s53!a*PdA=Xj7S9~_U{zX3d-A#rOcmagxf`bcSlwsm>ILG zB5%=v8FfQr;-i4Fqr1gKDO`BOT`?X?I-;edXgymZC+O}UaIo@wHTy};fJ*Ci3&BW<80<#WJmjCWhvd2i)a1FnWrYb)Q8v9NrhyJbW)x=r;#wYpJMlPt)rahe zl7QYY&b6|3*bOG3YmQZAYq)9>HQFBNKtZ>*vTv}}2fc|0H{gA(W>i1iRSL{9Gb$e? zB3dJ0$O%_(;#7tO=rKh@wtji{h~DrblBa<9i5)Pwasryx24Y)dy&atllzXBq+Z?vx z1z2J>1J*R-8;D{{^hacI9Y)7%TeCEY|2)(!cyhilWfmX(Fj1EcAt?lJCl)Sn~1j5iJ9Djt;iG(pS7_hqH%xP3`x2{4RpqZ++oI{$BdpasYbr#}kwC0+gaHwFH zwNO!r($1zV6!ZA4s2831>|+{KO4PEMZ6w4|r}6rIW-N+YCtTU)ZudB8mB@xqCgjv- zch5tnpJAvgFDl{LAcVXgWO3~{Kv|lgTOM7 zN}P!ZNe@~V0!s_!!2}GbSGH$eEl;f!q_3K>mc6$c-3??@phxesuFo@0f?=&G0Uqj= z^9mV>Jjy!T6&bMAiTtF(?23Lfb6ux~Y5RMRd2PAz7UX5y;P{1z!4j6r6S&OrK~xa` zqs@5ENlp>f4?!2?&W=R3omk$I9l4D2b|h@ULdlfsbvr2aX_pW0lNdyidX@HBf;fY< zG&#w`uKxm*VVnud8G)cBZF<8cNzUj{L&}s<&{S<|N^zqYS;GgT!*U5B+K9;6E$N(Z zKFG~TR{d_YyOA3RVYsaMJ@ji28cGKnlE%7h9LvWyc#Iam9>^><(+~xq{T2!C2>Xv5 zR9Q8@Voe4tta=xQt}*gvlw4EAL|++_Zj2#8vb2Pl;n0W6N^Kd>97}hJKNr( z6g>*`Q7~`QJtVf*6mm^kiO$kIR54ig%cA#HX8iS z2}wh0!SI_wXMyh_F9sArhfFHrykyiPn&a(C-%#n1y56^#A39^JH>Yy(FTPH!srTnG z(*YY8?~LA~@>sDSu<$$;tP#4_pfSIE9VWfxO%(79tc_Qjy(&l{EzWKODQOf)g9qan z#m)}#e!2f_>L4yF{R0LJ7%*VKfB}x|U8CYrsC?TRZdlx+ls#(yoiW4zNDgbYP?Q=s zg{SPv8>iG|HRs#WR?Nf)Z_x`YK8@hP(eq$6^JW^nu7m2wkt~VYOeHB1Y;f>D1FI+& zdZQIWFAuB|C0NU2c!0duI!-}}D{FPm0zU4CWyErrUB-{AK{XAQ=}0(pvI>PJx72}5 zisQrn^={-{6=uKM`KUjfTdluTv|okMPt*Y=8ol;pl|=|L>GXPRV%sJLX*k!>$RgA|KG@Yd(pvoYNPA~j(M=IHd2R)rk{ z_)UrFw2kgZmVM@Dz<)6FG4o=P*4dFk!4^41R0pw8n6{( z`^HaH`#MAxoGasEpc>ZoWS%J9p=ie5WSW=N(Yk{a!y7#~HAv5L#@z;(AXNm=FzTcO zd(-#iEv7#eJ`c9$aF#L0Sh!;!(AF?xS2wxTf7HvlheJA+YCNV_@cZf4{u7`mO2K^C zv+i>k#8C(mm9XEJ7iR-eUz&g#PZ(#o%ee5ZBc`O+5 z+usW`&7pyO+-uE&!!4)cDbx-E!EFjhL}-Rn{&rP@V87*oxM(G5wl!wCA0)HCJ8>RF z01`w|2bcA2e`iN_KGnBJ&#))-LZ)tOAnD6qLTe2;-LkI`XyH+M3601>a^`UQWFUwK zN1GZ&4YWMdUF&_<&AVsW==3yZ2=r-6!YEr}VdNf?$(d7wocs(X*E|%j6$oUDcOIuw zY$sS~Q`LAxhYOqWjt_>fEFjjJ>1y|YP)Lph(~zl508_(L4>Qjvg@C1Og|?GMdWsLy zP#_bn6W$y!g1X;2IQ_%IO$;Ls4|`2tg6;N{m7Z>WW^pTr>_8DlsUWaP*A!e&fpX!% z30Hhpw+*XBb`qgl8Rl`xN<7w-8lT<^I8#_!A69gKF`VN*gHQD_-1%s&{lk48n2Z`9 zF>VyCI~55xBnxsIq(&$Uq-mNnZR-K2z}REd&!?eZ_%2Wc!*It%rM~IQ_WDCt9GM<7 zb}czZkVbNIdS993>(qKA(ex>Z-lgURincL24og<6_I$3XO1uIfv*ct~dKZuPDXBiP zv1=$cZ0#>y=!(MGZVCsJyS7r;4Q2>Lmvw?)kqJfcJigOCuzAqW5f2m&F9 z8KjIjV*&@33Lla@@M9p-Y#axCM2kp|1vrEJgB2Ji%bvwl9gHPBQO6w@oBzO(G&%!$ zeB8Ew!21KRC)Aep7&fL1tJdN#Yl3Q*nq#IB~?}+eAv3dBAZ}iNFlJecxE>s zjo~F)c1zKoELdZR&I){pN@8}hB4&3baZ6jKYZf$)WzCN>Izbw4^cFPK39S0mvXL4`VCgQLI3elnGSKyw&AgTuGsjgrq4gyx-EJVy?hg z0HdwHX2c5VQ0{mjj&-oLHx7`%K2V8V*5@4dPgEF5EGr0 zLyuZar-@dzIYJqO*!^9pdj!U8ecfQ}#;By?#@-OD0Z!(|ejW3WAWIO>uRHJ+vc)oB7<4UPv#vs`R973V>k^>Bm{qNzf3DS4GxZA zw0jf5FodinB(c(Nbt{mcrxgeiF+elWQet2r9-rK;t%j-agTrH1U`t*q$SsZz=cLO> zb~Hq81y)H4(vFY4`%oTLXKKGUn~f@Ggsk#Y<>Mv z0e0!|AD3Rcgs2;OR^SPs0qX9=jDW1Sh((GB|4xzn+98%!QkfMf$2d^}$%It#EI?1F zSb>-M(0Cyl+k*2TS}zP-{6v(VGMJjFm3#3neRwgUI)st`9By$HL34F0srYDGxB!JmgHK}N z5^@?$;rImaVP{F1#6CBalQfc}+oi&?GBpJuJu~Jy0wl0-0_DjhgXHXaOe_^%v|a!* zU;|l~FcSzR!hqI?FTjoWM@G?Av>0O1%6nfq1XlHDjsKw4SVl4>mTf~t4F9zeI&*92 zf;+UYmSpO9O=uCpdfj1#JtzBc8_(bz5-aGT4Zo%mW7m9OE2isj+5J%Sz zFE0UKDPyePSl3JNf}SkO&G41Pzu(ly4u;<5IAWApeYeLeqOmEyNz)dy%wyU@u7;BNrA9 V_K#T|b$1TPYtm0_JY5Ege?0vldr1HQ literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~WKkiYg8_oHDh598ew703dOzzZ_4A6eulQMENk8BgrFaapg46zM8SZttdNLvVegsywqlpkgJumXfYXKzEHmysgQ== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~WKkiYg8_oHDh598ew703dOzzZ_4A6eulQMENk8BgrFaapg46zM8SZttdNLvVegsywqlpkgJumXfYXKzEHmysgQ== new file mode 100644 index 0000000000000000000000000000000000000000..4ca82e63eca9340939eea864aec7a88172654273 GIT binary patch literal 284 zcmV+%0ptECwJ-euNSz1(rqMVgFsW7mK$Jg^DT3lrp0PDFv*@NK#*}-I=K>QH3h`n_ zL_|OZFaRz9F97xJz+#NGYqU9HsCH+F5+0)l6=2K11A&I2RjYwENvclsA30$=#x9tM zgjTJ|GYR}&xcN`Z!`O&(J{>Fn(U5GjSdfsmLk}0hEQUBJ)FvVnXw_nmQJs!e$O*gA z%QnB4X_yEFTD6=B!UNJUiv)x+M?*Rv;;OJBP@Sf-HlU i%k&~6d611w1i3&pcsKHhu%fzP7!XWVoCUsVWQq(67=bka literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~ZVqvguHwOF9yshYRufXAqlRV-kDpOpOmK9YYfF9Kgugul4jDHhD1Tjp1SSVKRRdS3UjRduKDSc4L7ZAXIhB_Zw== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~ZVqvguHwOF9yshYRufXAqlRV-kDpOpOmK9YYfF9Kgugul4jDHhD1Tjp1SSVKRRdS3UjRduKDSc4L7ZAXIhB_Zw== new file mode 100644 index 0000000000000000000000000000000000000000..b94526be073ef9d95c5a13ca1ff0ce814ba2d41e GIT binary patch literal 273 zcmV+s0q*`NwJ-euNJR($RuDBJAZ_)7DATku5A|uh7`M&;`M(fFs0c(BTr=O#0Z0Hy za1bm2D*!D3&bf!L-=)QR$PkhOQ__nK@h49P0tt_Hu}42CYkOyB()e((ishNnE(SSJ z&Q7!UtDdUP4H>p+9A@lq&2g25IR$CfGLIwL#T-*or3Bkd8vk{jc7O$Ic^uI$)}VvN zLSkVq0KoYH{zIS%O&O1`WP#7F&kg6;Ps-Z3D-|E(oj>sp&bc#$wXMSIzi;pSt1R0v zEZVpDksJ<%wZ&i;b9nt zdjs#eH-y!#!t3+9WlB#hH{tKiARSEzakNBHd`uG}$iDY)vQ|?)%bIf-4FzSj7XaW6 z&k0?{XQ>;M<#ddLg^#ity!Rvi-VAN0OubaSDO^~#G{{Kb(T5T{LjO{ zz(5!*04o410ImHwenSlPw+s-%M##YhcxfR9i9sepMo-qhy?i#FI1*kjZqRx5e~_tYPF5flmsduNqEN-qKnl6s&PmjRm*uC zG!hQYY5@Q&r)_M+@k-=CbeD|xcM;2i$JyKXWZ8Sa^m_xXb?Hyn;It-4?s5KL8^1-@xyiVU$TcIf~B literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~b0zmz8Z3z_a9OMd5JVwTRJti00glveHwMcE0P6OP0W3NUGoHxOUnANszYx-cPrrXztOe4nJC7LMLm82ktgcsNg== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~b0zmz8Z3z_a9OMd5JVwTRJti00glveHwMcE0P6OP0W3NUGoHxOUnANszYx-cPrrXztOe4nJC7LMLm82ktgcsNg== new file mode 100644 index 0000000000000000000000000000000000000000..d4c24ec2d238b823a55c193f7ec4b491d17cac97 GIT binary patch literal 249 zcmV$#NLbvc^0H3j}2~ z7XU!{e-xbrpK9~s4Q}3iq~F|7Zu+WEwVA?%T^3}dXDG)_R&&XLV0&;F(6Z)uis~F# zgq-n@R%r#AbMElbccfs4%n%*$6uqbrFIg&Bl!lhI2Kp$fGH-6=jE9O%EJs4i+S5D% z5&&Ky|7ti4h7`nv;O3_?T}X@aS@`s5I709W)QES(jR@1K3x)x~RK;1~n?|O{>Fje^ literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~bdBtpJ01upXwWjbXJbpA_YzGatXNhCHdjiOWj5bI8z8-TYOEOkwAhtCLRgKTcIhh9Fwwng91U3WqoWG8tQ-ztg== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~bdBtpJ01upXwWjbXJbpA_YzGatXNhCHdjiOWj5bI8z8-TYOEOkwAhtCLRgKTcIhh9Fwwng91U3WqoWG8tQ-ztg== new file mode 100644 index 0000000000000000000000000000000000000000..c23ddbad404ae04e8314b4506f62261b3aa5dbc0 GIT binary patch literal 279 zcmV+y0qFiHwJ-euNOcGRRuMWRAj{?f-)}aLV?jU#pQF;xb1n6pDX-L;8Q-wxp7DJDL*c@JrbAp zd6Wnxkanf4c??yX#)gGJO)m$k%&H$-VbX^)7)a8(&^cKRjp0>k*Mqf)CfP{)JZN!A zKTuOes+aG*hea^SM%w2=D4pQ~+AjHh#)a5})ywzBI!0S2$oH-|I@43E>(~nrw&>tm z1i8Z#HAm5)h2vTR;j}0Kz;Ue6U?>=gbr7{6om$5pgzotvm8Lw^W$XnE08u+|Hr#<` d?80Wv%)7wsni|D3rUWtX{y^EKsADh+dVV>VF5t9Mw4hKRsti4zUL$p8!h z44en=b#si>Ez|Yz?#j-27Cg3oubW2%k2EV*w6Ajcqp^jxepdU>FcgRh$LBX=I8FAq{58 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~cZYsgQS58hzrxlF944opJloaCG4whcSszMSGSHg7Yl22WHXvnmr9CbyY3iL1A-yurqaaaZX_WNZ2v475fwF6bQ== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~cZYsgQS58hzrxlF944opJloaCG4whcSszMSGSHg7Yl22WHXvnmr9CbyY3iL1A-yurqaaaZX_WNZ2v475fwF6bQ== new file mode 100644 index 0000000000000000000000000000000000000000..6e02cda0181a2ab8d52fbf2145604bd4ac31c943 GIT binary patch literal 528 zcmV+r0`L7OwJ-euNIeh$_T5<|AdTn&Ta;<~(i3;c(#=6MKJ4aeh9p0+u@bM~8k;ax z2ta6fPykQ>PXND-h=sDvy$U7!opm(JWte-AcYe09cY)oIZTu;oYNg>0xcA;i*zXXe zedGv8fhPo|B%k8wpkX*=o4Yiers}-&Ls9t=vH~nZQMNhBBMHwAaGlVyjeBoWO0bRJ zhkMQwq#zzHe3bpZ&14A9ZQPrb5-eVl63#L#Vxeqvj)W(ez}4l6SW0rj%5a&pO52)y z3-A0agFR2MVYLJ9aqW#KRH?x_C@LRzjrQT*Yl_7Yjmw+eVJVh1=P7OY$Pu)wWzA8n z2}d-pNwY%;X-&M@9o9s#O>-nKTE-|UzmgrD4JsAlXdTQwt`kPyIq$i1jMZ5IuMgjO z=U;{`><5aVsmk zU{XchyR)l@h~E`%)ylK61t|qU&cW`@CKOv69FNT&7uQ0-cD&wW0QLgAF#mfHD)>P6 z=xv7Y$fY>hvFPA-2XNmF6C5Z`kaBb<0zel%t1Onwbt7fEwttTMI9%dW(O7>jO*5wo Sm&j5(n`lC);=+B?$R-&)%Ktn7 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~eTJSePoZS2IR5VW14fcQCV9omYTQ4czehSzIptFlRN0DJFZ8b9wd6QImnglUSL1By02e77T378yucTwmDqowtg== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~eTJSePoZS2IR5VW14fcQCV9omYTQ4czehSzIptFlRN0DJFZ8b9wd6QImnglUSL1By02e77T378yucTwmDqowtg== new file mode 100644 index 0000000000000000000000000000000000000000..5224d94a059c825b038d00981541570bed868ad4 GIT binary patch literal 271 zcmV+q0r37PwJ-euNHqum29Pu(Fo{?ILzF(JiTyU(H#Um|>$tHc$|H4sP6Jd4?SWxn zU{ES604e}00PWAwceGf4ybvL9j2u*emn;<^4h`*M4b)9jb>95Q3I7(WP!0+0Vvq+C z+|)@PYcqY1Rf&j%^bTdXjAD&Z5NjL91lq+K$EZ%mCgg;7v`Q<$S_{fCfp#$mV$eu9 zDAoc1A`(jTF(a}x51QfO1pY1JoX=xzZV)`){H5Rg(5-dzd(~M**SkA!{xWJ+!;_QS z^qZfBrTG{XjuMGerumj--~1g=0TKXCAfF@5VKJlt857+6RHh5*Q9cu&5-N@dI0!Yi V-EcU1kddCD95-3bCC9=x;XFXgn&T;| zlVAnpjDNIBJJ6hShmXFa1v_MiD1oQwMTU6E(!ruMw5&zYM^Tk|b0cRwT&!X_5?a=t z<_Qu2ULpT#I1Gjq#Dw7Hr!rkgi}G3c^k_Ij@Cww3cf*Yc)2a)G0l`$oS>T&SrpN$J Ba;N|R literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~gJOHZ3pjCV3fYGEq0RtzBNHPExcxieFaFQfQ0V1AuQpeNrigUOaTw8vthD7__wT069-Y3vjMa39agMpb3oflIQ== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~gJOHZ3pjCV3fYGEq0RtzBNHPExcxieFaFQfQ0V1AuQpeNrigUOaTw8vthD7__wT069-Y3vjMa39agMpb3oflIQ== new file mode 100644 index 0000000000000000000000000000000000000000..a99654ebeaac2549a9799bda0347f1c676edc550 GIT binary patch literal 1514 zcmV3w1M2di9<-yjx4Kl>a7HuLwuf3Pb4HPmq9MOxM@1@d$ zhKvnITMt&-IRGL_k>rq)Bt=rbmIJJPwHat%Ovxd8Il88vl=6DRNCN%dru_q-PmUiPEB7x1E8-Eyu^>nCv+dpVYC@xb3Uclo?#!D=yXEx75b z@pr@fE5=ZB7kfDp6PNcs-%DNBihXpjEi9}LcA7Q2i@jVu$bE|)*Hq(?=(i{FV7pdu z;%~87LCkV=O*L&7po`){%MVa+K&uXSsd+ge$B-E28mb=^P;hMLebEYlXezMF{+m4NlZ&gZqO zx^A4a^yB#DSo`7KxTassdnwm6YeiRqGKUo32$k{yr-#%*OF;_PG~W*^)hnSn#TZg3 z$Maf1hRm6_7Zt?paly8?i}uQLy*cR5Yme2zTJZ98-6f>Vme4aSbGlqr(D#ZHKPE&ILCNF=W}jC_$LlZi4#lJ`;)%0!|_3QVR*GC4p@CX@+fvaMCV z_Zi9S4P(-jNyHRIOcydVE@Wt2$k1q~9!Mk=C=fxC_nr(BCTuveVqhabD_;bEi>=G7 z`@Gg^%d3T8v7lOv^(@v?41t-phf&L^zpNIUbio*P#fPJ#7@p260{b zRQDJu11$ziP|szOvvZ4Y=Gh(OEUUAJ2!&*$$sJ_`Re{T#I>%yEi*4NWTY~9qY8=Y~ z@zwh!(Z7%tKm|(d>yqwoq|t2<7A}k~f>QCIgXzxpe&#+xC&Jkd;7cfUJ%&WX zpIT5|u!1R*qFFEm7*paQI!xq)A>$h?0!e-ogKbg{$k^ryBZxf7b6tfZigWxS8K4&> zOosxA;7p4v(r>ZS7vt*=j=6Y%j1~D;p-8T_l#&4H23s?yWcYa2 zc#g}C$)*@f!=z&l-^WNVX;K2?=kH2!H6!DeAiFY%0C~9(<~d)h9pD&Z*CT;37`Uwz ziUq_#u?3|r0D|9wE^cdrSLkN{A@}mmp>EOhQAVI^G*6DV2Taq`I|IQ4pZeZO7$T6e zy-E>$`5?kzDYqM1i@iw|kx%~HxK&vG#m6B9m5*>ZCRPc{se|DNP_mmKHLlCfX7oOMvu!d2E)Q9){@AK zfJZdWqPdnk*qV7+Rf#vl*0ehc-l@ZS5;oYl*z*(?iabFPmu5hD6`zj^{v0}4z5hC}E z*4$}ZVDPta-T&sR&2(6v(VtoOc4D0ic4pgj)A68UEs){xOF2ZqJU-v9sr literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~igQHq6VJZvaKCV-KEsjhondmWkHdA41wGB9QsqnRrbQq-vIqyHNuquiq23Gl2qMnw27936cz3UV0x_tFMUPGIQ== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~igQHq6VJZvaKCV-KEsjhondmWkHdA41wGB9QsqnRrbQq-vIqyHNuquiq23Gl2qMnw27936cz3UV0x_tFMUPGIQ== new file mode 100644 index 0000000000000000000000000000000000000000..f151346324da94cf8f86f55e72165b46185d3120 GIT binary patch literal 119 zcmV--0Equ6wJ-euNOc1Mf(95JP;DDiw&)8iZ4OpWgd~gu5M9#&L%Cw5PG)984U9)$ zxG@tJ6a2Y9!(z3Vx-wxLTo*Yi_-M~EC^QKu%0?BCn1*U<%77-#elLQzS-C&2mmP{^ Z4br^_bZ{ZXP_hUE09u)YhO7f^*bEIGEmi;k literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~kcjuAwJ8Po_bj9OBqwICyLkxCSGRnAZyYfTWIFWnfvPMpqrTsAjcvUVZ3QNAh7qG5mLqEDzdEC8bhhfgB1o_Pw== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~kcjuAwJ8Po_bj9OBqwICyLkxCSGRnAZyYfTWIFWnfvPMpqrTsAjcvUVZ3QNAh7qG5mLqEDzdEC8bhhfgB1o_Pw== new file mode 100644 index 0000000000000000000000000000000000000000..a75109fc411ce4d5a974c9ee0c960cd00d4b28ee GIT binary patch literal 281 zcmV+!0p|WFwJ-euNQDRhHW4@^Ad6T4n3g|jiA}?EZ)}!4f-uNwMXm$_j|IN5M0x=b z1Q6sVF90n7F918hT5J6rZI&9V-ybA}N6A43*z(hXz=ES)EP^_Us!#JFIbpxXGMI>r zcCiUG3EX_BV@m#s1Xz4|PJ zYrB4$->cZwoN{uTg_{r2Gj&zR%BN4Ve44M4c&sXypymq@08b!EwV7jMNQsg`%Vm05 fk-X%^CW2g`8oV2ML|9Q>FboK$D$WAmG%`g7<-UFm literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~lWKiC7Wr3Ea3Uv-HGWXPaIAQKg-6TEh0mcOKpF5NiYyCkgG18J74lrqWQrBabo86SXwhFqTCvdOeu_yH0983vg== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~lWKiC7Wr3Ea3Uv-HGWXPaIAQKg-6TEh0mcOKpF5NiYyCkgG18J74lrqWQrBabo86SXwhFqTCvdOeu_yH0983vg== new file mode 100644 index 0000000000000000000000000000000000000000..f862038602bb0fcf91c0a27ac2546ae2c3f7108f GIT binary patch literal 8525 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 Hz8!i4xPMo^ literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~lWido2-3HqlEXDFEk3SvPEA9k1qGBMebdvuHOLWa26PjudPFJp1VHjE7qKrIBwL_X6ol2lcDNhIIzL4Fxa8mNw== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~lWido2-3HqlEXDFEk3SvPEA9k1qGBMebdvuHOLWa26PjudPFJp1VHjE7qKrIBwL_X6ol2lcDNhIIzL4Fxa8mNw== new file mode 100644 index 0000000000000000000000000000000000000000..c07b2c8a23de10b5187330a20d4244653d758e88 GIT binary patch literal 11810 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 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~lp9CRqSSO7jY4ZQEtkK0z3E4B4PVDqabsbdsZIiiamLndYO2a1gXlMlMSnn1ecqy0tYw_TSA_eNjgzFgGzuC4Q== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~lp9CRqSSO7jY4ZQEtkK0z3E4B4PVDqabsbdsZIiiamLndYO2a1gXlMlMSnn1ecqy0tYw_TSA_eNjgzFgGzuC4Q== new file mode 100644 index 0000000000000000000000000000000000000000..1ae527b041ce168f9d8359e53b7b97c89fb2da27 GIT binary patch literal 260 zcmV+f0sH}(Bh1YCKc(pM6qGtV## z00hptGlZ2{h1Y+-R#|v@a~FT-CSi%f2f~t~9x_pwB>T?4i?y5zTGp7y03qS9nhOAM zmXUy^D&b?G`J1ar8|-+^6>apJlzHb*{GFR-??=6?&J7h-NjPTgXPV3N>SmfLz58_%YDB`MpeM!=rp9J{?jV4{#7_Y`fubglW|U!+>C_ K;wOi$taolqKM5(tCSbJYv_v)W9R{`n%~R_g;elz zhwv@3mN2dK)tPwEfl$Iw`qm_>9F)Gv8M)F|UsK(=IDUH5IR*>>TFW4^#Elq}BW@Ca Z7Y8xCQ(}y85A(ePf$$1vG2Z+{(F6J;c z3jp||=p=ZpUK?*d(r<1kH+|Ko+Du`>GRuOD^o?@dL}4yD5^E7o1GI}do}xMjRv~Bn zqg7glIp+=^eMgLS$P7_~r|3n6c*#;hVrgg>d!Ub^YI}1dXFO!=LUAOti#^Q~6989{ xteFWwz#ERuM{>e{iA6Ayq?R>D z7lFzan9&t^==SMyEqCG(uZinU6|&Ugl!{5sAbJ{tm;(kfSmA|R%@M4QTDxm zoraMj)UpOS7>$I6X)XZ3Bjm;7@d$bGP(JVdiobV6+k%lv5%^gV&bjw{6mPasLWnImOL&CF0>2d-SEmmJCK h(U<_{vo(nZ89p`5HZqQ|qPk!h5KL8^1-@xyiVS`HhAIF6 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~o8N2yY8BEOp6gioaL4IM3QveVzP4qqt8dLzOpThD1Atz0c6ZEVYC2yRUpsV3bzZxV9OZaROWKabI2u8ktFPdgQ== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~o8N2yY8BEOp6gioaL4IM3QveVzP4qqt8dLzOpThD1Atz0c6ZEVYC2yRUpsV3bzZxV9OZaROWKabI2u8ktFPdgQ== new file mode 100644 index 0000000000000000000000000000000000000000..b71898b44b570e4c75cfdd051665b9f6efa4b4f1 GIT binary patch literal 249 zcmVPbTB99c&bh-!-_T+mas!0G67=FiyyU1LvGlWxJ literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~oRMi0sbQLktfDDDGUS-SqBKpdDiD6HCztVES_ShfGr0KVcYENoIq7y9oUSDWL5B_P2gkdtWG8JEsJLxPbdWOZA== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~oRMi0sbQLktfDDDGUS-SqBKpdDiD6HCztVES_ShfGr0KVcYENoIq7y9oUSDWL5B_P2gkdtWG8JEsJLxPbdWOZA== new file mode 100644 index 0000000000000000000000000000000000000000..fdd67e34dc951ab44630a0505b3111970fe21488 GIT binary patch literal 199 zcmV;&0670BwJ-euNc9B(VvQ^!FpKB`I~+T%l}4r)U#UzrZ~8ClfTu(Vp0@eNBt=Gm z1eeNv^*EQ6d&i$;wY)gmkp9T|pxo4v0U*+Opt5gK5@-Z@1%|0=o%Yiv}~` zXx3Mcb1|9(xG;DKR!+eH5(4O82}tCRbfR8NX$wn4(kpF-6`=+N2Lb@#A|OQ~8z~O{ BT#Nt! literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~p6tTbJKE-fL9u16ulx83X9uqUEKDl_tkOfoDsu9ZSI5Gzowle_QZx78T-KVIHsqxzINi_UorLurHXxgKRVXCdw== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~p6tTbJKE-fL9u16ulx83X9uqUEKDl_tkOfoDsu9ZSI5Gzowle_QZx78T-KVIHsqxzINi_UorLurHXxgKRVXCdw== new file mode 100644 index 0000000000000000000000000000000000000000..4382f6cbbbbfea216b14f1509370a39693a3b3fb GIT binary patch literal 538 zcmV+#0_FWEwJ-euNR1ExCf-^lAg$&B8OJY`M(qhV0`%nW~q5`)JO&hp*oy z2z$s6QUXiqMN9n2lfkp_%CeT}M^P1e=V;RS5V8VGGAqm4B$@=y(LT7!huNn&Cn+6R z#;>C}XX;Ide+my{zwaYidUBuUB&7q2w?qhNoh89kmNjU?)I;DZ6HOc~QNFs6k+NEr zvE~%sIoic~re42lAI)(Muj{mq<{aaR!p56D(j?Y_o>GQ~{6M?f#h^s9_p4r3=Y|mt zg(nId%Fp*6BlziTDBkRmhLTtY1Dd#L*P1kb6&pGiT&l#=Hdb?7<&%8Qxif?nS^=;B z9(w0rXRXti%kX!O8lEVOP_V43r=TcontkVA$6_5YL>8;LfCXs&X3V4ne0j|nO7PC# z_&Y}p>o^?6!&pj{7HnhE_yIj2K!T%GsRsu`{xl;EP?F4MD}k#|rCk`JJi1!stjK`r zxg~jBEZAZw@JJYRf&37;t-_J3yacxB#sF?PuzMvCiX2U7-vEX^?I3rMdyt$tXiQ8( z0M0WnJ%}FmfO|k|MzqL1c5-3W*)3hbMK{bj(4Qd9(H#VUPV}r$EEnrW+H`I79P@EF c#HXOK-dq}HPAps^rFLy12%&}x_Dv&GWM~xv)Bpeg literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~q98SQTn3_Q0Xe5mzVf2fdiyct22wbH8Z5MjjSTdbiQfEvEa3TmVyiHOa9-5kzTES8di5UgFjO6Xx57HtC3oSFw== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~q98SQTn3_Q0Xe5mzVf2fdiyct22wbH8Z5MjjSTdbiQfEvEa3TmVyiHOa9-5kzTES8di5UgFjO6Xx57HtC3oSFw== new file mode 100644 index 0000000000000000000000000000000000000000..1982af4dbe11aebce0a2f7be5d93b8c244275aa8 GIT binary patch literal 248 zcmV?mq7?`@)le+yM8PK0!^r+ESr y09_&9syzyZ)Wg`|=BF}U2#fMn__Sm=LhuUIh4gz!jNkFbHX|WAwhY4n zZtuBwjMw3V--qwK_j9tiQJR937k_Vu^i-q5(X*=HLDh&L{NB&YT2CWl)*OgHaiFaB z01ON&ry4skd#VNg-fq_FeqL{dz~a3h@%MJN-NNeU^|o=r4a1<)zGGW*vf4`yhVk+= zB4+LNlx|IML#upf@5BM^z4sB?cL~8hc7kNkQ-TsfKJ96MQ9NSS7A&Nw>%6z4RenV9 zVLdcr)*uH35&%gczYx!e7*aRp0XILD=|XUnFT|(6#1R5lphmnKZbXlH{tKyAT3P@ar9JGd{7f2$iDY)vX;|;mNoY*6c&`#UI2hQ zJSX(mX?ny^meVZ`7$0Rdc<)F2y&Kw2x%#PkQ^;7hG{{Kb(TLgeJIpZ^}(hjuu-b1GE5yKw#Lxj*%#E~JsY^h*T99q^QC{t8*@7>55A2O?0j)a!A zr+ESs07amRnGAS!M#GT07zf<^RHh4|QN9kJ-VjF!T!9+#ZnzO)T6MuNAegE+3w+ba F6dBmvc(4Ef literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~tiADeO_TwvaQPD1doixRRMC_PNAOdsqUnhJ1Dg2fRw7Y7vFfpWvnFUwJ0kFT4H_Uo3ZSvWKZl6R38hCwVrCKNQ== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~tiADeO_TwvaQPD1doixRRMC_PNAOdsqUnhJ1Dg2fRw7Y7vFfpWvnFUwJ0kFT4H_Uo3ZSvWKZl6R38hCwVrCKNQ== new file mode 100644 index 0000000000000000000000000000000000000000..69ebbed20554195ef8345ee4866e7f49b974d3bb GIT binary patch literal 426 zcmV;b0agAewJ-euNDU1DHd;?3AdlOdASs1J(Q*-DM&fb}r|ee}xTkGvp*50`M-_k& zfY1a*07d{r08T!dTQ(F^$U8r|lwP(Ql8yi4DN=fqfT`+7+wTx$eZ&*c0y}8ir2S(F zp>en*7rXRMQ*_?>d9Zy1NdXmkl3Xm}i3q<8AGLnZ#xb-m8>SAzg2hi7g_8{xu}E?; zCxWL{;L7EZxJ{bd*l=+!PI4`#4DbA;Lp@L1TS)?@9Q%`rSR}caQeNv1&E8vi7o9tT zNaoR4G>aV+;XytgY`-0)9AK(aD)nIdO6$n^fD$2=@}Zb=bQ^i+CkZ&ZwPeFo9fSqR zd=4;mp7u!us_q;`(MbW1zwf;BXG0~!ykz{H9|lV>s#vhARCY1Jh{4Er{#;8XI6OEM z(|`nM{$|UA229yz3pD6`(0S))_&YxVBLHVZ{z@=M8%gGoF~QAGWx6on<)O2Xn;{02 zz3o)mRU?G?h6` U;SyhJ7fb|%P!$(|ZyK2*18We#7ytkO literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~uMikE6IBG3uS0y7s1OeC8H76OArcUEH7JKL6Unbdhak2hV1bIWI3Gv4GF3AJsI8r-sXKJzTg9AMoKIT82EYWVQ== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~uMikE6IBG3uS0y7s1OeC8H76OArcUEH7JKL6Unbdhak2hV1bIWI3Gv4GF3AJsI8r-sXKJzTg9AMoKIT82EYWVQ== new file mode 100644 index 0000000000000000000000000000000000000000..8c625992050ff7fd0057e54cda5c2f113fb8cba1 GIT binary patch literal 277 zcmV+w0qXuJwJ-euNM#5BMo={*Ad5Nx0G+(jGLLpNr;rtx(Ux3hQeh%+RRAi@y$b*m z0FnU#EC4G2D*(f%7gn79Hk$S7Mz-E$0vTi{<2>O&g~k)=_q68-<_|oOruQU z>RFoDub;(IgcHTobv>nua9Q;0D}!hP698Ete^Q(yWJqmgPH^*6nJxtJ^2PCV9bySV bIRhMo8ryC-9AR2@!7w10syGXL)5sJVi9K^r literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~vPhP9g3ABO9jTk2zjlB6pxhUvqAeEl9SHalrBJTTjGPwFtFs1lwq1Pr0YtAovl57f7s2qhUlqyoeWtR2-6f1rQ== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~vPhP9g3ABO9jTk2zjlB6pxhUvqAeEl9SHalrBJTTjGPwFtFs1lwq1Pr0YtAovl57f7s2qhUlqyoeWtR2-6f1rQ== new file mode 100644 index 0000000000000000000000000000000000000000..afe2795aa2ff932dd01ee3f306ea1e8c97468db6 GIT binary patch literal 912 zcmV;B18@8&wJ-euNIe_?7HyYA5OYLOwo;&!0;RxHR#s#w@L4F*N9Bd$Ur8pJywN-9 z)OA9>KcNEc=Rb}F%zQa~5z@V}Q+5ABy`{E@yJm0OSR?iarvP~Xb^tG}b4|e@%C5d6 z#9Gil#VlG$^NuuVfEy~c0AVa6^vYfVYO}!%mIw-@jTam%7aWxEq2Uo?07+#d5(I_j z1m|S~^FkvDfSDK;DLqnL7UUp7!a*Ssp)qT@ae|V8F|pARfJK|3P;p_>3F)NJwAj3? zXb}RWgg}c-MM)2m1RVk~Mqnc$mmM+=Xka)P0B9>QngSUPjLr`c1S>UYXeK9GrqeXd zK&_S>dz6DN*`uPE|N8od;lIAVQ5cyfI9qp6hX4Bdw)#loSAUdTt7>+AQ~%f3HwGRN zg80z`ebV;H4}+lkudi>C((a{oE^#Lq5}m%ModCVE*DqR0@uO!1|JT>I%3t|4DaWc< z`9--{?N>%o&N)>rHNnbWqbnU;u};PwrSQ((J9i#r$7jzwcOL%h>szeGR?C_$?7zOg z{Yuq7L6i1dRM+?RaJ91IN5p@9ee;*rx#Y>V54e-;zrMb)>8UTg?Apk(AgasjpcLWI^6&es{AR>)V#9m`z zR`5-<=U`Qe?s$r;*ku5Eds$LvQ^jzNcaC}DfJ?;J%41!{tb3TY-95R;xL?taJvSV5 zL%XEXi!`O{Xu)Wh(W!ng(B0OspcN);$~Y^X9h}el31TGx#Md2Ww|lt8+>H@0g)ELC zEf9JSq8K!M@GW708sQ=zoFcmp36;>`HAK(@SVaLF^qW%+xtV(8fqGT><~jtEc%>dh z;w^{cR-j>d(IEhfZPN^ matujxO$|*HimfozjwBSzl(3}2LIQV2aBvDMG+WQ~r$-8tm$d)@ literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~wnmRn7u3waaR4x6xGSb1Q7pofNvHx2E01LTjVeJdiQ6OTZt5ceYbMsDNqik2bpreDAJi64yFUwoKPEwKX65VvQ== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~wnmRn7u3waaR4x6xGSb1Q7pofNvHx2E01LTjVeJdiQ6OTZt5ceYbMsDNqik2bpreDAJi64yFUwoKPEwKX65VvQ== new file mode 100644 index 0000000000000000000000000000000000000000..0b008523b62f51df72f9f948788b2770fecf3663 GIT binary patch literal 250 zcmV7>|gE zNa47zhZZ zIe^1oMJK`Ow3xw-NBNopZ$8p*b|^P})~VV|VZt&E8R;9!aTBGvk3e<>q!;J{jstbkz!BoXr;G0IK$o2tn A0ssI2 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~x2zk2zegpEaxBxmbAZkuPV4uV05PiAYBwNp8HOV_9GNgNX2HpZ9Dy8x55P9yOFJTTEb6WqYfH-SelneFPa_hBA== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/data.0~x2zk2zegpEaxBxmbAZkuPV4uV05PiAYBwNp8HOV_9GNgNX2HpZ9Dy8x55P9yOFJTTEb6WqYfH-SelneFPa_hBA== new file mode 100644 index 0000000000000000000000000000000000000000..86f73ec91577b116be2d1b16b39a4973fc19c0cb GIT binary patch literal 271 zcmV+q0r37PwJ-euNHqum7LYU}Fo{?I1Cu|eiTyU3H#Um|>ymRdmM4cFivU%~NnjWl z7?cVu04o410Il_Nv^iR+epiSN9wP@6V9QGchyz2bRs(g@RG;QEIbpxWDwu|ZR;>qn z1dN`ki*&4f`W&kg5D95N^l%quwIxBQZ5j*Es?{E&I-O0(3ER;st-xvli((oJ(5m%3 z3K$3nX0-qSh=kI7cjBdKz!AE>(|i`^WF0G?G2m!^E8P68G5@{#tb%L1ewyFAFsqtR zPHw}^XUe7d7?Q_G#3j>win7h`4H5uPAU`C`aWSL{857+6RHh5@QN9zO7AuYiI0!Yi V-EcUw_UGtZTC6{2kPtXZ4l2M)o(d37N4waBI*O{#n;|*j-(nTbLq@w; z1d0UECBw*wb3%``*};IU0c#<)F&%l+R=4uBABIX%Yat9FGc;Cur&-sX6kByN&#!I@%nvcUi!9^$ Z&ESBlTeXISa6Yoj9y#Z3;R6}_+`x|f#6vq8z7!s|wa5nou=;Hla zHF8@+rr~(RN}^q{Jwo33Kfv^(;FDh##~~78_oY(auZ%^sK>%CtD@|QyQM(qENdd?u z<~YTjS>Xeh#=aSSjkk!5CnZMheDse+c$pxh(j33Xf8j;Rl~pfCH0ePIz+d+T52Qf= z{1f~@+kd`0RVAP5Nk8=$gJWW&o+|qNcWMbjJY>{_P*YCt5n@{uJ3Mj+lO5`_KN;af zl`n5(a!(9@9*H>|K>%&gZKRzwx|Q%+HrFM(K>duyYSqnzA>EE6mXDUm^FKaKgM>Ov z0EW(F#ouhBJ9;rlJ zF+`JeuRK6KlUjxZ$D4Nhhd8yG+oR*T)5Rv8RUR=rsH(^Z0{>sCCE8a5HWCQTz6Hgv FCO6}=+7|!- literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~GeEAhLfEKOAiceE4p1RPhsjT0dhJ2Wb2RSKJSRYrX9ZfzcsOvikLqISFX6sC7Isbk_Rcf-83_XdL07hDRZU0GA== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~GeEAhLfEKOAiceE4p1RPhsjT0dhJ2Wb2RSKJSRYrX9ZfzcsOvikLqISFX6sC7Isbk_Rcf-83_XdL07hDRZU0GA== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~IpeCQst3WyEeFx3O8E5q5iZZM5eL9RIc2h0V8osVyurjNSgMpwsp3KezPvTu1BtKkn-4kgXRw6aeSGASx-hFqA== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~IpeCQst3WyEeFx3O8E5q5iZZM5eL9RIc2h0V8osVyurjNSgMpwsp3KezPvTu1BtKkn-4kgXRw6aeSGASx-hFqA== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~KuzhAxn73MCO_MRDOxq-FB62tcgHGXjZGZF6te6LD5tgVdJfE3FNOylq0NoFsKhLyquDKwBiZBPFt_KSWb3mRA== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~KuzhAxn73MCO_MRDOxq-FB62tcgHGXjZGZF6te6LD5tgVdJfE3FNOylq0NoFsKhLyquDKwBiZBPFt_KSWb3mRA== new file mode 100644 index 0000000000000000000000000000000000000000..dfe41db86bcc0427b27455fa09534029bcf12f04 GIT binary patch literal 199 zcmV;&066~xK>$wCq}byb<5|I@ZDm^;7*otZINas8DstBks$jRzePLnsbUxwPkQqL- zkh@EM(5;=>;D#o>gx^nrwLmorOcOx>y!cP{4ZsBNV@^%8j!=517*xKX9ui+t9jR*s zl+%o3@D|Z*)?6yo`t)MZD862Mf4erS6(y@G9$M6P`=|A-K>(o>BGSgtE=ykwFvd|Y zlBg1?bU1_PaI>{h68$J&8odft#soHkD(^37Q6SbYkbggDBzDz?@R2MeyiaY_j%0w| BUF!e< literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~MfaWYZUDTCQgyMhJ4X_SfjovY9UPAVC2gdkXTbO16b3bR7M--Jm1jalGBCpPTsAMpM4Rx21VJpYW_9Cc08odyQ== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~MfaWYZUDTCQgyMhJ4X_SfjovY9UPAVC2gdkXTbO16b3bR7M--Jm1jalGBCpPTsAMpM4Rx21VJpYW_9Cc08odyQ== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~N69oVCYHM8jbvjtNsctAwLtICTqzYbf7tQ1LSJ_UGA-RebogHsYo6pK3a2IgU0bM1gnuvIboKWw3XpTHelX9QQ== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~N69oVCYHM8jbvjtNsctAwLtICTqzYbf7tQ1LSJ_UGA-RebogHsYo6pK3a2IgU0bM1gnuvIboKWw3XpTHelX9QQ== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~N8sxHXnTVrfytB-GSlFrIQwhMzRWeqdZ150UW3Uyz69uCswPMbuLLOP5zeCAqlu1mOCAFxwlysTxy8UvcQtwPg== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~N8sxHXnTVrfytB-GSlFrIQwhMzRWeqdZ150UW3Uyz69uCswPMbuLLOP5zeCAqlu1mOCAFxwlysTxy8UvcQtwPg== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~P7Mk64DUuvIoNqXXVjI0Hx1i1B4zAk4iKaLt8nSe-bxbuJEtQ2ubG0dmnB3LIJZmGcxTUU9F4Bla9biPtJeWBA== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~P7Mk64DUuvIoNqXXVjI0Hx1i1B4zAk4iKaLt8nSe-bxbuJEtQ2ubG0dmnB3LIJZmGcxTUU9F4Bla9biPtJeWBA== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~QWafzjNpkPwnvcW_0QZCgmUowbP0C7K1taLHMZDATNLbmGMEH-86pvkqpNT7QPZCNGtZC9E2zB0FZL4JJYAACA== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~QWafzjNpkPwnvcW_0QZCgmUowbP0C7K1taLHMZDATNLbmGMEH-86pvkqpNT7QPZCNGtZC9E2zB0FZL4JJYAACA== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~TtGk2OMZ41nBo21lWxoYU8xAONzltypy1w-qYLfPfWFh9XQ-4dmQGT60kLtLftCtndnghia9hN9PgbVANQpMEw== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~TtGk2OMZ41nBo21lWxoYU8xAONzltypy1w-qYLfPfWFh9XQ-4dmQGT60kLtLftCtndnghia9hN9PgbVANQpMEw== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~UUpc_QsELzcnra5tXuQQma-rX_hhC35IoNUDQoczIdzVYWTfiL77Di2bcD6GVvQ3C2Lfwkdh0eCoVwsSSaRonA== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~UUpc_QsELzcnra5tXuQQma-rX_hhC35IoNUDQoczIdzVYWTfiL77Di2bcD6GVvQ3C2Lfwkdh0eCoVwsSSaRonA== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~VScPuAn3CB-sLm3iIlFTWoHFLXOxaz_b6OL9WjVyW0Om4XjESqJdsT1C3vk_wPSj4JNfFschEmL3pVLer4xFtA== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~VScPuAn3CB-sLm3iIlFTWoHFLXOxaz_b6OL9WjVyW0Om4XjESqJdsT1C3vk_wPSj4JNfFschEmL3pVLer4xFtA== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~W-8rTV1mUbsWlUkByCXmOMWdWeEDl8a-GX2Nt4iMJyVG7Xz0j0V4mSCj0hy_x3_hRcmVVS9HNOlBCMBf9wUPpA== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~W-8rTV1mUbsWlUkByCXmOMWdWeEDl8a-GX2Nt4iMJyVG7Xz0j0V4mSCj0hy_x3_hRcmVVS9HNOlBCMBf9wUPpA== new file mode 100644 index 0000000000000000000000000000000000000000..de718acb095ece2de8c3e269b1a350b4b6e74266 GIT binary patch literal 3235 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 V3v(SBf7V{#z}v5-!O`i0k)*C?51{}6 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~WKkiYg8_oHDh598ew703dOzzZ_4A6eulQMENk8BgrFaapg46zM8SZttdNLvVegsywqlpkgJumXfYXKzEHmysgQ== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~WKkiYg8_oHDh598ew703dOzzZ_4A6eulQMENk8BgrFaapg46zM8SZttdNLvVegsywqlpkgJumXfYXKzEHmysgQ== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~ZVqvguHwOF9yshYRufXAqlRV-kDpOpOmK9YYfF9Kgugul4jDHhD1Tjp1SSVKRRdS3UjRduKDSc4L7ZAXIhB_Zw== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~ZVqvguHwOF9yshYRufXAqlRV-kDpOpOmK9YYfF9Kgugul4jDHhD1Tjp1SSVKRRdS3UjRduKDSc4L7ZAXIhB_Zw== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~_8Dh30MZln1kz7e_2zVrVWiHr7Lj3Pb_2Z00Rl2rWxZ9O59jRdoxlXQDc6qeurxWs9WcH3k8EnVbFvLcB5yRxg== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~_8Dh30MZln1kz7e_2zVrVWiHr7Lj3Pb_2Z00Rl2rWxZ9O59jRdoxlXQDc6qeurxWs9WcH3k8EnVbFvLcB5yRxg== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~_BP8QNt_vjtVJZ_qST_1F4NjYqOeKvr9d2oJQjxk1IRQU07vEWJbFDs8cgeTHeqzPxnhRJUvb2RyTwx_Hok5HA== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~_BP8QNt_vjtVJZ_qST_1F4NjYqOeKvr9d2oJQjxk1IRQU07vEWJbFDs8cgeTHeqzPxnhRJUvb2RyTwx_Hok5HA== new file mode 100644 index 0000000000000000000000000000000000000000..5130fdf1164a2546f2d221d24f094579cf0550fa GIT binary patch literal 133 zcmV;00DAudK>*^O!_o#wn=P9)Y;35W{q|@ae*G!`qhVK4jsrTVqO^PM&lNYAx)F8( z1hw;{ErvY>47-ZA#DsLHHD7Ox&5%I={Ktu-{f!VYPQ5&4aUk!ka{-_Hq(V=H#^ouy ni75kO(2Oy}4Ve%my^^h_tyQ~zzAJFu_vpg7E3zIkqN{#btHwbP literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~_MeJo_2NEDFOvTxmcSDvrHMBn_ykQk-FxuUpu4kpA2LQjDHEDZkQJL2SraatVbt-vitw3ffowrgrsh4yoqt-WA== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~_MeJo_2NEDFOvTxmcSDvrHMBn_ykQk-FxuUpu4kpA2LQjDHEDZkQJL2SraatVbt-vitw3ffowrgrsh4yoqt-WA== new file mode 100644 index 0000000000000000000000000000000000000000..742d7fa511d374972f842c53f066984bb7bbe03b GIT binary patch literal 133 zcmV;00DAudK>&&b2dkw?X7-8+Uy2gQ;i7kDT0z|djc^zrP_n7ft+Xoo@+!!4ZmQ_2 z+i|7ZqbI{|{okiE-9>cA`$aKB#vwrfoBo=yH%&yjAUnJ@M=7Sgu@}Yr+~!1DTEAVF nTHbzafYD2xXiC|e@Yu;ZvR+a(b~?63H3-4ati3OApzyN*4BSHs literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~aqdzXeslnvIED27p3mTJQB9tfXgDrZ5qmnN9YwG0wHAxmwwKRmxym-FjEQISAxCuCoAPFJbrHoDfL2gFc7N9rg== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~aqdzXeslnvIED27p3mTJQB9tfXgDrZ5qmnN9YwG0wHAxmwwKRmxym-FjEQISAxCuCoAPFJbrHoDfL2gFc7N9rg== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~b0zmz8Z3z_a9OMd5JVwTRJti00glveHwMcE0P6OP0W3NUGoHxOUnANszYx-cPrrXztOe4nJC7LMLm82ktgcsNg== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~b0zmz8Z3z_a9OMd5JVwTRJti00glveHwMcE0P6OP0W3NUGoHxOUnANszYx-cPrrXztOe4nJC7LMLm82ktgcsNg== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~bdBtpJ01upXwWjbXJbpA_YzGatXNhCHdjiOWj5bI8z8-TYOEOkwAhtCLRgKTcIhh9Fwwng91U3WqoWG8tQ-ztg== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~bdBtpJ01upXwWjbXJbpA_YzGatXNhCHdjiOWj5bI8z8-TYOEOkwAhtCLRgKTcIhh9Fwwng91U3WqoWG8tQ-ztg== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~c2ACzHhyoX9Fr4qAp_YxNKfPvwEO2SIwxxZP9kK_YV17AK0Pjy_kK3wLYYOuZGfUcpyiCL_pZjXSwZ7mQASfxQ== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~c2ACzHhyoX9Fr4qAp_YxNKfPvwEO2SIwxxZP9kK_YV17AK0Pjy_kK3wLYYOuZGfUcpyiCL_pZjXSwZ7mQASfxQ== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~cZYsgQS58hzrxlF944opJloaCG4whcSszMSGSHg7Yl22WHXvnmr9CbyY3iL1A-yurqaaaZX_WNZ2v475fwF6bQ== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~cZYsgQS58hzrxlF944opJloaCG4whcSszMSGSHg7Yl22WHXvnmr9CbyY3iL1A-yurqaaaZX_WNZ2v475fwF6bQ== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~eTJSePoZS2IR5VW14fcQCV9omYTQ4czehSzIptFlRN0DJFZ8b9wd6QImnglUSL1By02e77T378yucTwmDqowtg== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~eTJSePoZS2IR5VW14fcQCV9omYTQ4czehSzIptFlRN0DJFZ8b9wd6QImnglUSL1By02e77T378yucTwmDqowtg== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~ekUf8H5tNCXv57W-oul74CKZ4DDQNkXMjM5KxUI4K31LG1pzCWg7plcLMtG4RRzitHxWpSfp5IdACQlxk_P5sg== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~ekUf8H5tNCXv57W-oul74CKZ4DDQNkXMjM5KxUI4K31LG1pzCWg7plcLMtG4RRzitHxWpSfp5IdACQlxk_P5sg== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~gJOHZ3pjCV3fYGEq0RtzBNHPExcxieFaFQfQ0V1AuQpeNrigUOaTw8vthD7__wT069-Y3vjMa39agMpb3oflIQ== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~gJOHZ3pjCV3fYGEq0RtzBNHPExcxieFaFQfQ0V1AuQpeNrigUOaTw8vthD7__wT069-Y3vjMa39agMpb3oflIQ== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~igQHq6VJZvaKCV-KEsjhondmWkHdA41wGB9QsqnRrbQq-vIqyHNuquiq23Gl2qMnw27936cz3UV0x_tFMUPGIQ== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~igQHq6VJZvaKCV-KEsjhondmWkHdA41wGB9QsqnRrbQq-vIqyHNuquiq23Gl2qMnw27936cz3UV0x_tFMUPGIQ== new file mode 100644 index 0000000000000000000000000000000000000000..2cab62b502ee31f353193bf2725c8604486b794f GIT binary patch literal 133 zcmV;00DAudK>!M`FPiv}CS3yvFkW(p>?o+=))8(_{?j6T7b%1-iPHOO?^Kb literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~kcjuAwJ8Po_bj9OBqwICyLkxCSGRnAZyYfTWIFWnfvPMpqrTsAjcvUVZ3QNAh7qG5mLqEDzdEC8bhhfgB1o_Pw== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~kcjuAwJ8Po_bj9OBqwICyLkxCSGRnAZyYfTWIFWnfvPMpqrTsAjcvUVZ3QNAh7qG5mLqEDzdEC8bhhfgB1o_Pw== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~lWKiC7Wr3Ea3Uv-HGWXPaIAQKg-6TEh0mcOKpF5NiYyCkgG18J74lrqWQrBabo86SXwhFqTCvdOeu_yH0983vg== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~lWKiC7Wr3Ea3Uv-HGWXPaIAQKg-6TEh0mcOKpF5NiYyCkgG18J74lrqWQrBabo86SXwhFqTCvdOeu_yH0983vg== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~lWido2-3HqlEXDFEk3SvPEA9k1qGBMebdvuHOLWa26PjudPFJp1VHjE7qKrIBwL_X6ol2lcDNhIIzL4Fxa8mNw== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~lWido2-3HqlEXDFEk3SvPEA9k1qGBMebdvuHOLWa26PjudPFJp1VHjE7qKrIBwL_X6ol2lcDNhIIzL4Fxa8mNw== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~lp9CRqSSO7jY4ZQEtkK0z3E4B4PVDqabsbdsZIiiamLndYO2a1gXlMlMSnn1ecqy0tYw_TSA_eNjgzFgGzuC4Q== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~lp9CRqSSO7jY4ZQEtkK0z3E4B4PVDqabsbdsZIiiamLndYO2a1gXlMlMSnn1ecqy0tYw_TSA_eNjgzFgGzuC4Q== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~m_6asTdNRLkgO7w1RymmvbEXxfvc5kRaWr9dl1refmyA0UudaErZm_DYyTqyXlI1djq2RjUIwc-svS9woPCzAA== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~m_6asTdNRLkgO7w1RymmvbEXxfvc5kRaWr9dl1refmyA0UudaErZm_DYyTqyXlI1djq2RjUIwc-svS9woPCzAA== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~n92abrM6Fs1ehKrVN4MBNa2Xii2oyv8ELIt-1kG4gOJDMI-hRXuWG4NHbKKG55tId67mq9pX4fkuNJBKrDn7mQ== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~n92abrM6Fs1ehKrVN4MBNa2Xii2oyv8ELIt-1kG4gOJDMI-hRXuWG4NHbKKG55tId67mq9pX4fkuNJBKrDn7mQ== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~o27HIiKBIB9kGS0tYwROLtU0_dEIHkRZFaKkl7k4XeqCWRjaz0qUkWtqK_eVeQqPkST8GaTNpfJq3xsMaFGv5A== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~o27HIiKBIB9kGS0tYwROLtU0_dEIHkRZFaKkl7k4XeqCWRjaz0qUkWtqK_eVeQqPkST8GaTNpfJq3xsMaFGv5A== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~o8N2yY8BEOp6gioaL4IM3QveVzP4qqt8dLzOpThD1Atz0c6ZEVYC2yRUpsV3bzZxV9OZaROWKabI2u8ktFPdgQ== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~o8N2yY8BEOp6gioaL4IM3QveVzP4qqt8dLzOpThD1Atz0c6ZEVYC2yRUpsV3bzZxV9OZaROWKabI2u8ktFPdgQ== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~oRMi0sbQLktfDDDGUS-SqBKpdDiD6HCztVES_ShfGr0KVcYENoIq7y9oUSDWL5B_P2gkdtWG8JEsJLxPbdWOZA== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~oRMi0sbQLktfDDDGUS-SqBKpdDiD6HCztVES_ShfGr0KVcYENoIq7y9oUSDWL5B_P2gkdtWG8JEsJLxPbdWOZA== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~p6tTbJKE-fL9u16ulx83X9uqUEKDl_tkOfoDsu9ZSI5Gzowle_QZx78T-KVIHsqxzINi_UorLurHXxgKRVXCdw== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~p6tTbJKE-fL9u16ulx83X9uqUEKDl_tkOfoDsu9ZSI5Gzowle_QZx78T-KVIHsqxzINi_UorLurHXxgKRVXCdw== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~q98SQTn3_Q0Xe5mzVf2fdiyct22wbH8Z5MjjSTdbiQfEvEa3TmVyiHOa9-5kzTES8di5UgFjO6Xx57HtC3oSFw== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~q98SQTn3_Q0Xe5mzVf2fdiyct22wbH8Z5MjjSTdbiQfEvEa3TmVyiHOa9-5kzTES8di5UgFjO6Xx57HtC3oSFw== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~qGAQhsKtOU2ecfKSHCRFqUHYkq45Kk8_JtyCgwRcm6uzyut1vm-h7qBDnONG_2GUR6oDxKS5Hi7y1Ufd4abPrA== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~qGAQhsKtOU2ecfKSHCRFqUHYkq45Kk8_JtyCgwRcm6uzyut1vm-h7qBDnONG_2GUR6oDxKS5Hi7y1Ufd4abPrA== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~qeaq-sN8EXFDDNUMKf7zOVAiY6tTZ6maB-gwJ6OuOe-IcVKAwJbjh1YWBJG3EnNHuVSL8sfQbzsrSd2qvqKGTA== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~qeaq-sN8EXFDDNUMKf7zOVAiY6tTZ6maB-gwJ6OuOe-IcVKAwJbjh1YWBJG3EnNHuVSL8sfQbzsrSd2qvqKGTA== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~tiADeO_TwvaQPD1doixRRMC_PNAOdsqUnhJ1Dg2fRw7Y7vFfpWvnFUwJ0kFT4H_Uo3ZSvWKZl6R38hCwVrCKNQ== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~tiADeO_TwvaQPD1doixRRMC_PNAOdsqUnhJ1Dg2fRw7Y7vFfpWvnFUwJ0kFT4H_Uo3ZSvWKZl6R38hCwVrCKNQ== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~uMikE6IBG3uS0y7s1OeC8H76OArcUEH7JKL6Unbdhak2hV1bIWI3Gv4GF3AJsI8r-sXKJzTg9AMoKIT82EYWVQ== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~uMikE6IBG3uS0y7s1OeC8H76OArcUEH7JKL6Unbdhak2hV1bIWI3Gv4GF3AJsI8r-sXKJzTg9AMoKIT82EYWVQ== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~vPhP9g3ABO9jTk2zjlB6pxhUvqAeEl9SHalrBJTTjGPwFtFs1lwq1Pr0YtAovl57f7s2qhUlqyoeWtR2-6f1rQ== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~vPhP9g3ABO9jTk2zjlB6pxhUvqAeEl9SHalrBJTTjGPwFtFs1lwq1Pr0YtAovl57f7s2qhUlqyoeWtR2-6f1rQ== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~wnmRn7u3waaR4x6xGSb1Q7pofNvHx2E01LTjVeJdiQ6OTZt5ceYbMsDNqik2bpreDAJi64yFUwoKPEwKX65VvQ== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~wnmRn7u3waaR4x6xGSb1Q7pofNvHx2E01LTjVeJdiQ6OTZt5ceYbMsDNqik2bpreDAJi64yFUwoKPEwKX65VvQ== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~x2zk2zegpEaxBxmbAZkuPV4uV05PiAYBwNp8HOV_9GNgNX2HpZ9Dy8x55P9yOFJTTEb6WqYfH-SelneFPa_hBA== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~x2zk2zegpEaxBxmbAZkuPV4uV05PiAYBwNp8HOV_9GNgNX2HpZ9Dy8x55P9yOFJTTEb6WqYfH-SelneFPa_hBA== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~ycAlmBG9xahBqrP3TYGwhXy7Rjo8oJrQmb9JKHdom5z6R19ogHH7Ihj53tnq2QtzHRp_1l7fwNuvpcHR6YKRpA== b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Data/refs.0~ycAlmBG9xahBqrP3TYGwhXy7Rjo8oJrQmb9JKHdom5z6R19ogHH7Ihj53tnq2QtzHRp_1l7fwNuvpcHR6YKRpA== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Info.plist b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Info.plist new file mode 100644 index 0000000..0b15897 --- /dev/null +++ b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/Info.plist @@ -0,0 +1,29 @@ + + + + + dateCreated + 2026-02-22T09:32:20Z + externalLocations + + rootId + + hash + 0~GHwlKCoWd_sVutKM9JxOzl6WsKLMkGIfjadOeQvtsU5zwpnYy1DtkD90oypXv-gAL_VehJ4v8k3czEoqvHpoUQ== + + storage + + backend + fileBacked2 + compression + standard + + version + + major + 3 + minor + 56 + + + diff --git a/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/database.sqlite3 b/Tests/XcresultparserTests/TestAssets/parametrized.xcresult/database.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..f954f7841445f9372ce374b0416a8a98ac116061 GIT binary patch literal 258048 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 NhN$abtTkl%{{b%2pq>B# literal 0 HcmV?d00001 diff --git a/Tests/XcresultparserTests/XCCovClientTests.swift b/Tests/XcresultparserTests/XCCovClientTests.swift new file mode 100644 index 0000000..f0decae --- /dev/null +++ b/Tests/XcresultparserTests/XCCovClientTests.swift @@ -0,0 +1,119 @@ +import Foundation +@testable import XcresultparserLib +import Testing + +@MainActor +struct XCCovClientTests { + @Test + func testGetCoverageDataUsesExpectedCommandAndDecodes() throws { + let json = """ + { + "/tmp/Foo.swift": [ + { + "isExecutable": true, + "line": 1, + "executionCount": 2 + } + ] + } + """ + let shell = CapturingXCCovShell(response: Data(json.utf8)) + let client = XCCovClient(shell: shell) + let path = URL(fileURLWithPath: "/tmp/test.xcresult") + + let result = try client.getCoverageData(path: path) + + #expect(result.files["/tmp/Foo.swift"]?.count == 1) + #expect(shell.lastProgram == "/usr/bin/xcrun") + #expect(shell.lastArguments == ["xccov", "view", "--archive", "--json", "/tmp/test.xcresult"]) + } + + @Test + func testGetCoverageReportUsesExpectedCommandAndDecodes() throws { + let json = """ + { + "coveredLines": 1, + "executableLines": 2, + "lineCoverage": 0.5, + "targets": [ + { + "name": "MyTarget", + "lineCoverage": 0.5, + "executableLines": 2, + "coveredLines": 1, + "files": [ + { + "name": "Foo.swift", + "path": "/tmp/Foo.swift", + "lineCoverage": 0.5, + "executableLines": 2, + "coveredLines": 1, + "functions": [ + { + "name": "foo()", + "lineNumber": 1, + "lineCoverage": 0.5, + "executableLines": 2, + "coveredLines": 1, + "executionCount": 1 + } + ] + } + ] + } + ] + } + """ + let shell = CapturingXCCovShell(response: Data(json.utf8)) + let client = XCCovClient(shell: shell) + let path = URL(fileURLWithPath: "/tmp/test.xcresult") + + let result = try client.getCoverageReport(path: path) + + #expect(result.targets.first?.name == "MyTarget") + #expect(shell.lastProgram == "/usr/bin/xcrun") + #expect(shell.lastArguments == ["xccov", "view", "--report", "--json", "/tmp/test.xcresult"]) + } + + @Test + func testGetCoverageForFileUsesExpectedCommand() throws { + let shell = CapturingXCCovShell(response: Data(" 1: 1\n".utf8)) + let client = XCCovClient(shell: shell) + let path = URL(fileURLWithPath: "/tmp/test.xcresult") + + let result = try client.getCoverageForFile(path: path, filePath: "/tmp/Foo.swift") + + #expect(result == " 1: 1\n") + #expect(shell.lastProgram == "/usr/bin/xcrun") + #expect(shell.lastArguments == ["xccov", "view", "--archive", "--file", "/tmp/Foo.swift", "/tmp/test.xcresult"]) + } + + @Test + func testGetCoverageFileListUsesExpectedCommand() throws { + let shell = CapturingXCCovShell(response: Data("/tmp/Foo.swift\n/tmp/Bar.swift\n".utf8)) + let client = XCCovClient(shell: shell) + let path = URL(fileURLWithPath: "/tmp/test.xcresult") + + let result = try client.getCoverageFileList(path: path) + + #expect(result == ["/tmp/Foo.swift", "/tmp/Bar.swift", ""]) + #expect(shell.lastProgram == "/usr/bin/xcrun") + #expect(shell.lastArguments == ["xccov", "view", "--archive", "--file-list", "/tmp/test.xcresult"]) + } +} + +private final class CapturingXCCovShell: Commandline { + let response: Data + var lastProgram: String = "" + var lastArguments: [String] = [] + + init(response: Data) { + self.response = response + } + + func execute(program: String, with arguments: [String], at executionPath: URL?) throws -> Data { + lastProgram = program + lastArguments = arguments + return response + } +} diff --git a/Tests/XcresultparserTests/XCResultToolJunitXMLDataProviderTests.swift b/Tests/XcresultparserTests/XCResultToolJunitXMLDataProviderTests.swift index ade42f1..dbc0149 100644 --- a/Tests/XcresultparserTests/XCResultToolJunitXMLDataProviderTests.swift +++ b/Tests/XcresultparserTests/XCResultToolJunitXMLDataProviderTests.swift @@ -126,6 +126,105 @@ struct XCResultToolJunitXMLDataProviderTests { #expect(action.failureSummaries.first?.testCaseName == "DemoTests.testFail()") } + @Test + func testProviderAddsFailureDocumentLocation() throws { + let summaryJSON = """ + { + "title": "Test - Demo", + "environmentDescription": "Demo", + "topInsights": [], + "result": "Failed", + "totalTestCount": 1, + "passedTests": 0, + "failedTests": 1, + "skippedTests": 0, + "expectedFailures": 0, + "statistics": [], + "devicesAndConfigurations": [], + "testFailures": [ + { + "failureText": "failed - expected true", + "targetName": "DemoTests", + "testIdentifier": 1, + "testIdentifierString": "DemoTests/testFail()", + "testIdentifierURL": "test://com.apple.xcode/Demo/DemoTests/testFail", + "testName": "testFail()" + } + ], + "startTime": 100.0, + "finishTime": 120.0 + } + """ + + let testsJSON = """ + { + "testPlanConfigurations": [ + { + "configurationId": "1", + "configurationName": "Default" + } + ], + "devices": [], + "testNodes": [ + { + "name": "Test Plan", + "nodeType": "Test Plan", + "children": [ + { + "name": "Default", + "nodeType": "Test Plan Configuration", + "children": [ + { + "name": "DemoTests.xctest", + "nodeType": "Unit test bundle", + "children": [ + { + "name": "DemoTests", + "nodeType": "Test Suite", + "children": [ + { + "name": "testFail()", + "nodeType": "Test Case", + "result": "Failed", + "children": [ + { + "name": "DemoTests.swift:42: failed - expected true", + "nodeType": "Failure Message" + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + """ + + let shell = LookupShell( + responses: [ + "xcresulttool get test-results summary --path /tmp/test.xcresult": .success(Data(summaryJSON.utf8)), + "xcresulttool get test-results tests --path /tmp/test.xcresult": .success(Data(testsJSON.utf8)) + ] + ) + let client = XCResultToolClient(shell: shell) + guard let provider = XCResultToolJunitXMLDataProvider( + url: URL(fileURLWithPath: "/tmp/test.xcresult"), + client: client + ) else { + Issue.record("Expected provider initialization to succeed") + return + } + + let action = try #require(provider.testActions.first) + let summary = try #require(action.failureSummaries.first) + #expect(summary.documentLocation == "DemoTests.swift:42") + } + @Test func testProviderInitFailsIfXCResultToolFails() { let shell = LookupShell( @@ -142,6 +241,212 @@ struct XCResultToolJunitXMLDataProviderTests { #expect(provider == nil) } + + @Test + func testProviderMapsParameterizedTests() throws { + let summaryJSON = """ + { + "title": "Test - Demo", + "environmentDescription": "Demo", + "topInsights": [], + "result": "Failed", + "totalTestCount": 2, + "passedTests": 2, + "failedTests": 0, + "skippedTests": 0, + "expectedFailures": 0, + "statistics": [], + "devicesAndConfigurations": [], + "testFailures": [], + "startTime": 100.0, + "finishTime": 120.0 + } + """ + + let testsJSON = """ + { + "testPlanConfigurations": [ + { + "configurationId": "1", + "configurationName": "Default" + } + ], + "devices": [], + "testNodes": [ + { + "name": "Test Plan", + "nodeType": "Test Plan", + "children": [ + { + "name": "Default", + "nodeType": "Test Plan Configuration", + "children": [ + { + "name": "DemoTests.xctest", + "nodeType": "Unit test bundle", + "children": [ + { + "name": "DemoTests", + "nodeType": "Test Suite", + "children": [ + { + "name": "testParametrized(value:)", + "nodeType": "Test Case", + "result": "Passed", + "children": [ + { + "name": "false", + "nodeType": "Arguments", + "result": "Passed", + "durationInSeconds": 1.0 + }, + { + "name": "true", + "nodeType": "Arguments", + "result": "Passed", + "durationInSeconds": 2.0 + } + ] + }, + { + "name": "testMultiParam(value:count:)", + "nodeType": "Test Case", + "result": "Passed", + "children": [ + { + "name": "false, 3", + "nodeType": "Arguments", + "result": "Passed", + "durationInSeconds": 1.0 + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + """ + + let shell = LookupShell( + responses: [ + "xcresulttool get test-results summary --path /tmp/test.xcresult": .success(Data(summaryJSON.utf8)), + "xcresulttool get test-results tests --path /tmp/test.xcresult": .success(Data(testsJSON.utf8)) + ] + ) + let client = XCResultToolClient(shell: shell) + guard let provider = XCResultToolJunitXMLDataProvider( + url: URL(fileURLWithPath: "/tmp/test.xcresult"), + client: client + ) else { + Issue.record("Expected provider initialization to succeed") + return + } + + let action = try #require(provider.testActions.first) + let plan = try #require(action.testPlanRunSummaries.first) + let rootGroup = try #require(plan.testableSummaries.first?.tests.first) + let suite = try #require(rootGroup.subtestGroups.first) + #expect(suite.subtests.count == 3) + #expect(suite.subtests[0].name == "testParametrized(value: false)") + #expect(suite.subtests[1].name == "testParametrized(value: true)") + #expect(suite.subtests[2].name == "testMultiParam(value: false, count: 3)") + } + + @Test + func testProviderDoesNotMapExpectedFailureAsSkipped() throws { + let summaryJSON = """ + { + "title": "Test - Demo", + "environmentDescription": "Demo", + "topInsights": [], + "result": "Passed", + "totalTestCount": 1, + "passedTests": 1, + "failedTests": 0, + "skippedTests": 0, + "expectedFailures": 1, + "statistics": [], + "devicesAndConfigurations": [], + "testFailures": [], + "startTime": 100.0, + "finishTime": 101.0 + } + """ + + let testsJSON = """ + { + "testPlanConfigurations": [ + { + "configurationId": "1", + "configurationName": "Default" + } + ], + "devices": [], + "testNodes": [ + { + "name": "Test Plan", + "nodeType": "Test Plan", + "children": [ + { + "name": "Default", + "nodeType": "Test Plan Configuration", + "children": [ + { + "name": "DemoTests.xctest", + "nodeType": "Unit test bundle", + "children": [ + { + "name": "DemoTests", + "nodeType": "Test Suite", + "children": [ + { + "name": "testExpected()", + "nodeType": "Test Case", + "result": "Expected Failure", + "durationInSeconds": 1.0 + } + ] + } + ] + } + ] + } + ] + } + ] + } + """ + + let shell = LookupShell( + responses: [ + "xcresulttool get test-results summary --path /tmp/test.xcresult": .success(Data(summaryJSON.utf8)), + "xcresulttool get test-results tests --path /tmp/test.xcresult": .success(Data(testsJSON.utf8)) + ] + ) + let client = XCResultToolClient(shell: shell) + guard let provider = XCResultToolJunitXMLDataProvider( + url: URL(fileURLWithPath: "/tmp/test.xcresult"), + client: client + ) else { + Issue.record("Expected provider initialization to succeed") + return + } + + let action = try #require(provider.testActions.first) + let plan = try #require(action.testPlanRunSummaries.first) + let rootGroup = try #require(plan.testableSummaries.first?.tests.first) + let suite = try #require(rootGroup.subtestGroups.first) + let expectedFailureTest = try #require(suite.subtests.first) + + #expect(expectedFailureTest.isFailed == false) + #expect(expectedFailureTest.isSkipped == false) + } } private final class LookupShell: Commandline { diff --git a/Tests/XcresultparserTests/XcresultparserTests.swift b/Tests/XcresultparserTests/XcresultparserTests.swift index 5c673e1..24c5969 100644 --- a/Tests/XcresultparserTests/XcresultparserTests.swift +++ b/Tests/XcresultparserTests/XcresultparserTests.swift @@ -7,6 +7,8 @@ struct XcresultparserTests { @Test func testTextResultFormatter() throws { let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! + let durationValue = executionTimeValue(for: xcresultFile) + let dateValue = executionDateValue(for: xcresultFile) guard let resultParser = XCResultFormatter( with: xcresultFile, @@ -26,6 +28,8 @@ struct XcresultparserTests { Number of tests = 7 Number of failed tests = 1 Number of skipped tests = 0 + Execution time = \(durationValue)s + Execution date = \(dateValue) """ #expect(resultParser.summary == expectedSummary) #expect(resultParser.divider == "---------------------\n") @@ -38,6 +42,8 @@ struct XcresultparserTests { @Test func testTextResultFormatterTotalCoverageReportFormat() throws { let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! + let durationValue = executionTimeValue(for: xcresultFile) + let dateValue = executionDateValue(for: xcresultFile) guard let resultParser = XCResultFormatter( with: xcresultFile, @@ -58,6 +64,8 @@ struct XcresultparserTests { Number of tests = 7 Number of failed tests = 1 Number of skipped tests = 0 + Execution time = \(durationValue)s + Execution date = \(dateValue) """ #expect(expectedSummary == resultParser.summary) #expect(resultParser.divider == "---------------------\n") @@ -72,6 +80,8 @@ struct XcresultparserTests { @Test func testTextResultFormatterMethodsCoverageReportFormat() throws { let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! + let durationValue = executionTimeValue(for: xcresultFile) + let dateValue = executionDateValue(for: xcresultFile) guard let resultParser = XCResultFormatter( with: xcresultFile, @@ -92,6 +102,8 @@ struct XcresultparserTests { Number of tests = 7 Number of failed tests = 1 Number of skipped tests = 0 + Execution time = \(durationValue)s + Execution date = \(dateValue) """ #expect(resultParser.summary == expectedSummary) #expect(resultParser.divider == "---------------------\n") @@ -105,9 +117,30 @@ struct XcresultparserTests { #expect(lines[3].contains("CLIResultFormatter.swift:") == true) } + @Test + func testTextResultFormatterCanExcludeDateAndDurationFromSummary() throws { + let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! + + guard let resultParser = XCResultFormatter( + with: xcresultFile, + formatter: TextResultFormatter(), + coverageTargets: [], + summaryFields: "errors|warnings|analyzerWarnings|tests|failed|skipped" + ) else { + Issue.record("Unable to create XCResultFormatter with \(xcresultFile)") + return + } + + #expect(resultParser.summary.contains("Number of tests = 7")) + #expect(!resultParser.summary.contains("Execution time = ")) + #expect(!resultParser.summary.contains("Execution date = ")) + } + @Test func testCLIResultFormatter() throws { let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! + let durationValue = executionTimeValue(for: xcresultFile) + let dateValue = executionDateValue(for: xcresultFile) guard let resultParser = XCResultFormatter( with: xcresultFile, @@ -127,6 +160,8 @@ struct XcresultparserTests { Number of tests = 7\u{001B}[0m \u{001B}[31m Number of failed tests = 1\u{001B}[0m Number of skipped tests = 0\u{001B}[0m + Execution time = \(durationValue)s\u{001B}[0m + Execution date = \(dateValue)\u{001B}[0m """ #expect(resultParser.summary == expectedSummary) #expect(resultParser.divider == "-----------------\n") @@ -141,6 +176,8 @@ struct XcresultparserTests { @Test func testHTMLResultFormatter() throws { let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! + let durationValue = executionTimeValue(for: xcresultFile) + let dateValue = executionDateValue(for: xcresultFile) guard let resultParser = XCResultFormatter( with: xcresultFile, @@ -167,6 +204,8 @@ struct XcresultparserTests {

Number of tests = 7

Number of failed tests = 1

Number of skipped tests = 0

+

Execution time = \(durationValue)s

+

Execution date = \(dateValue)

""" #expect(resultParser.summary == expectedSummary) #expect(resultParser.divider == "
") @@ -175,9 +214,31 @@ struct XcresultparserTests { #expect(resultParser.documentSuffix.hasSuffix("")) } + @Test + func testHTMLResultFormatterParameterizedArguments() throws { + let xcresultFile = Bundle.module.url(forResource: "parametrized", withExtension: "xcresult")! + + guard let resultParser = XCResultFormatter( + with: xcresultFile, + formatter: HTMLResultFormatter(), + coverageTargets: [] + ) else { + Issue.record("Unable to create XCResultFormatter with \(xcresultFile)") + return + } + + #expect( + resultParser.testDetails.contains( + "testCoverageConverterPathnames(strictPathnames: false, projectRoot: \"/Users/fhaser/code/\")" + ) + ) + } + @Test func testMDResultFormatter() throws { let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! + let durationValue = executionTimeValue(for: xcresultFile) + let dateValue = executionDateValue(for: xcresultFile) guard let resultParser = XCResultFormatter( with: xcresultFile, @@ -189,7 +250,7 @@ struct XcresultparserTests { } #expect(resultParser.documentPrefix(title: "XCResults") == "") - let expectedSummary = "Errors: 0; Warnings: 3; Analyzer Warnings: 0; Tests: 7; Failed: 1; Skipped: 0" + let expectedSummary = "Errors: 0; Warnings: 3; Analyzer Warnings: 0; Tests: 7; Failed: 1; Skipped: 0; Execution time = \(durationValue)s; Execution date = \(dateValue)" #expect(resultParser.summary == expectedSummary) #expect(resultParser.divider == "\n---------------------\n") @@ -206,19 +267,56 @@ struct XcresultparserTests { #expect(resultParser.documentSuffix == "") } + @Test + func testMDResultFormatterParameterizedArguments() throws { + let xcresultFile = Bundle.module.url(forResource: "parametrized", withExtension: "xcresult")! + + guard let resultParser = XCResultFormatter( + with: xcresultFile, + formatter: MDResultFormatter(), + coverageTargets: [] + ) else { + Issue.record("Unable to create XCResultFormatter with \(xcresultFile)") + return + } + + #expect( + resultParser.testDetails.contains( + "testCoverageConverterPathnames(strictPathnames: false, projectRoot: \\\"/Users/fhaser/code/\\\")" + ) + ) + } + + @Test + func testTextResultFormatterParameterizedArguments() throws { + let xcresultFile = Bundle.module.url(forResource: "parametrized", withExtension: "xcresult")! + + guard let resultParser = XCResultFormatter( + with: xcresultFile, + formatter: TextResultFormatter(), + coverageTargets: [] + ) else { + Issue.record("Unable to create XCResultFormatter with \(xcresultFile)") + return + } + + #expect( + resultParser.testDetails.contains( + "testCoverageConverterPathnames(strictPathnames: false, projectRoot: \"/Users/fhaser/code/\")" + ) + ) + } + @Test(arguments: [true, false]) func testCoverageConverter(strictPathnames: Bool) throws { let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! let projectRoot = "" - guard let converter = CoverageConverter( + let converter = try CoverageConverter( with: xcresultFile, projectRoot: projectRoot, strictPathnames: strictPathnames - ) else { - Issue.record("Unable to create CoverageConverter from \(xcresultFile)") - return - } + ) let info = converter.targetsInfo #expect(info == "\nXcresultparserLib\nXcresultparserTests") @@ -254,19 +352,15 @@ struct XcresultparserTests { #expect(coverageForFile.contains(" 35: 0")) } - @Test(arguments: [true, false]) - func testCoverageConverterPathnames(strictPathnames: Bool) throws { + @Test(arguments: [true, false], ["/Users/notexistant/code/xcresultparser", "/Users/fhaser/code/"]) + func testCoverageConverterPathnames(strictPathnames: Bool, projectRoot: String) throws { let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! - let projectRoot = "/Users/notexistant/code/xcresultparser" - guard let converter = SonarCoverageConverter( + let converter = try SonarCoverageConverter( with: xcresultFile, projectRoot: projectRoot, strictPathnames: strictPathnames - ) else { - Issue.record("Unable to create CoverageConverter from \(xcresultFile)") - return - } + ) let xml = try converter.xmlString(quiet: true) @@ -284,14 +378,11 @@ struct XcresultparserTests { let projectRoot = "" let quiet = 1 - guard let converter = SonarCoverageConverter( + let converter = try SonarCoverageConverter( with: xcresultFile, projectRoot: projectRoot, strictPathnames: strictPathnames - ) else { - Issue.record("Unable to create CoverageConverter from \(xcresultFile)") - return - } + ) let rslt = try converter.xmlString(quiet: quiet == 1) #expect(rslt.starts(with: "")) let lines = rslt.components(separatedBy: .newlines) @@ -305,21 +396,80 @@ struct XcresultparserTests { #expect(lines[pos2 + 2] == " ") } + @Test + func testCoverageTargetFilterWithNoMatchesThrowsError() throws { + let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! + + do { + _ = try SonarCoverageConverter( + with: xcresultFile, + projectRoot: "", + coverageTargets: ["NonExistingTarget"], + strictPathnames: false + ) + Issue.record("Expected unknown coverage target error") + } catch let error as CoverageConverterError { + switch error { + case let .unknownCoverageTargets(requested, available): + #expect(requested == ["NonExistingTarget"]) + #expect(!available.isEmpty) + case .couldNotLoadCoverageReport: + Issue.record("Unexpected coverage report loading error") + } + } catch { + Issue.record("Unexpected error: \(error)") + } + } + + @Test + func testCoverageTargetFilterWithPartialUnknownThrowsError() throws { + let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! + + do { + _ = try SonarCoverageConverter( + with: xcresultFile, + projectRoot: "", + coverageTargets: ["XcresultparserLib", "MissingTarget"], + strictPathnames: false + ) + Issue.record("Expected unknown coverage target error") + } catch let error as CoverageConverterError { + switch error { + case let .unknownCoverageTargets(requested, _): + #expect(requested == ["MissingTarget"]) + case .couldNotLoadCoverageReport: + Issue.record("Unexpected coverage report loading error") + } + } catch { + Issue.record("Unexpected error: \(error)") + } + } + + @Test + func testFormatterInitFailsForUnknownCoverageTarget() { + let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! + + let resultParser = XCResultFormatter( + with: xcresultFile, + formatter: TextResultFormatter(), + coverageTargets: ["NonExistingTarget"] + ) + + #expect(resultParser == nil) + } + @Test func testSonarCoverageConverterExcludeFiles() throws { let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! let projectRoot = "" let quiet = 1 - guard let converter = SonarCoverageConverter( + let converter = try SonarCoverageConverter( with: xcresultFile, projectRoot: projectRoot, excludedPaths: ["OutputFormatting/Formatters"], strictPathnames: false - ) else { - Issue.record("Unable to create CoverageConverter from \(xcresultFile)") - return - } + ) let rslt = try converter.xmlString(quiet: quiet == 1) #expect(rslt.starts(with: "")) let lines = rslt.components(separatedBy: .newlines) @@ -337,14 +487,11 @@ struct XcresultparserTests { let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! let projectRoot = "" - guard let converter = CoberturaCoverageConverter( + let converter = try CoberturaCoverageConverter( with: xcresultFile, projectRoot: projectRoot, strictPathnames: false - ) else { - Issue.record("Unable to create CoverageConverter from \(xcresultFile)") - return - } + ) try assertXmlTestReportsAreEqual(expectedFileName: "cobertura", actual: converter) } @@ -353,18 +500,55 @@ struct XcresultparserTests { let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! let projectRoot = "" - guard let converter = CoberturaCoverageConverter( + let converter = try CoberturaCoverageConverter( with: xcresultFile, projectRoot: projectRoot, excludedPaths: ["OutputFormatting/Formatters"], strictPathnames: false - ) else { - Issue.record("Unable to create CoverageConverter from \(xcresultFile)") - return - } + ) try assertXmlTestReportsAreEqual(expectedFileName: "coberturaExcludingDirectory", actual: converter) } + @Test + func testCoberturaCoverageTargetFilterWithNoMatchesThrowsError() throws { + let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! + + do { + _ = try CoberturaCoverageConverter( + with: xcresultFile, + projectRoot: "", + coverageTargets: ["NonExistingTarget"], + strictPathnames: false + ) + Issue.record("Expected unknown coverage target error") + } catch let error as CoverageConverterError { + switch error { + case let .unknownCoverageTargets(requested, _): + #expect(requested == ["NonExistingTarget"]) + case .couldNotLoadCoverageReport: + Issue.record("Unexpected coverage report loading error") + } + } catch { + Issue.record("Unexpected error: \(error)") + } + } + + @Test + func testCoverageConverterNormalizesTargetFilePaths() { + let projectRoot = "/Users/demo/project" + let paths = [ + "/Users/demo/project/Sources/Foo.swift", + "Sources/Bar.swift" + ] + + let normalized = CoverageConverter.normalizedFilePaths(for: paths, projectRoot: projectRoot) + + #expect(normalized.contains("/Users/demo/project/Sources/Foo.swift")) + #expect(normalized.contains("Sources/Foo.swift")) + #expect(normalized.contains("Sources/Bar.swift")) + #expect(normalized.contains("/Users/demo/project/Sources/Bar.swift")) + } + @Test func testJunitXMLSonar() throws { JunitXML.resetCachedPathnames() @@ -491,6 +675,34 @@ struct XcresultparserTests { try assertXmlTestReportsAreEqual(expectedFileName: "junit_repeated", actual: junitXML) } + @Test + func testJunitXMLIncludesParameterizedArguments() { + let xcresultFile = Bundle.module.url(forResource: "parametrized", withExtension: "xcresult")! + let projectRoot = "" + guard let junitXML = JunitXML( + with: xcresultFile, + projectRoot: projectRoot, + format: .junit + ) else { + Issue.record("Unable to create JunitXML from \(xcresultFile)") + return + } + + let xmlString = junitXML.xmlString + #expect(xmlString.contains("testCoverageConverter(strictPathnames: false)")) + #expect(xmlString.contains("testCoverageConverter(strictPathnames: true)")) + #expect( + xmlString.contains( + "testCoverageConverterPathnames(strictPathnames: false, projectRoot: "/Users/fhaser/code/")" + ) + ) + #expect( + xmlString.contains( + "testCoverageConverterPathnames(strictPathnames: true, projectRoot: "/Users/notexistant/code/xcresultparser")" + ) + ) + } + @Test func testFailureSummariesReturnsAllMatchingFailures() throws { let test = makeJunitTest() @@ -1002,6 +1214,31 @@ struct XcresultparserTests { // MARK: helper functions + private func executionTimeValue(for xcresultFile: URL) -> String { + guard let summary = try? XCResultToolClient().getTestSummary(path: xcresultFile), + let start = summary.startTime, + let finish = summary.finishTime else { + return "" + } + let duration = max(0, finish - start) + let formatter = NumberFormatter() + formatter.maximumFractionDigits = 4 + formatter.locale = Locale(identifier: "en_US_POSIX") + return formatter.unwrappedString(for: duration) + } + + private func executionDateValue(for xcresultFile: URL) -> String { + guard let summary = try? XCResultToolClient().getTestSummary(path: xcresultFile), + let start = summary.startTime else { + return "" + } + let date = Date(timeIntervalSince1970: start) + let formatter = ISO8601DateFormatter() + formatter.timeZone = TimeZone(secondsFromGMT: 0) + formatter.formatOptions = [.withInternetDateTime] + return formatter.string(from: date) + } + private func makeJunitTest( identifier: String = "TestClass/testMethod", name: String = "testMethod", From 34f0c3b7800024ee6cd3a7636a88f62d8eca3b04 Mon Sep 17 00:00:00 2001 From: Alex da Franca Date: Sun, 22 Feb 2026 19:31:56 +0100 Subject: [PATCH 10/14] Refactor tests and run_regression_benchmark script --- .gitignore | 1 + .swiftformat | 187 +++++++++++----- notarize.sh => Scripts/notarize.sh | 3 + runSonar.sh => Scripts/runSonar.sh | 0 Scripts/run_regression_benchmark.sh | 211 ++++++++++++++++++ .../testAndRunSonar.sh | 0 .../CoverageAndFormatterDependencyTests.swift | 199 +++++++++++++++++ .../TestSupport/CapturingCommandline.swift | 23 ++ .../TestSupport/ToolClientTestDoubles.swift | 138 ++++++++++++ .../XCCovClientTests.swift | 24 +- .../XCResultToolClientTests.swift | 26 +-- 11 files changed, 711 insertions(+), 101 deletions(-) rename notarize.sh => Scripts/notarize.sh (98%) rename runSonar.sh => Scripts/runSonar.sh (100%) create mode 100755 Scripts/run_regression_benchmark.sh rename testAndRunSonar.sh => Scripts/testAndRunSonar.sh (100%) create mode 100644 Tests/XcresultparserTests/CoverageAndFormatterDependencyTests.swift create mode 100644 Tests/XcresultparserTests/TestSupport/CapturingCommandline.swift create mode 100644 Tests/XcresultparserTests/TestSupport/ToolClientTestDoubles.swift diff --git a/.gitignore b/.gitignore index 9e4c35e..8ea20a5 100644 --- a/.gitignore +++ b/.gitignore @@ -93,6 +93,7 @@ iOSInjectionProject/ # Compiled app, ready for notarization product/ AgentNotes/ +TestResults/ .scannerwork/ diff --git a/.swiftformat b/.swiftformat index 53a1ccf..ca5996c 100644 --- a/.swiftformat +++ b/.swiftformat @@ -1,69 +1,136 @@ ---swiftversion 5.9 +# old format options: +# --disable wrapMultilineStatementBraces +# --disable redundantSelf +# --disable preferForLoop +--swiftversion 6.2 + +--acronyms ID,URL,UUID --allman false ---assetliterals visual-width ---beforemarks ---binarygrouping none ---categorymark "MARK: %c" ---classthreshold 0 ---closingparen balanced -# --commas always ---conflictmarkers reject ---decimalgrouping none ---elseposition same-line ---emptybraces no-space ---enumthreshold 0 ---exponentcase lowercase ---exponentgrouping disabled ---extensionacl on-extension ---extensionlength 0 ---extensionmark "MARK: - %t + %c" ---fractiongrouping disabled +--allow-partial-wrapping true +--anonymous-for-each convert +--asset-literals visual-width +--async-capturing +--before-marks +--binary-grouping none +--blank-line-after-switch-case multiline-only +--call-site-paren default +--category-mark "MARK: %c" +--class-threshold 0 +--closing-paren balanced +--closure-void remove +--complex-attributes preserve +--computed-var-attributes preserve +--conditional-assignment always +--conflict-markers reject +--date-format system +--decimal-grouping none +--default-test-suite-attributes +--doc-comments preserve +--else-position same-line +--empty-braces no-space +--enum-namespaces always +--enum-threshold 0 +--equatable-macro none +--exponent-case lowercase +--exponent-grouping disabled +--extension-acl on-extension +--extension-mark "MARK: - %t + %c" +--extension-threshold 0 +--file-macro "#file" +--fraction-grouping disabled --fragment false ---funcattributes preserve ---groupedextension "MARK: %c" ---guardelse auto ---header ignore ---hexgrouping none ---hexliteralcase uppercase +--func-attributes preserve +--generic-types +--group-blank-lines true +--grouped-extension "MARK: %c" +--guard-else auto +--header "// new header text" +--hex-grouping none +--hex-literal-case uppercase --ifdef indent ---importgrouping alpha +--import-grouping alpha --indent 4 ---indentcase false ---lifecycle +--indent-case false +--indent-strings false +--inferred-types always +--init-coder-nil false +--language-mode 0 +--lifecycle +--line-after-marks true +--line-between-guards false --linebreaks lf ---markextensions always ---marktypes always ---maxwidth none ---modifierorder ---nevertrailing compactMap,map,flatMap ---nospaceoperators ---nowrapoperators ---octalgrouping none ---operatorfunc spaced ---organizetypes actor,class,enum,struct ---patternlet inline +--mark-categories true +--mark-class-threshold 0 +--mark-enum-threshold 0 +--mark-extension-threshold 0 +--mark-extensions always +--mark-struct-threshold 0 +--mark-types always +--markdown-files ignore +--max-width none +--modifier-order +--never-trailing compactMap,flatMap,map +--nil-init remove +--no-space-operators +--no-wrap-operators +--non-complex-attributes +--octal-grouping none +--operator-func spaced +--organization-mode visibility +--organize-types actor,class,enum,struct +--pattern-let hoist +--prefer-synthesized-init-for-internal-structs never +--preserve-acronyms +--preserve-decls +--preserved-property-types Package +--property-types inferred --ranges spaced ---redundanttype inferred +--redundant-async tests-only +--redundant-throws tests-only --self remove ---selfrequired +--self-required --semicolons never ---shortoptionals always ---smarttabs enabled ---stripunusedargs closure-only ---structthreshold 0 ---tabwidth unspecified ---trailingclosures ---trimwhitespace always ---typeattributes preserve ---typemark "MARK: - %t" ---varattributes preserve ---voidtype void ---wraparguments before-first ---wrapcollections before-first ---wrapconditions preserve ---wrapparameters before-first ---wrapreturntype preserve ---xcodeindentation disabled ---yodaswap always ---disable redundantReturn,wrapMultilineStatementBraces,trailingCommas,preferKeyPath +--short-optionals always +--single-line-for-each ignore +--smart-tabs enabled +--some-any true +--sort-swiftui-properties none +--sorted-patterns +--stored-var-attributes preserve +--strip-unused-args closure-only +--struct-threshold 0 +--swift-version 6.2 +--tab-width unspecified +--throw-capturing +--timezone system +--trailing-closures +--trailing-commas always +--trim-whitespace always +--type-attributes preserve +--type-blank-lines remove +--type-body-marks preserve +--type-delimiter space-after +--type-mark "MARK: - %t" +--type-marks +--type-order +--url-macro none +--visibility-marks +--visibility-order +--void-type Void +--wrap-arguments before-first +--wrap-collections before-first +--wrap-conditions preserve +--wrap-effects preserve +--wrap-enum-cases always +--wrap-parameters before-first +--wrap-return-type preserve +--wrap-string-interpolation default +--wrap-ternary default +--wrap-type-aliases preserve +--xcode-indentation disabled +--xctest-symbols +--yoda-swap always +--disable redundantMemberwiseInit,redundantProperty,conditionalAssignment +--disable docComments,docCommentsBeforeModifiers,fileHeader,opaqueGenericParameters,preferKeyPath,redundantReturn,trailingCommas,wrapMultilineStatementBraces +--enable isEmpty,privateStateVariables # ,propertyTypes,unusedPrivateDeclarations diff --git a/notarize.sh b/Scripts/notarize.sh similarity index 98% rename from notarize.sh rename to Scripts/notarize.sh index b90e0e5..c42cd55 100644 --- a/notarize.sh +++ b/Scripts/notarize.sh @@ -24,6 +24,7 @@ usage() ## Default values for this app, so I can invoke this script without parameters productName="xcresultparser" +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" while [ "$1" != "" ]; do case $1 in @@ -55,6 +56,8 @@ then exit 1 fi +cd "$ROOT_DIR" + # build the project for M1 and Intel: swift build -c release --arch arm64 --arch x86_64 diff --git a/runSonar.sh b/Scripts/runSonar.sh similarity index 100% rename from runSonar.sh rename to Scripts/runSonar.sh diff --git a/Scripts/run_regression_benchmark.sh b/Scripts/run_regression_benchmark.sh new file mode 100755 index 0000000..93fbcaa --- /dev/null +++ b/Scripts/run_regression_benchmark.sh @@ -0,0 +1,211 @@ +#!/usr/bin/env bash +set -u + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +TEST_DIR="$ROOT_DIR/TestResults" +NEW_BIN="$TEST_DIR/xcresultparser" +OLD_BIN="$TEST_DIR/lastVersion/xcresultparser" +NEW_OUT_DIR="$TEST_DIR/newResults" +OLD_OUT_DIR="$TEST_DIR/oldResults" +CSV_FILE="$TEST_DIR/timings.csv" +MD_FILE="$TEST_DIR/timings.md" +CHART_CSV_FILE="$TEST_DIR/timings_chart.csv" +RAW_CSV_FILE="$TEST_DIR/.timings_raw.csv" + +TEST_BUNDLE="$ROOT_DIR/Tests/XcresultparserTests/TestAssets/test.xcresult" +ERROR_BUNDLE="$ROOT_DIR/Tests/XcresultparserTests/TestAssets/resultWithCompileError.xcresult" +SUFFIX="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --bundle) + TEST_BUNDLE="$2" + shift 2 + ;; + --error-bundle) + ERROR_BUNDLE="$2" + shift 2 + ;; + --suffix) + SUFFIX="$2" + shift 2 + ;; + *) + echo "Unknown argument: $1" >&2 + exit 1 + ;; + esac +done + +if [[ -z "$ERROR_BUNDLE" ]]; then + ERROR_BUNDLE="$TEST_BUNDLE" +fi +if [[ -n "$SUFFIX" ]]; then + NEW_OUT_DIR="$TEST_DIR/newResults_${SUFFIX}" + OLD_OUT_DIR="$TEST_DIR/oldResults_${SUFFIX}" + CSV_FILE="$TEST_DIR/timings_${SUFFIX}.csv" + MD_FILE="$TEST_DIR/timings_${SUFFIX}.md" + CHART_CSV_FILE="$TEST_DIR/timings_chart_${SUFFIX}.csv" + RAW_CSV_FILE="$TEST_DIR/.timings_raw_${SUFFIX}.csv" +fi + +mkdir -p "$NEW_OUT_DIR" "$OLD_OUT_DIR" +rm -f "$NEW_OUT_DIR"/* "$OLD_OUT_DIR"/* "$CSV_FILE" "$MD_FILE" "$CHART_CSV_FILE" "$RAW_CSV_FILE" + +if [[ ! -x "$NEW_BIN" ]]; then + echo "Missing executable: $NEW_BIN" >&2 + exit 1 +fi +if [[ ! -x "$OLD_BIN" ]]; then + echo "Missing executable: $OLD_BIN" >&2 + exit 1 +fi + +echo "version,case,binary,bundle,seconds,exit_code,bytes,sha256" > "$RAW_CSV_FILE" + +run_case() { + local version_label="$1" + local bin="$2" + local out_dir="$3" + local case_name="$4" + local bundle="$5" + shift 5 + local args=("$@") + + local out_file="$out_dir/${case_name}.out" + local err_file="$out_dir/${case_name}.stderr" + local time_file="$out_dir/${case_name}.time" + + local rc=0 + /usr/bin/time -p -o "$time_file" "$bin" "${args[@]}" "$bundle" > "$out_file" 2> "$err_file" || rc=$? + + local seconds + seconds=$(awk '/^real /{print $2}' "$time_file" | tail -n 1) + [[ -z "$seconds" ]] && seconds="NA" + + local bytes + bytes=$(wc -c < "$out_file" | tr -d ' ') + + local checksum + checksum=$(shasum -a 256 "$out_file" | awk '{print $1}') + + echo "$version_label,$case_name,$bin,$bundle,$seconds,$rc,$bytes,$checksum" >> "$RAW_CSV_FILE" +} + +run_version() { + local version_label="$1" + local bin="$2" + local out_dir="$3" + + run_case "$version_label" "$bin" "$out_dir" "txt" "$TEST_BUNDLE" -o txt + run_case "$version_label" "$bin" "$out_dir" "cli" "$TEST_BUNDLE" -o cli + run_case "$version_label" "$bin" "$out_dir" "html" "$TEST_BUNDLE" -o html + run_case "$version_label" "$bin" "$out_dir" "md" "$TEST_BUNDLE" -o md + run_case "$version_label" "$bin" "$out_dir" "junit" "$TEST_BUNDLE" -o junit + run_case "$version_label" "$bin" "$out_dir" "xml_tests" "$TEST_BUNDLE" -o xml + run_case "$version_label" "$bin" "$out_dir" "xml_coverage" "$TEST_BUNDLE" -c -o xml + run_case "$version_label" "$bin" "$out_dir" "cobertura" "$TEST_BUNDLE" -o cobertura + run_case "$version_label" "$bin" "$out_dir" "warnings" "$TEST_BUNDLE" -o warnings + run_case "$version_label" "$bin" "$out_dir" "errors" "$ERROR_BUNDLE" -o errors + run_case "$version_label" "$bin" "$out_dir" "warnings_and_errors" "$ERROR_BUNDLE" -o warnings-and-errors +} + +run_version "new" "$NEW_BIN" "$NEW_OUT_DIR" +run_version "old" "$OLD_BIN" "$OLD_OUT_DIR" + +awk -F',' ' + NR==1 {next} + { + version=$1 + key=$2 + sec=$5 + bytes=$7 + if (version=="new") { + new_sec[key]=sec + new_bytes[key]=bytes + } else if (version=="old") { + old_sec[key]=sec + old_bytes[key]=bytes + } + keys[key]=1 + } + END { + print "case,seconds_old,seconds_new,bytes_old,bytes_new" + for (k in keys) { + printf "%s,%s,%s,%s,%s\n", k, old_sec[k], new_sec[k], old_bytes[k], new_bytes[k] + } + } +' "$RAW_CSV_FILE" | sort > "$CSV_FILE" + +cat > "$MD_FILE" <<'HEADER' +# Regression Timing And Output Comparison + +| Case | New (s) | Old (s) | Delta (s) | New/Old | +|---|---:|---:|---:|---:| +HEADER + +awk -F',' ' + NR==1 {next} + { + key=$1 + os=$2+0 + ns=$3+0 + delta=ns-os + ratio=(os==0)?0:(ns/os) + printf "| %s | %.3f | %.3f | %.3f | %.3f |\n", key, ns, os, delta, ratio + } +' "$CSV_FILE" >> "$MD_FILE" + +CASE_ORDER="txt cli html md junit xml_tests xml_coverage cobertura warnings errors warnings_and_errors" + +awk -F',' -v order="$CASE_ORDER" ' + BEGIN { + split(order, ordered, " ") + } + NR==1 {next} + { + key=$1 + old_sec=$2 + new_sec=$3 + value["old",key]=old_sec + value["new",key]=new_sec + } + END { + printf "version" + for (i=1; i<=length(ordered); i++) { + if (ordered[i] != "") { + printf ",%s", ordered[i] + } + } + printf "\n" + versions[1]="new" + versions[2]="old" + for (v=1; v<=2; v++) { + ver=versions[v] + printf "%s", ver + for (i=1; i<=length(ordered); i++) { + key=ordered[i] + if (key != "") { + printf ",%s", value[ver,key] + } + } + printf "\n" + } + } +' "$CSV_FILE" > "$CHART_CSV_FILE" + +{ + echo "" + echo "## Timing Matrix" + echo "" + echo "| Version | txt | cli | html | md | junit | xml_tests | xml_coverage | cobertura | warnings | errors | warnings_and_errors |" + echo "|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|" + awk -F',' 'NR>1 {printf "| %s | %s | %s | %s | %s | %s | %s | %s | %s | %s | %s | %s |\n", $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12}' "$CHART_CSV_FILE" +} >> "$MD_FILE" + +echo "Created:" +echo "- $CSV_FILE" +echo "- $MD_FILE" +echo "- $CHART_CSV_FILE" +echo "- $NEW_OUT_DIR" +echo "- $OLD_OUT_DIR" diff --git a/testAndRunSonar.sh b/Scripts/testAndRunSonar.sh similarity index 100% rename from testAndRunSonar.sh rename to Scripts/testAndRunSonar.sh diff --git a/Tests/XcresultparserTests/CoverageAndFormatterDependencyTests.swift b/Tests/XcresultparserTests/CoverageAndFormatterDependencyTests.swift new file mode 100644 index 0000000..202c200 --- /dev/null +++ b/Tests/XcresultparserTests/CoverageAndFormatterDependencyTests.swift @@ -0,0 +1,199 @@ +import Foundation +@testable import XcresultparserLib +import Testing + +@MainActor +struct CoverageAndFormatterDependencyTests { + @Test + func testCoverageConverterMapsCoverageReportClientFailureToCouldNotLoadCoverageReport() throws { + let savedShellFactory = DependencyFactory.createShell + let savedXCResultFactory = DependencyFactory.createXCResultToolClient + let savedXCCovFactory = DependencyFactory.createXCCovClient + defer { + DependencyFactory.createShell = savedShellFactory + DependencyFactory.createXCResultToolClient = savedXCResultFactory + DependencyFactory.createXCCovClient = savedXCCovFactory + } + + let xcresultClient = StubXCResultToolClient() + xcresultClient.getTestSummaryHandler = { _ in try TestModelFactory.summary(startTime: 1, finishTime: 2) } + let xccovClient = StubXCCovClient() + xccovClient.getCoverageReportHandler = { _ in throw TestDoubleError.forced } + + DependencyFactory.createShell = { CapturingCommandline(response: Data()) } + DependencyFactory.createXCResultToolClient = { _ in xcresultClient } + DependencyFactory.createXCCovClient = { _ in xccovClient } + + do { + _ = try CoverageConverter( + with: URL(fileURLWithPath: "/tmp/fake.xcresult"), + strictPathnames: false + ) + Issue.record("Expected coverage report loading failure.") + } catch let error as CoverageConverterError { + #expect(error == .couldNotLoadCoverageReport) + } + } + + @Test + func testCoverageConverterAcceptsNormalizedCoverageTargetFromProtocolClient() throws { + let savedShellFactory = DependencyFactory.createShell + let savedXCResultFactory = DependencyFactory.createXCResultToolClient + let savedXCCovFactory = DependencyFactory.createXCCovClient + defer { + DependencyFactory.createShell = savedShellFactory + DependencyFactory.createXCResultToolClient = savedXCResultFactory + DependencyFactory.createXCCovClient = savedXCCovFactory + } + + let xcresultClient = StubXCResultToolClient() + xcresultClient.getTestSummaryHandler = { _ in try TestModelFactory.summary() } + let xccovClient = StubXCCovClient() + xccovClient.getCoverageReportHandler = { _ in TestModelFactory.coverageReport(targetNames: ["App.framework", "Lib"]) } + + DependencyFactory.createShell = { CapturingCommandline(response: Data()) } + DependencyFactory.createXCResultToolClient = { _ in xcresultClient } + DependencyFactory.createXCCovClient = { _ in xccovClient } + + let converter = try CoverageConverter( + with: URL(fileURLWithPath: "/tmp/fake.xcresult"), + coverageTargets: ["App"], + strictPathnames: false + ) + + #expect(converter.targetsInfo.contains("App.framework")) + } + + @Test + func testCoverageConverterUnknownTargetErrorIncludesAvailableTargetsFromProtocolClient() throws { + let savedShellFactory = DependencyFactory.createShell + let savedXCResultFactory = DependencyFactory.createXCResultToolClient + let savedXCCovFactory = DependencyFactory.createXCCovClient + defer { + DependencyFactory.createShell = savedShellFactory + DependencyFactory.createXCResultToolClient = savedXCResultFactory + DependencyFactory.createXCCovClient = savedXCCovFactory + } + + let xcresultClient = StubXCResultToolClient() + xcresultClient.getTestSummaryHandler = { _ in try TestModelFactory.summary() } + let xccovClient = StubXCCovClient() + xccovClient.getCoverageReportHandler = { _ in TestModelFactory.coverageReport(targetNames: ["App.framework", "Lib"]) } + + DependencyFactory.createShell = { CapturingCommandline(response: Data()) } + DependencyFactory.createXCResultToolClient = { _ in xcresultClient } + DependencyFactory.createXCCovClient = { _ in xccovClient } + + do { + _ = try CoverageConverter( + with: URL(fileURLWithPath: "/tmp/fake.xcresult"), + coverageTargets: ["Missing"], + strictPathnames: false + ) + Issue.record("Expected unknown coverage target error.") + } catch let error as CoverageConverterError { + switch error { + case let .unknownCoverageTargets(requested, available): + #expect(requested == ["Missing"]) + #expect(Set(available) == Set(["App.framework", "Lib"])) + case .couldNotLoadCoverageReport: + Issue.record("Unexpected couldNotLoadCoverageReport.") + } + } + } + + @Test + func testFormatterCanInitializeWithoutCoverageReportIfNoCoverageFilterRequested() throws { + let savedShellFactory = DependencyFactory.createShell + let savedXCResultFactory = DependencyFactory.createXCResultToolClient + let savedXCCovFactory = DependencyFactory.createXCCovClient + defer { + DependencyFactory.createShell = savedShellFactory + DependencyFactory.createXCResultToolClient = savedXCResultFactory + DependencyFactory.createXCCovClient = savedXCCovFactory + } + + let xcresultClient = StubXCResultToolClient() + xcresultClient.getBuildResultsHandler = { _ in try TestModelFactory.buildResults() } + xcresultClient.getTestSummaryHandler = { _ in try TestModelFactory.summary() } + xcresultClient.getTestsHandler = { _ in try TestModelFactory.tests() } + let xccovClient = StubXCCovClient() + xccovClient.getCoverageReportHandler = { _ in throw TestDoubleError.forced } + + DependencyFactory.createShell = { CapturingCommandline(response: Data()) } + DependencyFactory.createXCResultToolClient = { _ in xcresultClient } + DependencyFactory.createXCCovClient = { _ in xccovClient } + + let formatter = XCResultFormatter( + with: URL(fileURLWithPath: "/tmp/fake.xcresult"), + formatter: TextResultFormatter(), + coverageTargets: [] + ) + + #expect(formatter != nil) + } + + @Test + func testFormatterFailsIfCoverageFilterRequestedAndCoverageReportUnavailable() throws { + let savedShellFactory = DependencyFactory.createShell + let savedXCResultFactory = DependencyFactory.createXCResultToolClient + let savedXCCovFactory = DependencyFactory.createXCCovClient + defer { + DependencyFactory.createShell = savedShellFactory + DependencyFactory.createXCResultToolClient = savedXCResultFactory + DependencyFactory.createXCCovClient = savedXCCovFactory + } + + let xcresultClient = StubXCResultToolClient() + xcresultClient.getBuildResultsHandler = { _ in try TestModelFactory.buildResults() } + xcresultClient.getTestSummaryHandler = { _ in try TestModelFactory.summary() } + xcresultClient.getTestsHandler = { _ in try TestModelFactory.tests() } + let xccovClient = StubXCCovClient() + xccovClient.getCoverageReportHandler = { _ in throw TestDoubleError.forced } + + DependencyFactory.createShell = { CapturingCommandline(response: Data()) } + DependencyFactory.createXCResultToolClient = { _ in xcresultClient } + DependencyFactory.createXCCovClient = { _ in xccovClient } + + let formatter = XCResultFormatter( + with: URL(fileURLWithPath: "/tmp/fake.xcresult"), + formatter: TextResultFormatter(), + coverageTargets: ["App"] + ) + + #expect(formatter == nil) + } + + @Test + func testFormatterAcceptsNormalizedCoverageTargetFromProtocolClient() throws { + let savedShellFactory = DependencyFactory.createShell + let savedXCResultFactory = DependencyFactory.createXCResultToolClient + let savedXCCovFactory = DependencyFactory.createXCCovClient + defer { + DependencyFactory.createShell = savedShellFactory + DependencyFactory.createXCResultToolClient = savedXCResultFactory + DependencyFactory.createXCCovClient = savedXCCovFactory + } + + let xcresultClient = StubXCResultToolClient() + xcresultClient.getBuildResultsHandler = { _ in try TestModelFactory.buildResults() } + xcresultClient.getTestSummaryHandler = { _ in try TestModelFactory.summary() } + xcresultClient.getTestsHandler = { _ in try TestModelFactory.tests() } + let xccovClient = StubXCCovClient() + xccovClient.getCoverageReportHandler = { _ in + TestModelFactory.coverageReport(targetNames: ["App.framework"]) + } + + DependencyFactory.createShell = { CapturingCommandline(response: Data()) } + DependencyFactory.createXCResultToolClient = { _ in xcresultClient } + DependencyFactory.createXCCovClient = { _ in xccovClient } + + let formatter = XCResultFormatter( + with: URL(fileURLWithPath: "/tmp/fake.xcresult"), + formatter: TextResultFormatter(), + coverageTargets: ["App"] + ) + + #expect(formatter != nil) + } +} diff --git a/Tests/XcresultparserTests/TestSupport/CapturingCommandline.swift b/Tests/XcresultparserTests/TestSupport/CapturingCommandline.swift new file mode 100644 index 0000000..3ef87a5 --- /dev/null +++ b/Tests/XcresultparserTests/TestSupport/CapturingCommandline.swift @@ -0,0 +1,23 @@ +import Foundation +@testable import XcresultparserLib + +final class CapturingCommandline: Commandline { + let response: Data + let error: Error? + var lastProgram: String = "" + var lastArguments: [String] = [] + + init(response: Data, error: Error? = nil) { + self.response = response + self.error = error + } + + func execute(program: String, with arguments: [String], at executionPath: URL?) throws -> Data { + lastProgram = program + lastArguments = arguments + if let error { + throw error + } + return response + } +} diff --git a/Tests/XcresultparserTests/TestSupport/ToolClientTestDoubles.swift b/Tests/XcresultparserTests/TestSupport/ToolClientTestDoubles.swift new file mode 100644 index 0000000..103c129 --- /dev/null +++ b/Tests/XcresultparserTests/TestSupport/ToolClientTestDoubles.swift @@ -0,0 +1,138 @@ +import Foundation +@testable import XcresultparserLib + +enum TestDoubleError: Error { + case forced + case unimplemented +} + +final class StubXCResultToolClient: XCResultToolProviding { + var getBuildResultsHandler: (URL) throws -> XCBuildResults = { _ in throw TestDoubleError.unimplemented } + var getTestSummaryHandler: (URL) throws -> XCSummary = { _ in throw TestDoubleError.unimplemented } + var getTestsHandler: (URL) throws -> XCTests = { _ in throw TestDoubleError.unimplemented } + var getTestDetailsHandler: (URL, String) throws -> XCTestDetails = { _, _ in throw TestDoubleError.unimplemented } + var getActivitiesHandler: (URL, String) throws -> XCActivities = { _, _ in throw TestDoubleError.unimplemented } + var getMetricsHandler: (URL, String) throws -> [XCTestWithMetrics] = { _, _ in throw TestDoubleError.unimplemented } + + func getBuildResults(path: URL) throws -> XCBuildResults { + try getBuildResultsHandler(path) + } + + func getTestSummary(path: URL) throws -> XCSummary { + try getTestSummaryHandler(path) + } + + func getTests(path: URL) throws -> XCTests { + try getTestsHandler(path) + } + + func getTestDetails(path: URL, testId: String) throws -> XCTestDetails { + try getTestDetailsHandler(path, testId) + } + + func getActivities(path: URL, testId: String) throws -> XCActivities { + try getActivitiesHandler(path, testId) + } + + func getMetrics(path: URL, testId: String) throws -> [XCTestWithMetrics] { + try getMetricsHandler(path, testId) + } +} + +final class StubXCCovClient: XCCovProviding { + var getCoverageDataHandler: (URL) throws -> FileCoverage = { _ in throw TestDoubleError.unimplemented } + var getCoverageReportHandler: (URL) throws -> CoverageReport = { _ in throw TestDoubleError.unimplemented } + var getCoverageForFileHandler: (URL, String) throws -> String = { _, _ in throw TestDoubleError.unimplemented } + var getCoverageFileListHandler: (URL) throws -> [String] = { _ in throw TestDoubleError.unimplemented } + + func getCoverageData(path: URL) throws -> FileCoverage { + try getCoverageDataHandler(path) + } + + func getCoverageReport(path: URL) throws -> CoverageReport { + try getCoverageReportHandler(path) + } + + func getCoverageForFile(path: URL, filePath: String) throws -> String { + try getCoverageForFileHandler(path, filePath) + } + + func getCoverageFileList(path: URL) throws -> [String] { + try getCoverageFileListHandler(path) + } +} + +enum TestModelFactory { + static func buildResults(warningCount: Int = 0) throws -> XCBuildResults { + let json = """ + { + "analyzerWarningCount": 0, + "analyzerWarnings": [], + "destination": { + "deviceId": "device-1", + "deviceName": "My Mac" + }, + "endTime": 2.0, + "errorCount": 0, + "errors": [], + "startTime": 1.0, + "warningCount": \(warningCount), + "warnings": [] + } + """ + return try JSONDecoder().decode(XCBuildResults.self, from: Data(json.utf8)) + } + + static func summary(startTime: Double? = nil, finishTime: Double? = nil) throws -> XCSummary { + let startFragment = startTime.map { "\"startTime\": \($0)," } ?? "" + let finishFragment = finishTime.map { "\"finishTime\": \($0)," } ?? "" + let json = """ + { + "title": "Test", + "environmentDescription": "Env", + "topInsights": [], + "result": "Passed", + "totalTestCount": 0, + "passedTests": 0, + "failedTests": 0, + "skippedTests": 0, + "expectedFailures": 0, + "statistics": [], + "devicesAndConfigurations": [], + "testFailures": [], + \(startFragment) + \(finishFragment) + "placeholder": true + } + """ + return try JSONDecoder().decode(XCSummary.self, from: Data(json.utf8)) + } + + static func tests() throws -> XCTests { + let json = """ + { + "testPlanConfigurations": [], + "devices": [], + "testNodes": [] + } + """ + return try JSONDecoder().decode(XCTests.self, from: Data(json.utf8)) + } + + static func coverageReport(targetNames: [String]) -> CoverageReport { + CoverageReport( + coveredLines: 0, + executableLines: 0, + lineCoverage: 0, + targets: targetNames.map { name in + CoverageTarget( + name: name, + lineCoverage: 0, + executableLines: 0, + coveredLines: 0, + files: [] + ) + } + ) + } +} diff --git a/Tests/XcresultparserTests/XCCovClientTests.swift b/Tests/XcresultparserTests/XCCovClientTests.swift index f0decae..907f440 100644 --- a/Tests/XcresultparserTests/XCCovClientTests.swift +++ b/Tests/XcresultparserTests/XCCovClientTests.swift @@ -17,7 +17,7 @@ struct XCCovClientTests { ] } """ - let shell = CapturingXCCovShell(response: Data(json.utf8)) + let shell = CapturingCommandline(response: Data(json.utf8)) let client = XCCovClient(shell: shell) let path = URL(fileURLWithPath: "/tmp/test.xcresult") @@ -64,7 +64,7 @@ struct XCCovClientTests { ] } """ - let shell = CapturingXCCovShell(response: Data(json.utf8)) + let shell = CapturingCommandline(response: Data(json.utf8)) let client = XCCovClient(shell: shell) let path = URL(fileURLWithPath: "/tmp/test.xcresult") @@ -77,7 +77,7 @@ struct XCCovClientTests { @Test func testGetCoverageForFileUsesExpectedCommand() throws { - let shell = CapturingXCCovShell(response: Data(" 1: 1\n".utf8)) + let shell = CapturingCommandline(response: Data(" 1: 1\n".utf8)) let client = XCCovClient(shell: shell) let path = URL(fileURLWithPath: "/tmp/test.xcresult") @@ -90,7 +90,7 @@ struct XCCovClientTests { @Test func testGetCoverageFileListUsesExpectedCommand() throws { - let shell = CapturingXCCovShell(response: Data("/tmp/Foo.swift\n/tmp/Bar.swift\n".utf8)) + let shell = CapturingCommandline(response: Data("/tmp/Foo.swift\n/tmp/Bar.swift\n".utf8)) let client = XCCovClient(shell: shell) let path = URL(fileURLWithPath: "/tmp/test.xcresult") @@ -101,19 +101,3 @@ struct XCCovClientTests { #expect(shell.lastArguments == ["xccov", "view", "--archive", "--file-list", "/tmp/test.xcresult"]) } } - -private final class CapturingXCCovShell: Commandline { - let response: Data - var lastProgram: String = "" - var lastArguments: [String] = [] - - init(response: Data) { - self.response = response - } - - func execute(program: String, with arguments: [String], at executionPath: URL?) throws -> Data { - lastProgram = program - lastArguments = arguments - return response - } -} diff --git a/Tests/XcresultparserTests/XCResultToolClientTests.swift b/Tests/XcresultparserTests/XCResultToolClientTests.swift index 011aaf5..c074ecb 100644 --- a/Tests/XcresultparserTests/XCResultToolClientTests.swift +++ b/Tests/XcresultparserTests/XCResultToolClientTests.swift @@ -19,7 +19,7 @@ struct XCResultToolClientTests { "errors": [] } """ - let shell = CapturingShell(response: Data(json.utf8)) + let shell = CapturingCommandline(response: Data(json.utf8)) let client = XCResultToolClient(shell: shell) let path = URL(fileURLWithPath: "/tmp/test.xcresult") @@ -46,7 +46,7 @@ struct XCResultToolClientTests { "hasMediaAttachments": false } """ - let shell = CapturingShell(response: Data(json.utf8)) + let shell = CapturingCommandline(response: Data(json.utf8)) let client = XCResultToolClient(shell: shell) let path = URL(fileURLWithPath: "/tmp/test.xcresult") @@ -86,7 +86,7 @@ struct XCResultToolClientTests { ] } """ - let shell = CapturingShell(response: Data(json.utf8)) + let shell = CapturingCommandline(response: Data(json.utf8)) let client = XCResultToolClient(shell: shell) let result = try client.getMetrics(path: URL(fileURLWithPath: "/tmp/test.xcresult"), testId: "PerfTests/testExample()") @@ -98,7 +98,7 @@ struct XCResultToolClientTests { @Test func testGetMetricsDecodesEmptyArray() throws { - let shell = CapturingShell(response: Data("[]".utf8)) + let shell = CapturingCommandline(response: Data("[]".utf8)) let client = XCResultToolClient(shell: shell) let result = try client.getMetrics(path: URL(fileURLWithPath: "/tmp/test.xcresult"), testId: "NoPerf/test") @@ -108,7 +108,7 @@ struct XCResultToolClientTests { @Test func testGetMetricsThrowsForUnexpectedPayload() throws { - let shell = CapturingShell(response: Data("{\"foo\":\"bar\"}".utf8)) + let shell = CapturingCommandline(response: Data("{\"foo\":\"bar\"}".utf8)) let client = XCResultToolClient(shell: shell) do { @@ -126,19 +126,3 @@ struct XCResultToolClientTests { } } } - -private final class CapturingShell: Commandline { - let response: Data - var lastProgram: String = "" - var lastArguments: [String] = [] - - init(response: Data) { - self.response = response - } - - func execute(program: String, with arguments: [String], at executionPath: URL?) throws -> Data { - lastProgram = program - lastArguments = arguments - return response - } -} From 8541c1669a986ac8d0b7ba34040cea606f7f1266 Mon Sep 17 00:00:00 2001 From: Alex da Franca Date: Sun, 22 Feb 2026 19:35:24 +0100 Subject: [PATCH 11/14] Swiftformat definsive --- .../xcschemes/xcresultparser.xcscheme | 4 +++ .../CoberturaCoverageConverter.swift | 35 +++++++++---------- .../xcresultparser/CoverageConverter.swift | 4 +-- .../XCResultToolJunitXMLDataProvider.swift | 4 +-- Sources/xcresultparser/JunitXML.swift | 15 ++++---- .../XCResultToolModels/XCActivityNode.swift | 1 - .../Models/XCResultToolModels/XCBug.swift | 1 - .../XCCommonFailureInsight.swift | 1 - .../XCResultToolModels/XCDestination.swift | 2 -- .../XCDeviceAndConfigurationSummary.swift | 1 - .../XCLongestTestRunsInsight.swift | 1 - .../XCResultToolModels/XCStatistic.swift | 1 - .../XCTestNode+Extensions.swift | 4 +-- .../XCResultToolModels/XCTestNode.swift | 2 +- .../XCResultToolModels/XCTestNodeType.swift | 2 +- .../XCResultToolModels/XCTestResult.swift | 2 +- .../xcresultparser/XCResultFormatter.swift | 8 ++--- 17 files changed, 42 insertions(+), 46 deletions(-) diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/xcresultparser.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/xcresultparser.xcscheme index 884a90a..3a57c7e 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/xcresultparser.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/xcresultparser.xcscheme @@ -82,6 +82,10 @@ argument = "/Users/alex/xcodebuild_result.xcresult" isEnabled = "NO"> + + diff --git a/Sources/xcresultparser/CoberturaCoverageConverter.swift b/Sources/xcresultparser/CoberturaCoverageConverter.swift index bb7141b..6a31a9a 100644 --- a/Sources/xcresultparser/CoberturaCoverageConverter.swift +++ b/Sources/xcresultparser/CoberturaCoverageConverter.swift @@ -48,7 +48,7 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { dtd.name = "coverage" // dtd.systemID = "http://cobertura.sourceforge.net/xml/coverage-04.dtd" dtd.systemID = - "https://github.com/cobertura/cobertura/blob/master/cobertura/src/site/htdocs/xml/coverage-04.dtd" + "https://github.com/cobertura/cobertura/blob/master/cobertura/src/site/htdocs/xml/coverage-04.dtd" let rootElement = makeRootElement() @@ -91,7 +91,6 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { fileLines.append(line) } - let fileInfoInst = FileInfo(path: relativePath ?? fileName, lines: fileLines) fileInfo.append(fileInfoInst) } @@ -132,7 +131,7 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { classElement.addAttribute(XMLNode.nodeAttribute(withName: "name", stringValue: className)) classElement.addAttribute(XMLNode.nodeAttribute(withName: "filename", stringValue: file.path)) - let fileLineCoverage = Float(file.lines.filter { $0.coverage > 0 }.count) / Float(file.lines.count) + let fileLineCoverage = Float(file.lines.count(where: { $0.coverage > 0 })) / Float(file.lines.count) classElement.addAttribute(XMLNode.nodeAttribute(withName: "line-rate", stringValue: "\(fileLineCoverage)")) classElement.addAttribute(XMLNode.nodeAttribute(withName: "branch-rate", stringValue: "1.0")) classElement.addAttribute(XMLNode.nodeAttribute(withName: "complexity", stringValue: "0.0")) @@ -192,7 +191,7 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { // Helper methods for creating valid Cobertura XML structure private func createValidPackageName(from pathComponents: [Substring]) -> String { // Use original simple logic: join all path components except the filename with dots - return pathComponents[0.. String { @@ -211,7 +210,7 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { let totalLines = packageFiles.reduce(0) { $0 + $1.lines.count } let coveredLines = packageFiles.reduce(0) { total, file in - total + file.lines.filter { $0.coverage > 0 }.count + total + file.lines.count(where: { $0.coverage > 0 }) } return totalLines > 0 ? Float(coveredLines) / Float(totalLines) : 0.0 @@ -223,7 +222,7 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { conforming SGML systems and applications as defined in ISO 8879, provided this notice is included in all copies. --> - + @@ -234,47 +233,47 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/Sources/xcresultparser/CoverageConverter.swift b/Sources/xcresultparser/CoverageConverter.swift index 82b0fa4..24c5ed7 100644 --- a/Sources/xcresultparser/CoverageConverter.swift +++ b/Sources/xcresultparser/CoverageConverter.swift @@ -70,8 +70,8 @@ public class CoverageConverter { throw CoverageConverterError.couldNotLoadCoverageReport } - self.xcresultToolClient = resolvedXCResultToolClient - self.xccovClient = resolvedXCCovClient + xcresultToolClient = resolvedXCResultToolClient + xccovClient = resolvedXCCovClient resultFileURL = url coverageReport = report self.projectRoot = projectRoot diff --git a/Sources/xcresultparser/DataProviders/JunitXML/XCResultToolJunitXMLDataProvider.swift b/Sources/xcresultparser/DataProviders/JunitXML/XCResultToolJunitXMLDataProvider.swift index cfd9a2e..7e1ee35 100644 --- a/Sources/xcresultparser/DataProviders/JunitXML/XCResultToolJunitXMLDataProvider.swift +++ b/Sources/xcresultparser/DataProviders/JunitXML/XCResultToolJunitXMLDataProvider.swift @@ -107,8 +107,8 @@ struct XCResultToolJunitXMLDataProvider: JunitXMLDataProviding { } private func findConfigurationChildren(in node: XCTestNode, configuration: XCConfiguration) -> [XCTestNode] { - if node.nodeType == .testPlanConfiguration && - (node.name == configuration.configurationName || node.nodeIdentifier == configuration.configurationId) { + if node.nodeType == .testPlanConfiguration, + node.name == configuration.configurationName || node.nodeIdentifier == configuration.configurationId { return node.children ?? [] } diff --git a/Sources/xcresultparser/JunitXML.swift b/Sources/xcresultparser/JunitXML.swift index a41bb81..f6d3159 100644 --- a/Sources/xcresultparser/JunitXML.swift +++ b/Sources/xcresultparser/JunitXML.swift @@ -180,7 +180,7 @@ public struct JunitXML: XmlSerializable { guard group.identifierString.hasSuffix(".xctest") || group.subtestGroups.isEmpty else { return group.subtestGroups.reduce([XMLElement]()) { rslt, - subGroup in + subGroup in return rslt + createTestSuite( subGroup, failureSummaries: failureSummaries, @@ -236,11 +236,11 @@ public struct JunitXML: XmlSerializable { configurationName: String ) -> XMLElement { let node = testReportFormat == .sonar ? - group.sonarFileXML( - projectRoot: projectRoot, - configurationName: configurationName, - relativePathNames: relativePathNames - ) : group.testSuiteXML(numFormatter: numFormatter) + group.sonarFileXML( + projectRoot: projectRoot, + configurationName: configurationName, + relativePathNames: relativePathNames + ) : group.testSuiteXML(numFormatter: numFormatter) for thisTest in tests { let testcase = createTestCase( @@ -489,7 +489,8 @@ private extension JunitFailureSummary { let relative = relativePart(of: url, relativeTo: projectRoot) if let comps = URLComponents(url: url, resolvingAgainstBaseURL: false), let line = comps.fragment?.components(separatedBy: "&").first( - where: { $0.starts(with: "StartingLineNumber") }), + where: { $0.starts(with: "StartingLineNumber") } + ), let num = line.components(separatedBy: "=").last { value += " (\(relative):\(num))" } else { diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCActivityNode.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCActivityNode.swift index 353c0eb..41a3aa2 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCActivityNode.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCActivityNode.swift @@ -14,4 +14,3 @@ struct XCActivityNode: Codable { let attachments: [XCAttachment]? let childActivities: [XCActivityNode]? } - diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCBug.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCBug.swift index 196450c..fbd2a3b 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCBug.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCBug.swift @@ -11,4 +11,3 @@ struct XCBug: Codable { let identifier: String? let title: String? } - diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCCommonFailureInsight.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCCommonFailureInsight.swift index daf8c7d..1bcfbf5 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCCommonFailureInsight.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCCommonFailureInsight.swift @@ -12,4 +12,3 @@ struct XCCommonFailureInsight: Codable { let description: String let associatedTestIdentifiers: [String] } - diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCDestination.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCDestination.swift index 19357e6..2c3829d 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCDestination.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCDestination.swift @@ -9,5 +9,3 @@ struct XCDestination: Codable { let deviceName: String let configurationName: String } - - diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCDeviceAndConfigurationSummary.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCDeviceAndConfigurationSummary.swift index 606ffa5..5a4e19a 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCDeviceAndConfigurationSummary.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCDeviceAndConfigurationSummary.swift @@ -14,4 +14,3 @@ struct XCDeviceAndConfigurationSummary: Codable { let skippedTests: Int let expectedFailures: Int } - diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCLongestTestRunsInsight.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCLongestTestRunsInsight.swift index 8cea914..32748cc 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCLongestTestRunsInsight.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCLongestTestRunsInsight.swift @@ -17,4 +17,3 @@ struct XCLongestTestRunsInsight: Codable { let durationOfSlowTests: Double let meanTime: String } - diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCStatistic.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCStatistic.swift index f1177dc..9da64d2 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCStatistic.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCStatistic.swift @@ -10,4 +10,3 @@ struct XCStatistic: Codable { let title: String let subtitle: String } - diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCTestNode+Extensions.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCTestNode+Extensions.swift index 5c77d77..a9bef68 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCTestNode+Extensions.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCTestNode+Extensions.swift @@ -58,7 +58,7 @@ private extension String { return "\(self) [\(parameterValue)]" } - let signature = String(self[index(after: openParenIndex).. [XCTestNode] { - if node.nodeType == .testPlanConfiguration && - (node.name == configuration.configurationName || node.nodeIdentifier == configuration.configurationId) { + if node.nodeType == .testPlanConfiguration, + node.name == configuration.configurationName || node.nodeIdentifier == configuration.configurationId { return node.children ?? [] } From c16278b56605d6389f1ecd1bdfb5a7088ee7669b Mon Sep 17 00:00:00 2001 From: Alex da Franca Date: Sun, 22 Feb 2026 19:44:20 +0100 Subject: [PATCH 12/14] swiftformat --- .swiftformat | 3 +-- .../CoberturaCoverageConverter.swift | 2 +- .../xcresultparser/CoverageConverter.swift | 6 +++--- .../XCResultToolJunitXMLDataProvider.swift | 7 +++---- Sources/xcresultparser/JunitXML.swift | 21 +++++++++---------- .../XCFailureDistributionInsight.swift | 8 +++---- .../XCResultToolModels/XCTestDetails.swift | 12 +++++------ .../XCResultToolModels/XCTestFailure.swift | 6 +++--- .../XCTestNode+Extensions.swift | 7 +++---- .../XCResultToolModels/XCTestNode.swift | 6 +++--- .../CoverageReportFormat.swift | 8 +++---- .../Markdown/MDResultFormatter.swift | 2 -- .../OutputFormatting/OutputFormat.swift | 8 +++---- .../xcresultparser/XCResultFormatter.swift | 7 +++---- 14 files changed, 48 insertions(+), 55 deletions(-) diff --git a/.swiftformat b/.swiftformat index ca5996c..27ca9dd 100644 --- a/.swiftformat +++ b/.swiftformat @@ -131,6 +131,5 @@ --xcode-indentation disabled --xctest-symbols --yoda-swap always ---disable redundantMemberwiseInit,redundantProperty,conditionalAssignment --disable docComments,docCommentsBeforeModifiers,fileHeader,opaqueGenericParameters,preferKeyPath,redundantReturn,trailingCommas,wrapMultilineStatementBraces ---enable isEmpty,privateStateVariables # ,propertyTypes,unusedPrivateDeclarations +--enable isEmpty,privateStateVariables,propertyTypes,unusedPrivateDeclarations diff --git a/Sources/xcresultparser/CoberturaCoverageConverter.swift b/Sources/xcresultparser/CoberturaCoverageConverter.swift index 6a31a9a..4880fd0 100644 --- a/Sources/xcresultparser/CoberturaCoverageConverter.swift +++ b/Sources/xcresultparser/CoberturaCoverageConverter.swift @@ -67,7 +67,7 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { // Get the xccov results as a JSON. let coverageJson = try getCoverageDataAsJSON() - var fileInfo: [FileInfo] = [] + var fileInfo = [FileInfo]() for (fileName, value) in coverageJson.files { guard isTargetIncluded(forFile: fileName) else { continue diff --git a/Sources/xcresultparser/CoverageConverter.swift b/Sources/xcresultparser/CoverageConverter.swift index 24c5ed7..8f03f52 100644 --- a/Sources/xcresultparser/CoverageConverter.swift +++ b/Sources/xcresultparser/CoverageConverter.swift @@ -96,10 +96,10 @@ public class CoverageConverter { projectRoot: projectRoot ) self.excludedPaths = Set(excludedPaths) - if let summary = try? resolvedXCResultToolClient.getTestSummary(path: url) { - startTime = summary.startTime + startTime = if let summary = try? resolvedXCResultToolClient.getTestSummary(path: url) { + summary.startTime } else { - startTime = nil + nil } } diff --git a/Sources/xcresultparser/DataProviders/JunitXML/XCResultToolJunitXMLDataProvider.swift b/Sources/xcresultparser/DataProviders/JunitXML/XCResultToolJunitXMLDataProvider.swift index 7e1ee35..55b60a6 100644 --- a/Sources/xcresultparser/DataProviders/JunitXML/XCResultToolJunitXMLDataProvider.swift +++ b/Sources/xcresultparser/DataProviders/JunitXML/XCResultToolJunitXMLDataProvider.swift @@ -167,11 +167,10 @@ struct XCResultToolJunitXMLDataProvider: JunitXMLDataProviding { private func mapTest(node: XCTestNode, testClassName: String?) -> JunitTest { let result = node.result ?? .unknown - let identifier: String - if let testClassName { - identifier = "\(testClassName)/\(node.name)" + let identifier: String = if let testClassName { + "\(testClassName)/\(node.name)" } else { - identifier = node.name + node.name } return JunitTest( identifier: identifier, diff --git a/Sources/xcresultparser/JunitXML.swift b/Sources/xcresultparser/JunitXML.swift index f6d3159..237cf32 100644 --- a/Sources/xcresultparser/JunitXML.swift +++ b/Sources/xcresultparser/JunitXML.swift @@ -86,18 +86,18 @@ public struct JunitXML: XmlSerializable { ) { self.dataProvider = dataProvider var isDirectory: ObjCBool = false - if SharedInstances.fileManager.fileExists(atPath: projectRoot, isDirectory: &isDirectory), - isDirectory.boolValue == true { - self.projectRoot = URL(fileURLWithPath: projectRoot) + self.projectRoot = if SharedInstances.fileManager.fileExists(atPath: projectRoot, isDirectory: &isDirectory), + isDirectory.boolValue == true { + URL(fileURLWithPath: projectRoot) } else { - self.projectRoot = nil + nil } testReportFormat = format - if testReportFormat == .sonar { - nodeNames = NodeNames.sonarNodeNames + nodeNames = if testReportFormat == .sonar { + NodeNames.sonarNodeNames } else { - nodeNames = NodeNames.defaultNodeNames + NodeNames.defaultNodeNames } self.relativePathNames = relativePathNames } @@ -504,11 +504,10 @@ private extension JunitFailureSummary { let textNode = XMLNode(kind: .text) textNode.objectValue = value failure.addChild(textNode) - let shortMessage: String - if let producingTarget { - shortMessage = "\(issueType) in \(producingTarget): \(testCaseName)" + let shortMessage = if let producingTarget { + "\(issueType) in \(producingTarget): \(testCaseName)" } else { - shortMessage = "\(issueType): \(testCaseName)" + "\(issueType): \(testCaseName)" } failure.addAttribute(name: "message", stringValue: shortMessage) failure.addAttribute(name: "type", stringValue: issueType) diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCFailureDistributionInsight.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCFailureDistributionInsight.swift index cef042b..5029ce3 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCFailureDistributionInsight.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCFailureDistributionInsight.swift @@ -24,13 +24,13 @@ extension XCFailureDistributionInsight { title = try values.decode(String.self, forKey: .title) distributionPercent = try values.decode(Double.self, forKey: .distributionPercent) associatedTestIdentifiers = try values.decode([String].self, forKey: .associatedTestIdentifiers) - if let intImpact = try? values.decode(Int.self, forKey: .impact) { - impact = intImpact + impact = if let intImpact = try? values.decode(Int.self, forKey: .impact) { + intImpact } else if let stringImpact = try? values.decode(String.self, forKey: .impact), let parsedImpact = Int(stringImpact) { - impact = parsedImpact + parsedImpact } else { - impact = 0 + 0 } bug = try? values.decode(String.self, forKey: .bug) tag = try? values.decode(String.self, forKey: .tag) diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCTestDetails.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCTestDetails.swift index 74d236b..af3a99c 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCTestDetails.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCTestDetails.swift @@ -41,15 +41,15 @@ extension XCTestDetails { devices = try values.decode([XCDevice].self, forKey: .devices) testRuns = try values.decode([XCTestNode].self, forKey: .testRuns) testResult = try values.decode(XCTestResult.self, forKey: .testResult) - if let performanceFlag = try? values.decode(Bool.self, forKey: .hasPerformanceMetrics) { - hasPerformanceMetrics = performanceFlag + hasPerformanceMetrics = if let performanceFlag = try? values.decode(Bool.self, forKey: .hasPerformanceMetrics) { + performanceFlag } else { - hasPerformanceMetrics = (try? values.decode(String.self, forKey: .hasPerformanceMetrics)) == "true" + (try? values.decode(String.self, forKey: .hasPerformanceMetrics)) == "true" } - if let mediaFlag = try? values.decode(Bool.self, forKey: .hasMediaAttachments) { - hasMediaAttachments = mediaFlag + hasMediaAttachments = if let mediaFlag = try? values.decode(Bool.self, forKey: .hasMediaAttachments) { + mediaFlag } else { - hasMediaAttachments = (try? values.decode(String.self, forKey: .hasMediaAttachments)) == "true" + (try? values.decode(String.self, forKey: .hasMediaAttachments)) == "true" } testIdentifierURL = try? values.decode(String.self, forKey: .testIdentifierURL) durationInSeconds = try? values.decode(Double.self, forKey: .durationInSeconds) diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCTestFailure.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCTestFailure.swift index c3bbadd..dab86fe 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCTestFailure.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCTestFailure.swift @@ -26,10 +26,10 @@ extension XCTestFailure { testIdentifier = try values.decode(Int64.self, forKey: .testIdentifier) testIdentifierString = try values.decode(String.self, forKey: .testIdentifierString) let testIdentifierURLString = try? values.decode(String.self, forKey: .testIdentifierURL) - if let testIdentifierURLString { - testIdentifierURL = URL(string: testIdentifierURLString) + testIdentifierURL = if let testIdentifierURLString { + URL(string: testIdentifierURLString) } else { - testIdentifierURL = nil + nil } } } diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCTestNode+Extensions.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCTestNode+Extensions.swift index a9bef68..d982d02 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCTestNode+Extensions.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCTestNode+Extensions.swift @@ -14,11 +14,10 @@ extension XCTestNode { } func mapArgumentTest(argument: XCTestNode, testClassName: String?) -> MappedArgumentTest { - let baseIdentifier: String - if let testClassName { - baseIdentifier = "\(testClassName)/\(name)" + let baseIdentifier: String = if let testClassName { + "\(testClassName)/\(name)" } else { - baseIdentifier = name + name } return MappedArgumentTest( identifier: baseIdentifier.formatWithParameter(argument.name), diff --git a/Sources/xcresultparser/Models/XCResultToolModels/XCTestNode.swift b/Sources/xcresultparser/Models/XCResultToolModels/XCTestNode.swift index e562d5e..6642f41 100644 --- a/Sources/xcresultparser/Models/XCResultToolModels/XCTestNode.swift +++ b/Sources/xcresultparser/Models/XCResultToolModels/XCTestNode.swift @@ -34,10 +34,10 @@ extension XCTestNode { result = try? values.decode(XCTestResult.self, forKey: .result) nodeIdentifier = try? values.decode(String.self, forKey: .nodeIdentifier) let nodeIdentifierURLString = try? values.decode(String.self, forKey: .nodeIdentifierURL) - if let nodeIdentifierURLString { - nodeIdentifierURL = URL(string: nodeIdentifierURLString) + nodeIdentifierURL = if let nodeIdentifierURLString { + URL(string: nodeIdentifierURLString) } else { - nodeIdentifierURL = nil + nil } children = (try? values.decode([XCTestNode].self, forKey: .children)) ?? [XCTestNode]() duration = try? values.decode(String.self, forKey: .duration) diff --git a/Sources/xcresultparser/OutputFormatting/CoverageReportFormat.swift b/Sources/xcresultparser/OutputFormatting/CoverageReportFormat.swift index 4c6fdd8..9d76363 100644 --- a/Sources/xcresultparser/OutputFormatting/CoverageReportFormat.swift +++ b/Sources/xcresultparser/OutputFormatting/CoverageReportFormat.swift @@ -7,11 +7,11 @@ public enum CoverageReportFormat: String { case methods, classes, targets, totals public init(string: String?) { - if let input = string?.lowercased(), - let fmt = CoverageReportFormat(rawValue: input) { - self = fmt + self = if let input = string?.lowercased(), + let fmt = CoverageReportFormat(rawValue: input) { + fmt } else { - self = .methods + .methods } } } diff --git a/Sources/xcresultparser/OutputFormatting/Formatters/Markdown/MDResultFormatter.swift b/Sources/xcresultparser/OutputFormatting/Formatters/Markdown/MDResultFormatter.swift index f40cbe9..5ada78c 100644 --- a/Sources/xcresultparser/OutputFormatting/Formatters/Markdown/MDResultFormatter.swift +++ b/Sources/xcresultparser/OutputFormatting/Formatters/Markdown/MDResultFormatter.swift @@ -7,8 +7,6 @@ import Foundation public struct MDResultFormatter: XCResultFormatting { - private let indentWidth = " " - public let testFailIcon = "🔴  " public let testPassIcon = "🟢  " public let testSkipIcon = "⚪️  " diff --git a/Sources/xcresultparser/OutputFormatting/OutputFormat.swift b/Sources/xcresultparser/OutputFormatting/OutputFormat.swift index 4f4513b..762630d 100644 --- a/Sources/xcresultparser/OutputFormatting/OutputFormat.swift +++ b/Sources/xcresultparser/OutputFormatting/OutputFormat.swift @@ -12,11 +12,11 @@ public enum OutputFormat: String { case warningsAndErrors = "warnings-and-errors" public init(string: String?) { - if let input = string?.lowercased(), - let fmt = OutputFormat(rawValue: input) { - self = fmt + self = if let input = string?.lowercased(), + let fmt = OutputFormat(rawValue: input) { + fmt } else { - self = .cli + .cli } } } diff --git a/Sources/xcresultparser/XCResultFormatter.swift b/Sources/xcresultparser/XCResultFormatter.swift index d63ffc8..a014153 100644 --- a/Sources/xcresultparser/XCResultFormatter.swift +++ b/Sources/xcresultparser/XCResultFormatter.swift @@ -627,11 +627,10 @@ public struct XCResultFormatter { private func mapTest(node: XCTestNode, testClassName: String?) -> FormattedTest { let result = node.result ?? .unknown - let identifier: String - if let testClassName { - identifier = "\(testClassName)/\(node.name)" + let identifier: String = if let testClassName { + "\(testClassName)/\(node.name)" } else { - identifier = node.name + node.name } return FormattedTest( identifier: identifier, From 2e62561ccc49f2e2e986dcdde534ed32c5129744 Mon Sep 17 00:00:00 2001 From: Alex da Franca Date: Mon, 23 Feb 2026 10:40:50 +0100 Subject: [PATCH 13/14] Fix issues found by claude --- .gitignore | 1 + .../xcresultparser/CoverageConverter.swift | 5 ++- .../XCResultToolJunitXMLDataProvider.swift | 26 +--------------- .../Extensions/String+MD5.swift | 5 +++ Sources/xcresultparser/JunitXML.swift | 7 +++++ .../Models/CodeClimate/Issue.swift | 2 +- .../Formatters/HTML/HTMLResultFormatter.swift | 10 +++++- .../SharedTypes/FailureMessageDetail.swift | 31 +++++++++++++++++++ .../SharedTypes/Services/Shell.swift | 4 +++ .../xcresultparser/XCResultFormatter.swift | 29 ++--------------- 10 files changed, 65 insertions(+), 55 deletions(-) create mode 100644 Sources/xcresultparser/SharedTypes/FailureMessageDetail.swift diff --git a/.gitignore b/.gitignore index 8ea20a5..43abdb5 100644 --- a/.gitignore +++ b/.gitignore @@ -94,6 +94,7 @@ iOSInjectionProject/ product/ AgentNotes/ TestResults/ +CLAUDE.md .scannerwork/ diff --git a/Sources/xcresultparser/CoverageConverter.swift b/Sources/xcresultparser/CoverageConverter.swift index 8f03f52..89f45ea 100644 --- a/Sources/xcresultparser/CoverageConverter.swift +++ b/Sources/xcresultparser/CoverageConverter.swift @@ -10,6 +10,7 @@ import Foundation public enum CoverageConverterError: LocalizedError, Equatable { case couldNotLoadCoverageReport case unknownCoverageTargets(requested: [String], available: [String]) + case notImplemented public var errorDescription: String? { switch self { @@ -19,6 +20,8 @@ public enum CoverageConverterError: LocalizedError, Equatable { let requestedList = requested.joined(separator: ", ") let availableList = available.joined(separator: ", ") return "Unknown coverage target(s): \(requestedList). Available targets: \(availableList)" + case .notImplemented: + return "xmlString(quiet:) must be implemented by a CoverageConverter subclass." } } } @@ -104,7 +107,7 @@ public class CoverageConverter { } public func xmlString(quiet: Bool) throws -> String { - fatalError("xmlString is not implemented") + throw CoverageConverterError.notImplemented } public var targetsInfo: String { diff --git a/Sources/xcresultparser/DataProviders/JunitXML/XCResultToolJunitXMLDataProvider.swift b/Sources/xcresultparser/DataProviders/JunitXML/XCResultToolJunitXMLDataProvider.swift index 55b60a6..d19a7d9 100644 --- a/Sources/xcresultparser/DataProviders/JunitXML/XCResultToolJunitXMLDataProvider.swift +++ b/Sources/xcresultparser/DataProviders/JunitXML/XCResultToolJunitXMLDataProvider.swift @@ -238,7 +238,7 @@ struct XCResultToolJunitXMLDataProvider: JunitXMLDataProviding { if node.nodeType == .failureMessage, let currentIdentifier, - let detail = parseFailureMessage(node.name) { + let detail = FailureMessageDetail(from: node.name) { result[currentIdentifier, default: []].append(detail) } @@ -259,26 +259,6 @@ struct XCResultToolJunitXMLDataProvider: JunitXMLDataProviding { return node.name } - private func parseFailureMessage(_ raw: String) -> FailureMessageDetail? { - let parts = raw.split(separator: ":", maxSplits: 2, omittingEmptySubsequences: false) - guard parts.count == 3 else { - return nil - } - let file = String(parts[0]) - let line = String(parts[1]).trimmingCharacters(in: .whitespacesAndNewlines) - guard !line.isEmpty, line.allSatisfy(\.isNumber) else { - return nil - } - let message = String(parts[2]).trimmingCharacters(in: .whitespacesAndNewlines) - guard !file.isEmpty, !message.isEmpty else { - return nil - } - return FailureMessageDetail( - message: message, - documentLocation: "\(file):\(line)" - ) - } - private func bestFailureMessage( for failure: XCTestFailure, in candidates: [FailureMessageDetail] @@ -291,7 +271,3 @@ struct XCResultToolJunitXMLDataProvider: JunitXMLDataProviding { } } -private struct FailureMessageDetail { - let message: String - let documentLocation: String -} diff --git a/Sources/xcresultparser/Extensions/String+MD5.swift b/Sources/xcresultparser/Extensions/String+MD5.swift index 432d0c4..be67cdf 100644 --- a/Sources/xcresultparser/Extensions/String+MD5.swift +++ b/Sources/xcresultparser/Extensions/String+MD5.swift @@ -12,4 +12,9 @@ extension String { let digest = Insecure.MD5.hash(data: Data(utf8)) return digest.map { String(format: "%02hhx", $0) }.joined() } + + func sha256() -> String { + let digest = SHA256.hash(data: Data(utf8)) + return digest.map { String(format: "%02hhx", $0) }.joined() + } } diff --git a/Sources/xcresultparser/JunitXML.swift b/Sources/xcresultparser/JunitXML.swift index 237cf32..183fab8 100644 --- a/Sources/xcresultparser/JunitXML.swift +++ b/Sources/xcresultparser/JunitXML.swift @@ -345,6 +345,7 @@ extension JunitTest { } private extension JunitTestGroup { + private static let cacheLock = NSLock() private static var cachedPathnames = [String: String]() struct TestMetrics { @@ -383,6 +384,8 @@ private extension JunitTestGroup { // only used in unit testing static func resetCachedPathnames() { + cacheLock.lock() + defer { cacheLock.unlock() } cachedPathnames.removeAll() } @@ -390,6 +393,8 @@ private extension JunitTestGroup { guard !fileName.contains("/") else { return fileName } + cacheLock.lock() + defer { cacheLock.unlock() } let candidates = cachedPathnames.values.filter { $0.hasSuffix("/\(fileName)") || $0 == fileName } guard !candidates.isEmpty else { return nil @@ -405,6 +410,8 @@ private extension JunitTestGroup { guard let projectRootUrl else { return identifierString } + Self.cacheLock.lock() + defer { Self.cacheLock.unlock() } if Self.cachedPathnames.isEmpty { cacheAllClassNames(in: projectRootUrl, relativePathNames: relativePathNames) } diff --git a/Sources/xcresultparser/Models/CodeClimate/Issue.swift b/Sources/xcresultparser/Models/CodeClimate/Issue.swift index 7606148..dc2326e 100644 --- a/Sources/xcresultparser/Models/CodeClimate/Issue.swift +++ b/Sources/xcresultparser/Models/CodeClimate/Issue.swift @@ -52,7 +52,7 @@ extension Issue { self.severity = severity engineName = "Xcode Result Bundle Tool" location = IssueLocation(issueLocationInfo: issueLocationInfo, projectRoot: projectRoot) - fingerprint = "\(issueSummary.issueType)-\(issueSummary.message)-\(location.fingerprint)".md5() + fingerprint = "\(issueSummary.issueType)-\(issueSummary.message)-\(location.fingerprint)".sha256() type = .issue categories = [] content = IssueContent(body: "\(issueSummary.issueType) • \(issueSummary.message)") diff --git a/Sources/xcresultparser/OutputFormatting/Formatters/HTML/HTMLResultFormatter.swift b/Sources/xcresultparser/OutputFormatting/Formatters/HTML/HTMLResultFormatter.swift index 12aea39..74381e3 100644 --- a/Sources/xcresultparser/OutputFormatting/Formatters/HTML/HTMLResultFormatter.swift +++ b/Sources/xcresultparser/OutputFormatting/Formatters/HTML/HTMLResultFormatter.swift @@ -144,6 +144,14 @@ public struct HTMLResultFormatter: XCResultFormatting { return node } + private func htmlEncoded(_ string: String) -> String { + string + .replacingOccurrences(of: "&", with: "&") + .replacingOccurrences(of: "<", with: "<") + .replacingOccurrences(of: ">", with: ">") + .replacingOccurrences(of: "\"", with: """) + } + // swiftlint:disable:next function_body_length private func htmlDocStart(with title: String) -> String { """ @@ -151,7 +159,7 @@ public struct HTMLResultFormatter: XCResultFormatting { - \(title) + \(htmlEncoded(title))