Skip to content

Commit

Permalink
Version 1.1.4
Browse files Browse the repository at this point in the history
Added simple markdown. (Implemented for use in Teams Webhook message).
  • Loading branch information
Alex da Franca committed Jun 1, 2022
1 parent 766b339 commit ddbbd73
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 34 deletions.
8 changes: 8 additions & 0 deletions .swiftpm/xcode/xcshareddata/xcschemes/xcresultparser.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,18 @@
argument = "/Users/alex/xcodebuild_result.xcresult"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "-f"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "-h"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "-o md"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "-c"
isEnabled = "NO">
Expand Down
16 changes: 13 additions & 3 deletions CommandlineTool/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,33 @@ import Foundation
import ArgumentParser
import XcresultparserLib

private let marketingVersion = "1.1.3"
private let marketingVersion = "1.1.4"

struct xcresultparser: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "xcresultparser \(marketingVersion)\nInterpret binary .xcresult files and print summary in different formats: txt, xml, html or colored cli output."
)

@Option(name: .shortAndLong, help: "The output format. It can be either 'txt', 'cli', 'html' or 'xml'. In case of 'xml' JUnit format for test results and generic format (Sonarqube) for coverage data is used.")
@Option(name: .shortAndLong, help: "The output format. It can be either 'txt', 'cli', 'html', 'md' or 'xml'. In case of 'xml' JUnit format for test results and generic format (Sonarqube) for coverage data is used.")
var outputFormat: String?

@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")
var coverageTargets: [String] = []

@Option(name: .shortAndLong, help: "The fields in the summary. Default is all: errors|warnings|analyzerWarnings|tests|failed|skipped")
var summaryFields: String?

@Flag(name: .shortAndLong, help: "Whether to print coverage data.")
var coverage: Int

@Flag(name: .shortAndLong, help: "Whether to print test results.")
var noTestResult: Int

@Flag(name: .shortAndLong, help: "Whether to only print failed tests.")
var failedTestsOnly: Int

@Flag(name: .shortAndLong, help: "Quiet. Don't print status output.")
var quiet: Int
Expand Down Expand Up @@ -83,7 +89,9 @@ struct xcresultparser: ParsableCommand {
guard let resultParser = XCResultFormatter(
with: URL(fileURLWithPath: xcresult),
formatter: outputFormatter,
coverageTargets: coverageTargets
coverageTargets: coverageTargets,
failedTestsOnly: (failedTestsOnly == 1),
summaryFields: summaryFields ?? "errors|warnings|analyzerWarnings|tests|failed|skipped"
) else {
throw ParseError.argumentError
}
Expand Down Expand Up @@ -114,6 +122,8 @@ struct xcresultparser: ParsableCommand {
case .xml:
// outputFormatter is not used in case of .xml
return TextResultFormatter()
case .md:
return MDResultFormatter()
}
}
}
Expand Down
27 changes: 22 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Interpret binary .xcresult files and print summary in different formats:
- colored command line output
- xml
- html
- markdown

In case of 'xml' JUnit format for test results and generic format (Sonarqube) for coverage data is used.

