Skip to content

Instantly share code, notes, and snippets.

@csaby02
Created November 20, 2018 14:16
Show Gist options
  • Save csaby02/ab2441715a89865a7e8e29804df23dc6 to your computer and use it in GitHub Desktop.
Save csaby02/ab2441715a89865a7e8e29804df23dc6 to your computer and use it in GitHub Desktop.
#!/usr/bin/env xcrun swift
/**
USAGE
<script> filePath -targetsToExclude Target1 Target2 -packagesToExclude Package1 Package2
*/
import Foundation
// This script should be called with at least 1 parameter
if CommandLine.arguments.count < 2 {
exit(0)
}
// First parameter should be the file path of the xccov generated json file
let filePath = CommandLine.arguments[1]
let targetsToExcludeKey = "-targetsToExclude"
let packagesToExcludeKey = "-packagesToExclude"
var targetsToExclude = [String]()
var packagesToExclude = [String]()
if CommandLine.arguments.count > 2 {
let parameters = Array(CommandLine.arguments[2..<CommandLine.arguments.count])
let targetsKeyIndex = parameters.index(of: targetsToExcludeKey)
let packagesKeyIndex = parameters.index(of: packagesToExcludeKey)
switch (targetsKeyIndex, packagesKeyIndex) {
case let (.some(targetsIndex), .some(packagesIndex)) where targetsIndex < packagesIndex:
targetsToExclude.append(contentsOf: parameters[targetsIndex + 1..<packagesIndex])
packagesToExclude.append(contentsOf: parameters[packagesIndex + 1..<parameters.count])
case let (.some(targetsIndex), .some(packagesIndex)) where targetsIndex > packagesIndex:
targetsToExclude.append(contentsOf: parameters[packagesIndex + 1..<targetsIndex])
packagesToExclude.append(contentsOf: parameters[targetsIndex + 1..<parameters.count])
case let (.some(targetsIndex), .none):
targetsToExclude.append(contentsOf: parameters[targetsIndex + 1..<parameters.count])
case let (.none, .some(packagesIndex)):
packagesToExclude.append(contentsOf: parameters[packagesIndex + 1..<parameters.count])
default:
break
}
}
/*
The structure of xccov coverage report generated by the command line tool is represented by the following structures:
*/
struct FunctionCoverageReport: Codable {
let coveredLines: Int
let executableLines: Int
let executionCount: Int
let lineCoverage: Double
let lineNumber: Int
let name: String
}
struct FileCoverageReport: Codable {
let coveredLines: Int
let executableLines: Int
let functions: [FunctionCoverageReport]
let lineCoverage: Double
let name: String
let path: String
}
struct TargetCoverageReport: Codable {
let buildProductPath: String
let coveredLines: Int
let executableLines: Int
let files: [FileCoverageReport]
let lineCoverage: Double
let name: String
}
struct CoverageReport: Codable {
let executableLines: Int
let targets: [TargetCoverageReport]
let lineCoverage: Double
let coveredLines: Int
}
// Trying to get the JSON String from the input parameter filePath
guard let json = try? String(contentsOfFile: filePath, encoding: .utf8), let data = json.data(using: .utf8) else {
exit(0)
}
// Trying to decode the JSON into CoverageReport structure
guard let report = try? JSONDecoder().decode(CoverageReport.self, from: data) else {
exit(0)
}
extension String {
func contains(elementOfArray: [String]) -> Bool {
for element in elementOfArray {
if self.contains(element) {
return true
}
}
return false
}
}
func generateCoberturaReport(from coverageReport: CoverageReport) -> String {
let currentDirectoryPath = FileManager.default.currentDirectoryPath
let dtd = try! XMLDTD(contentsOf: URL(string: "http://cobertura.sourceforge.net/xml/coverage-04.dtd")!)
dtd.name = "coverage"
dtd.systemID = "http://cobertura.sourceforge.net/xml/coverage-04.dtd"
let rootElement = XMLElement(name: "coverage")
rootElement.addAttribute(XMLNode.attribute(withName: "line-rate", stringValue: "\(coverageReport.lineCoverage)") as! XMLNode)
rootElement.addAttribute(XMLNode.attribute(withName: "branch-rate", stringValue: "1.0") as! XMLNode)
rootElement.addAttribute(XMLNode.attribute(withName: "lines-covered", stringValue: "\(coverageReport.coveredLines)") as! XMLNode)
rootElement.addAttribute(XMLNode.attribute(withName: "lines-valid", stringValue: "\(coverageReport.executableLines)") as! XMLNode)
rootElement.addAttribute(XMLNode.attribute(withName: "timestamp", stringValue: "\(Date().timeIntervalSince1970)") as! XMLNode)
rootElement.addAttribute(XMLNode.attribute(withName: "version", stringValue: "diff_coverage 0.1") as! XMLNode)
rootElement.addAttribute(XMLNode.attribute(withName: "complexity", stringValue: "0.0") as! XMLNode)
rootElement.addAttribute(XMLNode.attribute(withName: "branches-valid", stringValue: "1.0") as! XMLNode)
rootElement.addAttribute(XMLNode.attribute(withName: "branches-covered", stringValue: "1.0") as! XMLNode)
let doc = XMLDocument(rootElement: rootElement)
doc.version = "1.0"
doc.dtd = dtd
doc.documentContentKind = .xml
let sourceElement = XMLElement(name: "sources")
rootElement.addChild(sourceElement)
sourceElement.addChild(XMLElement(name: "source", stringValue: currentDirectoryPath))
let packagesElement = XMLElement(name: "packages")
rootElement.addChild(packagesElement)
var allFiles = [FileCoverageReport]()
for targetCoverageReport in report.targets {
// Filter out targets
if targetCoverageReport.name.contains(elementOfArray: targetsToExclude) {
continue
}
// Filter out files by package
let targetFiles = targetCoverageReport.files.filter { !$0.path.contains(elementOfArray: packagesToExclude) }
allFiles.append(contentsOf: targetFiles)
}
// Sort files to avoid duplicated packages
allFiles = allFiles.sorted(by: { $0.path > $1.path })
var currentPackage = ""
var currentPackageElement: XMLElement!
var isNewPackage = false
for fileCoverageReport in allFiles {
// Define file path relative to source!
let filePath = fileCoverageReport.path.replacingOccurrences(of: currentDirectoryPath + "/", with: "")
let pathComponents = filePath.split(separator: "/")
let packageName = pathComponents[0..<pathComponents.count - 1].joined(separator: ".")
isNewPackage = currentPackage != packageName
if isNewPackage {
currentPackageElement = XMLElement(name: "package")
packagesElement.addChild(currentPackageElement)
}
currentPackage = packageName
if isNewPackage {
currentPackageElement.addAttribute(XMLNode.attribute(withName: "name", stringValue: packageName) as! XMLNode)
currentPackageElement.addAttribute(XMLNode.attribute(withName: "line-rate", stringValue: "\(fileCoverageReport.lineCoverage)") as! XMLNode)
currentPackageElement.addAttribute(XMLNode.attribute(withName: "branch-rate", stringValue: "1.0") as! XMLNode)
currentPackageElement.addAttribute(XMLNode.attribute(withName: "complexity", stringValue: "0.0") as! XMLNode)
}
let classElement = XMLElement(name: "class")
classElement.addAttribute(XMLNode.attribute(withName: "name", stringValue: "\(packageName).\((fileCoverageReport.name as NSString).deletingPathExtension)") as! XMLNode)
classElement.addAttribute(XMLNode.attribute(withName: "filename", stringValue: "\(filePath)") as! XMLNode)
classElement.addAttribute(XMLNode.attribute(withName: "line-rate", stringValue: "\(fileCoverageReport.lineCoverage)") as! XMLNode)
classElement.addAttribute(XMLNode.attribute(withName: "branch-rate", stringValue: "1.0") as! XMLNode)
classElement.addAttribute(XMLNode.attribute(withName: "complexity", stringValue: "0.0") as! XMLNode)
currentPackageElement.addChild(classElement)
let linesElement = XMLElement(name: "lines")
classElement.addChild(linesElement)
for functionCoverageReport in fileCoverageReport.functions {
for index in 0..<functionCoverageReport.executableLines {
// Function coverage report won't be 100% reliable without parsing it by file (would need to use xccov view --file filePath currentDirectory + Build/Logs/Test/*.xccovarchive)
let lineElement = XMLElement(kind: .element, options: .nodeCompactEmptyElement)
lineElement.name = "line"
lineElement.addAttribute(XMLNode.attribute(withName: "number", stringValue: "\(functionCoverageReport.lineNumber + index)") as! XMLNode)
lineElement.addAttribute(XMLNode.attribute(withName: "branch", stringValue: "false") as! XMLNode)
let lineHits: Int
if index < functionCoverageReport.coveredLines {
lineHits = functionCoverageReport.executionCount
} else {
lineHits = 0
}
lineElement.addAttribute(XMLNode.attribute(withName: "hits", stringValue: "\(lineHits)") as! XMLNode)
linesElement.addChild(lineElement)
}
}
}
return doc.xmlString(options: [.nodePrettyPrint])
}
print("\(generateCoberturaReport(from: report))")
@nfrydenholm
Copy link

Thanks for the info @ollitapa 👍

@twittemb
Copy link

twittemb commented Jul 25, 2020

If this can help, I've made a tool based on that script that can be installed with brew to be more CI friendly. I have also structured the code so it can be easily extended with new output formats:

https://github.com/twittemb/XcodeCoverageConverter

Thanks @csaby02 for his original code.

@Kylmakalle
Copy link

This script may not work with Gitlab due to missing classes XML tag.
Read more here for info and solution - https://gist.github.com/aYukiWatanabe/2d3211e81104bda20b1850a90e1366d2#gistcomment-3944707

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment