Skip to content

Commit

Permalink
Support targets in coverage
Browse files Browse the repository at this point in the history
Added support for the targets filter for the coverage functions as well
Added new method to just list all target names contained in the xcresult archive
  • Loading branch information
Alex da Franca committed Feb 11, 2023
1 parent 6f2ad93 commit 43a2b29
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 54 deletions.
16 changes: 16 additions & 0 deletions .swiftpm/xcode/xcshareddata/xcschemes/xcresultparser.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,22 @@
argument = "/Users/alex/xcodebuild_result.xcresult"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "/Users/alex/Downloads/coverage.xcresult"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "-t FRBNetworking"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "-t FRBUIKit"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "-i"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "-s errors|tests|failed|skipped"
isEnabled = "NO">
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Version 1.3 - 2023-02-11
### CHANGES:
Added support for the targets filter for the coverage functions as well
Added new method to just list all target names contained in the xcresult archive

## Version 1.2.2 - 2023-01-08
### CHANGES:
Fixes the junit output formatter, set cobertura timestamp to test execution time and improves the entire test suite.
Expand Down
34 changes: 30 additions & 4 deletions CommandlineTool/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation
import ArgumentParser
import XcresultparserLib

private let marketingVersion = "1.2.2"
private let marketingVersion = "1.3"

