From 43a2b296660a48c98343ff9eb667d67501478e3a Mon Sep 17 00:00:00 2001 From: Alex da Franca Date: Sat, 11 Feb 2023 22:33:00 +0100 Subject: [PATCH] Support targets in coverage 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 --- .../xcschemes/xcresultparser.xcscheme | 16 ++++++++ CHANGELOG.md | 5 +++ CommandlineTool/main.swift | 34 ++++++++++++++-- README.md | 24 +++++++---- .../CoberturaCoverageConverter.swift | 34 +++++++++------- .../xcresultparser/CoverageConverter.swift | 40 +++++++++++++------ .../SonarCoverageConverter.swift | 34 +++++++++------- .../xcresultparser/XCResultFormatter.swift | 2 +- 8 files changed, 135 insertions(+), 54 deletions(-) diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/xcresultparser.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/xcresultparser.xcscheme index b704a03..4cd28da 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/xcresultparser.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/xcresultparser.xcscheme @@ -79,6 +79,22 @@ argument = "/Users/alex/xcodebuild_result.xcresult" isEnabled = "NO"> + + + + + + + + diff --git a/CHANGELOG.md b/CHANGELOG.md index ccd7e3c..ca24cee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/CommandlineTool/main.swift b/CommandlineTool/main.swift index 46c0c80..76e04d1 100644 --- a/CommandlineTool/main.swift +++ b/CommandlineTool/main.swift @@ -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( @@ -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") @@ -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 @@ -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) @@ -72,7 +79,11 @@ 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) @@ -80,12 +91,27 @@ struct xcresultparser: ParsableCommand { } 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 { diff --git a/README.md b/README.md index d946978..d6b9c48 100644 --- a/README.md +++ b/README.md @@ -77,11 +77,11 @@ You should see the tool respond like this: ``` Error: Missing expected argument '' -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 ] [--project-root ] [--coverage-targets ...] [--summary-fields ] [--coverage ...] [--no-test-result ...] [--failed-tests-only ...] [--quiet ...] [--version ...] [] +USAGE: xcresultparser [--output-format ] [--project-root ] [--coverage-targets ...] [--summary-fields ] [--coverage ...] [--no-test-result ...] [--failed-tests-only ...] [--quiet ...] [--target-info ...] [--version ...] [] ARGUMENTS: The path to the .xcresult file. @@ -89,15 +89,17 @@ ARGUMENTS: OPTIONS: -o, --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 The name of the project root. If present paths and urls are relative to the specified directory. -t, --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 The fields in the summary. Default is all: errors|warnings|analyzerWarnings|tests|failed|skipped @@ -105,6 +107,7 @@ OPTIONS: -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. ``` @@ -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: ``` diff --git a/Sources/xcresultparser/CoberturaCoverageConverter.swift b/Sources/xcresultparser/CoberturaCoverageConverter.swift index 6168e45..c3337fd 100644 --- a/Sources/xcresultparser/CoberturaCoverageConverter.swift +++ b/Sources/xcresultparser/CoberturaCoverageConverter.swift @@ -66,7 +66,6 @@ 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 @@ -74,22 +73,29 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { 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() diff --git a/Sources/xcresultparser/CoverageConverter.swift b/Sources/xcresultparser/CoverageConverter.swift index 6198075..687d380 100644 --- a/Sources/xcresultparser/CoverageConverter.swift +++ b/Sources/xcresultparser/CoverageConverter.swift @@ -25,9 +25,13 @@ public class CoverageConverter { let codeCoverage: CodeCoverage let invocationRecord: ActionsInvocationRecord let coverageRegexp: NSRegularExpression? + let coverageTargets: Set - 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 @@ -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) @@ -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") } @@ -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") } } diff --git a/Sources/xcresultparser/SonarCoverageConverter.swift b/Sources/xcresultparser/SonarCoverageConverter.swift index 37a192c..e037e8c 100644 --- a/Sources/xcresultparser/SonarCoverageConverter.swift +++ b/Sources/xcresultparser/SonarCoverageConverter.swift @@ -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() diff --git a/Sources/xcresultparser/XCResultFormatter.swift b/Sources/xcresultparser/XCResultFormatter.swift index fb153a0..9001383 100644 --- a/Sources/xcresultparser/XCResultFormatter.swift +++ b/Sources/xcresultparser/XCResultFormatter.swift @@ -368,7 +368,7 @@ extension NumberFormatter { } } -private extension CodeCoverage { +extension CodeCoverage { func targets(filteredBy filter: [String]) -> Set { let targetNames = targets.map { $0.name } guard !filter.isEmpty else {