-
-
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))") |
No, not yet - luckily we're still building with Xcode 10...
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 :)
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.
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 :)
@cristianbanarescu could you share the slather command which works for you? I was unable to use slather since Xcode 11 :(
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
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
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?
@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
Thanks for the info @ollitapa 👍
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.
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
Thanks! Also, did you try running this script on XCode 11 generated files ? I don't think it will work on those files :(