├── .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(onFailure failure: (Failure) -> Result<[Output], Failure> = { .failure($0) }, 11 | onSuccess success: (Success.Element) -> Result) -> Result<[Output], Failure> { 12 | switch self { 13 | case .failure(let errorValue): 14 | return failure(errorValue) 15 | case .success(let successCollection): 16 | let initial: Result<[Output], Failure> = .success([Output]()) 17 | return successCollection 18 | .map { success($0) } 19 | .reduce(initial) { previous, current -> Result<[Output], Failure> in 20 | switch (previous, current) { 21 | case (.success(let outputs), .success(let output)): 22 | return .success(outputs+[output]) 23 | case (.failure(let error), _): 24 | return .failure(error) 25 | case (_, .failure(let error)): 26 | return .failure(error) 27 | } 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Core/Commons/Tools/Result+Verbose.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Result+Print.swift 3 | // 4 | // 5 | // Created by Thibault Wittemberg on 2020-06-06. 6 | // 7 | 8 | public extension Result { 9 | @discardableResult 10 | func verbose(_ verbose: Bool, onFailure failure: (Failure) -> Void = { _ in }, onSuccess success: (Success) -> Void = { _ in }) -> Self { 11 | if verbose { 12 | switch self { 13 | case .failure(let error): 14 | failure(error) 15 | case .success(let value): 16 | success(value) 17 | } 18 | } 19 | 20 | return self 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Core/Commons/Tools/String+Contains.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Contains.swift 3 | // 4 | // 5 | // Created by Thibault Wittemberg on 2020-06-01. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension String { 11 | func contains(elementsOf array: [String]) -> Bool { 12 | array.reduce(false) { previousResult, element -> Bool in 13 | previousResult || self.contains(element) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Core/Commons/Tools/String+Read.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Read.swift 3 | // 4 | // 5 | // Created by Thibault Wittemberg on 2020-06-03. 6 | // 7 | 8 | public extension String { 9 | func read() -> Result { 10 | guard let content = try? String(contentsOfFile: self, encoding: .utf8) else { 11 | return .failure(.unableToReadFile(self)) 12 | } 13 | 14 | return .success(content) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Core/Commons/Tools/String+Write.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Write.swift 3 | // 4 | // 5 | // Created by Thibault Wittemberg on 2020-06-03. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension String { 11 | func write(toFile filename: String, atPath path: String) -> Result { 12 | let file = NSString.path(withComponents: [path, filename]) 13 | guard (try? FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true)) != nil, 14 | (try? self.write(toFile: file, atomically: true, encoding: .utf8)) != nil else { 15 | return .failure(.unableToWriteFile(self)) 16 | } 17 | 18 | return .success(()) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Core/Converters/CoberturaXmlConverter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoberturaXmlConverter.swift 3 | // 4 | // 5 | // Created by Thibault Wittemberg on 2020-05-30. 6 | // 7 | 8 | import Foundation 9 | import Resources 10 | 11 | public extension Xccov.Converters { 12 | enum CoberturaXml {} 13 | } 14 | 15 | public extension Xccov.Converters.CoberturaXml { 16 | static func convert(coverageReport: CoverageReport) -> Result { 17 | Self.convert(coverageReport: coverageReport, 18 | timeStamp: Date().timeIntervalSince1970, 19 | currentDirectoryPath: FileManager.default.currentDirectoryPath) 20 | } 21 | 22 | //swiftlint:disable:next function_body_length 23 | static func convert(coverageReport: CoverageReport, 24 | timeStamp: TimeInterval = Date().timeIntervalSince1970, 25 | currentDirectoryPath: String = FileManager.default.currentDirectoryPath) -> Result { 26 | guard 27 | let dtd = try? XMLDTD(data: Resources.coverageDTD) else { 28 | return .failure(.conversionFailed("DTD could not be constructed")) 29 | } 30 | 31 | dtd.name = "coverage" 32 | dtd.systemID = "http://cobertura.sourceforge.net/xml/coverage-04.dtd" 33 | 34 | let rootElement = Self.makeRootElement(coverageReport: coverageReport, timeStamp: timeStamp) 35 | 36 | let doc = XMLDocument(rootElement: rootElement) 37 | doc.version = "1.0" 38 | doc.dtd = dtd 39 | doc.documentContentKind = .xml 40 | 41 | let sourceElement = XMLElement(name: "sources") 42 | rootElement.addChild(sourceElement) 43 | sourceElement.addChild(XMLElement(name: "source", stringValue: currentDirectoryPath)) 44 | 45 | let packagesElement = XMLElement(name: "packages") 46 | rootElement.addChild(packagesElement) 47 | 48 | // Sort files to avoid duplicated packages 49 | let allFiles = coverageReport.targets.flatMap { $0.files }.sorted { $0.path > $1.path } 50 | 51 | var currentPackage = "" 52 | var currentPackageElement: XMLElement! 53 | var isNewPackage = false 54 | 55 | var currentClassesElement = XMLElement() 56 | 57 | 58 | allFiles.forEach { fileCoverageReport in 59 | // Define file path relative to source! 60 | let filePath = fileCoverageReport.path.replacingOccurrences(of: currentDirectoryPath + "/", with: "") 61 | let pathComponents = filePath.split(separator: "/") 62 | let packageName = pathComponents[0.. XMLElement { 120 | let rootElement = XMLElement(name: "coverage") 121 | rootElement.addAttribute(XMLNode.nodeAttribute(withName: "line-rate", stringValue: "\(coverageReport.lineCoverage)")) 122 | rootElement.addAttribute(XMLNode.nodeAttribute(withName: "branch-rate", stringValue: "1.0")) 123 | rootElement.addAttribute(XMLNode.nodeAttribute(withName: "lines-covered", stringValue: "\(coverageReport.coveredLines)")) 124 | rootElement.addAttribute(XMLNode.nodeAttribute(withName: "lines-valid", stringValue: "\(coverageReport.executableLines)")) 125 | rootElement.addAttribute(XMLNode.nodeAttribute(withName: "timestamp", stringValue: "\(timeStamp)")) 126 | rootElement.addAttribute(XMLNode.nodeAttribute(withName: "version", stringValue: "diff_coverage 0.1")) 127 | rootElement.addAttribute(XMLNode.nodeAttribute(withName: "complexity", stringValue: "0.0")) 128 | rootElement.addAttribute(XMLNode.nodeAttribute(withName: "branches-valid", stringValue: "1.0")) 129 | rootElement.addAttribute(XMLNode.nodeAttribute(withName: "branches-covered", stringValue: "1.0")) 130 | 131 | return rootElement 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Sources/Core/Converters/FailableConverter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FailableConverter.swift 3 | // 4 | // 5 | // Created by Thibault Wittemberg on 2020-07-05. 6 | // 7 | 8 | public extension Xccov.Converters { 9 | enum FailableConverter {} 10 | } 11 | 12 | public extension Xccov.Converters.FailableConverter { 13 | static func convert(coverageReport: CoverageReport) -> Result { 14 | .failure(Xccov.Error.conversionFailed("")) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Core/Converters/SonarqubeXmlConverter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SonarqubeXmlConverter.swift 3 | // 4 | // 5 | // Created by Harris, David (D.A.) on 1/19/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Xccov.Converters { 11 | enum SonarqubeXml {} 12 | } 13 | 14 | public extension Xccov.Converters.SonarqubeXml { 15 | static func convert(coverageReport: CoverageReport) -> Result { 16 | Self.convert(coverageReport: coverageReport, 17 | currentDirectoryPath: FileManager.default.currentDirectoryPath) 18 | } 19 | 20 | static func convert(coverageReport: CoverageReport, 21 | currentDirectoryPath: String = FileManager.default.currentDirectoryPath) -> Result { 22 | let rootElement = XMLElement(name: "coverage") 23 | rootElement.addAttribute(XMLNode.nodeAttribute(withName: "version", stringValue: "1")) 24 | 25 | let doc = XMLDocument(rootElement: rootElement) 26 | 27 | // Sort files to avoid duplicated packages 28 | let allFiles = coverageReport.targets.flatMap { $0.files }.sorted { $0.path < $1.path } 29 | 30 | 31 | allFiles.forEach { fileCoverageReport in 32 | // Define file path relative to source 33 | let filePath = fileCoverageReport.path.replacingOccurrences(of: currentDirectoryPath + "/", with: "") 34 | 35 | let fileElement = XMLElement(name: "file") 36 | fileElement.addAttribute(XMLNode.nodeAttribute(withName: "path", stringValue: filePath)) 37 | 38 | rootElement.addChild(fileElement) 39 | 40 | for functionCoverageReport in fileCoverageReport.functions { 41 | for index in 0.. 0 { 55 | lineElement.addAttribute(XMLNode.nodeAttribute(withName: "covered", stringValue: "true")) 56 | } else { 57 | lineElement.addAttribute(XMLNode.nodeAttribute(withName: "covered", stringValue: "false")) 58 | } 59 | 60 | fileElement.addChild(lineElement) 61 | } 62 | } 63 | 64 | } 65 | 66 | 67 | return .success(doc.xmlString(options: [.nodePrettyPrint])) 68 | } 69 | } 70 | 71 | -------------------------------------------------------------------------------- /Sources/Core/Converters/XMLNode+NodeAttribute.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XMLNode+NodeAttribute.swift 3 | // 4 | // 5 | // Created by Harris, David (D.A.) on 1/20/21. 6 | // 7 | 8 | import Foundation 9 | 10 | extension XMLNode { 11 | static func nodeAttribute(withName name: String, stringValue value: String) -> XMLNode { 12 | guard let attribute = XMLNode.attribute(withName: name, stringValue: value) as? XMLNode else { 13 | return XMLNode() 14 | } 15 | 16 | return attribute 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Core/Converters/Xccov+Converter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Xccov+Converter.swift 3 | // 4 | // 5 | // Created by Thibault Wittemberg on 2020-05-31. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Xccov { 11 | enum Converters {} 12 | } 13 | 14 | public extension Xccov.Converters { 15 | typealias Converter = (CoverageReport) -> Result 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Core/Decoders/JsonDecoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JsonDecoder.swift 3 | // 4 | // 5 | // Created by Thibault Wittemberg on 2020-05-31. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Xccov.Decoders { 11 | enum Json {} 12 | } 13 | 14 | public extension Xccov.Decoders.Json { 15 | static func decode(imported: Import) -> Result { 16 | guard !imported.content.isEmpty else { 17 | return .failure(.contentIsEmpty(imported.filename)) 18 | } 19 | 20 | guard let data = imported.content.data(using: .utf8) else { 21 | return .failure(.contentIsNotConvertibleToData(imported.filename)) 22 | } 23 | 24 | guard let coverageReport = try? JSONDecoder().decode(CoverageReport.self, from: data) else { 25 | return .failure(.contentIsNotACoverageReport(imported.filename)) 26 | } 27 | 28 | return .success(coverageReport) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Core/Decoders/Xccov+Decoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Xccov+Decoder.swift 3 | // 4 | // 5 | // Created by Thibault Wittemberg on 2020-05-30. 6 | // 7 | 8 | public extension Xccov { 9 | enum Decoders {} 10 | } 11 | 12 | public extension Xccov.Decoders { 13 | typealias Decoder = (Import) -> Result 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Core/Filters/PackagesFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PackagesFilter.swift 3 | // 4 | // 5 | // Created by Thibault Wittemberg on 2020-06-01. 6 | // 7 | 8 | public extension Xccov.Filters { 9 | enum Packages {} 10 | } 11 | 12 | public extension Xccov.Filters.Packages { 13 | static func filter(coverageReport: CoverageReport, packagesToExclude: [String]) -> CoverageReport { 14 | guard !packagesToExclude.isEmpty else { return coverageReport } 15 | 16 | let targetsToKeep = coverageReport.targets.map { target -> TargetCoverageReport in 17 | let filesToKeep = target.files.filter { !$0.path.contains(elementsOf: packagesToExclude) } 18 | return TargetCoverageReport(buildProductPath: target.buildProductPath, 19 | coveredLines: target.coveredLines, 20 | executableLines: target.executableLines, 21 | files: filesToKeep, 22 | lineCoverage: target.lineCoverage, 23 | name: target.name) 24 | } 25 | 26 | let filteredCoverageReport = CoverageReport(executableLines: coverageReport.executableLines, 27 | targets: targetsToKeep, 28 | lineCoverage: coverageReport.lineCoverage, 29 | coveredLines: coverageReport.coveredLines) 30 | return filteredCoverageReport 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/Core/Filters/TargetsFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TargetsFilter.swift 3 | // 4 | // 5 | // Created by Thibault Wittemberg on 2020-06-01. 6 | // 7 | 8 | public extension Xccov.Filters { 9 | enum Targets {} 10 | } 11 | 12 | public extension Xccov.Filters.Targets { 13 | static func filter(coverageReport: CoverageReport, targetsToExclude: [String]) -> CoverageReport { 14 | guard !targetsToExclude.isEmpty else { return coverageReport } 15 | 16 | let targetsToKeep = coverageReport.targets.filter { !$0.name.contains(elementsOf: targetsToExclude) } 17 | let filteredCoverageReport = CoverageReport(executableLines: coverageReport.executableLines, 18 | targets: targetsToKeep, 19 | lineCoverage: coverageReport.lineCoverage, 20 | coveredLines: coverageReport.coveredLines) 21 | return filteredCoverageReport 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Core/Filters/Xccov+Filter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Xccov+Filter.swift 3 | // 4 | // 5 | // Created by Thibault Wittemberg on 2020-06-01. 6 | // 7 | 8 | public extension Xccov { 9 | enum Filters {} 10 | } 11 | -------------------------------------------------------------------------------- /Sources/Core/Xccov.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Xccov.swift 3 | // 4 | // 5 | // Created by Thibault Wittemberg on 2020-05-31. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum Xccov { 11 | public enum Error: LocalizedError, Equatable { 12 | case unableToReadFile(String) 13 | case unableToWriteFile(String) 14 | case contentIsEmpty(String) 15 | case contentIsNotConvertibleToData(String) 16 | case contentIsNotACoverageReport(String) 17 | case conversionFailed(String) 18 | case commandError(String) 19 | 20 | public var errorDescription: String? { 21 | switch self { 22 | case .unableToReadFile(let message): 23 | return "Unable to read file: \(message)" 24 | case .unableToWriteFile(let message): 25 | return "Unable to write file: \(message)" 26 | case .contentIsEmpty(let message): 27 | return "Content is empty: \(message)" 28 | case .contentIsNotConvertibleToData(let message): 29 | return "Content is not convertible to data: \(message)" 30 | case .contentIsNotACoverageReport(let message): 31 | return "Content is not a coverage report: \(message)" 32 | case .conversionFailed(let message): 33 | return "Conversion failed: \(message)" 34 | case .commandError(let message): 35 | return "Command error: \(message)" 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Resources/Bundled/Resources.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Resources.swift 3 | // 4 | // 5 | // Created by Ilia Shoshin on 25.5.2023. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | 11 | @_cdecl("copyCoverageDTD") 12 | func copyCoverageDTD(_ size: UnsafeMutablePointer) -> UnsafeMutablePointer? { 13 | size.pointee = BundleResource.coverageDTD.size 14 | return BundleResource.coverageDTD.unsafeMutablePointer 15 | } 16 | 17 | class BundleResource { 18 | var data: Data 19 | var size: Int = 0 20 | 21 | // note: call `deallocate()` on the pointer when the data is no longer needed 22 | var unsafeMutablePointer: UnsafeMutablePointer? { 23 | let mutablePointer = UnsafeMutablePointer.allocate(capacity: size) 24 | data.copyBytes(to: mutablePointer, count: size) 25 | return mutablePointer 26 | } 27 | 28 | static var coverageDTD: BundleResource = { 29 | guard let resource = BundleResource(name: "coverage-04", extension: "dtd") else { 30 | fatalError("failed to find coverage-04.dtd") 31 | } 32 | return resource 33 | }() 34 | 35 | init?(name: String, `extension`: String) { 36 | guard let url = Bundle.module.url(forResource: name, withExtension: `extension`), 37 | let data = try? Data(contentsOf: url) else { 38 | return nil 39 | } 40 | self.data = data 41 | self.size = data.count 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/Resources/Bundled/coverage-04.dtd: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /Sources/Resources/Embedded/Resources.c: -------------------------------------------------------------------------------- 1 | // 2 | // Resources.c 3 | // 4 | // 5 | // Created by Ilia Shoshin on 25.5.2023. 6 | // 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | /** 14 | * The function extracts the __coverage_dtd section from the __DATA segment. 15 | * 16 | * @param[out] size Data size 17 | * @return A copy of the data 18 | * Note: Call `free()` when the data is no longer required. 19 | * 20 | * Resources cannot be embeded into some targets (e.g. test targets). 21 | * The `_mh_execute_header` symbol is specific to the main executable file, and it points to the 22 | * Mach-O header of that executable in memory. Test targets, on the other hand, are not built as 23 | * standalone executables, so they don't have their own Mach-O headers and the `_mh_execute_header` 24 | * symbol is undefined. 25 | * 26 | * The `Copy Rule` is not necessary here, but has been chosen to make it easier to mock the function in Swift: 27 | * 28 | * ```swift 29 | * @_cdecl("copyCoverageDTD") 30 | * func copyCoverageDTD(_ size: UnsafeMutablePointer) -> UnsafeMutablePointer? { 31 | * let data = ... 32 | * size.pointee = data.size 33 | * let mutablePointer = UnsafeMutablePointer.allocate(capacity: data.size) 34 | * data.copyBytes(to: mutablePointer, count: data.size) 35 | * return mutablePointer 36 | * } 37 | * ``` 38 | */ 39 | uint8_t *copyCoverageDTD(size_t *size) { 40 | unsigned long dataSize; 41 | const uint8_t *data = getsectiondata(&_mh_execute_header, "__DATA", "__coverage_dtd", &dataSize); 42 | uint8_t *buffer = (uint8_t *)malloc(dataSize); 43 | memcpy(buffer, data, dataSize); 44 | *size = dataSize; 45 | return buffer; 46 | } 47 | -------------------------------------------------------------------------------- /Sources/Resources/Main/Resources.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Resources.swift 3 | // 4 | // 5 | // Created by Ilia Shoshin on 25.5.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | // Import the C function 11 | @_silgen_name("copyCoverageDTD") 12 | func copyCoverageDTD(_ size: UnsafeMutablePointer) -> UnsafeMutablePointer? 13 | 14 | public class Resources { 15 | public static var coverageDTD: Data { 16 | var size: Int = 0 17 | guard let dataPtr = copyCoverageDTD(&size) else { 18 | fatalError("coverage.dtd is not available") 19 | } 20 | return Data(bytesNoCopy: dataPtr, count: size, deallocator: .free) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /Sources/XcodeCoverageConverter/main.swift: -------------------------------------------------------------------------------- 1 | import ArgumentParser 2 | import Core 3 | import Foundation 4 | 5 | struct Xcc: ParsableCommand { 6 | static let configuration = CommandConfiguration( 7 | abstract: "A Swift command-line tool to convert xccov outputs into continuous integration friendly formats", 8 | version: "0.2.2", 9 | subcommands: [Generate.self], 10 | defaultSubcommand: Generate.self) 11 | 12 | init() { } 13 | } 14 | 15 | struct Generate: ParsableCommand { 16 | static let configuration = CommandConfiguration(abstract: "Generates a converted file from xccov results") 17 | 18 | @Argument(help: "The full path to the json file to convert") 19 | private var jsonFile: String 20 | 21 | @Argument(help: "The path of the output file") 22 | private var outputPath: String 23 | 24 | @Argument(help: "The output formats") 25 | private var outputFormats: [Xccov.Commands.Generate.Output] 26 | 27 | @Option(help: "The targets to exclude") 28 | private var excludeTargets: [String] 29 | 30 | @Option(help: "The packages to exclude") 31 | private var excludePackages: [String] 32 | 33 | @Flag(name: .long, help: "Show extra logging for debugging purposes") 34 | private var verbose: Bool = false 35 | 36 | func run() throws { 37 | let result = Xccov.Commands.Generate.execute(jsonFile: jsonFile, 38 | outputPath: outputPath, 39 | outputs: outputFormats, 40 | excludeTargets: excludeTargets, 41 | excludePackages: excludePackages, 42 | verbose: verbose) 43 | 44 | switch result { 45 | case .success: 46 | throw CleanExit.message("All good") 47 | case .failure(let error): 48 | let message = "\(error.localizedDescription)\n" 49 | FileHandle.standardError.write(message.data(using: .utf8)!) 50 | throw ExitCode.failure 51 | } 52 | } 53 | } 54 | 55 | Xcc.main() 56 | -------------------------------------------------------------------------------- /Tests/CoreTests/Commands/GenerateCommandTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GenerateCommandTests.swift 3 | // 4 | // 5 | // Created by Thibault Wittemberg on 2020-07-05. 6 | // 7 | 8 | @testable import Core 9 | import XCTest 10 | 11 | final class GenerateCommandTests: XCTestCase { 12 | func testFilename_return_expected_value_for_cobertura() { 13 | // Given: a Cobertura command output 14 | let sut = Xccov.Commands.Generate.Output.coberturaXml 15 | 16 | // When: requesting the filename 17 | // Then: it is the expected one 18 | XCTAssertEqual(sut.filename, "cobertura.xml") 19 | } 20 | 21 | func testConvert_to_coberturalXml_return_success_when_coverageReport_is_valid() { 22 | // Given: a valid json report 23 | let data = converterFixtureCoverageJson.data(using: .utf8) 24 | let coverageReport = try! JSONDecoder().decode(CoverageReport.self, from: data!) 25 | 26 | // When: converting it to Cobertura xml 27 | let receivedResult = Xccov.Commands.Generate.convert(report: coverageReport, to: .coberturaXml) 28 | 29 | // Then: it is a success 30 | XCTAssertNoThrow(try receivedResult.get()) 31 | } 32 | 33 | func testConvert_to_failable_return_failure() { 34 | // Given: a json report 35 | let data = converterFixtureCoverageJson.data(using: .utf8) 36 | let coverageReport = try! JSONDecoder().decode(CoverageReport.self, from: data!) 37 | 38 | // When: converting it to Failable output 39 | let receivedResult = Xccov.Commands.Generate.convert(report: coverageReport, to: .failable) 40 | 41 | // Then: it is a failure 42 | XCTAssertEqual(receivedResult, Result.failure(Xccov.Error.conversionFailed(""))) 43 | } 44 | 45 | func testConvert_to_several_outputs_return_success_when_coverageReport_is_valid() { 46 | // Given: a valid json report 47 | let data = converterFixtureCoverageJson.data(using: .utf8) 48 | let coverageReport = try! JSONDecoder().decode(CoverageReport.self, from: data!) 49 | 50 | // When: converting it to several output formats 51 | let receivedResult = Xccov.Commands.Generate.convert(report: coverageReport, to: [.coberturaXml]) 52 | 53 | // Then: it is a success with the count of output formats 54 | XCTAssertNoThrow(try receivedResult.get()) 55 | XCTAssertEqual((try receivedResult.get()).count, 1) 56 | } 57 | 58 | func testConvert_to_several_outputs_return_failure_when_a_conversion_fails() { 59 | // Given: a json report 60 | let data = converterFixtureCoverageJson.data(using: .utf8) 61 | let coverageReport = try! JSONDecoder().decode(CoverageReport.self, from: data!) 62 | 63 | // When: converting it to several output formats 64 | let receivedResult = Xccov.Commands.Generate.convert(report: coverageReport, to: [.coberturaXml, .failable, .failable]) 65 | 66 | // Then: it is a failure 67 | XCTAssertEqual(receivedResult, Result.failure(Xccov.Error.conversionFailed(""))) 68 | } 69 | 70 | func testExecute_writes_cobertura_xml_to_outputPath() { 71 | // Given: a json report in a temporary directory 72 | let tmpDir = FileManager.default.uniqueTemporaryDirectory() 73 | defer { try? FileManager.default.removeItem(at: tmpDir) } 74 | _ = converterFixtureCoverageJson.write(toFile: "report.json", atPath: tmpDir.path) 75 | 76 | // When: converting it to a cobertura-xml using the export method 77 | let receivedResult = Xccov.Commands.Generate.execute( 78 | jsonFile: "\(tmpDir.path)/report.json", 79 | outputPath: "\(tmpDir.path)/reports/coverage/", 80 | outputs: [.coberturaXml], 81 | excludeTargets: [], excludePackages: [], verbose: false 82 | ) 83 | 84 | // Then: a file "cobertura.xml" exists in the specified outputPath 85 | XCTAssertTrue(FileManager.default.fileExists(atPath: "\(tmpDir.path)/reports/coverage/cobertura.xml")) 86 | XCTAssertNoThrow(try receivedResult.get()) 87 | } 88 | 89 | static var allTests = [ 90 | ("testFilename_return_expected_value_for_cobertura", testFilename_return_expected_value_for_cobertura), 91 | ("testConvert_to_coberturalXml_return_success_when_coverageReport_is_valid", testConvert_to_coberturalXml_return_success_when_coverageReport_is_valid), 92 | ("testConvert_to_failable_return_failure", testConvert_to_failable_return_failure), 93 | ("testConvert_to_several_outputs_return_success_when_coverageReport_is_valid", testConvert_to_several_outputs_return_success_when_coverageReport_is_valid), 94 | ("testConvert_to_several_outputs_return_failure_when_a_conversion_fails", testConvert_to_several_outputs_return_failure_when_a_conversion_fails), 95 | ("testExecute_writes_cobertura_xml_to_outputPath", testExecute_writes_cobertura_xml_to_outputPath), 96 | ] 97 | } 98 | 99 | private extension FileManager { 100 | 101 | func uniqueTemporaryDirectory(_ uuid: UUID = .init()) -> URL { 102 | let result = self.temporaryDirectory.appendingPathComponent(uuid.uuidString) 103 | try? self.createDirectory(at: result, withIntermediateDirectories: true, attributes: nil) 104 | return result 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /Tests/CoreTests/Commons/Tools/Result+MapEachTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Result+MapEachTests.swift 3 | // 4 | // 5 | // Created by Thibault Wittemberg on 2020-07-04. 6 | // 7 | 8 | @testable import Core 9 | import XCTest 10 | 11 | private enum MockError: Error, Equatable { 12 | case someError 13 | } 14 | 15 | final class Result_MapEachTests: XCTestCase { 16 | func testMapEach_give_error_to_onFailure_function_and_return_result_from_onFailure() { 17 | let expectedError = MockError.someError 18 | var receivedError: MockError? 19 | let expectedResult = Result<[Int],MockError>.failure(.someError) 20 | var receivedResult: Result<[Int],MockError>? 21 | let spyFailureFunction: (MockError) -> Result<[Int], MockError> = { error in 22 | receivedError = error 23 | return .failure(error) 24 | } 25 | 26 | // Given: a result being a failure 27 | let sut = Result<[Int],MockError>.failure(.someError) 28 | 29 | // When: calling mapEach on it 30 | receivedResult = sut.mapEach(onFailure: spyFailureFunction, onSuccess: { return .success($0) }) 31 | 32 | // Then: the error received in the onFailure function is the one from the failure 33 | XCTAssertEqual(receivedError, expectedError) 34 | 35 | // Then: the result of mapEach is the one returned by the onFailure function 36 | XCTAssertEqual(receivedResult, expectedResult) 37 | } 38 | 39 | func testMapEach_give_values_to_onSuccess_function_and_return_result_from_onSuccess() { 40 | let expectedValues = [1, 2, 3, 4, 5] 41 | var receivedValues = [Int]() 42 | let expectedResult = Result<[String],MockError>.success(["1", "2", "3", "4", "5"]) 43 | var receivedResult: Result<[String],MockError>? 44 | 45 | // Given: a result being a success 46 | let spySuccessFunction: (Int) -> Result = { value in 47 | receivedValues.append(value) 48 | return .success("\(value)") 49 | } 50 | let sut = Result<[Int],MockError>.success([1, 2, 3, 4, 5]) 51 | receivedResult = sut.mapEach(onSuccess: spySuccessFunction) 52 | 53 | XCTAssertEqual(receivedValues, expectedValues) 54 | XCTAssertEqual(receivedResult, expectedResult) 55 | } 56 | 57 | func testMapEach_return_failure_if_onSuccess_return_failure() { 58 | let expectedResult = Result<[String],MockError>.failure(.someError) 59 | var receivedResult: Result<[String],MockError>? 60 | 61 | // Given: a result being a success 62 | let spySuccessFunction: (Int) -> Result = { value in 63 | if value == 3 { 64 | return .failure(MockError.someError) 65 | } 66 | return .success("\(value)") 67 | } 68 | 69 | let sut = Result<[Int],MockError>.success([1, 2, 3, 4, 5]) 70 | receivedResult = sut.mapEach(onSuccess: spySuccessFunction) 71 | 72 | XCTAssertEqual(receivedResult, expectedResult) 73 | } 74 | 75 | static var allTests = [ 76 | ("testMapEach_give_error_to_onFailure_function_and_return_result_from_onFailure", testMapEach_give_error_to_onFailure_function_and_return_result_from_onFailure), 77 | ("testMapEach_give_values_to_onSuccess_function_and_return_result_from_onSuccess", testMapEach_give_values_to_onSuccess_function_and_return_result_from_onSuccess), 78 | ("testMapEach_return_failure_if_onSuccess_return_failure", testMapEach_return_failure_if_onSuccess_return_failure), 79 | ] 80 | } 81 | -------------------------------------------------------------------------------- /Tests/CoreTests/Commons/Tools/Result+VerboseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Result+VerboseTests.swift 3 | // 4 | // 5 | // Created by Thibault Wittemberg on 2020-07-04. 6 | // 7 | 8 | @testable import Core 9 | import XCTest 10 | 11 | private enum MockError: Error, Equatable { 12 | case someError 13 | } 14 | 15 | final class Result_VerboseTests: XCTestCase { 16 | func testVerbose_do_not_call_onFailure_onSuccess_when_verbose_is_false() { 17 | var onFailureIsCalled = false 18 | var onSuccessIsCalled = false 19 | 20 | let spyOnFailureFunction: (MockError) -> Void = { _ in onFailureIsCalled = true } 21 | let spyOnSuccessFunction: (Int) -> Void = { _ in onSuccessIsCalled = true } 22 | 23 | // Given: a result 24 | let sut = Result.failure(.someError) 25 | 26 | // When: calling verbose with verbose = false 27 | sut.verbose(false, onFailure: spyOnFailureFunction, onSuccess: spyOnSuccessFunction) 28 | 29 | // Then: onFailure and onSuccess are not called 30 | XCTAssertFalse(onFailureIsCalled) 31 | XCTAssertFalse(onSuccessIsCalled) 32 | } 33 | 34 | func testVerbose_call_onFailure_when_verbose_is_true_and_result_is_failure() { 35 | let expectedError = MockError.someError 36 | var receivedError: MockError? 37 | var onSuccessIsCalled = false 38 | 39 | let spyOnFailureFunction: (MockError) -> Void = { error in 40 | receivedError = error 41 | } 42 | let spyOnSuccessFunction: (Int) -> Void = { _ in onSuccessIsCalled = true } 43 | 44 | // Given: a result that is a failure 45 | let sut = Result.failure(.someError) 46 | 47 | // When: calling verbose with verbose = true 48 | sut.verbose(true, onFailure: spyOnFailureFunction, onSuccess: spyOnSuccessFunction) 49 | 50 | // Then: only onFailure is called with the expected error 51 | XCTAssertFalse(onSuccessIsCalled) 52 | XCTAssertEqual(receivedError, expectedError) 53 | } 54 | 55 | func testVerbose_call_onSuccess_when_verbose_is_true_and_result_is_success() { 56 | let expectedValue = 1 57 | var receivedValue: Int? 58 | var onFailureIsCalled = false 59 | 60 | let spyOnSuccessFunction: (Int) -> Void = { value in 61 | receivedValue = value 62 | } 63 | let spyOnFailureFunction: (MockError) -> Void = { _ in onFailureIsCalled = true } 64 | 65 | // Given: a result that is a success 66 | let sut = Result.success(expectedValue) 67 | 68 | // When: calling verbose with verbose = true 69 | sut.verbose(true, onFailure: spyOnFailureFunction, onSuccess: spyOnSuccessFunction) 70 | 71 | // Then: only onSuccess is called with the expected value 72 | XCTAssertFalse(onFailureIsCalled) 73 | XCTAssertEqual(receivedValue, expectedValue) 74 | } 75 | 76 | static var allTests = [ 77 | ("testVerbose_do_not_call_onFailure_onSuccess_when_verbose_is_false", testVerbose_do_not_call_onFailure_onSuccess_when_verbose_is_false), 78 | ("testVerbose_call_onFailure_when_verbose_is_true_and_result_is_failure", testVerbose_call_onFailure_when_verbose_is_true_and_result_is_failure), 79 | ("testVerbose_call_onSuccess_when_verbose_is_true_and_result_is_success", testVerbose_call_onSuccess_when_verbose_is_true_and_result_is_success), 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /Tests/CoreTests/Commons/Tools/String+ContainsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+ContainsTests.swift 3 | // 4 | // 5 | // Created by Thibault Wittemberg on 2020-06-01. 6 | // 7 | 8 | @testable import Core 9 | import XCTest 10 | 11 | final class String_ContainsTests: XCTestCase { 12 | 13 | func testContains_returns_true_when_string_has_element() { 14 | // Given: a string 15 | // Given: an array of strings that contains a part of the sut 16 | let sut = "kirk.enterprise" 17 | let array = ["spock", "mckoy", "kirk"] 18 | 19 | // When: checking whether or not the array contains a part of the sut 20 | // Then: the result is true 21 | XCTAssertTrue(sut.contains(elementsOf: array)) 22 | } 23 | 24 | func testContains_returns_false_when_string_has_noElement() { 25 | // Given: a string 26 | // Given: an array of strings that does not contains a part of the sut 27 | let sut = "kirk.enterprise" 28 | let array = ["spock", "mckoy"] 29 | 30 | // When: checking whether or not the array contains a part of the sut 31 | // Then: the result is false 32 | XCTAssertFalse(sut.contains(elementsOf: array)) 33 | } 34 | 35 | static var allTests = [ 36 | ("testContains_returns_true_when_string_has_element", testContains_returns_true_when_string_has_element), 37 | ("testContains_returns_false_when_string_has_noElement", testContains_returns_false_when_string_has_noElement), 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /Tests/CoreTests/Converters/CoberturaXmlConverterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoberturaXmlConverterTests.swift 3 | // 4 | // 5 | // Created by Thibault Wittemberg on 2020-05-30. 6 | // 7 | 8 | @testable import Core 9 | import XCTest 10 | 11 | final class CoberturaXmlConverterTests: XCTestCase { 12 | func testConvert_return_the_expected_xml() { 13 | // Given: a json report 14 | let data = converterFixtureCoverageJson.data(using: .utf8) 15 | let coverageReport = try! JSONDecoder().decode(CoverageReport.self, from: data!) 16 | 17 | // When: converting to CoberturaXML 18 | let result = Xccov.Converters.CoberturaXml.convert(coverageReport: coverageReport, 19 | timeStamp: TimeInterval(argument: "1593913837.1738548")!, 20 | currentDirectoryPath: "XcodeCoverageConverter") 21 | let receivedResult = try! result.get() 22 | 23 | // Then: the xml is the expected one 24 | XCTAssertEqual(receivedResult, coberturaFixtureCoverageXml) 25 | } 26 | 27 | static var allTests = [ 28 | ("testConvert_return_the_expected_xml", testConvert_return_the_expected_xml), 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /Tests/CoreTests/Converters/Fixtures/ConverterCoverageJson.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConverterCoverageJson.swift 3 | // 4 | // 5 | // Created by Thibault Wittemberg on 2020-07-04. 6 | // 7 | 8 | let converterFixtureCoverageJson = """ 9 | {"coveredLines":661,"lineCoverage":0.9924924924924925,"targets":[{"coveredLines":172,"lineCoverage":1,"files":[{"coveredLines":17,"lineCoverage":1,"path":"/Users/twittemberg/Development/Genetec/iOS-FeedbackLoop/Tests/FeedbackLoopTests/TestReducer.swift","functions":[{"coveredLines":6,"lineCoverage":1,"lineNumber":9,"executionCount":25,"name":"static FeedbackLoopTests.TestReducer.reducer(state: FeedbackLoopTests.TestState, event: FeedbackLoopTests.TestEvent) -> FeedbackLoopTests.TestState","executableLines":6},{"coveredLines":11,"lineCoverage":1,"lineNumber":16,"executionCount":25,"name":"static FeedbackLoopTests.TestReducer.reduceNextEvent(from: FeedbackLoopTests.TestState) -> FeedbackLoopTests.TestState","executableLines":11}],"name":"TestReducer.swift","executableLines":17},{"coveredLines":155,"lineCoverage":1,"path":"/Users/twittemberg/Development/Genetec/iOS-FeedbackLoop/Tests/FeedbackLoopTests/FeedbackLoopIntegrationTests.swift","functions":[{"coveredLines":1,"lineCoverage":1,"lineNumber":13,"executionCount":3,"name":"variable initialization expression of FeedbackLoopTests.FeedbackLoopIntegrationTests.(subscriptions in _F7C74F052D2F841D215039F3597350AB) : Swift.Set","executableLines":1},{"coveredLines":6,"lineCoverage":1,"lineNumber":15,"executionCount":31,"name":"variable initialization expression of FeedbackLoopTests.FeedbackLoopIntegrationTests.effectA : (FeedbackLoopTests.TestState) -> Combine.AnyPublisher","executableLines":6},{"coveredLines":6,"lineCoverage":1,"lineNumber":22,"executionCount":30,"name":"variable initialization expression of FeedbackLoopTests.FeedbackLoopIntegrationTests.effectB : (FeedbackLoopTests.TestState) -> Combine.AnyPublisher","executableLines":6},{"coveredLines":6,"lineCoverage":1,"lineNumber":29,"executionCount":28,"name":"variable initialization expression of FeedbackLoopTests.FeedbackLoopIntegrationTests.effectC : (FeedbackLoopTests.TestState) -> Combine.AnyPublisher","executableLines":6},{"coveredLines":32,"lineCoverage":1,"lineNumber":36,"executionCount":1,"name":"FeedbackLoopTests.FeedbackLoopIntegrationTests.test_execute_withIncrementalStateInMultipleSideEffect_stateOrderIsRespected() -> ()","executableLines":32},{"coveredLines":6,"lineCoverage":1,"lineNumber":57,"executionCount":7,"name":"closure #1 (FeedbackLoopTests.TestState) -> () in FeedbackLoopTests.FeedbackLoopIntegrationTests.test_execute_withIncrementalStateInMultipleSideEffect_stateOrderIsRespected() -> ()","executableLines":6},{"coveredLines":1,"lineCoverage":1,"lineNumber":66,"executionCount":1,"name":"implicit closure #1 () throws -> Swift.Array in FeedbackLoopTests.FeedbackLoopIntegrationTests.test_execute_withIncrementalStateInMultipleSideEffect_stateOrderIsRespected() -> ()","executableLines":1},{"coveredLines":1,"lineCoverage":1,"lineNumber":66,"executionCount":1,"name":"implicit closure #2 () throws -> Swift.Array in FeedbackLoopTests.FeedbackLoopIntegrationTests.test_execute_withIncrementalStateInMultipleSideEffect_stateOrderIsRespected() -> ()","executableLines":1},{"coveredLines":31,"lineCoverage":1,"lineNumber":69,"executionCount":1,"name":"FeedbackLoopTests.FeedbackLoopIntegrationTests.test_execute_withIncrementalStateInMultipleSideEffect_withAdd_stateOrderIsRespected() -> ()","executableLines":31},{"coveredLines":6,"lineCoverage":1,"lineNumber":89,"executionCount":7,"name":"closure #1 (FeedbackLoopTests.TestState) -> () in FeedbackLoopTests.FeedbackLoopIntegrationTests.test_execute_withIncrementalStateInMultipleSideEffect_withAdd_stateOrderIsRespected() -> ()","executableLines":6},{"coveredLines":1,"lineCoverage":1,"lineNumber":98,"executionCount":1,"name":"implicit closure #1 () throws -> Swift.Array in FeedbackLoopTests.FeedbackLoopIntegrationTests.test_execute_withIncrementalStateInMultipleSideEffect_withAdd_stateOrderIsRespected() -> ()","executableLines":1},{"coveredLines":1,"lineCoverage":1,"lineNumber":98,"executionCount":1,"name":"implicit closure #2 () throws -> Swift.Array in FeedbackLoopTests.FeedbackLoopIntegrationTests.test_execute_withIncrementalStateInMultipleSideEffect_withAdd_stateOrderIsRespected() -> ()","executableLines":1},{"coveredLines":45,"lineCoverage":1,"lineNumber":101,"executionCount":1,"name":"FeedbackLoopTests.FeedbackLoopIntegrationTests.test_execute_withIncrementalStateInMultipleSideEffect_uiEvent_stateOrderIsRespected() -> ()","executableLines":45},{"coveredLines":1,"lineCoverage":1,"lineNumber":106,"executionCount":8,"name":"closure #1 (FeedbackLoopTests.TestState) -> () in FeedbackLoopTests.FeedbackLoopIntegrationTests.test_execute_withIncrementalStateInMultipleSideEffect_uiEvent_stateOrderIsRespected() -> ()","executableLines":1},{"coveredLines":3,"lineCoverage":1,"lineNumber":107,"executionCount":1,"name":"closure #2 () -> Combine.AnyPublisher in FeedbackLoopTests.FeedbackLoopIntegrationTests.test_execute_withIncrementalStateInMultipleSideEffect_uiEvent_stateOrderIsRespected() -> ()","executableLines":3},{"coveredLines":6,"lineCoverage":1,"lineNumber":135,"executionCount":14,"name":"closure #3 (FeedbackLoopTests.TestState) -> () in FeedbackLoopTests.FeedbackLoopIntegrationTests.test_execute_withIncrementalStateInMultipleSideEffect_uiEvent_stateOrderIsRespected() -> ()","executableLines":6},{"coveredLines":1,"lineCoverage":1,"lineNumber":144,"executionCount":1,"name":"implicit closure #1 () throws -> Swift.Array in FeedbackLoopTests.FeedbackLoopIntegrationTests.test_execute_withIncrementalStateInMultipleSideEffect_uiEvent_stateOrderIsRespected() -> ()","executableLines":1},{"coveredLines":1,"lineCoverage":1,"lineNumber":144,"executionCount":1,"name":"implicit closure #2 () throws -> Swift.Array in FeedbackLoopTests.FeedbackLoopIntegrationTests.test_execute_withIncrementalStateInMultipleSideEffect_uiEvent_stateOrderIsRespected() -> ()","executableLines":1}],"name":"FeedbackLoopIntegrationTests.swift","executableLines":155}],"name":"FeedbackLoopTests.xctest","executableLines":172,"buildProductPath":"/Users/twittemberg/Development/Genetec/iOS-FeedbackLoop/Build/Build/Products/Debug-iphonesimulator/FeedbackLoopTests.xctest/FeedbackLoopTests"},{"coveredLines":79,"lineCoverage":0.98750000000000004,"files":[{"coveredLines":45,"lineCoverage":0.97826086956521741,"path":"/Users/twittemberg/Development/Genetec/iOS-FeedbackLoop/Sources/FeedbackLoop/SideEffects.swift","functions":[{"coveredLines":10,"lineCoverage":1,"lineNumber":14,"executionCount":9,"name":"static FeedbackLoop.SideEffects.transform(_: (A) -> Combine.AnyPublisher, with: FeedbackLoop.ExecutionStrategy) -> (Combine.AnyPublisher) -> Combine.AnyPublisher","executableLines":10},{"coveredLines":7,"lineCoverage":0.875,"lineNumber":15,"executionCount":9,"name":"closure #1 (Combine.AnyPublisher) -> Combine.AnyPublisher in static FeedbackLoop.SideEffects.transform(_: (A) -> Combine.AnyPublisher, with: FeedbackLoop.ExecutionStrategy) -> (Combine.AnyPublisher) -> Combine.AnyPublisher","executableLines":8},{"coveredLines":13,"lineCoverage":1,"lineNumber":28,"executionCount":4,"name":"static FeedbackLoop.SideEffects.transform(from: (A) -> (), combinedWith: Combine.AnyPublisher) -> (Combine.AnyPublisher) -> Combine.AnyPublisher","executableLines":13},{"coveredLines":8,"lineCoverage":1,"lineNumber":29,"executionCount":4,"name":"closure #1 (Combine.AnyPublisher) -> Combine.AnyPublisher in static FeedbackLoop.SideEffects.transform(from: (A) -> (), combinedWith: Combine.AnyPublisher) -> (Combine.AnyPublisher) -> Combine.AnyPublisher","executableLines":8},{"coveredLines":4,"lineCoverage":1,"lineNumber":32,"executionCount":14,"name":"closure #1 (A) -> Combine.AnyPublisher in closure #1 (Combine.AnyPublisher) -> Combine.AnyPublisher in static FeedbackLoop.SideEffects.transform(from: (A) -> (), combinedWith: Combine.AnyPublisher) -> (Combine.AnyPublisher) -> Combine.AnyPublisher","executableLines":4},{"coveredLines":3,"lineCoverage":1,"lineNumber":37,"executionCount":4,"name":"closure #2 (Combine.AnyPublisher) -> Combine.AnyPublisher in static FeedbackLoop.SideEffects.transform(from: (A) -> (), combinedWith: Combine.AnyPublisher) -> (Combine.AnyPublisher) -> Combine.AnyPublisher","executableLines":3}],"name":"SideEffects.swift","executableLines":46},{"coveredLines":34,"lineCoverage":1,"path":"/Users/twittemberg/Development/Genetec/iOS-FeedbackLoop/Sources/FeedbackLoop/FeedbackLoop.swift","functions":[{"coveredLines":4,"lineCoverage":1,"lineNumber":24,"executionCount":12,"name":"FeedbackLoop.FeedbackLoop.init(with: Swift.Array<(Combine.AnyPublisher) -> Combine.AnyPublisher>, reducedBy: (A, B) -> A) -> FeedbackLoop.FeedbackLoop","executableLines":4},{"coveredLines":3,"lineCoverage":1,"lineNumber":29,"executionCount":10,"name":"FeedbackLoop.FeedbackLoop.add(sideEffect: (Combine.AnyPublisher) -> Combine.AnyPublisher) -> ()","executableLines":3},{"coveredLines":16,"lineCoverage":1,"lineNumber":35,"executionCount":6,"name":"FeedbackLoop.FeedbackLoop.execute(initialState: A, on: __C.OS_dispatch_queue) -> Combine.AnyPublisher","executableLines":16},{"coveredLines":5,"lineCoverage":1,"lineNumber":38,"executionCount":6,"name":"closure #1 (Combine.CurrentValueSubject) -> Combine.Publishers.MergeMany> in FeedbackLoop.FeedbackLoop.execute(initialState: A, on: __C.OS_dispatch_queue) -> Combine.AnyPublisher","executableLines":5},{"coveredLines":3,"lineCoverage":1,"lineNumber":39,"executionCount":13,"name":"closure #1 ((Combine.AnyPublisher) -> Combine.AnyPublisher) -> Combine.AnyPublisher in closure #1 (Combine.CurrentValueSubject) -> Combine.Publishers.MergeMany> in FeedbackLoop.FeedbackLoop.execute(initialState: A, on: __C.OS_dispatch_queue) -> Combine.AnyPublisher","executableLines":3},{"coveredLines":3,"lineCoverage":1,"lineNumber":46,"executionCount":34,"name":"closure #2 (A) -> () in FeedbackLoop.FeedbackLoop.execute(initialState: A, on: __C.OS_dispatch_queue) -> Combine.AnyPublisher","executableLines":3}],"name":"FeedbackLoop.swift","executableLines":34}],"name":"FeedbackLoop.framework","executableLines":80,"buildProductPath":"/Users/twittemberg/Development/Genetec/iOS-FeedbackLoop/Build/Build/Products/Debug-iphonesimulator/FeedbackLoop.framework/FeedbackLoop"},{"coveredLines":66,"lineCoverage":0.95652173913043481,"files":[{"coveredLines":66,"lineCoverage":0.95652173913043481,"path":"/Users/twittemberg/Development/Genetec/iOS-FeedbackLoop/Sources/FeedbackLoopSwiftUI/SwiftUIContext.swift","functions":[{"coveredLines":1,"lineCoverage":1,"lineNumber":24,"executionCount":9,"name":"variable initialization expression of FeedbackLoopSwiftUI.SwiftUIContext.subscriptions : Swift.Set","executableLines":1},{"coveredLines":1,"lineCoverage":1,"lineNumber":25,"executionCount":9,"name":"variable initialization expression of FeedbackLoopSwiftUI.SwiftUIContext.events : Combine.PassthroughSubject","executableLines":1},{"coveredLines":13,"lineCoverage":1,"lineNumber":27,"executionCount":3,"name":"closure #1 () -> Combine.AnyPublisher in FeedbackLoopSwiftUI.SwiftUIContext.stream.getter : Combine.AnyPublisher","executableLines":13},{"coveredLines":3,"lineCoverage":1,"lineNumber":32,"executionCount":3,"name":"closure #2 () -> Combine.AnyPublisher in closure #1 () -> Combine.AnyPublisher in FeedbackLoopSwiftUI.SwiftUIContext.stream.getter : Combine.AnyPublisher","executableLines":3},{"coveredLines":11,"lineCoverage":1,"lineNumber":43,"executionCount":9,"name":"FeedbackLoopSwiftUI.SwiftUIContext.init(feedbackLoop: FeedbackLoop.FeedbackLoop, initialState: A, filterDistinct: Swift.Bool) -> FeedbackLoopSwiftUI.SwiftUIContext","executableLines":11},{"coveredLines":5,"lineCoverage":1,"lineNumber":56,"executionCount":6,"name":"FeedbackLoopSwiftUI.SwiftUIContext.__allocating_init(initialState: A, filterDistinct: Swift.Bool) -> FeedbackLoopSwiftUI.SwiftUIContext","executableLines":5},{"coveredLines":0,"lineCoverage":0,"lineNumber":57,"executionCount":0,"name":"closure #1 (A, B) -> A in FeedbackLoopSwiftUI.SwiftUIContext.init(initialState: A, filterDistinct: Swift.Bool) -> FeedbackLoopSwiftUI.SwiftUIContext","executableLines":1},{"coveredLines":3,"lineCoverage":1,"lineNumber":62,"executionCount":2,"name":"FeedbackLoopSwiftUI.SwiftUIContext.subscribe() -> Combine.AnyCancellable","executableLines":3},{"coveredLines":0,"lineCoverage":0,"lineNumber":63,"executionCount":0,"name":"closure #1 (Combine.Subscribers.Completion) -> () in FeedbackLoopSwiftUI.SwiftUIContext.subscribe() -> Combine.AnyCancellable","executableLines":1},{"coveredLines":1,"lineCoverage":1,"lineNumber":63,"executionCount":4,"name":"closure #2 (A) -> () in FeedbackLoopSwiftUI.SwiftUIContext.subscribe() -> Combine.AnyCancellable","executableLines":1},{"coveredLines":3,"lineCoverage":1,"lineNumber":66,"executionCount":1,"name":"FeedbackLoopSwiftUI.SwiftUIContext.start() -> ()","executableLines":3},{"coveredLines":4,"lineCoverage":1,"lineNumber":70,"executionCount":10,"name":"FeedbackLoopSwiftUI.SwiftUIContext.render(state: A) -> ()","executableLines":4},{"coveredLines":1,"lineCoverage":1,"lineNumber":71,"executionCount":8,"name":"implicit closure #1 () throws -> Swift.Bool in FeedbackLoopSwiftUI.SwiftUIContext.render(state: A) -> ()","executableLines":1},{"coveredLines":3,"lineCoverage":1,"lineNumber":75,"executionCount":6,"name":"FeedbackLoopSwiftUI.SwiftUIContext.emit(B) -> ()","executableLines":3},{"coveredLines":4,"lineCoverage":1,"lineNumber":79,"executionCount":2,"name":"FeedbackLoopSwiftUI.SwiftUIContext.binding(for: Swift.KeyPath, event: (A1) -> B) -> SwiftUI.Binding","executableLines":4},{"coveredLines":1,"lineCoverage":1,"lineNumber":80,"executionCount":4,"name":"closure #1 () -> A1 in FeedbackLoopSwiftUI.SwiftUIContext.binding(for: Swift.KeyPath, event: (A1) -> B) -> SwiftUI.Binding","executableLines":1},{"coveredLines":5,"lineCoverage":1,"lineNumber":84,"executionCount":1,"name":"FeedbackLoopSwiftUI.SwiftUIContext.binding(for: Swift.KeyPath, event: B) -> SwiftUI.Binding","executableLines":5},{"coveredLines":3,"lineCoverage":1,"lineNumber":85,"executionCount":1,"name":"closure #1 (A1) -> B in FeedbackLoopSwiftUI.SwiftUIContext.binding(for: Swift.KeyPath, event: B) -> SwiftUI.Binding","executableLines":3},{"coveredLines":3,"lineCoverage":1,"lineNumber":90,"executionCount":1,"name":"FeedbackLoopSwiftUI.SwiftUIContext.binding(for: Swift.KeyPath) -> SwiftUI.Binding","executableLines":3},{"coveredLines":1,"lineCoverage":1,"lineNumber":91,"executionCount":3,"name":"closure #1 () -> A1 in FeedbackLoopSwiftUI.SwiftUIContext.binding(for: Swift.KeyPath) -> SwiftUI.Binding","executableLines":1},{"coveredLines":0,"lineCoverage":0,"lineNumber":91,"executionCount":0,"name":"closure #2 (A1) -> () in FeedbackLoopSwiftUI.SwiftUIContext.binding(for: Swift.KeyPath) -> SwiftUI.Binding","executableLines":1}],"name":"SwiftUIContext.swift","executableLines":69}],"name":"FeedbackLoopSwiftUI.framework","executableLines":69,"buildProductPath":"/Users/twittemberg/Development/Genetec/iOS-FeedbackLoop/Build/Build/Products/Debug-iphonesimulator/FeedbackLoopSwiftUI.framework/FeedbackLoopSwiftUI"},{"coveredLines":3,"lineCoverage":1,"files":[{"coveredLines":3,"lineCoverage":1,"path":"/Users/twittemberg/Development/Genetec/iOS-FeedbackLoop/Sources/Tools/ScopeFunctions/HasScopeFunctions.swift","functions":[{"coveredLines":3,"lineCoverage":1,"lineNumber":11,"executionCount":8,"name":"(extension in Tools):Tools.HasScopeFunctions.let((A) -> A1) -> A1","executableLines":3}],"name":"HasScopeFunctions.swift","executableLines":3}],"name":"Tools.framework","executableLines":3,"buildProductPath":"/Users/twittemberg/Development/Genetec/iOS-FeedbackLoop/Build/Build/Products/Debug-iphonesimulator/Tools.framework/Tools"},{"coveredLines":301,"lineCoverage":0.99668874172185429,"files":[{"coveredLines":3,"lineCoverage":1,"path":"/Users/twittemberg/Development/Genetec/iOS-FeedbackLoop/Tests/FeedbackLoopSwiftUITests/TestState.swift","functions":[{"coveredLines":3,"lineCoverage":1,"lineNumber":14,"executionCount":7,"name":"FeedbackLoopSwiftUITests.TestState.isInitial.getter : Swift.Bool","executableLines":3}],"name":"TestState.swift","executableLines":3},{"coveredLines":298,"lineCoverage":0.99665551839464883,"path":"/Users/twittemberg/Development/Genetec/iOS-FeedbackLoop/Tests/FeedbackLoopSwiftUITests/SwiftUIContextTests.swift","functions":[{"coveredLines":1,"lineCoverage":1,"lineNumber":16,"executionCount":9,"name":"variable initialization expression of FeedbackLoopSwiftUITests.SwiftUIContextTests.(subscriptions in _36916CF3CC1ABF1D8B5DB026E5D86D76) : Swift.Array","executableLines":1},{"coveredLines":30,"lineCoverage":1,"lineNumber":18,"executionCount":1,"name":"FeedbackLoopSwiftUITests.SwiftUIContextTests.test_binding_receive_the_good_substate_and_emit_an_event_when_the_binding_is_mutated() -> ()","executableLines":30},{"coveredLines":4,"lineCoverage":1,"lineNumber":25,"executionCount":1,"name":"closure #1 (FeedbackLoopSwiftUITests.TestEvent) -> () in FeedbackLoopSwiftUITests.SwiftUIContextTests.test_binding_receive_the_good_substate_and_emit_an_event_when_the_binding_is_mutated() -> ()","executableLines":4},{"coveredLines":4,"lineCoverage":1,"lineNumber":31,"executionCount":1,"name":"closure #2 (Swift.Bool) -> FeedbackLoopSwiftUITests.TestEvent in FeedbackLoopSwiftUITests.SwiftUIContextTests.test_binding_receive_the_good_substate_and_emit_an_event_when_the_binding_is_mutated() -> ()","executableLines":4},{"coveredLines":1,"lineCoverage":1,"lineNumber":37,"executionCount":1,"name":"implicit closure #1 () throws -> Swift.Bool in FeedbackLoopSwiftUITests.SwiftUIContextTests.test_binding_receive_the_good_substate_and_emit_an_event_when_the_binding_is_mutated() -> ()","executableLines":1},{"coveredLines":1,"lineCoverage":1,"lineNumber":37,"executionCount":1,"name":"implicit closure #2 () throws -> Swift.Bool in FeedbackLoopSwiftUITests.SwiftUIContextTests.test_binding_receive_the_good_substate_and_emit_an_event_when_the_binding_is_mutated() -> ()","executableLines":1},{"coveredLines":1,"lineCoverage":1,"lineNumber":45,"executionCount":1,"name":"implicit closure #3 () throws -> Swift.Bool in FeedbackLoopSwiftUITests.SwiftUIContextTests.test_binding_receive_the_good_substate_and_emit_an_event_when_the_binding_is_mutated() -> ()","executableLines":1},{"coveredLines":1,"lineCoverage":1,"lineNumber":45,"executionCount":1,"name":"implicit closure #4 () throws -> Swift.Bool in FeedbackLoopSwiftUITests.SwiftUIContextTests.test_binding_receive_the_good_substate_and_emit_an_event_when_the_binding_is_mutated() -> ()","executableLines":1},{"coveredLines":1,"lineCoverage":1,"lineNumber":46,"executionCount":1,"name":"implicit closure #5 () throws -> Swift.Optional in FeedbackLoopSwiftUITests.SwiftUIContextTests.test_binding_receive_the_good_substate_and_emit_an_event_when_the_binding_is_mutated() -> ()","executableLines":1},{"coveredLines":1,"lineCoverage":1,"lineNumber":46,"executionCount":1,"name":"implicit closure #6 () throws -> Swift.Optional in FeedbackLoopSwiftUITests.SwiftUIContextTests.test_binding_receive_the_good_substate_and_emit_an_event_when_the_binding_is_mutated() -> ()","executableLines":1},{"coveredLines":25,"lineCoverage":1,"lineNumber":49,"executionCount":1,"name":"FeedbackLoopSwiftUITests.SwiftUIContextTests.test_binding_emit_an_event_when_the_binding_is_mutated() -> ()","executableLines":25},{"coveredLines":4,"lineCoverage":1,"lineNumber":55,"executionCount":1,"name":"closure #1 (FeedbackLoopSwiftUITests.TestEvent) -> () in FeedbackLoopSwiftUITests.SwiftUIContextTests.test_binding_emit_an_event_when_the_binding_is_mutated() -> ()","executableLines":4},{"coveredLines":1,"lineCoverage":1,"lineNumber":64,"executionCount":1,"name":"implicit closure #1 () throws -> Swift.Bool in FeedbackLoopSwiftUITests.SwiftUIContextTests.test_binding_emit_an_event_when_the_binding_is_mutated() -> ()","executableLines":1},{"coveredLines":1,"lineCoverage":1,"lineNumber":64,"executionCount":1,"name":"implicit closure #2 () throws -> Swift.Bool in FeedbackLoopSwiftUITests.SwiftUIContextTests.test_binding_emit_an_event_when_the_binding_is_mutated() -> ()","executableLines":1},{"coveredLines":1,"lineCoverage":1,"lineNumber":72,"executionCount":1,"name":"implicit closure #3 () throws -> Swift.Optional in FeedbackLoopSwiftUITests.SwiftUIContextTests.test_binding_emit_an_event_when_the_binding_is_mutated() -> ()","executableLines":1},{"coveredLines":1,"lineCoverage":1,"lineNumber":72,"executionCount":1,"name":"implicit closure #4 () throws -> Swift.Optional in FeedbackLoopSwiftUITests.SwiftUIContextTests.test_binding_emit_an_event_when_the_binding_is_mutated() -> ()","executableLines":1},{"coveredLines":16,"lineCoverage":1,"lineNumber":75,"executionCount":1,"name":"FeedbackLoopSwiftUITests.SwiftUIContextTests.test_binding_wrap_the_good_subState() -> ()","executableLines":16},{"coveredLines":1,"lineCoverage":1,"lineNumber":83,"executionCount":1,"name":"implicit closure #1 () throws -> Swift.Bool in FeedbackLoopSwiftUITests.SwiftUIContextTests.test_binding_wrap_the_good_subState() -> ()","executableLines":1},{"coveredLines":1,"lineCoverage":1,"lineNumber":83,"executionCount":1,"name":"implicit closure #2 () throws -> Swift.Bool in FeedbackLoopSwiftUITests.SwiftUIContextTests.test_binding_wrap_the_good_subState() -> ()","executableLines":1},{"coveredLines":1,"lineCoverage":1,"lineNumber":89,"executionCount":1,"name":"implicit closure #3 () throws -> Swift.Bool in FeedbackLoopSwiftUITests.SwiftUIContextTests.test_binding_wrap_the_good_subState() -> ()","executableLines":1},{"coveredLines":1,"lineCoverage":1,"lineNumber":89,"executionCount":1,"name":"implicit closure #4 () throws -> Swift.Bool in FeedbackLoopSwiftUITests.SwiftUIContextTests.test_binding_wrap_the_good_subState() -> ()","executableLines":1},{"coveredLines":20,"lineCoverage":1,"lineNumber":92,"executionCount":1,"name":"FeedbackLoopSwiftUITests.SwiftUIContextTests.testEmitEvents_emit_send_events_in_the_eventStream() -> ()","executableLines":20},{"coveredLines":4,"lineCoverage":1,"lineNumber":99,"executionCount":1,"name":"closure #1 (FeedbackLoopSwiftUITests.TestEvent) -> () in FeedbackLoopSwiftUITests.SwiftUIContextTests.testEmitEvents_emit_send_events_in_the_eventStream() -> ()","executableLines":4},{"coveredLines":1,"lineCoverage":1,"lineNumber":110,"executionCount":1,"name":"implicit closure #1 () throws -> Swift.Optional in FeedbackLoopSwiftUITests.SwiftUIContextTests.testEmitEvents_emit_send_events_in_the_eventStream() -> ()","executableLines":1},{"coveredLines":1,"lineCoverage":1,"lineNumber":110,"executionCount":1,"name":"implicit closure #2 () throws -> Swift.Optional in FeedbackLoopSwiftUITests.SwiftUIContextTests.testEmitEvents_emit_send_events_in_the_eventStream() -> ()","executableLines":1},{"coveredLines":21,"lineCoverage":1,"lineNumber":113,"executionCount":1,"name":"FeedbackLoopSwiftUITests.SwiftUIContextTests.testRender_update_publishedState_with_distinct_states() -> ()","executableLines":21},{"coveredLines":4,"lineCoverage":1,"lineNumber":120,"executionCount":1,"name":"closure #1 (Swift.Array) -> () in FeedbackLoopSwiftUITests.SwiftUIContextTests.testRender_update_publishedState_with_distinct_states() -> ()","executableLines":4},{"coveredLines":1,"lineCoverage":1,"lineNumber":132,"executionCount":1,"name":"implicit closure #1 () throws -> Swift.Array in FeedbackLoopSwiftUITests.SwiftUIContextTests.testRender_update_publishedState_with_distinct_states() -> ()","executableLines":1},{"coveredLines":1,"lineCoverage":1,"lineNumber":132,"executionCount":1,"name":"implicit closure #2 () throws -> Swift.Array in FeedbackLoopSwiftUITests.SwiftUIContextTests.testRender_update_publishedState_with_distinct_states() -> ()","executableLines":1},{"coveredLines":21,"lineCoverage":1,"lineNumber":135,"executionCount":1,"name":"FeedbackLoopSwiftUITests.SwiftUIContextTests.testRender_update_publishedState_with_whatever_states() -> ()","executableLines":21},{"coveredLines":4,"lineCoverage":1,"lineNumber":142,"executionCount":1,"name":"closure #1 (Swift.Array) -> () in FeedbackLoopSwiftUITests.SwiftUIContextTests.testRender_update_publishedState_with_whatever_states() -> ()","executableLines":4},{"coveredLines":1,"lineCoverage":1,"lineNumber":154,"executionCount":1,"name":"implicit closure #1 () throws -> Swift.Array in FeedbackLoopSwiftUITests.SwiftUIContextTests.testRender_update_publishedState_with_whatever_states() -> ()","executableLines":1},{"coveredLines":1,"lineCoverage":1,"lineNumber":154,"executionCount":1,"name":"implicit closure #2 () throws -> Swift.Array in FeedbackLoopSwiftUITests.SwiftUIContextTests.testRender_update_publishedState_with_whatever_states() -> ()","executableLines":1},{"coveredLines":29,"lineCoverage":1,"lineNumber":157,"executionCount":1,"name":"FeedbackLoopSwiftUITests.SwiftUIContextTests.testStream_make_a_stream_with_a_uiSideEffect_involving_renderState_andEventStream() -> ()","executableLines":29},{"coveredLines":3,"lineCoverage":1,"lineNumber":161,"executionCount":1,"name":"closure #1 (FeedbackLoopSwiftUITests.TestState, FeedbackLoopSwiftUITests.TestEvent) -> FeedbackLoopSwiftUITests.TestState in FeedbackLoopSwiftUITests.SwiftUIContextTests.testStream_make_a_stream_with_a_uiSideEffect_involving_renderState_andEventStream() -> ()","executableLines":3},{"coveredLines":4,"lineCoverage":1,"lineNumber":170,"executionCount":1,"name":"closure #2 (Swift.Array) -> () in FeedbackLoopSwiftUITests.SwiftUIContextTests.testStream_make_a_stream_with_a_uiSideEffect_involving_renderState_andEventStream() -> ()","executableLines":4},{"coveredLines":0,"lineCoverage":0,"lineNumber":176,"executionCount":0,"name":"closure #3 (Combine.Subscribers.Completion) -> () in FeedbackLoopSwiftUITests.SwiftUIContextTests.testStream_make_a_stream_with_a_uiSideEffect_involving_renderState_andEventStream() -> ()","executableLines":1},{"coveredLines":1,"lineCoverage":1,"lineNumber":176,"executionCount":2,"name":"closure #4 (FeedbackLoopSwiftUITests.TestState) -> () in FeedbackLoopSwiftUITests.SwiftUIContextTests.testStream_make_a_stream_with_a_uiSideEffect_involving_renderState_andEventStream() -> ()","executableLines":1},{"coveredLines":1,"lineCoverage":1,"lineNumber":184,"executionCount":1,"name":"implicit closure #1 () throws -> Swift.Array in FeedbackLoopSwiftUITests.SwiftUIContextTests.testStream_make_a_stream_with_a_uiSideEffect_involving_renderState_andEventStream() -> ()","executableLines":1},{"coveredLines":1,"lineCoverage":1,"lineNumber":184,"executionCount":1,"name":"implicit closure #2 () throws -> Swift.Array in FeedbackLoopSwiftUITests.SwiftUIContextTests.testStream_make_a_stream_with_a_uiSideEffect_involving_renderState_andEventStream() -> ()","executableLines":1},{"coveredLines":29,"lineCoverage":1,"lineNumber":187,"executionCount":1,"name":"FeedbackLoopSwiftUITests.SwiftUIContextTests.testSubscribe_subscribe_to_the_state_stream() -> ()","executableLines":29},{"coveredLines":3,"lineCoverage":1,"lineNumber":191,"executionCount":1,"name":"closure #1 (FeedbackLoopSwiftUITests.TestState, FeedbackLoopSwiftUITests.TestEvent) -> FeedbackLoopSwiftUITests.TestState in FeedbackLoopSwiftUITests.SwiftUIContextTests.testSubscribe_subscribe_to_the_state_stream() -> ()","executableLines":3},{"coveredLines":4,"lineCoverage":1,"lineNumber":200,"executionCount":1,"name":"closure #2 (Swift.Array) -> () in FeedbackLoopSwiftUITests.SwiftUIContextTests.testSubscribe_subscribe_to_the_state_stream() -> ()","executableLines":4},{"coveredLines":1,"lineCoverage":1,"lineNumber":214,"executionCount":1,"name":"implicit closure #1 () throws -> Swift.Array in FeedbackLoopSwiftUITests.SwiftUIContextTests.testSubscribe_subscribe_to_the_state_stream() -> ()","executableLines":1},{"coveredLines":1,"lineCoverage":1,"lineNumber":214,"executionCount":1,"name":"implicit closure #2 () throws -> Swift.Array in FeedbackLoopSwiftUITests.SwiftUIContextTests.testSubscribe_subscribe_to_the_state_stream() -> ()","executableLines":1},{"coveredLines":32,"lineCoverage":1,"lineNumber":217,"executionCount":1,"name":"FeedbackLoopSwiftUITests.SwiftUIContextTests.testStart_subscribe_and_manage_subscription_to_the_state_stream() -> ()","executableLines":32},{"coveredLines":3,"lineCoverage":1,"lineNumber":221,"executionCount":1,"name":"closure #1 (FeedbackLoopSwiftUITests.TestState, FeedbackLoopSwiftUITests.TestEvent) -> FeedbackLoopSwiftUITests.TestState in FeedbackLoopSwiftUITests.SwiftUIContextTests.testStart_subscribe_and_manage_subscription_to_the_state_stream() -> ()","executableLines":3},{"coveredLines":4,"lineCoverage":1,"lineNumber":230,"executionCount":1,"name":"closure #2 (Swift.Array) -> () in FeedbackLoopSwiftUITests.SwiftUIContextTests.testStart_subscribe_and_manage_subscription_to_the_state_stream() -> ()","executableLines":4},{"coveredLines":1,"lineCoverage":1,"lineNumber":239,"executionCount":1,"name":"implicit closure #1 () throws -> Swift.Int in FeedbackLoopSwiftUITests.SwiftUIContextTests.testStart_subscribe_and_manage_subscription_to_the_state_stream() -> ()","executableLines":1},{"coveredLines":1,"lineCoverage":1,"lineNumber":239,"executionCount":1,"name":"implicit closure #2 () throws -> Swift.Int in FeedbackLoopSwiftUITests.SwiftUIContextTests.testStart_subscribe_and_manage_subscription_to_the_state_stream() -> ()","executableLines":1},{"coveredLines":1,"lineCoverage":1,"lineNumber":247,"executionCount":1,"name":"implicit closure #3 () throws -> Swift.Array in FeedbackLoopSwiftUITests.SwiftUIContextTests.testStart_subscribe_and_manage_subscription_to_the_state_stream() -> ()","executableLines":1},{"coveredLines":1,"lineCoverage":1,"lineNumber":247,"executionCount":1,"name":"implicit closure #4 () throws -> Swift.Array in FeedbackLoopSwiftUITests.SwiftUIContextTests.testStart_subscribe_and_manage_subscription_to_the_state_stream() -> ()","executableLines":1}],"name":"SwiftUIContextTests.swift","executableLines":299}],"name":"FeedbackLoopSwiftUITests.xctest","executableLines":302,"buildProductPath":"/Users/twittemberg/Development/Genetec/iOS-FeedbackLoop/Build/Build/Products/Debug-iphonesimulator/FeedbackLoopSwiftUITests.xctest/FeedbackLoopSwiftUITests"},{"coveredLines":40,"lineCoverage":1,"files":[{"coveredLines":20,"lineCoverage":1,"path":"/Users/twittemberg/Development/Genetec/iOS-FeedbackLoop/Tests/ToolsTests/ScopeFunctions/NSObject+HasScopeFunctionsTests.swift","functions":[{"coveredLines":15,"lineCoverage":1,"lineNumber":13,"executionCount":1,"name":"ToolsTests.NSObject_HasScopeFunctionsTests.test_let_executes_the_block() throws -> ()","executableLines":15},{"coveredLines":4,"lineCoverage":1,"lineNumber":20,"executionCount":1,"name":"closure #1 (__C.NSObject) -> Swift.Int in ToolsTests.NSObject_HasScopeFunctionsTests.test_let_executes_the_block() throws -> ()","executableLines":4},{"coveredLines":1,"lineCoverage":1,"lineNumber":26,"executionCount":1,"name":"implicit closure #1 () throws -> Swift.Bool in ToolsTests.NSObject_HasScopeFunctionsTests.test_let_executes_the_block() throws -> ()","executableLines":1}],"name":"NSObject+HasScopeFunctionsTests.swift","executableLines":20},{"coveredLines":20,"lineCoverage":1,"path":"/Users/twittemberg/Development/Genetec/iOS-FeedbackLoop/Tests/ToolsTests/ScopeFunctions/CurrentValueSubject+HasScopeFunctionsTests.swift","functions":[{"coveredLines":15,"lineCoverage":1,"lineNumber":14,"executionCount":1,"name":"ToolsTests.CurrentValueSubject_HasScopeFunctionsTests.test_let_executes_the_block() throws -> ()","executableLines":15},{"coveredLines":4,"lineCoverage":1,"lineNumber":21,"executionCount":1,"name":"closure #1 (Combine.CurrentValueSubject<(), Swift.Never>) -> Swift.Int in ToolsTests.CurrentValueSubject_HasScopeFunctionsTests.test_let_executes_the_block() throws -> ()","executableLines":4},{"coveredLines":1,"lineCoverage":1,"lineNumber":27,"executionCount":1,"name":"implicit closure #1 () throws -> Swift.Bool in ToolsTests.CurrentValueSubject_HasScopeFunctionsTests.test_let_executes_the_block() throws -> ()","executableLines":1}],"name":"CurrentValueSubject+HasScopeFunctionsTests.swift","executableLines":20}],"name":"ToolsTests.xctest","executableLines":40,"buildProductPath":"/Users/twittemberg/Development/Genetec/iOS-FeedbackLoop/Build/Build/Products/Debug-iphonesimulator/ToolsTests.xctest/ToolsTests"}],"executableLines":666} 10 | """ 11 | -------------------------------------------------------------------------------- /Tests/CoreTests/Converters/Fixtures/ConverterCoverageXml.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConverterCoverageXml.swift 3 | // 4 | // 5 | // Created by Thibault Wittemberg on 2020-07-04. 6 | // 7 | 8 | let coberturaFixtureCoverageXml = """ 9 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | ]> 55 | 56 | 57 | 58 | XcodeCoverageConverter 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | """ 794 | -------------------------------------------------------------------------------- /Tests/CoreTests/Converters/Fixtures/SonarqubeCoverageXml.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Harris, David (D.A.) on 1/19/21. 6 | // 7 | 8 | import Foundation 9 | 10 | let sonarqubeFixtureCoverageXml = """ 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | """ 701 | -------------------------------------------------------------------------------- /Tests/CoreTests/Converters/SonarqubeXmlConverterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SonarqubeXmlConverterTests.swift 3 | // 4 | // 5 | // Created by Harris, David (D.A.) on 1/19/21. 6 | // 7 | 8 | @testable import Core 9 | import XCTest 10 | 11 | final class SonarqubeXmlConverterTests: XCTestCase { 12 | func testConvert_return_the_expected_xml() { 13 | // Given: a json report 14 | let data = converterFixtureCoverageJson.data(using: .utf8) 15 | let coverageReport = try! JSONDecoder().decode(CoverageReport.self, from: data!) 16 | 17 | // When: converting to Sonarqube XML 18 | let result = Xccov.Converters.SonarqubeXml.convert(coverageReport: coverageReport) 19 | let receivedResult = try! result.get() 20 | 21 | // Then: the xml is the expected one 22 | XCTAssertEqual(receivedResult, sonarqubeFixtureCoverageXml) 23 | } 24 | 25 | static var allTests = [ 26 | ("testConvert_return_the_expected_xml", testConvert_return_the_expected_xml), 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /Tests/CoreTests/Decoders/JsonDecoderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JsonDecoderTests.swift 3 | // 4 | // 5 | // Created by Thibault Wittemberg on 2020-07-04. 6 | // 7 | 8 | @testable import Core 9 | import XCTest 10 | 11 | final class JsonDecoderTests: XCTestCase { 12 | func testDecode_return_failure_when_content_is_empty() { 13 | let expectedResult = Result.failure(.contentIsEmpty("filename")) 14 | 15 | // Given: an empty content 16 | let imported = Import(content: "", filename: "filename") 17 | 18 | // When: decoding it 19 | let receivedResult = Xccov.Decoders.Json.decode(imported: imported) 20 | 21 | // Then: the result is a failure with a .contentIsEmpty error 22 | XCTAssertEqual(receivedResult, expectedResult) 23 | } 24 | 25 | func testDecode_return_failure_when_content_is_not_a_coverage_report() { 26 | let expectedResult = Result.failure(.contentIsNotACoverageReport("filename")) 27 | 28 | // Given: a content that is not a coverage report 29 | let imported = Import(content: "notCoverage", filename: "filename") 30 | 31 | // When: decoding it 32 | let receivedResult = Xccov.Decoders.Json.decode(imported: imported) 33 | 34 | // Then: the result is a failure with a .contentIsNotACoverageReport error 35 | XCTAssertEqual(receivedResult, expectedResult) 36 | } 37 | 38 | func testDecode_return_success_when_content_is_a_coverage_report() { 39 | let data = decoderFixtureCoverageJson.data(using: .utf8) 40 | let coverageReport = try! JSONDecoder().decode(CoverageReport.self, from: data!) 41 | 42 | let expectedResult = Result.success(coverageReport) 43 | 44 | // Given: a content that is a coverage report 45 | let imported = Import(content: decoderFixtureCoverageJson, filename: "filename") 46 | 47 | // When: decoding it 48 | let receivedResult = Xccov.Decoders.Json.decode(imported: imported) 49 | 50 | // Then: the result is a success with the expected CoverageReport 51 | XCTAssertEqual(receivedResult, expectedResult) 52 | } 53 | 54 | static var allTests = [ 55 | ("testDecode_return_failure_when_content_is_empty", testDecode_return_failure_when_content_is_empty), 56 | ("testDecode_return_failure_when_content_is_not_a_coverage_report", testDecode_return_failure_when_content_is_not_a_coverage_report), 57 | ("testDecode_return_success_when_content_is_a_coverage_report", testDecode_return_success_when_content_is_a_coverage_report), 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /Tests/CoreTests/Filters/PackagesFilterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PackagesFilterTests.swift 3 | // 4 | // 5 | // Created by Thibault Wittemberg on 2020-06-01. 6 | // 7 | 8 | @testable import Core 9 | import XCTest 10 | 11 | final class PackagesFilterTests: XCTestCase { 12 | func testFilter_removes_packages_to_exclude() { 13 | // Given: an input coverageReport with 4 different packages 14 | let file1 = FileCoverageReport(coveredLines: 0, executableLines: 0, functions: [], lineCoverage: 0, name: "1", path: "/common/file1") 15 | let file2 = FileCoverageReport(coveredLines: 0, executableLines: 0, functions: [], lineCoverage: 0, name: "2", path: "/client1/file2") 16 | let file3 = FileCoverageReport(coveredLines: 0, executableLines: 0, functions: [], lineCoverage: 0, name: "3", path: "/client2/file3") 17 | let file4 = FileCoverageReport(coveredLines: 0, executableLines: 0, functions: [], lineCoverage: 0, name: "4", path: "/common/file4") 18 | let file5 = FileCoverageReport(coveredLines: 0, executableLines: 0, functions: [], lineCoverage: 0, name: "5", path: "/client3/file5") 19 | let file6 = FileCoverageReport(coveredLines: 0, executableLines: 0, functions: [], lineCoverage: 0, name: "6", path: "/client1/file6") 20 | 21 | let target1 = TargetCoverageReport(buildProductPath: "", coveredLines: 0, executableLines: 0, files: [file1, file2, file3], lineCoverage: 0, name: "1") 22 | let target2 = TargetCoverageReport(buildProductPath: "", coveredLines: 0, executableLines: 0, files: [file4, file5, file6], lineCoverage: 0, name: "2") 23 | let coverageReport = CoverageReport(executableLines: 0, targets: [target1, target2], lineCoverage: 0, coveredLines: 0) 24 | 25 | let expectedTarget1 = TargetCoverageReport(buildProductPath: "", coveredLines: 0, executableLines: 0, files: [file1], lineCoverage: 0, name: "1") 26 | let expectedTarget2 = TargetCoverageReport(buildProductPath: "", coveredLines: 0, executableLines: 0, files: [file4, file5], lineCoverage: 0, name: "2") 27 | let expectedReport = CoverageReport(executableLines: 0, targets: [expectedTarget1, expectedTarget2], lineCoverage: 0, coveredLines: 0) 28 | 29 | // When: filtering the coverage report by excluding packages client1 and client2 30 | let receivedReport = Xccov.Filters.Packages.filter(coverageReport: coverageReport, packagesToExclude: ["client1", "client2"]) 31 | 32 | // Then: the filtered coverage report has only 2 differente packages left 33 | XCTAssertEqual(receivedReport, expectedReport) 34 | } 35 | 36 | static var allTests = [ 37 | ("testFilter_removes_packages_to_exclude", testFilter_removes_packages_to_exclude), 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /Tests/CoreTests/Filters/TargetsFilterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TargetsFilterTests.swift 3 | // 4 | // 5 | // Created by Thibault Wittemberg on 2020-06-01. 6 | // 7 | 8 | @testable import Core 9 | import XCTest 10 | 11 | final class TargetsFilterTests: XCTestCase { 12 | func testFilter_removes_targets_to_exclude() { 13 | // Given: an input coverageReport with target 1, 2 and 3 14 | let target1 = TargetCoverageReport(buildProductPath: "", coveredLines: 0, executableLines: 0, files: [], lineCoverage: 0, name: "1") 15 | let target2 = TargetCoverageReport(buildProductPath: "", coveredLines: 0, executableLines: 0, files: [], lineCoverage: 0, name: "2") 16 | let target3 = TargetCoverageReport(buildProductPath: "", coveredLines: 0, executableLines: 0, files: [], lineCoverage: 0, name: "3") 17 | let coverageReport = CoverageReport(executableLines: 0, targets: [target1, target2, target3], lineCoverage: 0, coveredLines: 0) 18 | let expectedReport = CoverageReport(executableLines: 0, targets: [target1], lineCoverage: 0, coveredLines: 0) 19 | 20 | // When: filtering the coverage report by excluding targets 2 and 3 21 | let receivedReport = Xccov.Filters.Targets.filter(coverageReport: coverageReport, targetsToExclude: ["2", "3"]) 22 | 23 | // Then: the filtered coverage report has only target 1 24 | XCTAssertEqual(receivedReport, expectedReport) 25 | } 26 | 27 | static var allTests = [ 28 | ("testFilter_removes_targets_to_exclude", testFilter_removes_targets_to_exclude), 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /Tests/CoreTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCTestManifests.swift 3 | // 4 | // 5 | // Created by Thibault Wittemberg on 2020-07-05. 6 | // 7 | 8 | import XCTest 9 | 10 | #if !canImport(ObjectiveC) 11 | public func allTests() -> [XCTestCaseEntry] { 12 | return [ 13 | testCase(GenerateCommandTests.allTests), 14 | testCase(Result_MapEachTests.allTests), 15 | testCase(Result_VerboseTests.allTests), 16 | testCase(String_ContainsTests.allTests), 17 | testCase(CoberturaXmlConverterTests.allTests), 18 | testCase(JsonDecoderTests.allTests), 19 | testCase(PackagesFilterTests.allTests), 20 | testCase(TargetsFilterTests.allTests), 21 | testCase(SonarqubeXmlConverterTests.allTests) 22 | ] 23 | } 24 | #endif 25 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import XcodeCoverageConverterTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += XcodeCoverageConverterTests.allTests() 7 | XCTMain(tests) 8 | --------------------------------------------------------------------------------