Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hotfix/high number fix #30

Merged
merged 5 commits into from
May 25, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 17 additions & 10 deletions CommandlineTool/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ struct xcresultparser: ParsableCommand {
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', '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.")
@Option(name: .shortAndLong, help: "The output format. It can be either 'txt', 'cli', 'html', 'md', 'xml', 'junit', 'cobertura', 'warnings' and 'errors'. 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.")
var outputFormat: String?

@Option(name: .shortAndLong, help: "The name of the project root. If present paths and urls are relative to the specified directory.")
Expand Down Expand Up @@ -62,19 +62,22 @@ struct xcresultparser: ParsableCommand {
try outputTargetNames(for: xcresult)
return
}
if format == .xml {
switch format {
case .xml:
if coverage == 1 {
try outputSonarXML(for: xcresult)
} else {
try outputJUnitXML(for: xcresult, with: .sonar)
}
} else if format == .junit {
case .junit:
try outputJUnitXML(for: xcresult, with: .junit)
} else if format == .cobertura {
case .cobertura:
coverage = 1
try outputCoberturaXML(for: xcresult)
} else {
case .cli, .md, .txt, .html:
try outputDescription(for: xcresult)
case .warnings, .errors:
try outputIssuesJSON(for: xcresult, format: format)
}
}

Expand All @@ -101,6 +104,14 @@ struct xcresultparser: ParsableCommand {
let rslt = try converter.xmlString(quiet: quiet == 1)
writeToStdOut(rslt)
}

private func outputIssuesJSON(for xcresult: String, format: OutputFormat) throws {
guard let converter = IssuesJSON(with: URL(fileURLWithPath: xcresult)) else {
throw ParseError.argumentError
}
let rslt = try converter.jsonString(format: format, quiet: quiet == 1)
writeToStdOut(rslt)
}

private func outputTargetNames(for xcresult: String) throws {
guard let converter = SonarCoverageConverter(
Expand Down Expand Up @@ -161,11 +172,7 @@ struct xcresultparser: ParsableCommand {
return HTMLResultFormatter()
case .txt:
return TextResultFormatter()
case .cobertura:
fallthrough
case .junit:
fallthrough
case .xml:
case .cobertura, .junit, .xml, .warnings, .errors:
// outputFormatter is not used in case of .xml
return TextResultFormatter()
case .md:
Expand Down
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ let package = Package(
.copy("TestAssets/junit_merged.xml"),
.copy("TestAssets/sonarTestExecution.xml"),
.copy("TestAssets/cobertura.xml"),
.copy("TestAssets/warnings.json"),
]
)
]
Expand Down
25 changes: 20 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Interpret binary .xcresult files and print summary in different formats:
- cobertura
- html
- markdown
- warnings
- errors

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

Expand Down Expand Up @@ -85,7 +87,7 @@ You should see the tool respond like this:
```
Error: Missing expected argument '<xcresult-file>'

OVERVIEW: xcresultparser 1.3
OVERVIEW: xcresultparser 1.6.0
Interpret binary .xcresult files and print summary in different formats: txt,
xml, html or colored cli output.

Expand All @@ -97,10 +99,11 @@ ARGUMENTS:
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' sonar generic format for test results and
generic format (Sonarqube) for coverage data is used.
In the case of 'cobertura', --coverage is implied.
'html', 'md', 'xml', 'junit', 'cobertura', 'warnings'
and 'errors'. 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.
Expand Down Expand Up @@ -199,6 +202,18 @@ Simple markdown formatting for test results. (We use it for display in a Teams W
./xcresultparser -o md test.xcresult > teamsWebhook.txt
```

### Code Climate output
JSON output for Code Climate checks
```
./xcresultparser -o warnings test.xcresult > climate.json
```

### Error output
JSON output describing errors
```
./xcresultparser -o errors test.xcresult > errors.json
```

#### 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
8 changes: 2 additions & 6 deletions Sources/xcresultparser/CoberturaCoverageConverter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,9 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable {

for lineData in value {
let lineNum = lineData.line
guard var covered = lineData.executionCount else {
guard let covered = lineData.executionCount else {
continue
}
// If the line coverage count is a MAX_INT, just set it to 1
if covered == Int.max {
covered = 1
}
let line = LineInfo(lineNumber: String(lineNum), coverage: covered)
fileLines.append(line)
}
Expand Down Expand Up @@ -186,7 +182,7 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable {

private struct LineInfo {
let lineNumber: String
let coverage: Int
let coverage: UInt64
}

private struct FileInfo {
Expand Down
5 changes: 0 additions & 5 deletions Sources/xcresultparser/CoverageConverter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ public class CoverageConverter {
let projectRoot: String
let codeCoverage: CodeCoverage
let invocationRecord: ActionsInvocationRecord
let coverageRegexp: NSRegularExpression?
let coverageTargets: Set<String>

public init?(
Expand All @@ -46,11 +45,7 @@ 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)
}

public func xmlString(quiet: Bool) throws -> String {
Expand Down
15 changes: 15 additions & 0 deletions Sources/xcresultparser/Extensions/Data+MD5.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// Data+MD5.swift
//
// Created by Alex da Franca on 22.05.24.
//

import Foundation
import CryptoKit

extension Data {
func md5() -> String {
let digest = Insecure.MD5.hash(data: self)
return digest.map { String(format: "%02hhx", $0) }.joined()
}
}
15 changes: 15 additions & 0 deletions Sources/xcresultparser/Extensions/String+MD5.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// String+MD5.swift
//
// Created by Alex da Franca on 22.05.24.
//

import Foundation
import CryptoKit

extension String {
func md5() -> String {
let digest = Insecure.MD5.hash(data: Data(utf8))
return digest.map { String(format: "%02hhx", $0) }.joined()
}
}
48 changes: 48 additions & 0 deletions Sources/xcresultparser/IssuesJSON.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// IssuesJSON.swift
//
// Created by Alex da Franca on 22.05.24.
//

import Foundation
import XCResultKit

/// Output some infos about warnings and issues
///
/// [Code Climate Specification](https://github.com/codeclimate/platform/blob/master/spec/analyzers/SPEC.md)
/// [Gitlab Code Climate Support](https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool)
///
public struct IssuesJSON {
let resultFile: XCResultFile
let checkName: String
let invocationRecord: ActionsInvocationRecord

public init?(with url: URL) {
resultFile = XCResultFile(url: url)
guard let invocationRecord = resultFile.getInvocationRecord(),
let checkdata = try? Data(contentsOf: url.appendingPathComponent("Info.plist")) else {
return nil
}
self.invocationRecord = invocationRecord
checkName = checkdata.md5()
}

public func jsonString(format: OutputFormat, quiet: Bool = false) throws -> String {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let jsonData: Data
if format == .errors {
let errors = invocationRecord.issues.errorSummaries
.map { Issue(issueSummary: $0, severity: .blocker, checkName: checkName) }
jsonData = try encoder.encode(errors)
} else {
let warnings = invocationRecord.issues.warningSummaries
.map { Issue(issueSummary: $0, severity: .minor, checkName: checkName) }
let analyzerWarnings = invocationRecord.issues.analyzerWarningSummaries
.map { Issue(issueSummary: $0, severity: .info, checkName: checkName) }
a7ex marked this conversation as resolved.
Show resolved Hide resolved
let combined = warnings + analyzerWarnings
jsonData = try encoder.encode(combined)
}
return String(decoding: jsonData, as: UTF8.self)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// IssueLocationInfo.swift
//
// Created by Alex da Franca on 22.05.24.
//

import Foundation
import XCResultKit

/// Helper object to convert from InvocationRecoord.DocumentLocation to Code Climate objects
///
struct IssueLocationInfo {
let filePath: String
let startLine: Int
let endLine: Int
let startColumn: Int
let endColumn: Int

init?(with documentLocation: DocumentLocation?) {
guard let documentLocation,
let url = URL(string: documentLocation.url) else {
return nil
}
// guard documentLocation.concreteTypeName == "DVTTextDocumentLocation" else {
// fatalError("I want to know, when that happens. concreteTypeName is \(documentLocation.concreteTypeName)")
// }
a7ex marked this conversation as resolved.
Show resolved Hide resolved
filePath = url.path
guard let fragment = url.fragment else {
startLine = 0
endLine = 0
startColumn = 0
endColumn = 0
return
}
let pairs = fragment.components(separatedBy: "&")
var startline = 0
var endline = 0
var startcolumn = 0
var endcolumn = 0
for pair in pairs {
let location = pair.components(separatedBy: "=")
guard location.count > 1 else { continue }
switch location[0] {
case "EndingColumnNumber":
endcolumn = Int(location[1]) ?? 0
case "EndingLineNumber":
endline = Int(location[1]) ?? 0
case "StartingColumnNumber":
startcolumn = Int(location[1]) ?? 0
case "StartingLineNumber":
startline = Int(location[1]) ?? 0
default:
break
}
}
startLine = startline
endLine = endline
startColumn = startcolumn
endColumn = endcolumn
}
}
53 changes: 53 additions & 0 deletions Sources/xcresultparser/Models/CodeClimate/Issue.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// Issue.swift
//
// Created by Alex da Franca on 22.05.24.
//

import Foundation
import XCResultKit

struct Issue: Codable {
/// Required. A description of the code quality violation.
let description: String
/// Required. A unique name representing the static analysis check that emitted this issue.
let checkName: String
/// Optional. A unique, deterministic identifier for the specific issue being reported to allow a user to exclude it from future analyses. For example, an MD5 hash.
let fingerprint: String
/// Optional. A Severity string (info, minor, major, critical, or blocker) describing the potential impact of the issue found..
let severity: IssueSeverity
let engineName: String
/// Required. A Location object representing the place in the source code where the issue was discovered.
let location: IssueLocation
/// Required. Must always be "issue".
let type: IssueType
/// Required. At least one category indicating the nature of the issue being reported.
let categories: [IssueCategory]
/// Optional. A markdown snippet describing the issue, including deeper explanations and links to other resources.
let content: IssueContent

enum CodingKeys: String, CodingKey {
case description, fingerprint, severity, location, type, categories, content
case checkName = "check_name"
case engineName = "engine_name"
}
}

extension Issue {
init(issueSummary: IssueSummary, severity: IssueSeverity, checkName: String) {
let issueLocationInfo = IssueLocationInfo(with: issueSummary.documentLocationInCreatingWorkspace)
description = "\(issueSummary.issueType) - \(issueSummary.message)"
a7ex marked this conversation as resolved.
Show resolved Hide resolved
self.checkName = checkName
fingerprint = "\(issueSummary.issueType) - \(issueSummary.message)".md5()
a7ex marked this conversation as resolved.
Show resolved Hide resolved
self.severity = severity
engineName = issueSummary.producingTarget ?? ""
a7ex marked this conversation as resolved.
Show resolved Hide resolved
location = IssueLocation(issueLocationInfo: issueLocationInfo)
type = .issue
categories = []
content = IssueContent(body: "\(issueSummary.issueType) - \(issueSummary.message)")
}
}

enum IssueType: String, Codable {
case issue
}
18 changes: 18 additions & 0 deletions Sources/xcresultparser/Models/CodeClimate/IssueCategory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// IssueCategory.swift
//
// Created by Alex da Franca on 22.05.24.
//

import Foundation

enum IssueCategory: String, Codable {
case bugRisk = "Bug Risk"
case clarity = "Clarity"
case compatibility = "Compatibility"
case complexity = "Complexity"
case duplication = "Duplication"
case performance = "Performance"
case security = "Security"
case style = "Style"
}
11 changes: 11 additions & 0 deletions Sources/xcresultparser/Models/CodeClimate/IssueContent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// IssueContent.swift
//
// Created by Alex da Franca on 22.05.24.
//

import Foundation

struct IssueContent: Codable {
let body: String
}
Loading
Loading