Skip to content

Instantly share code, notes, and snippets.

@csaby02
Created November 20, 2018 14:16
Show Gist options
  • Star 22 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • 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))")
@ollitapa
Copy link

WOW, this is really great!! You should make this to own project! Works already better than Slather for me!

@cristianbanarescu
Copy link

I am having troubles with this script when using XCode 11 to generate code coverage reports. The structure of xccov coverage report generated by the command line tool has changed.

@ollitapa
Copy link

Json generation changed a bit. Try

xcrun xccov view --report --json /path/to/xcresult > coverage.json

@cristianbanarescu
Copy link

I am getting: invalid option: --report :(

@ollitapa
Copy link

It should be right:

xccov view --report [--only-targets | --files-for-target <target name> | --functions-for-file <name or path>] [--json] result_bundle.xcresult

Maybe you have the old Xcode command line tools still selected?

@cristianbanarescu
Copy link

I have installed the recent version of XCode command line tools and ran the following command:

xcrun xcov view --report --json Run-MyApp-2019.10.24_17-33-06-+0300.xcresult/ > coverage.json

I am still getting that error ( invalid option: --report ) :(

@ollitapa
Copy link

What do you get for versions? Mine are:

➜ xcodebuild -version
Xcode 11.1
Build version 11A1027

and

➜ xcrun --version
xcrun version 48.

@cristianbanarescu
Copy link

Yep. Mine are the same as yours. I am guessing you are running this commands locally and not on CI server, right ?

@ollitapa
Copy link

Yeah, I've got a CI-server that I control, so I know it has the same Xcode installed. I'm sorry I'm not able to help more.

@cristianbanarescu
Copy link

It's okay. Thank you anyway! Have a nice day!

@jmiltner
Copy link

jmiltner commented Nov 15, 2019

FYI - I noticed this will produce empty coverage results when uploading the generated XML to AzureDevOps. Checking the XML, I noticed that the <classes> container is missing. After adding it, AzureDevOps was happy with the results:

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)
  currentClassesElement = XMLElement(name: "classes")  //<-- add <classes> container
  currentPackageElement.addChild(currentClassesElement)
}

let classElement = XMLElement(name: "class")
classElement.addAttribute(XMLNode.attribute(withName: "name", stringValue: className) 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)
currentClassesElement.addChild(classElement)  //<-- add class element to <classes> container

@cristianbanarescu
Copy link

Thanks! Also, did you try running this script on XCode 11 generated files ? I don't think it will work on those files :(

@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