struct xcresultparser: ParsableCommand {
static let configuration = CommandConfiguration(
Expand All @@ -22,7 +22,7 @@ struct xcresultparser: ParsableCommand {
@Option(name: .shortAndLong, help: "The name of the project root. If present paths and urls are relative to the specified directory.")
var projectRoot: String?

@Option(name: [.customShort("t"), .customLong("coverage-targets")], help: "Specify which targets to calculate coverage from")
@Option(name: [.customShort("t"), .customLong("coverage-targets")], help: "Specify which targets to calculate coverage from. You can use more than one -t option to specify a list of targets.")
var coverageTargets: [String] = []

@Option(name: .shortAndLong, help: "The fields in the summary. Default is all: errors|warnings|analyzerWarnings|tests|failed|skipped")
Expand All @@ -40,6 +40,9 @@ struct xcresultparser: ParsableCommand {
@Flag(name: .shortAndLong, help: "Quiet. Don't print status output.")
var quiet: Int

@Flag(name: [.customShort("i"), .customLong("target-info")], help: "Just print the targets contained in the xcresult.")
var printTargets: Int

@Flag(name: .shortAndLong, help: "Show version number.")
var version: Int

Expand All @@ -55,6 +58,10 @@ struct xcresultparser: ParsableCommand {
!xcresult.isEmpty else {
throw ParseError.argumentError
}
guard printTargets != 1 else {
try outputTargetNames(for: xcresult)
return
}
if format == .xml {
if coverage == 1 {
try outputSonarXML(for: xcresult)
Expand All @@ -72,20 +79,39 @@ struct xcresultparser: ParsableCommand {
}

private func outputSonarXML(for xcresult: String) throws {
guard let converter = SonarCoverageConverter(with: URL(fileURLWithPath: xcresult), projectRoot: projectRoot ?? "") else {
guard let converter = SonarCoverageConverter(
with: URL(fileURLWithPath: xcresult),
projectRoot: projectRoot ?? "",
coverageTargets: coverageTargets
) else {
throw ParseError.argumentError
}
let rslt = try converter.xmlString(quiet: quiet == 1)
writeToStdOut(rslt)
}

private func outputCoberturaXML(for xcresult: String) throws {
guard let converter = CoberturaCoverageConverter(with: URL(fileURLWithPath: xcresult), projectRoot: projectRoot ?? "") else {
guard let converter = CoberturaCoverageConverter(
with: URL(fileURLWithPath: xcresult),
projectRoot: projectRoot ?? "",
coverageTargets: coverageTargets
) else {
throw ParseError.argumentError
}
let rslt = try converter.xmlString(quiet: quiet == 1)
writeToStdOut(rslt)
}

private func outputTargetNames(for xcresult: String) throws {
guard let converter = SonarCoverageConverter(
with: URL(fileURLWithPath: xcresult),
projectRoot: projectRoot ?? "",
coverageTargets: coverageTargets
) else {
throw ParseError.argumentError
}
writeToStdOut(converter.targetsInfo)
}

private func outputJUnitXML(for xcresult: String,
with format: TestReportFormat) throws {
Expand Down
24 changes: 16 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,34 +77,37 @@ You should see the tool respond like this:
```
Error: Missing expected argument '<xcresult-file>'
OVERVIEW: xcresultparser 1.1.5
OVERVIEW: xcresultparser 1.3
Interpret binary .xcresult files and print summary in different formats: txt,
xml, html or colored cli output.
USAGE: xcresultparser [--output-format <output-format>] [--project-root <project-root>] [--coverage-targets <coverage-targets> ...] [--summary-fields <summary-fields>] [--coverage ...] [--no-test-result ...] [--failed-tests-only ...] [--quiet ...] [--version ...] [<xcresult-file>]
USAGE: xcresultparser [--output-format <output-format>] [--project-root <project-root>] [--coverage-targets <coverage-targets> ...] [--summary-fields <summary-fields>] [--coverage ...] [--no-test-result ...] [--failed-tests-only ...] [--quiet ...] [--target-info ...] [--version ...] [<xcresult-file>]
ARGUMENTS:
<xcresult-file> The path to the .xcresult file.
OPTIONS:
-o, --output-format <output-format>
The output format. It can be either 'txt', 'cli',
'html', 'md', 'xml', 'junit', or 'cobertura'. In case of 'xml'
generic format (Sonarqube) for test results and generic format
(Sonarqube) for coverage data is used. In the case of
'cobertura', --coverage is implied.
'html', 'md', 'xml', 'junit', or 'cobertura'. In case
of 'xml' sonar generic format for test results and
generic format (Sonarqube) for coverage data is used.
In the case of 'cobertura', --coverage is implied.
-p, --project-root <project-root>
The name of the project root. If present paths and
urls are relative to the specified directory.
-t, --coverage-targets <coverage-targets>
Specify which targets to calculate coverage from
Specify which targets to calculate coverage from. You
can use more than one -t option to specify a list of
targets.
-s, --summary-fields <summary-fields>
The fields in the summary. Default is all:
errors|warnings|analyzerWarnings|tests|failed|skipped
-c, --coverage Whether to print coverage data.
-n, --no-test-result Whether to print test results.
-f, --failed-tests-only Whether to only print failed tests.
-q, --quiet Quiet. Don't print status output.
-i, --target-info Just print the targets contained in the xcresult.
-v, --version Show version number.
-h, --help Show help information.
```
Expand Down Expand Up @@ -150,11 +153,16 @@ Create an xml file in generic test exectuion xml format:
./xcresultparser -o xml test.xcresult > sonarTestExecution.xml
```

Create an xml file in generic code coverage xml format:
Create an xml file in generic code coverage xml format for all targets:
```
./xcresultparser -c -o xml test.xcresult > sonarCoverage.xml
```

Create an xml file in generic code coverage xml format, but only for two of the targets "foo" and "baz":
```
./xcresultparser -c -o xml test.xcresult -t foo -t baz > sonarCoverage.xml
```

### Cobertura XML output
Create xml file in [Cobertura](https://cobertura.github.io/cobertura/) format:
```
Expand Down
34 changes: 20 additions & 14 deletions Sources/xcresultparser/CoberturaCoverageConverter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,30 +66,36 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable {
rootElement.addChild(packagesElement)

let fileInfoSemaphore = DispatchSemaphore(value: 1)
let files = try coverageFileList()
var fileInfo: [FileInfo] = []

// since we need to invoke xccov for each file, it takes pretty much time
// so we invoke it in parallel on 8 threads, that speeds up things considerably
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 8 //Deadlock if this is = 1
queue.qualityOfService = .userInitiated
for file in files {
guard !file.isEmpty else { continue }
if !quiet {
writeToStdError("Coverage for: \(file)\n")

for target in codeCoverage.targets {
if !coverageTargets.isEmpty {
guard coverageTargets.contains(target.name) else { continue }
}
let op = BlockOperation { [self] in
do {
let coverage = try fileCoverage(for: file, relativeTo: projectRoot)
fileInfoSemaphore.wait()
fileInfo.append(coverage)
fileInfoSemaphore.signal()
} catch {
writeToStdErrorLn(error.localizedDescription)
for codeCovFile in target.files {
let file = codeCovFile.path
guard !file.isEmpty else { continue }
if !quiet {
writeToStdError("Coverage for: \(file)\n")
}
let op = BlockOperation { [self] in
do {
let coverage = try fileCoverage(for: file, relativeTo: projectRoot)
fileInfoSemaphore.wait()
fileInfo.append(coverage)
fileInfoSemaphore.signal()
} catch {
writeToStdErrorLn(error.localizedDescription)
}
}
queue.addOperation(op)
}
queue.addOperation(op)
}
// This will block until all our operation have compleated (or been canceled)
queue.waitUntilAllOperationsAreFinished()
Expand Down
40 changes: 27 additions & 13 deletions Sources/xcresultparser/CoverageConverter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,13 @@ public class CoverageConverter {
let codeCoverage: CodeCoverage
let invocationRecord: ActionsInvocationRecord
let coverageRegexp: NSRegularExpression?
let coverageTargets: Set<String>

public init?(with url: URL,
projectRoot: String = "") {
public init?(
with url: URL,
projectRoot: String = "",
coverageTargets: [String] = []
) {
resultFile = XCResultFile(url: url)
guard let record = resultFile.getCodeCoverage() else {
return nil
Expand All @@ -38,6 +42,8 @@ public class CoverageConverter {
return nil
}
self.invocationRecord = invocationRecord

self.coverageTargets = record.targets(filteredBy: coverageTargets)

let pattern = #"(\d+):\s*(\d)"#
coverageRegexp = try? NSRegularExpression(pattern: pattern, options: .anchorsMatchLines)
Expand All @@ -47,6 +53,12 @@ public class CoverageConverter {
fatalError("xmlString is not implemented")
}

public var targetsInfo: String {
return codeCoverage.targets.reduce("") {rslt, item in
return "\(rslt)\n\(item.name)"
}
}

func writeToStdErrorLn(_ str: String) {
writeToStdError("\(str)\n")
}
Expand Down Expand Up @@ -74,28 +86,30 @@ public class CoverageConverter {
relative
}


func coverageFileList() throws -> [String] {
func coverageForFile(path: String) throws -> String {
var arguments = ["xccov", "view"]
if resultFile.url.pathExtension == "xcresult" {
arguments.append("--archive")
}
arguments.append("--file-list")
arguments.append("--file")
arguments.append(path)
arguments.append(resultFile.url.path)
let filelistData = try Shell.execute(program: "/usr/bin/xcrun", with: arguments)
return String(decoding: filelistData, as: UTF8.self).components(separatedBy: "\n")
let coverageData = try Shell.execute(program: "/usr/bin/xcrun", with: arguments)
return String(decoding: coverageData, as: UTF8.self)
}

func coverageForFile(path: String) throws -> String {

// This method was replaced by going through all files in all targets
// That allows us to filter by targets easier
// It is not used at the moment, but is left here just to cover this xccov function
func coverageFileList() throws -> [String] {
var arguments = ["xccov", "view"]
if resultFile.url.pathExtension == "xcresult" {
arguments.append("--archive")
}
arguments.append("--file")
arguments.append(path)
arguments.append("--file-list")
arguments.append(resultFile.url.path)
let coverageData = try Shell.execute(program: "/usr/bin/xcrun", with: arguments)
return String(decoding: coverageData, as: UTF8.self)
let filelistData = try Shell.execute(program: "/usr/bin/xcrun", with: arguments)
return String(decoding: filelistData, as: UTF8.self).components(separatedBy: "\n")
}
}

Expand Down
34 changes: 20 additions & 14 deletions Sources/xcresultparser/SonarCoverageConverter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,29 +22,35 @@ public class SonarCoverageConverter: CoverageConverter, XmlSerializable {
let coverageXML = XMLElement(name: "coverage")
coverageXML.addAttribute(name: "version", stringValue: "1")
let coverageXMLSemaphore = DispatchSemaphore(value: 1)
let files = try coverageFileList()

// since we need to invoke xccov for each file, it takes pretty much time
// so we invoke it in parallel on 8 threads, that speeds up things considerably
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 8 //Deadlock if this is = 1
queue.qualityOfService = .userInitiated
for file in files {
guard !file.isEmpty else { continue }
if !quiet {
writeToStdError("Coverage for: \(file)\n")

for target in codeCoverage.targets {
if !coverageTargets.isEmpty {
guard coverageTargets.contains(target.name) else { continue }
}
let op = BlockOperation { [self] in
do {
let coverage = try fileCoverageXML(for: file, relativeTo: projectRoot)
coverageXMLSemaphore.wait()
coverageXML.addChild(coverage)
coverageXMLSemaphore.signal()
} catch {
writeToStdErrorLn(error.localizedDescription)
for codeCovFile in target.files {
let file = codeCovFile.path
guard !file.isEmpty else { continue }
if !quiet {
writeToStdError("Coverage for: \(file)\n")
}
let op = BlockOperation { [self] in
do {
let coverage = try fileCoverageXML(for: file, relativeTo: projectRoot)
coverageXMLSemaphore.wait()
coverageXML.addChild(coverage)
coverageXMLSemaphore.signal()
} catch {
writeToStdErrorLn(error.localizedDescription)
}
}
queue.addOperation(op)
}
queue.addOperation(op)
}
// This will block until all our operation have compleated (or been canceled)
queue.waitUntilAllOperationsAreFinished()
Expand Down
2 changes: 1 addition & 1 deletion Sources/xcresultparser/XCResultFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ extension NumberFormatter {
}
}

private extension CodeCoverage {
extension CodeCoverage {
func targets(filteredBy filter: [String]) -> Set<String> {
let targetNames = targets.map { $0.name }
guard !filter.isEmpty else {
Expand Down

0 comments on commit 43a2b29

Please sign in to comment.