├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── contents.xcworkspacedata
│ └── xcshareddata
│ └── xcschemes
│ └── XcodeCoverageConverter.xcscheme
├── LICENSE
├── Makefile
├── Package.resolved
├── Package.swift
├── README.md
├── Sources
├── Core
│ ├── Commands
│ │ ├── GenerateCommand.swift
│ │ └── Xccov+Command.swift
│ ├── Commons
│ │ ├── Models
│ │ │ ├── CoverageReport.swift
│ │ │ ├── Export.swift
│ │ │ ├── FileCoverageReport.swift
│ │ │ ├── FunctionCoverageReport.swift
│ │ │ ├── Import.swift
│ │ │ └── TargetCoverageReport.swift
│ │ └── Tools
│ │ │ ├── Export+Write.swift
│ │ │ ├── Import+Read.swift
│ │ │ ├── Result+MapEach.swift
│ │ │ ├── Result+Verbose.swift
│ │ │ ├── String+Contains.swift
│ │ │ ├── String+Read.swift
│ │ │ └── String+Write.swift
│ ├── Converters
│ │ ├── CoberturaXmlConverter.swift
│ │ ├── FailableConverter.swift
│ │ ├── SonarqubeXmlConverter.swift
│ │ ├── XMLNode+NodeAttribute.swift
│ │ └── Xccov+Converter.swift
│ ├── Decoders
│ │ ├── JsonDecoder.swift
│ │ └── Xccov+Decoder.swift
│ ├── Filters
│ │ ├── PackagesFilter.swift
│ │ ├── TargetsFilter.swift
│ │ └── Xccov+Filter.swift
│ └── Xccov.swift
├── Resources
│ ├── Bundled
│ │ ├── Resources.swift
│ │ └── coverage-04.dtd
│ ├── Embedded
│ │ └── Resources.c
│ └── Main
│ │ └── Resources.swift
└── XcodeCoverageConverter
│ └── main.swift
└── Tests
├── CoreTests
├── Commands
│ ├── Fixtures
│ │ └── CommandCoverageJson.swift
│ └── GenerateCommandTests.swift
├── Commons
│ └── Tools
│ │ ├── Result+MapEachTests.swift
│ │ ├── Result+VerboseTests.swift
│ │ └── String+ContainsTests.swift
├── Converters
│ ├── CoberturaXmlConverterTests.swift
│ ├── Fixtures
│ │ ├── ConverterCoverageJson.swift
│ │ ├── ConverterCoverageXml.swift
│ │ └── SonarqubeCoverageXml.swift
│ └── SonarqubeXmlConverterTests.swift
├── Decoders
│ ├── Fixtures
│ │ └── DecoderCoverageJson.swift
│ └── JsonDecoderTests.swift
├── Filters
│ ├── PackagesFilterTests.swift
│ └── TargetsFilterTests.swift
└── XCTestManifests.swift
└── LinuxMain.swift
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on: [push]
4 |
5 | jobs:
6 | SPM:
7 | name: "Swift Package Manager"
8 | runs-on: macOS-latest
9 | steps:
10 | - uses: actions/checkout@v2
11 | - name: Run tests
12 | run: set -o pipefail && swift test
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /DerivedData
3 | /.build
4 | /Packages
5 | /*.xcodeproj
6 | xcuserdata/
7 | *.json
8 | *.xml
9 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/XcodeCoverageConverter.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
43 |
49 |
50 |
51 |
57 |
63 |
64 |
65 |
71 |
77 |
78 |
79 |
85 |
91 |
92 |
93 |
99 |
105 |
106 |
107 |
113 |
119 |
120 |
121 |
127 |
133 |
134 |
135 |
141 |
147 |
148 |
149 |
155 |
161 |
162 |
163 |
169 |
175 |
176 |
177 |
178 |
179 |
185 |
186 |
188 |
194 |
195 |
196 |
198 |
204 |
205 |
206 |
208 |
214 |
215 |
216 |
218 |
224 |
225 |
226 |
228 |
234 |
235 |
236 |
238 |
244 |
245 |
246 |
248 |
254 |
255 |
256 |
257 |
258 |
268 |
270 |
276 |
277 |
278 |
279 |
285 |
287 |
293 |
294 |
295 |
296 |
298 |
299 |
302 |
303 |
304 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Thibault Wittemberg
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | SHELL = /bin/bash
2 |
3 | prefix ?= /usr/local
4 | bindir ?= $(prefix)/bin
5 | srcdir = Sources
6 |
7 | REPODIR = $(shell pwd)
8 | BUILDDIR = $(REPODIR)/.build
9 | SOURCES = $(wildcard $(srcdir)/**/*.swift)
10 |
11 | .DEFAULT_GOAL = all
12 |
13 | .PHONY: all
14 | all: Xcodecoverageconverter
15 |
16 | Xcodecoverageconverter: $(SOURCES)
17 | @swift build \
18 | -c release \
19 | --disable-sandbox \
20 | --scratch-path "$(BUILDDIR)"
21 |
22 | .PHONY: install
23 | install: Xcodecoverageconverter
24 | @install -d "$(bindir)"
25 | @install "$(BUILDDIR)/release/xcc" "$(bindir)"
26 |
27 | .PHONY: uninstall
28 | uninstall:
29 | @rm -rf "$(bindir)/xcc"
30 |
31 | .PHONY: test
32 | test:
33 | @swift test
34 |
35 | .PHONY: smoke
36 | smoke: Xcodecoverageconverter
37 | xcodebuild -scheme XcodeCoverageConverter test -derivedDataPath DerivedData -destination "platform=macOS"
38 | xcrun xccov view --report --json DerivedData/Logs/Test/*.xcresult > coverage.json
39 | leaks -atExit -- "$(BUILDDIR)"/release/xcc generate coverage.json . cobertura-xml
40 |
41 | .PHONY: clean
42 | distclean:
43 | @rm -f $(BUILDDIR)/release
44 |
45 | .PHONY: clean
46 | clean: distclean
47 | @rm -rf $(BUILDDIR)
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "swift-argument-parser",
6 | "repositoryURL": "https://github.com/apple/swift-argument-parser",
7 | "state": {
8 | "branch": null,
9 | "revision": "223d62adc52d51669ae2ee19bdb8b7d9fd6fcd9c",
10 | "version": "0.0.6"
11 | }
12 | }
13 | ]
14 | },
15 | "version": 1
16 | }
17 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "XcodeCoverageConverter",
8 | platforms: [
9 | .macOS(.v10_12),
10 | ],
11 | products: [
12 | .executable(name: "xcc", targets: ["XcodeCoverageConverter"])
13 | ],
14 | dependencies: [
15 | // Dependencies declare other packages that this package depends on.
16 | // .package(url: /* package url */, from: "1.0.0"),
17 | .package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0")
18 | ],
19 | targets: [
20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
21 | // Targets can depend on other targets in this package, and on products in packages which this package depends on.
22 | .target(
23 | name: "XcodeCoverageConverter",
24 | dependencies: ["Core", "ResourcesEmbedded"],
25 | path: "Sources/XcodeCoverageConverter"),
26 | .target(
27 | name: "Core",
28 | dependencies: [
29 | .target(name: "Resources"),
30 | .product(name: "ArgumentParser", package: "swift-argument-parser")
31 | ],
32 | path: "Sources/Core"),
33 | // Resources
34 | .target(name: "Resources",
35 | path: "Sources/Resources/Main"),
36 | .target(name: "ResourcesBundled",
37 | path: "Sources/Resources/Bundled",
38 | resources: [.copy("coverage-04.dtd")]),
39 | .target(name: "ResourcesEmbedded",
40 | path: "Sources/Resources/Embedded",
41 | linkerSettings: [.unsafeFlags(
42 | ["-Xlinker", "-sectcreate",
43 | "-Xlinker", "__DATA",
44 | "-Xlinker", "__coverage_dtd",
45 | "-Xlinker", "Sources/Resources/Bundled/coverage-04.dtd"]
46 | // verify if the file is embedded by running
47 | // `otool -X -s __DATA __coverage_dtd | xxd -rma`
48 | )]),
49 | // Tests
50 | .testTarget(
51 | name: "CoreTests",
52 | dependencies: ["Core", "ResourcesBundled"],
53 | path: "Tests/CoreTests")
54 | ]
55 | )
56 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # XcodeCoverageConverter
2 |
3 | This tool aims to convert Xcode generated code coverage data into CI friendly formats.
4 |
5 | Please execute `xcc generate --help` for all options.
6 |
7 | ### Installation
8 |
9 | `brew install twittemb/formulae/Xcodecoverageconverter`
10 |
11 | ### From xccov JSON to Cobertura XML
12 |
13 | - 1: Generate code coverage data when unit testing. You have to add the following options to the xcodebuild command line: `-derivedDataPath Build/ -enableCodeCoverage YES`
14 | - 2: Generate JSON from the code coverage data: `xcrun xccov view --report --json Build/Logs/Test/*.xcresult > coverage.json`
15 | - 3: Run xcc to convert the report into a Cobertura XML file: `/usr/local/bin/xcc generate coverage.json . cobertura-xml --exclude-packages Tests` (this command excludes the Tests package from the export)
16 |
17 | The XML output can then be uploaded to your CI provider as an artefact. It has been sucessfully tested with Azure DevOps pipelines.
18 |
19 | ### Output formats
20 |
21 | `xcc` currently supports these output formats:
22 |
23 | - cobertura XML: `cobertura-xml`
24 | - sonarqube XML: `sonarqube-xml`
25 |
26 | You can specify several output formats in the CLI `/usr/local/bin/xcc generate coverage.json . cobertura-xml sonarqube-xml`
27 |
28 | ### Contribution
29 |
30 | PR are of course welcome. To add new input or output formats, please refer to how `Decoders` and `Converters` are implemented.
31 |
32 | ### Credits
33 |
34 | This tool is based on the following gist:
35 |
36 | [https://gist.github.com/csaby02/ab2441715a89865a7e8e29804df23dc6](https://gist.github.com/csaby02/ab2441715a89865a7e8e29804df23dc6)
37 |
38 | Thanks to its author.
39 |
--------------------------------------------------------------------------------
/Sources/Core/Commands/GenerateCommand.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GenerateCommand.swift
3 | //
4 | //
5 | // Created by Thibault Wittemberg on 2020-06-02.
6 | //
7 |
8 | import ArgumentParser
9 |
10 | public extension Xccov.Commands {
11 | enum Generate {}
12 | }
13 |
14 | public extension Xccov.Commands.Generate {
15 | enum Output: String, ExpressibleByArgument {
16 | case coberturaXml = "cobertura-xml"
17 | case sonarqubeXml = "sonarqube-xml"
18 | case failable = "failable"
19 |
20 | var converter: Xccov.Converters.Converter {
21 | switch self {
22 | case .coberturaXml:
23 | return Xccov.Converters.CoberturaXml.convert(coverageReport:)
24 | case .sonarqubeXml:
25 | return Xccov.Converters.SonarqubeXml.convert(coverageReport:)
26 | case .failable:
27 | return Xccov.Converters.FailableConverter.convert(coverageReport:)
28 | }
29 | }
30 |
31 | var filename: String {
32 | switch self {
33 | case .coberturaXml:
34 | return "cobertura.xml"
35 | case .sonarqubeXml:
36 | return "sonarqube.xml"
37 | case .failable:
38 | return "failable.xml"
39 |
40 | }
41 | }
42 | }
43 |
44 | static func convert(report: CoverageReport, to outputFormat: Output) -> Result {
45 | outputFormat.converter(report).map { Export(content: $0, filename: outputFormat.filename) }
46 | }
47 |
48 | static func convert(report: CoverageReport,
49 | to outputFormats: [Output]) -> Result<[Export], Xccov.Error> {
50 | outputFormats
51 | .map { Self.convert(report: report, to: $0) }
52 | .reduce(Result.success([Export]())) { (previous, current) -> Result<[Export], Xccov.Error> in
53 | switch (previous, current) {
54 | case let (.success(exports), .success(export)):
55 | return .success(exports+[export])
56 | case (.failure(let error), _):
57 | return .failure(error)
58 | case (_, .failure(let error)):
59 | return .failure(error)
60 | }
61 | }
62 | }
63 |
64 | static func execute(jsonFile: String,
65 | outputPath: String,
66 | outputs: [Output],
67 | excludeTargets: [String],
68 | excludePackages: [String],
69 | verbose: Bool) -> Result {
70 | Import
71 | .read(filename: jsonFile)
72 | .verbose(verbose,
73 | onFailure: { failure in print("The following error occured while importing the file: \(failure)") },
74 | onSuccess: { print("\($0.filename) has been imported") })
75 | .flatMap { imported in Xccov.Decoders.Json.decode(imported: imported) }
76 | .verbose(verbose,
77 | onFailure: { failure in print("The following error occured while decoding the file: \(failure)") },
78 | onSuccess: { _ in print("The json payload has been decoded") })
79 | .map { Xccov.Filters.Targets.filter(coverageReport: $0, targetsToExclude: excludeTargets) }
80 | .verbose(verbose,
81 | onFailure: { failure in print("The following error occured while excluding targets: \(failure)") },
82 | onSuccess: { _ in print("The payload has been filtered by removing the following targets: \(excludeTargets)") })
83 | .map { Xccov.Filters.Packages.filter(coverageReport: $0, packagesToExclude: excludePackages) }
84 | .verbose(verbose,
85 | onFailure: { failure in print("The following error occured while excluding packages: \(failure)") },
86 | onSuccess: { _ in print("The file payload has been filtered by removing the following packages: \(excludePackages)") })
87 | .flatMap { Self.convert(report: $0, to: outputs) }
88 | .verbose(verbose,
89 | onFailure: { failure in print("The following error occured while converting to output formats, \(failure)") },
90 | onSuccess: { _ in print("The payload has been exported to output formats with success") })
91 | .mapEach { $0.write(atPath: outputPath) }
92 | .map { _ in () }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/Sources/Core/Commands/Xccov+Command.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Xccov+Command.swift
3 | //
4 | //
5 | // Created by Thibault Wittemberg on 2020-06-02.
6 | //
7 |
8 | import Foundation
9 |
10 | public extension Xccov {
11 | enum Commands {}
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/Core/Commons/Models/CoverageReport.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CoverageReport.swift
3 | //
4 | //
5 | // Created by Thibault Wittemberg on 2020-05-30.
6 | //
7 |
8 | public struct CoverageReport: Decodable, Equatable {
9 | public let executableLines: Int
10 | public var targets: [TargetCoverageReport]
11 | public let lineCoverage: Double
12 | public let coveredLines: Int
13 |
14 | public init(executableLines: Int,
15 | targets: [TargetCoverageReport],
16 | lineCoverage: Double,
17 | coveredLines: Int) {
18 | self.executableLines = executableLines
19 | self.targets = targets
20 | self.lineCoverage = lineCoverage
21 | self.coveredLines = coveredLines
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/Core/Commons/Models/Export.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Export.swift
3 | //
4 | //
5 | // Created by Thibault Wittemberg on 2020-06-03.
6 | //
7 |
8 | public struct Export: Equatable {
9 | public let content: String
10 | public let filename: String
11 |
12 | public init(content: String, filename: String) {
13 | self.content = content
14 | self.filename = filename
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/Core/Commons/Models/FileCoverageReport.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FileCoverageReport.swift
3 | //
4 | //
5 | // Created by Thibault Wittemberg on 2020-05-30.
6 | //
7 |
8 | public struct FileCoverageReport: Decodable, Equatable {
9 | public let coveredLines: Int
10 | public let executableLines: Int
11 | public let functions: [FunctionCoverageReport]
12 | public let lineCoverage: Double
13 | public let name: String
14 | public let path: String
15 |
16 | public init(coveredLines: Int,
17 | executableLines: Int,
18 | functions: [FunctionCoverageReport],
19 | lineCoverage: Double,
20 | name: String,
21 | path: String) {
22 | self.coveredLines = coveredLines
23 | self.executableLines = executableLines
24 | self.functions = functions
25 | self.lineCoverage = lineCoverage
26 | self.name = name
27 | self.path = path
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/Core/Commons/Models/FunctionCoverageReport.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FunctionCoverageReport.swift
3 | //
4 | //
5 | // Created by Thibault Wittemberg on 2020-05-30.
6 | //
7 |
8 | public struct FunctionCoverageReport: Decodable, Equatable {
9 | public let coveredLines: Int
10 | public let executableLines: Int
11 | public let executionCount: Int
12 | public let lineCoverage: Double
13 | public let lineNumber: Int
14 | public let name: String
15 |
16 | public init(coveredLines: Int,
17 | executableLines: Int,
18 | executionCount: Int,
19 | lineCoverage: Double,
20 | lineNumber: Int,
21 | name: String) {
22 | self.coveredLines = coveredLines
23 | self.executableLines = executableLines
24 | self.executionCount = executionCount
25 | self.lineCoverage = lineCoverage
26 | self.lineNumber = lineNumber
27 | self.name = name
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/Core/Commons/Models/Import.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Import.swift
3 | //
4 | //
5 | // Created by Thibault Wittemberg on 2020-06-03.
6 | //
7 |
8 | public struct Import {
9 | public let content: String
10 | public let filename: String
11 |
12 | public init(content: String, filename: String) {
13 | self.content = content
14 | self.filename = filename
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/Core/Commons/Models/TargetCoverageReport.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TargetCoverageReport.swift
3 | //
4 | //
5 | // Created by Thibault Wittemberg on 2020-05-30.
6 | //
7 |
8 | public struct TargetCoverageReport: Decodable, Equatable {
9 | public let buildProductPath: String
10 | public let coveredLines: Int
11 | public let executableLines: Int
12 | public let files: [FileCoverageReport]
13 | public let lineCoverage: Double
14 | public let name: String
15 |
16 | public init(buildProductPath: String,
17 | coveredLines: Int,
18 | executableLines: Int,
19 | files: [FileCoverageReport],
20 | lineCoverage: Double,
21 | name: String) {
22 | self.buildProductPath = buildProductPath
23 | self.coveredLines = coveredLines
24 | self.executableLines = executableLines
25 | self.files = files
26 | self.lineCoverage = lineCoverage
27 | self.name = name
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/Core/Commons/Tools/Export+Write.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Export+Write.swift
3 | //
4 | //
5 | // Created by Thibault Wittemberg on 2020-06-03.
6 | //
7 |
8 | public extension Export {
9 | func write(atPath path: String) -> Result {
10 | self.content.write(toFile: self.filename, atPath: path).map { self }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/Core/Commons/Tools/Import+Read.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Import+Read.swift
3 | //
4 | //
5 | // Created by Thibault Wittemberg on 2020-06-03.
6 | //
7 |
8 | public extension Import {
9 | static func read(filename: String) -> Result {
10 | filename.read().map { Import(content: $0, filename: filename) }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/Core/Commons/Tools/Result+MapEach.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Result+Apple.swift
3 | //
4 | //
5 | // Created by Thibault Wittemberg on 2020-05-31.
6 | //
7 |
8 | public extension Result where Success: Collection {
9 | @discardableResult
10 | func mapEach