From b2ebf38dba6673a10962653cae6ace873d6a9564 Mon Sep 17 00:00:00 2001 From: Alex da Franca Date: Wed, 22 May 2024 01:29:14 +0200 Subject: [PATCH 1/5] Fix an issue with the max integer in executionCount --- .../CoberturaCoverageConverter.swift | 10 +- .../xcresultparser/CoverageConverter.swift | 5 - .../Models/Coverage/LineDetail.swift | 2 +- .../Models/Coverage/Subrange.swift | 2 +- .../TestAssets/cobertura.xml | 2930 ++++++++--------- .../XcresultparserTests.swift | 3 +- 6 files changed, 1474 insertions(+), 1478 deletions(-) diff --git a/Sources/xcresultparser/CoberturaCoverageConverter.swift b/Sources/xcresultparser/CoberturaCoverageConverter.swift index 7d735a7..4f78f40 100644 --- a/Sources/xcresultparser/CoberturaCoverageConverter.swift +++ b/Sources/xcresultparser/CoberturaCoverageConverter.swift @@ -72,13 +72,13 @@ 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 - } +// if covered == Int.max { +// covered = 1 +// } let line = LineInfo(lineNumber: String(lineNum), coverage: covered) fileLines.append(line) } @@ -186,7 +186,7 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { private struct LineInfo { let lineNumber: String - let coverage: Int + let coverage: Double } private struct FileInfo { diff --git a/Sources/xcresultparser/CoverageConverter.swift b/Sources/xcresultparser/CoverageConverter.swift index 6067137..9a471c6 100644 --- a/Sources/xcresultparser/CoverageConverter.swift +++ b/Sources/xcresultparser/CoverageConverter.swift @@ -28,7 +28,6 @@ public class CoverageConverter { let projectRoot: String let codeCoverage: CodeCoverage let invocationRecord: ActionsInvocationRecord - let coverageRegexp: NSRegularExpression? let coverageTargets: Set public init?( @@ -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 { diff --git a/Sources/xcresultparser/Models/Coverage/LineDetail.swift b/Sources/xcresultparser/Models/Coverage/LineDetail.swift index 447c794..f158763 100644 --- a/Sources/xcresultparser/Models/Coverage/LineDetail.swift +++ b/Sources/xcresultparser/Models/Coverage/LineDetail.swift @@ -10,6 +10,6 @@ import Foundation struct LineDetail: Decodable { let isExecutable: Bool let line: Int - let executionCount: Int? + let executionCount: Double? let subranges: [Subrange]? } diff --git a/Sources/xcresultparser/Models/Coverage/Subrange.swift b/Sources/xcresultparser/Models/Coverage/Subrange.swift index 8d7117e..6e2a88a 100644 --- a/Sources/xcresultparser/Models/Coverage/Subrange.swift +++ b/Sources/xcresultparser/Models/Coverage/Subrange.swift @@ -9,6 +9,6 @@ import Foundation // Subrange information struct struct Subrange: Decodable { let column: Int - let executionCount: Int + let executionCount: Double let length: Int } diff --git a/Tests/XcresultparserTests/TestAssets/cobertura.xml b/Tests/XcresultparserTests/TestAssets/cobertura.xml index ef1d3ef..8dd0b94 100644 --- a/Tests/XcresultparserTests/TestAssets/cobertura.xml +++ b/Tests/XcresultparserTests/TestAssets/cobertura.xml @@ -54,165 +54,165 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -221,426 +221,426 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + @@ -649,14 +649,14 @@ - - - - - - - - + + + + + + + + @@ -665,12 +665,12 @@ - - - - - - + + + + + + @@ -679,59 +679,59 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -740,63 +740,63 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -805,227 +805,227 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1034,69 +1034,69 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1105,502 +1105,502 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + \ No newline at end of file diff --git a/Tests/XcresultparserTests/XcresultparserTests.swift b/Tests/XcresultparserTests/XcresultparserTests.swift index 6eb5f73..f5995b2 100644 --- a/Tests/XcresultparserTests/XcresultparserTests.swift +++ b/Tests/XcresultparserTests/XcresultparserTests.swift @@ -130,7 +130,8 @@ final class XcresultparserTests: XCTestCase { XCTFail("Unable to create CoverageConverter from \(xcresultFile)") return } - +// let url = URL(fileURLWithPath: "/Users/alex/Desktop/vergleich.xml") +// try converter.xmlString.write(to: url, atomically: true, encoding: .utf8) try assertXmlTestReportsAreEqual(expectedFileName: "cobertura", actual: converter) } From 4f6d3329292b48d4a85e7a7728c10ebad1da7a51 Mon Sep 17 00:00:00 2001 From: Alex da Franca Date: Wed, 22 May 2024 01:47:14 +0200 Subject: [PATCH 2/5] Cap the executionCount at Int.max and revert the output to Int --- .../CoberturaCoverageConverter.swift | 10 +- .../TestAssets/cobertura.xml | 2930 ++++++++--------- 2 files changed, 1469 insertions(+), 1471 deletions(-) diff --git a/Sources/xcresultparser/CoberturaCoverageConverter.swift b/Sources/xcresultparser/CoberturaCoverageConverter.swift index 4f78f40..9de3ae6 100644 --- a/Sources/xcresultparser/CoberturaCoverageConverter.swift +++ b/Sources/xcresultparser/CoberturaCoverageConverter.swift @@ -75,11 +75,9 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { 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) + // If the line coverage count is a MAX_INT, just cap it at MAX_INT + let coveredAsInt = covered > Double(Int.max) ? Int.max : Int(covered) + let line = LineInfo(lineNumber: String(lineNum), coverage: coveredAsInt) fileLines.append(line) } @@ -186,7 +184,7 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { private struct LineInfo { let lineNumber: String - let coverage: Double + let coverage: Int } private struct FileInfo { diff --git a/Tests/XcresultparserTests/TestAssets/cobertura.xml b/Tests/XcresultparserTests/TestAssets/cobertura.xml index 8dd0b94..ef1d3ef 100644 --- a/Tests/XcresultparserTests/TestAssets/cobertura.xml +++ b/Tests/XcresultparserTests/TestAssets/cobertura.xml @@ -54,165 +54,165 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -221,426 +221,426 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + @@ -649,14 +649,14 @@ - - - - - - - - + + + + + + + + @@ -665,12 +665,12 @@ - - - - - - + + + + + + @@ -679,59 +679,59 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -740,63 +740,63 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -805,227 +805,227 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1034,69 +1034,69 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1105,502 +1105,502 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - \ No newline at end of file + From 7445f45a950ad71cc9915dce4ec95cf4f9a889a2 Mon Sep 17 00:00:00 2001 From: Alex da Franca Date: Thu, 23 May 2024 00:01:59 +0200 Subject: [PATCH 3/5] Add warnings and errors export suited for Code Climate --- CommandlineTool/main.swift | 27 ++++-- Package.swift | 1 + README.md | 25 ++++- .../xcresultparser/Extensions/Data+MD5.swift | 15 +++ .../Extensions/String+MD5.swift | 15 +++ Sources/xcresultparser/IssuesJSON.swift | 48 ++++++++++ .../IssueLocationInfo.swift | 61 ++++++++++++ .../Models/CodeClimate/Issue.swift | 53 +++++++++++ .../Models/CodeClimate/IssueCategory.swift | 18 ++++ .../Models/CodeClimate/IssueContent.swift | 11 +++ .../Models/CodeClimate/IssueLocation.swift | 44 +++++++++ .../CodeClimate/IssueLocationData.swift | 13 +++ .../CodeClimate/IssuePositionData.swift | 13 +++ .../Models/CodeClimate/IssueSeverity.swift | 11 +++ .../Models/CodeClimate/PositionData.swift | 12 +++ .../OutputFormatting/OutputFormat.swift | 2 +- .../TestAssets/warnings.json | 95 +++++++++++++++++++ .../XcresultparserTests.swift | 47 ++++++++- 18 files changed, 494 insertions(+), 17 deletions(-) create mode 100644 Sources/xcresultparser/Extensions/Data+MD5.swift create mode 100644 Sources/xcresultparser/Extensions/String+MD5.swift create mode 100644 Sources/xcresultparser/IssuesJSON.swift create mode 100644 Sources/xcresultparser/Models/CodeClimate/IntermediateObjects/IssueLocationInfo.swift create mode 100644 Sources/xcresultparser/Models/CodeClimate/Issue.swift create mode 100644 Sources/xcresultparser/Models/CodeClimate/IssueCategory.swift create mode 100644 Sources/xcresultparser/Models/CodeClimate/IssueContent.swift create mode 100644 Sources/xcresultparser/Models/CodeClimate/IssueLocation.swift create mode 100644 Sources/xcresultparser/Models/CodeClimate/IssueLocationData.swift create mode 100644 Sources/xcresultparser/Models/CodeClimate/IssuePositionData.swift create mode 100644 Sources/xcresultparser/Models/CodeClimate/IssueSeverity.swift create mode 100644 Sources/xcresultparser/Models/CodeClimate/PositionData.swift create mode 100644 Tests/XcresultparserTests/TestAssets/warnings.json diff --git a/CommandlineTool/main.swift b/CommandlineTool/main.swift index c8a8f56..74a7bf5 100644 --- a/CommandlineTool/main.swift +++ b/CommandlineTool/main.swift @@ -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.") @@ -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) } } @@ -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( @@ -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: diff --git a/Package.swift b/Package.swift index ebe955e..b663cdd 100644 --- a/Package.swift +++ b/Package.swift @@ -61,6 +61,7 @@ let package = Package( .copy("TestAssets/junit_merged.xml"), .copy("TestAssets/sonarTestExecution.xml"), .copy("TestAssets/cobertura.xml"), + .copy("TestAssets/warnings.json"), ] ) ] diff --git a/README.md b/README.md index c75b9ee..cb6e164 100644 --- a/README.md +++ b/README.md @@ -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. @@ -85,7 +87,7 @@ You should see the tool respond like this: ``` Error: Missing expected argument '' -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. @@ -97,10 +99,11 @@ ARGUMENTS: OPTIONS: -o, --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 The name of the project root. If present paths and urls are relative to the specified directory. @@ -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. diff --git a/Sources/xcresultparser/Extensions/Data+MD5.swift b/Sources/xcresultparser/Extensions/Data+MD5.swift new file mode 100644 index 0000000..130ea82 --- /dev/null +++ b/Sources/xcresultparser/Extensions/Data+MD5.swift @@ -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() + } +} diff --git a/Sources/xcresultparser/Extensions/String+MD5.swift b/Sources/xcresultparser/Extensions/String+MD5.swift new file mode 100644 index 0000000..7d83c47 --- /dev/null +++ b/Sources/xcresultparser/Extensions/String+MD5.swift @@ -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() + } +} diff --git a/Sources/xcresultparser/IssuesJSON.swift b/Sources/xcresultparser/IssuesJSON.swift new file mode 100644 index 0000000..eb30372 --- /dev/null +++ b/Sources/xcresultparser/IssuesJSON.swift @@ -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) } + let combined = warnings + analyzerWarnings + jsonData = try encoder.encode(combined) + } + return String(decoding: jsonData, as: UTF8.self) + } +} diff --git a/Sources/xcresultparser/Models/CodeClimate/IntermediateObjects/IssueLocationInfo.swift b/Sources/xcresultparser/Models/CodeClimate/IntermediateObjects/IssueLocationInfo.swift new file mode 100644 index 0000000..ae645aa --- /dev/null +++ b/Sources/xcresultparser/Models/CodeClimate/IntermediateObjects/IssueLocationInfo.swift @@ -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)") +// } + 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 + } +} diff --git a/Sources/xcresultparser/Models/CodeClimate/Issue.swift b/Sources/xcresultparser/Models/CodeClimate/Issue.swift new file mode 100644 index 0000000..9d971c6 --- /dev/null +++ b/Sources/xcresultparser/Models/CodeClimate/Issue.swift @@ -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)" + self.checkName = checkName + fingerprint = "\(issueSummary.issueType) - \(issueSummary.message)".md5() + self.severity = severity + engineName = issueSummary.producingTarget ?? "" + location = IssueLocation(issueLocationInfo: issueLocationInfo) + type = .issue + categories = [] + content = IssueContent(body: "\(issueSummary.issueType) - \(issueSummary.message)") + } +} + +enum IssueType: String, Codable { + case issue +} diff --git a/Sources/xcresultparser/Models/CodeClimate/IssueCategory.swift b/Sources/xcresultparser/Models/CodeClimate/IssueCategory.swift new file mode 100644 index 0000000..e969021 --- /dev/null +++ b/Sources/xcresultparser/Models/CodeClimate/IssueCategory.swift @@ -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" +} diff --git a/Sources/xcresultparser/Models/CodeClimate/IssueContent.swift b/Sources/xcresultparser/Models/CodeClimate/IssueContent.swift new file mode 100644 index 0000000..d9f8526 --- /dev/null +++ b/Sources/xcresultparser/Models/CodeClimate/IssueContent.swift @@ -0,0 +1,11 @@ +// +// IssueContent.swift +// +// Created by Alex da Franca on 22.05.24. +// + +import Foundation + +struct IssueContent: Codable { + let body: String +} diff --git a/Sources/xcresultparser/Models/CodeClimate/IssueLocation.swift b/Sources/xcresultparser/Models/CodeClimate/IssueLocation.swift new file mode 100644 index 0000000..d2f43c9 --- /dev/null +++ b/Sources/xcresultparser/Models/CodeClimate/IssueLocation.swift @@ -0,0 +1,44 @@ +// +// IssueLocation.swift +// +// Created by Alex da Franca on 22.05.24. +// + +import Foundation + +struct IssueLocation: Codable, Equatable { + /// The relative path to the file containing the code quality violation. + let path: String + let lines: IssueLocationData + let positions: IssuePositionData +} + +extension IssueLocation { + static func ==(lhs: IssueLocation, rhs: IssueLocation) -> Bool { + return lhs.positions.begin.line == rhs.positions.begin.line && + lhs.positions.end.line == rhs.positions.end.line && + lhs.positions.begin.column == rhs.positions.begin.column && + lhs.positions.end.column == rhs.positions.end.column && + lhs.path == rhs.path + } +} + +extension IssueLocation { + init(issueLocationInfo: IssueLocationInfo?) { + guard let issueLocationInfo else { + path = "" + lines = IssueLocationData(begin: 0, end: 0) + positions = IssuePositionData( + begin: PositionData(line: 0, column: 0), + end: PositionData(line: 0, column: 0) + ) + return + } + path = issueLocationInfo.filePath + lines = IssueLocationData(begin: issueLocationInfo.startLine, end: issueLocationInfo.endLine) + positions = IssuePositionData( + begin: PositionData(line: issueLocationInfo.startLine, column: issueLocationInfo.startColumn), + end: PositionData(line: issueLocationInfo.endLine, column: issueLocationInfo.endColumn) + ) + } +} diff --git a/Sources/xcresultparser/Models/CodeClimate/IssueLocationData.swift b/Sources/xcresultparser/Models/CodeClimate/IssueLocationData.swift new file mode 100644 index 0000000..2926ac6 --- /dev/null +++ b/Sources/xcresultparser/Models/CodeClimate/IssueLocationData.swift @@ -0,0 +1,13 @@ +// +// IssueLocationData.swift +// +// Created by Alex da Franca on 22.05.24. +// + +import Foundation + +struct IssueLocationData: Codable { + /// The line on which the code quality violation occurred. + let begin: Int + let end: Int +} diff --git a/Sources/xcresultparser/Models/CodeClimate/IssuePositionData.swift b/Sources/xcresultparser/Models/CodeClimate/IssuePositionData.swift new file mode 100644 index 0000000..42f8c27 --- /dev/null +++ b/Sources/xcresultparser/Models/CodeClimate/IssuePositionData.swift @@ -0,0 +1,13 @@ +// +// IssuePositionData.swift +// +// Created by Alex da Franca on 22.05.24. +// + +import Foundation + +struct IssuePositionData: Codable { + /// The line on which the code quality violation occurred. + let begin: PositionData + let end: PositionData +} diff --git a/Sources/xcresultparser/Models/CodeClimate/IssueSeverity.swift b/Sources/xcresultparser/Models/CodeClimate/IssueSeverity.swift new file mode 100644 index 0000000..3d42b8e --- /dev/null +++ b/Sources/xcresultparser/Models/CodeClimate/IssueSeverity.swift @@ -0,0 +1,11 @@ +// +// IssueSeverity.swift +// +// Created by Alex da Franca on 22.05.24. +// + +import Foundation + +enum IssueSeverity: String, Codable { + case blocker, critical, major, minor, info +} diff --git a/Sources/xcresultparser/Models/CodeClimate/PositionData.swift b/Sources/xcresultparser/Models/CodeClimate/PositionData.swift new file mode 100644 index 0000000..19f40f5 --- /dev/null +++ b/Sources/xcresultparser/Models/CodeClimate/PositionData.swift @@ -0,0 +1,12 @@ +// +// PositionData.swift +// +// Created by Alex da Franca on 22.05.24. +// + +import Foundation + +struct PositionData: Codable { + let line: Int + let column: Int +} diff --git a/Sources/xcresultparser/OutputFormatting/OutputFormat.swift b/Sources/xcresultparser/OutputFormatting/OutputFormat.swift index 50292b3..183f97c 100644 --- a/Sources/xcresultparser/OutputFormatting/OutputFormat.swift +++ b/Sources/xcresultparser/OutputFormatting/OutputFormat.swift @@ -8,7 +8,7 @@ import Foundation public enum OutputFormat: String { - case txt, cli, html, xml, junit, cobertura, md + case txt, cli, html, xml, junit, cobertura, md, warnings, errors public init(string: String?) { if let input = string?.lowercased(), diff --git a/Tests/XcresultparserTests/TestAssets/warnings.json b/Tests/XcresultparserTests/TestAssets/warnings.json new file mode 100644 index 0000000..6db9984 --- /dev/null +++ b/Tests/XcresultparserTests/TestAssets/warnings.json @@ -0,0 +1,95 @@ +[ + { + "description" : "No-usage - Initialization of immutable value 'expectedSummary' was never used; consider replacing with assignment to '_' or removing it", + "categories" : [ + + ], + "type" : "issue", + "location" : { + "lines" : { + "end" : 17, + "begin" : 17 + }, + "positions" : { + "end" : { + "column" : 12, + "line" : 17 + }, + "begin" : { + "column" : 12, + "line" : 17 + } + }, + "path" : "\/Users\/fhaeser\/code\/xcresultparser\/Tests\/XcresultparserTests\/XcresultparserTests.swift" + }, + "engine_name" : "", + "content" : { + "body" : "No-usage - Initialization of immutable value 'expectedSummary' was never used; consider replacing with assignment to '_' or removing it" + }, + "severity" : "minor", + "check_name" : "2365629d946fc0fe1fdea2b4cab49c70", + "fingerprint" : "127d796599edc2d1c51b78ef0d32f1f0" + }, + { + "categories" : [ + + ], + "type" : "issue", + "fingerprint" : "127d796599edc2d1c51b78ef0d32f1f0", + "content" : { + "body" : "No-usage - Initialization of immutable value 'expectedSummary' was never used; consider replacing with assignment to '_' or removing it" + }, + "location" : { + "lines" : { + "end" : 47, + "begin" : 47 + }, + "positions" : { + "begin" : { + "line" : 47, + "column" : 12 + }, + "end" : { + "line" : 47, + "column" : 12 + } + }, + "path" : "\/Users\/fhaeser\/code\/xcresultparser\/Tests\/XcresultparserTests\/XcresultparserTests.swift" + }, + "check_name" : "2365629d946fc0fe1fdea2b4cab49c70", + "engine_name" : "", + "description" : "No-usage - Initialization of immutable value 'expectedSummary' was never used; consider replacing with assignment to '_' or removing it", + "severity" : "minor" + }, + { + "categories" : [ + + ], + "description" : "No-usage - Initialization of immutable value 'expectedSummary' was never used; consider replacing with assignment to '_' or removing it", + "type" : "issue", + "fingerprint" : "127d796599edc2d1c51b78ef0d32f1f0", + "severity" : "minor", + "check_name" : "2365629d946fc0fe1fdea2b4cab49c70", + "location" : { + "path" : "\/Users\/fhaeser\/code\/xcresultparser\/Tests\/XcresultparserTests\/XcresultparserTests.swift", + "lines" : { + "begin" : 86, + "end" : 86 + }, + "positions" : { + "end" : { + "column" : 12, + "line" : 86 + }, + "begin" : { + "line" : 86, + "column" : 12 + } + } + }, + "content" : { + "body" : "No-usage - Initialization of immutable value 'expectedSummary' was never used; consider replacing with assignment to '_' or removing it" + }, + "engine_name" : "" + } +] \ No newline at end of file diff --git a/Tests/XcresultparserTests/XcresultparserTests.swift b/Tests/XcresultparserTests/XcresultparserTests.swift index f5995b2..42444b9 100644 --- a/Tests/XcresultparserTests/XcresultparserTests.swift +++ b/Tests/XcresultparserTests/XcresultparserTests.swift @@ -176,6 +176,36 @@ final class XcresultparserTests: XCTestCase { } try assertXmlTestReportsAreEqual(expectedFileName: "junit_merged", actual: junitXML) } + + func testCleanCodeWarnings() throws { + let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! + guard let converter = IssuesJSON(with: xcresultFile) else { + XCTFail("Unable to create warnings json from \(xcresultFile)") + return + } + let rslt = try converter.jsonString(format: .warnings, quiet: true) + let result = try JSONDecoder().decode([Issue].self, from: Data(rslt.utf8)) + + let expectedFile = Bundle.module.url(forResource: "warnings", withExtension: "json")! + let expectedData = try Data(contentsOf: expectedFile) + let expectedObject = try JSONDecoder().decode([Issue].self, from: expectedData) + + XCTAssertEqual(result.count, expectedObject.count) + let first = try XCTUnwrap(result.first) + let last = try XCTUnwrap(result.last) + XCTAssertNotNil(expectedObject.first(where: { $0.checkName == first.checkName && $0.location == first.location })) + XCTAssertNotNil(expectedObject.first(where: { $0.checkName == last.checkName && $0.location == last.location })) + } + + func testCleanCodeErrors() throws { + let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! + guard let converter = IssuesJSON(with: xcresultFile) else { + XCTFail("Unable to create warnings json from \(xcresultFile)") + return + } + let rslt = try converter.jsonString(format: .errors, quiet: true) + XCTAssertEqual("[\n\n]", rslt) + } func testOutputFormat() { var sut = OutputFormat(string: "txt") @@ -186,9 +216,24 @@ final class XcresultparserTests: XCTestCase { sut = OutputFormat(string: "html") XCTAssertEqual(OutputFormat.html, sut) - + sut = OutputFormat(string: "cli") XCTAssertEqual(OutputFormat.cli, sut) + + sut = OutputFormat(string: "cobertura") + XCTAssertEqual(OutputFormat.cobertura, sut) + + sut = OutputFormat(string: "junit") + XCTAssertEqual(OutputFormat.junit, sut) + + sut = OutputFormat(string: "md") + XCTAssertEqual(OutputFormat.md, sut) + + sut = OutputFormat(string: "warnings") + XCTAssertEqual(OutputFormat.warnings, sut) + + sut = OutputFormat(string: "errors") + XCTAssertEqual(OutputFormat.errors, sut) sut = OutputFormat(string: "") XCTAssertEqual(OutputFormat.cli, sut) From 7b437c1ed506d598773c7d58b5da2b4f663191fb Mon Sep 17 00:00:00 2001 From: Alex da Franca Date: Thu, 23 May 2024 00:41:40 +0200 Subject: [PATCH 4/5] Replaced the Double -> Int.max "dance" with just using UInt64. Thanks for the hint, alexdeem. --- Sources/xcresultparser/CoberturaCoverageConverter.swift | 6 ++---- Sources/xcresultparser/Models/Coverage/LineDetail.swift | 2 +- Sources/xcresultparser/Models/Coverage/Subrange.swift | 2 +- notarize.sh | 3 +++ 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Sources/xcresultparser/CoberturaCoverageConverter.swift b/Sources/xcresultparser/CoberturaCoverageConverter.swift index 9de3ae6..7537266 100644 --- a/Sources/xcresultparser/CoberturaCoverageConverter.swift +++ b/Sources/xcresultparser/CoberturaCoverageConverter.swift @@ -75,9 +75,7 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { guard let covered = lineData.executionCount else { continue } - // If the line coverage count is a MAX_INT, just cap it at MAX_INT - let coveredAsInt = covered > Double(Int.max) ? Int.max : Int(covered) - let line = LineInfo(lineNumber: String(lineNum), coverage: coveredAsInt) + let line = LineInfo(lineNumber: String(lineNum), coverage: covered) fileLines.append(line) } @@ -184,7 +182,7 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { private struct LineInfo { let lineNumber: String - let coverage: Int + let coverage: UInt64 } private struct FileInfo { diff --git a/Sources/xcresultparser/Models/Coverage/LineDetail.swift b/Sources/xcresultparser/Models/Coverage/LineDetail.swift index f158763..fb28d95 100644 --- a/Sources/xcresultparser/Models/Coverage/LineDetail.swift +++ b/Sources/xcresultparser/Models/Coverage/LineDetail.swift @@ -10,6 +10,6 @@ import Foundation struct LineDetail: Decodable { let isExecutable: Bool let line: Int - let executionCount: Double? + let executionCount: UInt64? let subranges: [Subrange]? } diff --git a/Sources/xcresultparser/Models/Coverage/Subrange.swift b/Sources/xcresultparser/Models/Coverage/Subrange.swift index 6e2a88a..2cb36c8 100644 --- a/Sources/xcresultparser/Models/Coverage/Subrange.swift +++ b/Sources/xcresultparser/Models/Coverage/Subrange.swift @@ -9,6 +9,6 @@ import Foundation // Subrange information struct struct Subrange: Decodable { let column: Int - let executionCount: Double + let executionCount: UInt64 let length: Int } diff --git a/notarize.sh b/notarize.sh index 3ea1f14..01c22ce 100644 --- a/notarize.sh +++ b/notarize.sh @@ -59,6 +59,9 @@ fi swift build -c release --arch arm64 --arch x86_64 # move the result from the .build folder to the product folder +if [ ! -d product ]; then + mkdir product +fi cp ".build/apple/Products/Release/$productName" "product/$productName" # Now codesign the app with hardening (-o) From e45a6ed41672b9ed30358338441f4d3ab0c51c5e Mon Sep 17 00:00:00 2001 From: Alex da Franca Date: Thu, 23 May 2024 23:15:08 +0200 Subject: [PATCH 5/5] Implement PR comments --- CommandlineTool/main.swift | 8 ++--- README.md | 11 +++---- .../CoberturaCoverageConverter.swift | 2 +- .../xcresultparser/CoverageConverter.swift | 22 -------------- .../Extensions/String+relativePath.swift | 30 +++++++++++++++++++ Sources/xcresultparser/IssuesJSON.swift | 17 +++++++---- .../IssueLocationInfo.swift | 4 +-- .../Models/CodeClimate/Issue.swift | 12 ++++---- .../Models/CodeClimate/IssueLocation.swift | 10 +++++-- .../OutputFormatting/OutputFormat.swift | 1 + .../SonarCoverageConverter.swift | 2 +- .../XcresultparserTests.swift | 17 +++++++++++ 12 files changed, 87 insertions(+), 49 deletions(-) create mode 100644 Sources/xcresultparser/Extensions/String+relativePath.swift diff --git a/CommandlineTool/main.swift b/CommandlineTool/main.swift index 74a7bf5..4dae2ec 100644 --- a/CommandlineTool/main.swift +++ b/CommandlineTool/main.swift @@ -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', '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.") + @Option(name: .shortAndLong, help: "The output format. It can be either 'txt', 'cli', 'html', 'md', 'xml', 'junit', 'cobertura', 'warnings', 'errors' and '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.") @@ -76,7 +76,7 @@ struct xcresultparser: ParsableCommand { try outputCoberturaXML(for: xcresult) case .cli, .md, .txt, .html: try outputDescription(for: xcresult) - case .warnings, .errors: + case .warnings, .errors, .warningsAndErrors: try outputIssuesJSON(for: xcresult, format: format) } } @@ -106,7 +106,7 @@ struct xcresultparser: ParsableCommand { } private func outputIssuesJSON(for xcresult: String, format: OutputFormat) throws { - guard let converter = IssuesJSON(with: URL(fileURLWithPath: xcresult)) else { + guard let converter = IssuesJSON(with: URL(fileURLWithPath: xcresult), projectRoot: projectRoot ?? "") else { throw ParseError.argumentError } let rslt = try converter.jsonString(format: format, quiet: quiet == 1) @@ -172,7 +172,7 @@ struct xcresultparser: ParsableCommand { return HTMLResultFormatter() case .txt: return TextResultFormatter() - case .cobertura, .junit, .xml, .warnings, .errors: + case .cobertura, .junit, .xml, .warnings, .errors, .warningsAndErrors: // outputFormatter is not used in case of .xml return TextResultFormatter() case .md: diff --git a/README.md b/README.md index cb6e164..ccca5e6 100644 --- a/README.md +++ b/README.md @@ -99,11 +99,12 @@ ARGUMENTS: OPTIONS: -o, --output-format 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. + 'html', 'md', 'xml', 'junit', 'cobertura', + 'warnings', 'errors' and '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 The name of the project root. If present paths and urls are relative to the specified directory. diff --git a/Sources/xcresultparser/CoberturaCoverageConverter.swift b/Sources/xcresultparser/CoberturaCoverageConverter.swift index 7537266..77f3847 100644 --- a/Sources/xcresultparser/CoberturaCoverageConverter.swift +++ b/Sources/xcresultparser/CoberturaCoverageConverter.swift @@ -79,7 +79,7 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { fileLines.append(line) } - let fileInfoInst = FileInfo(path: relativePath(for: fileName, relativeTo: projectRoot), lines: fileLines) + let fileInfoInst = FileInfo(path: fileName.relativePath(relativeTo: projectRoot), lines: fileLines) fileInfo.append(fileInfoInst) } // Sort files to avoid duplicated packages diff --git a/Sources/xcresultparser/CoverageConverter.swift b/Sources/xcresultparser/CoverageConverter.swift index 9a471c6..558e2e4 100644 --- a/Sources/xcresultparser/CoverageConverter.swift +++ b/Sources/xcresultparser/CoverageConverter.swift @@ -70,21 +70,6 @@ public class CoverageConverter { } } - func relativePath(for path: String, relativeTo projectRoot: String) -> String { - guard !projectRoot.isEmpty else { - return path - } - let projectRootTrimmed = projectRoot.trimmingCharacters(in: CharacterSet(charactersIn: "/")) - let parts = path.components(separatedBy: "/\(projectRootTrimmed)") - guard parts.count > 1 else { - return path - } - let relative = parts[parts.count - 1] - return relative.starts(with: "/") ? - String(relative.dropFirst()) : - relative - } - // Use the xccov commandline tool to get results as JSON. func getCoverageDataAsJSON() throws -> FileCoverage { var arguments = ["xccov", "view"] @@ -131,10 +116,3 @@ public class CoverageConverter { } } -extension String { - func text(in range: NSRange) -> String { - let idx1 = index(startIndex, offsetBy: range.location) - let idx2 = index(idx1, offsetBy: range.length) - return String(self[idx1 ..< idx2]) - } -} diff --git a/Sources/xcresultparser/Extensions/String+relativePath.swift b/Sources/xcresultparser/Extensions/String+relativePath.swift new file mode 100644 index 0000000..df2b98f --- /dev/null +++ b/Sources/xcresultparser/Extensions/String+relativePath.swift @@ -0,0 +1,30 @@ +// +// String+relativePath.swift +// +// Created by Alex da Franca on 23.05.24. +// + +import Foundation + +extension String { + func text(in range: NSRange) -> String { + let idx1 = index(startIndex, offsetBy: range.location) + let idx2 = index(idx1, offsetBy: range.length) + return String(self[idx1 ..< idx2]) + } + + func relativePath(relativeTo projectRoot: String) -> String { + guard !projectRoot.isEmpty else { + return self + } + let projectRootTrimmed = projectRoot.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + let parts = components(separatedBy: "/\(projectRootTrimmed)") + guard parts.count > 1 else { + return self + } + let relative = parts[parts.count - 1] + return relative.starts(with: "/") ? + String(relative.dropFirst()) : + relative + } +} diff --git a/Sources/xcresultparser/IssuesJSON.swift b/Sources/xcresultparser/IssuesJSON.swift index eb30372..2cd8814 100644 --- a/Sources/xcresultparser/IssuesJSON.swift +++ b/Sources/xcresultparser/IssuesJSON.swift @@ -14,10 +14,11 @@ import XCResultKit /// public struct IssuesJSON { let resultFile: XCResultFile + let projectRoot: String let checkName: String let invocationRecord: ActionsInvocationRecord - public init?(with url: URL) { + public init?(with url: URL, projectRoot: String = "") { resultFile = XCResultFile(url: url) guard let invocationRecord = resultFile.getInvocationRecord(), let checkdata = try? Data(contentsOf: url.appendingPathComponent("Info.plist")) else { @@ -25,6 +26,7 @@ public struct IssuesJSON { } self.invocationRecord = invocationRecord checkName = checkdata.md5() + self.projectRoot = projectRoot } public func jsonString(format: OutputFormat, quiet: Bool = false) throws -> String { @@ -33,14 +35,19 @@ public struct IssuesJSON { let jsonData: Data if format == .errors { let errors = invocationRecord.issues.errorSummaries - .map { Issue(issueSummary: $0, severity: .blocker, checkName: checkName) } + .map { Issue(issueSummary: $0, severity: .blocker, checkName: checkName, projectRoot: projectRoot) } jsonData = try encoder.encode(errors) } else { let warnings = invocationRecord.issues.warningSummaries - .map { Issue(issueSummary: $0, severity: .minor, checkName: checkName) } + .map { Issue(issueSummary: $0, severity: .minor, checkName: checkName, projectRoot: projectRoot) } let analyzerWarnings = invocationRecord.issues.analyzerWarningSummaries - .map { Issue(issueSummary: $0, severity: .info, checkName: checkName) } - let combined = warnings + analyzerWarnings + .map { Issue(issueSummary: $0, severity: .major, checkName: checkName, projectRoot: projectRoot) } + var combined = warnings + analyzerWarnings + if format == .warningsAndErrors { + let errors = invocationRecord.issues.errorSummaries + .map { Issue(issueSummary: $0, severity: .blocker, checkName: checkName, projectRoot: projectRoot) } + combined += errors + } jsonData = try encoder.encode(combined) } return String(decoding: jsonData, as: UTF8.self) diff --git a/Sources/xcresultparser/Models/CodeClimate/IntermediateObjects/IssueLocationInfo.swift b/Sources/xcresultparser/Models/CodeClimate/IntermediateObjects/IssueLocationInfo.swift index ae645aa..7e78ed2 100644 --- a/Sources/xcresultparser/Models/CodeClimate/IntermediateObjects/IssueLocationInfo.swift +++ b/Sources/xcresultparser/Models/CodeClimate/IntermediateObjects/IssueLocationInfo.swift @@ -21,9 +21,7 @@ struct IssueLocationInfo { 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)") -// } + // documentLocation.concreteTypeName: "DVTTextDocumentLocation" filePath = url.path guard let fragment = url.fragment else { startLine = 0 diff --git a/Sources/xcresultparser/Models/CodeClimate/Issue.swift b/Sources/xcresultparser/Models/CodeClimate/Issue.swift index 9d971c6..9e8de96 100644 --- a/Sources/xcresultparser/Models/CodeClimate/Issue.swift +++ b/Sources/xcresultparser/Models/CodeClimate/Issue.swift @@ -34,17 +34,17 @@ struct Issue: Codable { } extension Issue { - init(issueSummary: IssueSummary, severity: IssueSeverity, checkName: String) { + init(issueSummary: IssueSummary, severity: IssueSeverity, checkName: String, projectRoot: String = "") { let issueLocationInfo = IssueLocationInfo(with: issueSummary.documentLocationInCreatingWorkspace) - description = "\(issueSummary.issueType) - \(issueSummary.message)" + description = "\(issueSummary.issueType) • \(issueSummary.message)" self.checkName = checkName - fingerprint = "\(issueSummary.issueType) - \(issueSummary.message)".md5() self.severity = severity - engineName = issueSummary.producingTarget ?? "" - location = IssueLocation(issueLocationInfo: issueLocationInfo) + engineName = "Xcode Result Bundle Tool" + location = IssueLocation(issueLocationInfo: issueLocationInfo, projectRoot: projectRoot) + fingerprint = "\(issueSummary.issueType)-\(issueSummary.message)-\(location.fingerprint)".md5() type = .issue categories = [] - content = IssueContent(body: "\(issueSummary.issueType) - \(issueSummary.message)") + content = IssueContent(body: "\(issueSummary.issueType) • \(issueSummary.message)") } } diff --git a/Sources/xcresultparser/Models/CodeClimate/IssueLocation.swift b/Sources/xcresultparser/Models/CodeClimate/IssueLocation.swift index d2f43c9..f782e65 100644 --- a/Sources/xcresultparser/Models/CodeClimate/IssueLocation.swift +++ b/Sources/xcresultparser/Models/CodeClimate/IssueLocation.swift @@ -24,7 +24,7 @@ extension IssueLocation { } extension IssueLocation { - init(issueLocationInfo: IssueLocationInfo?) { + init(issueLocationInfo: IssueLocationInfo?, projectRoot: String = "") { guard let issueLocationInfo else { path = "" lines = IssueLocationData(begin: 0, end: 0) @@ -34,7 +34,7 @@ extension IssueLocation { ) return } - path = issueLocationInfo.filePath + path = issueLocationInfo.filePath.relativePath(relativeTo: projectRoot) lines = IssueLocationData(begin: issueLocationInfo.startLine, end: issueLocationInfo.endLine) positions = IssuePositionData( begin: PositionData(line: issueLocationInfo.startLine, column: issueLocationInfo.startColumn), @@ -42,3 +42,9 @@ extension IssueLocation { ) } } + +extension IssueLocation { + var fingerprint: String { + return "\(path)-\(positions.begin.line)-\(positions.begin.column)" + } +} diff --git a/Sources/xcresultparser/OutputFormatting/OutputFormat.swift b/Sources/xcresultparser/OutputFormatting/OutputFormat.swift index 183f97c..4f4513b 100644 --- a/Sources/xcresultparser/OutputFormatting/OutputFormat.swift +++ b/Sources/xcresultparser/OutputFormatting/OutputFormat.swift @@ -9,6 +9,7 @@ import Foundation public enum OutputFormat: String { case txt, cli, html, xml, junit, cobertura, md, warnings, errors + case warningsAndErrors = "warnings-and-errors" public init(string: String?) { if let input = string?.lowercased(), diff --git a/Sources/xcresultparser/SonarCoverageConverter.swift b/Sources/xcresultparser/SonarCoverageConverter.swift index 35d112f..5b273b4 100644 --- a/Sources/xcresultparser/SonarCoverageConverter.swift +++ b/Sources/xcresultparser/SonarCoverageConverter.swift @@ -37,7 +37,7 @@ public class SonarCoverageConverter: CoverageConverter, XmlSerializable { relativeTo projectRoot: String ) throws -> XMLElement { let fileElement = XMLElement(name: "file") - fileElement.addAttribute(name: "path", stringValue: relativePath(for: file, relativeTo: projectRoot)) + fileElement.addAttribute(name: "path", stringValue: file.relativePath(relativeTo: projectRoot)) for lineData in coverageData where lineData.isExecutable { let line = XMLElement(name: "lineToCover") line.addAttribute(name: "lineNumber", stringValue: String(lineData.line)) diff --git a/Tests/XcresultparserTests/XcresultparserTests.swift b/Tests/XcresultparserTests/XcresultparserTests.swift index 42444b9..72f45ce 100644 --- a/Tests/XcresultparserTests/XcresultparserTests.swift +++ b/Tests/XcresultparserTests/XcresultparserTests.swift @@ -197,6 +197,20 @@ final class XcresultparserTests: XCTestCase { XCTAssertNotNil(expectedObject.first(where: { $0.checkName == last.checkName && $0.location == last.location })) } + func testCleanCodeWarningsWithRelativePath() throws { + let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! + guard let converter = IssuesJSON(with: xcresultFile, projectRoot: "xcresultparser/") else { + XCTFail("Unable to create warnings json from \(xcresultFile)") + return + } + let rslt = try converter.jsonString(format: .warnings, quiet: true) + let result = try JSONDecoder().decode([Issue].self, from: Data(rslt.utf8)) + .sorted { lhs, rhs in + lhs.location.path > rhs.location.path + } + XCTAssertEqual("Tests/XcresultparserTests/XcresultparserTests.swift", result.first?.location.path) + } + func testCleanCodeErrors() throws { let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! guard let converter = IssuesJSON(with: xcresultFile) else { @@ -234,6 +248,9 @@ final class XcresultparserTests: XCTestCase { sut = OutputFormat(string: "errors") XCTAssertEqual(OutputFormat.errors, sut) + + sut = OutputFormat(string: "warnings-and-errors") + XCTAssertEqual(OutputFormat.warningsAndErrors, sut) sut = OutputFormat(string: "") XCTAssertEqual(OutputFormat.cli, sut)