Skip to content

Instantly share code, notes, and snippets.

@swhitty
Created January 24, 2025 06:29
Show Gist options
  • Save swhitty/49b2a78ac167cb06729de30a98a09dde to your computer and use it in GitHub Desktop.
Save swhitty/49b2a78ac167cb06729de30a98a09dde to your computer and use it in GitHub Desktop.
Script to convert Xcode 16 results JSON in to Junit XML
#!/usr/bin/swift
import Foundation
let resultsJSON = CommandLine.arguments.count > 1 ? URL(filePath: CommandLine.arguments[1]) : nil
guard let resultsJSON else {
print("usage: ./junit.swift <test_results.json>")
exit(70)
}
let results = try JSONDecoder().decode(TestResults.self, from: Data(contentsOf: resultsJSON))
print(makeJunit(from: results))
func makeJunit(from results: TestResults) -> String {
let suites = results.childNodes(ofType: .testSuite)
let allTests = results.childNodes(ofType: .testCase)
let allFailed = allTests.filter { $0.node.result == "Failed" }
var lines = [String]()
lines.append(#"<testsuites name="All tests" tests="\#(allTests.count)" failures="\#(allFailed.count)">"#)
for (path, suite) in suites {
let fullName = [path, suite.name].compactMap { $0 }.joined(separator: ".")
let tests = suite.childNodes(ofType: .testCase)
let failed = tests.filter { $0.node.result == "Failed" }
lines.append(#" <testsuite name="\#(fullName)" tests="\#(tests.count)" failures="\#(failed.count)">"#)
for (_, test) in tests {
let duration = test.duration?.dropLast() ?? ""
let failures = test.childNodes(ofType: .testFailure)
if !failures.isEmpty {
lines.append(#" <testcase classname="\#(fullName)" name="\#(test.name)" time="\#(duration)">"#)
if let details = test.details {
lines.append(#" <failure message="\#(details)" />"#)
}
for (_, failure) in failures {
let message = failure.name
.replacingOccurrences(of: "&", with: "&amp;")
.replacingOccurrences(of: "\"", with: "&quot;")
.replacingOccurrences(of: "<", with: "&lt;")
.replacingOccurrences(of: ">", with: "&gt;")
lines.append(#" <failure message="\#(message)" />"#)
}
lines.append(#" </testcase>"#)
}
if test.result == "Passed" {
lines.append(#" <testcase classname="\#(fullName)" name="\#(test.name)" time="\#(duration)" />"#)
}
}
lines.append(#" </testsuite>"#)
}
lines.append(#"</testsuites>"#)
return lines.joined(separator: "\n")
}
struct TestResults: Decodable {
var testNodes: [Node]
struct Node: Decodable {
var name: String
var nodeType: NodeType
var result: String
var children: [Node]?
var details: String?
var duration: String?
var nodeIdentifier: String?
}
enum NodeType: String, Decodable {
case testPlan = "Test Plan"
case testBundle = "Unit test bundle"
case testSuite = "Test Suite"
case testCase = "Test Case"
case testFailure = "Failure Message"
case testRepetition = "Repetition"
case unknown = "unknown"
init(from decoder: Decoder) throws {
let value = try decoder.singleValueContainer().decode(String.self)
self = .init(rawValue: value) ?? .unknown
}
}
}
extension TestResults {
func childNodes(ofType type: NodeType) -> [(path: String?, node: TestResults.Node)] {
testNodes.flatMap { $0.childNodes(ofType: type) }
}
}
extension TestResults.Node {
func childNodes(ofType type: TestResults.NodeType, path: String? = nil) -> [(path: String?, node: TestResults.Node)] {
var nodes = [(path: String?, node: TestResults.Node)]()
for child in (children ?? []) {
if child.nodeType == type {
nodes.append((path: path, node: child))
}
let newPath = [path, child.name].compactMap { $0 }.joined(separator: ".")
nodes.append(contentsOf: child.childNodes(ofType: type, path: newPath))
}
return nodes
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment