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))")
@jmiltner
Copy link

No, not yet - luckily we're still building with Xcode 10...

@cristianbanarescu
Copy link

Oh, i see. Well, I tried and did not succeed and I switched to using Slather framework. I noticed that is faster than using XCOV and some script to transform JSON to XML for Cobertura. Slather directly outputs the xml file that you want :)

@jmiltner
Copy link

Yes, we've been using slather in other projects, but it requires an additional install - the beauty of your script is it just uses what's available anyway... Thanks for that, btw.

@cristianbanarescu
Copy link

Actually, it is not my script, but that's ok. Well, I guess you can continue using the script. I stopped using it since XCode 11 caused me some problems and I think that using Slather ( even if it means installing it ) is faster :)

@Speakus
Copy link

Speakus commented Jan 7, 2020

@cristianbanarescu could you share the slather command which works for you? I was unable to use slather since Xcode 11 :(

@ollitapa
Copy link

ollitapa commented Jan 8, 2020

I'm running these commands and it works just fine with Xcode 11.3

xcrun xccov view --report --json /Users/olli/Library/Developer/Xcode/DerivedData/MyProject-bydisxbacbtoxnfsrziycoktswmr/Logs/Test/Test-MyProject-2020.01.07_14-40-33-+0200.xcresult > sonar-reports/coverage.json
xcrun swift xccov-json-to-cobertura-xml.swift sonar-reports/coverage.json > sonar-reports/cobertura.xml

@cristianbanarescu
Copy link

cristianbanarescu commented Jan 8, 2020

@Speakus

Before the 'slather' command, you need to use the 'scan' command, like this:

scan(scheme: "SchemeName",
code_coverage: true,
clean: true,
workspace: "./MyApp.xcworkspace",
derived_data_path: "../testOutput/",
skip_build: true)

slather(build_directory: "../testOutput/",
scheme: "SchemeName",
workspace: "./MyApp.xcworkspace",
proj: "./MyApp.xcodeproj",
output_directory: "../testOutput/slatherOutput",
cobertura_xml: true,
ignore: ["Pods/", "ThirdParty/", “Frameworks/*"])

For this 'ignore' part, please note the after the '/', you need to add an asterisk ( * ) in order to ignore all the subfolders for the specified folder. It seems like the comments in github ignore the '*' that i have put inside the 'ignore' parameter

Also, please note that the slather command MUST be run inside the same folder where you will run the 'scan' command

@nfrydenholm
Copy link

I'm running these commands and it works just fine with Xcode 11.3

xcrun xccov view --report --json /Users/olli/Library/Developer/Xcode/DerivedData/MyProject-bydisxbacbtoxnfsrziycoktswmr/Logs/Test/Test-MyProject-2020.01.07_14-40-33-+0200.xcresult > sonar-reports/coverage.json
xcrun swift xccov-json-to-cobertura-xml.swift sonar-reports/coverage.json > sonar-reports/cobertura.xml

@ollitapa I see you put both these files in a "sonar-reports" folder, so I assume you are using Sonarqube?
Have you found a way to have it accept code coverage either in the json format produced by xccov, or in the cobertura xml format? Or do you transform it into the "generic sonar coverage" format?

@ollitapa
Copy link

@nfrydenholm I'm using https://github.com/Idean/sonar-swift plugin. It accepts the format directly. You can take a look at my sonar confs here: https://gist.github.com/ollitapa/57bbd6911d6a1c421e1f51cf5f653e6b The junit file is done by Trainer https://github.com/fastlane-community/trainer

@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