Expand Down Expand Up @@ -74,24 +75,34 @@ You should see the tool respond like this:
```
Error: Missing expected argument '<xcresult-file>'
OVERVIEW: Interpret binary .xcresult files and print summary in different formats: txt, xml, html or colored cli output.
OVERVIEW: xcresultparser 1.1.4
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> ...] [--coverage ...] [--no-test-result ...] [--quiet ...] <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 ...] [--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' or 'xml'. In case of 'xml' JUnit format for test results and generic format
(Sonarqube) for coverage data is used.
The output format. It can be either 'txt', 'cli',
'html', 'md' or 'xml'. In case of 'xml' JUnit format
for test results and generic format (Sonarqube) for
coverage data is used.
-p, --project-root <project-root>
The name of the project root. If present paths and urls are relative to the specified directory.
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
-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.
-v, --version Show version number.
-h, --help Show help information.
```
Now that a copy of `xcresultparser` is in your search path, delete it from your desktop.
Expand Down Expand Up @@ -136,6 +147,12 @@ Create an xml file in generic code coverage xml format:
./xcresultparser -c -o xml test.xcresult > sonar.xml
```

### Markdown output
Simple markdown formatting for test results. (We use it for display in a Teams Webhook)
```
./xcresultparser -o md test.xcresult > teamsWebhook.txt
```

#### About paths for the sonarqube scanner
The tools to get the data from the xcresult archive yield absolute path names.
So you must provide an absolute pathname to the *sonar.sources* paramater of the *sonar-scanner* CLI tool and it must of course match the directory, where *xcodebuild* ran the tests and created the *.xcresult* archive.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//
// MDResultFormatter.swift
//
// Created by Alex da Franca on 31.05.22.
//

import Foundation

public struct MDResultFormatter: XCResultFormatting {
private let indentWidth = " "

public let testFailIcon = "🔴"
public let testPassIcon = "🟢"

public init() { }

public func documentPrefix(title: String) -> String {
return ""
}
public var documentSuffix: String {
return ""
}
public var accordionOpenTag: String {
return ""
}
public var accordionCloseTag: String {
return ""
}
public var tableOpenTag: String {
return ""
}
public var tableCloseTag: String {
return ""
}
public var divider: String {
return "---------------------\n"
}
public func resultSummaryLine(_ item: String, failed: Bool) -> String {
return "* " + item
}
public func resultSummaryLineWarning(_ item: String, hasWarnings: Bool) -> String {
return "* " + item
}
public func testConfiguration(_ item: String) -> String {
return "## " + item
}
public func testTarget(_ item: String, failed: Bool) -> String {
return "### " + item
}
public func testClass(_ item: String, failed: Bool) -> String {
return "#### " + item
}
public func singleTestItem(_ item: String, failed: Bool) -> String {
return "* " + item
}
public func failedTestItem(_ item: String, message: String) -> String {
return "* " + item + "\n" +
" * " + message
}
public func codeCoverageTargetSummary(_ item: String) -> String {
return item
}
public func codeCoverageFileSummary(_ item: String) -> String {
return "## " + item
}
public func codeCoverageFunctionSummary(_ items: [String]) -> String {
return "### " + items.joined(separator: " ")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,19 @@ public protocol XCResultFormatting {
func testClass(_ item: String, failed: Bool) -> String
func singleTestItem(_ item: String, failed: Bool) -> String
func failedTestItem(_ item: String, message: String) -> String
var testFailIcon: String { get }
var testPassIcon: String { get }

func codeCoverageTargetSummary(_ item: String) -> String
func codeCoverageFileSummary(_ item: String) -> String
func codeCoverageFunctionSummary(_ items: [String]) -> String
}

public extension XCResultFormatting {
var testFailIcon: String {
return "✖︎"
}
var testPassIcon: String {
return ""
}
}
2 changes: 1 addition & 1 deletion Sources/xcresultparser/OutputFormatting/OutputFormat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import Foundation

public enum OutputFormat: String {
case txt, cli, html, xml
case txt, cli, html, xml, md

public init(string: String?) {
if let input = string?.lowercased(),
Expand Down
89 changes: 64 additions & 25 deletions Sources/xcresultparser/XCResultFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ import Foundation
import XCResultKit

public struct XCResultFormatter {

private enum SummaryField: String {
case errors, warnings, analyzerWarnings, tests, failed, skipped
}
private struct SummaryFields {
let enabledFields: Set<SummaryField>
init(specifiers: String) {
enabledFields = Set(
specifiers
.components(separatedBy: "|")
.compactMap { SummaryField(rawValue: $0)}
)
}
}

// MARK: - Properties

Expand All @@ -17,6 +31,8 @@ public struct XCResultFormatter {
private let codeCoverage: CodeCoverage?
private let outputFormatter: XCResultFormatting
private let coverageTargets: Set<String>
private let failedTestsOnly: Bool
private let summaryFields: SummaryFields

private var numFormatter: NumberFormatter = {
let numFormatter = NumberFormatter()
Expand All @@ -32,9 +48,12 @@ public struct XCResultFormatter {

// MARK: - Initializer

public init?(with url: URL,
formatter: XCResultFormatting,
coverageTargets: [String] = []
public init?(
with url: URL,
formatter: XCResultFormatting,
coverageTargets: [String] = [],
failedTestsOnly: Bool = false,
summaryFields: String = "errors|warnings|analyzerWarnings|tests|failed|skipped"
) {
resultFile = XCResultFile(url: url)
guard let record = resultFile.getInvocationRecord() else {
Expand All @@ -44,6 +63,8 @@ public struct XCResultFormatter {
outputFormatter = formatter
codeCoverage = resultFile.getCodeCoverage()
self.coverageTargets = codeCoverage?.targets(filteredBy: coverageTargets) ?? []
self.failedTestsOnly = failedTestsOnly
self.summaryFields = SummaryFields(specifiers: summaryFields)

//if let logsId = invocationRecord?.actions.last?.actionResult.logRef?.id {
// let testLogs = resultFile.getLogs(id: logsId)
Expand Down Expand Up @@ -93,24 +114,36 @@ public struct XCResultFormatter {
lines.append(
outputFormatter.testConfiguration("Summary")
)
lines.append(
outputFormatter.resultSummaryLine("Number of errors = \(errorCount)", failed: errorCount != 0)
)
lines.append(
outputFormatter.resultSummaryLineWarning("Number of warnings = \(warningCount)", hasWarnings: warningCount != 0)
)
lines.append(
outputFormatter.resultSummaryLineWarning("Number of analyzer warnings = \(analyzerWarningCount)", hasWarnings: analyzerWarningCount != 0)
)
lines.append(
outputFormatter.resultSummaryLine("Number of tests = \(testsCount)", failed: false)
)
lines.append(
outputFormatter.resultSummaryLine("Number of failed tests = \(testsFailedCount)", failed: testsFailedCount != 0)
)
lines.append(
outputFormatter.resultSummaryLine("Number of skipped tests = \(testsSkippedCount)", failed: testsSkippedCount != 0)
)
if summaryFields.enabledFields.contains(.errors) {
lines.append(
outputFormatter.resultSummaryLine("Number of errors = \(errorCount)", failed: errorCount != 0)
)
}
if summaryFields.enabledFields.contains(.warnings) {
lines.append(
outputFormatter.resultSummaryLineWarning("Number of warnings = \(warningCount)", hasWarnings: warningCount != 0)
)
}
if summaryFields.enabledFields.contains(.analyzerWarnings) {
lines.append(
outputFormatter.resultSummaryLineWarning("Number of analyzer warnings = \(analyzerWarningCount)", hasWarnings: analyzerWarningCount != 0)
)
}
if summaryFields.enabledFields.contains(.tests) {
lines.append(
outputFormatter.resultSummaryLine("Number of tests = \(testsCount)", failed: false)
)
}
if summaryFields.enabledFields.contains(.failed) {
lines.append(
outputFormatter.resultSummaryLine("Number of failed tests = \(testsFailedCount)", failed: testsFailedCount != 0)
)
}
if summaryFields.enabledFields.contains(.skipped) {
lines.append(
outputFormatter.resultSummaryLine("Number of skipped tests = \(testsSkippedCount)", failed: testsSkippedCount != 0)
)
}
return lines
}

Expand Down Expand Up @@ -142,6 +175,10 @@ public struct XCResultFormatter {

private func createTestSummaryInfo(_ group: ActionTestSummaryGroup, level: Int, failureSummaries: [TestFailureIssueSummary]) -> [String] {
var lines = [String]()
if failedTestsOnly,
!group.hasFailedTests {
return lines
}
let header = "\(group.nameString) (\(numFormatter.unwrappedString(for: group.duration)))"

switch level {
Expand Down Expand Up @@ -169,9 +206,11 @@ public struct XCResultFormatter {
)
}
for thisTest in group.subtests {
lines.append(
actionTestFileStatusString(for: thisTest, failureSummaries: failureSummaries)
)
if !failedTestsOnly || thisTest.isFailed {
lines.append(
actionTestFileStatusString(for: thisTest, failureSummaries: failureSummaries)
)
}
}
if !outputFormatter.accordionCloseTag.isEmpty {
lines.append(
Expand All @@ -183,7 +222,7 @@ public struct XCResultFormatter {

private func actionTestFileStatusString(for testData: ActionTestMetadata, failureSummaries: [TestFailureIssueSummary]) -> String {
let duration = numFormatter.unwrappedString(for: testData.duration)
let icon = testData.isFailed ? "✖︎": ""
let icon = testData.isFailed ? outputFormatter.testFailIcon: outputFormatter.testPassIcon
let testTitle = "\(icon) \(testData.name) (\(duration))"
let testCaseName = testData.identifier.replacingOccurrences(of: "/", with: ".")
if let summary = failureSummaries.first(where: { $0.testCaseName == testCaseName }) {
Expand Down

0 comments on commit ddbbd73

Please sign in to comment.