From bfb56001c056152e0bf1232d0f50845e063aa307 Mon Sep 17 00:00:00 2001 From: Alex da Franca Date: Mon, 25 Nov 2024 05:27:05 +0100 Subject: [PATCH 1/9] enhance project root handling --- Sources/xcresultparser/JunitXML.swift | 49 ++++++++++++++++----------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/Sources/xcresultparser/JunitXML.swift b/Sources/xcresultparser/JunitXML.swift index 98a4a4c..98e6147 100644 --- a/Sources/xcresultparser/JunitXML.swift +++ b/Sources/xcresultparser/JunitXML.swift @@ -48,7 +48,7 @@ public struct JunitXML: XmlSerializable { // MARK: - Properties private let resultFile: XCResultFile - private let projectRoot: String + private let projectRoot: URL? private let invocationRecord: ActionsInvocationRecord private let testReportFormat: TestReportFormat @@ -70,7 +70,15 @@ public struct JunitXML: XmlSerializable { guard let record = resultFile.getInvocationRecord() else { return nil } - self.projectRoot = projectRoot + + var isDirectory: ObjCBool = false + if FileManager.default.fileExists(atPath: projectRoot, isDirectory: &isDirectory), + isDirectory.boolValue == true { + self.projectRoot = URL(fileURLWithPath: projectRoot) + } else { + self.projectRoot = nil + } + invocationRecord = record testReportFormat = format if testReportFormat == .sonar { @@ -332,38 +340,41 @@ private extension ActionTestSummaryGroup { return testsuite } - func sonarFileXML(projectRoot: String) -> XMLElement { + func sonarFileXML(projectRoot: URL?) -> XMLElement { let testsuite = XMLElement(name: "file") testsuite.addAttribute(name: "path", stringValue: relativeFilenameGuess(in: projectRoot)) return testsuite } - private func relativeFilenameGuess(in projectRoot: String) -> String { - guard !projectRoot.isEmpty else { + private static var cachedPathnames = [String: String]() + private func relativeFilenameGuess(in projectRootUrl: URL?) -> String { + guard let projectRootUrl else { return identifierString } - var isDirectory: ObjCBool = false - guard FileManager.default.fileExists(atPath: projectRoot, isDirectory: &isDirectory), - isDirectory.boolValue == true else { - return identifierString + if let cachedValue = Self.cachedPathnames[identifierString] { + return cachedValue } - let url = URL(fileURLWithPath: projectRoot) let arguments = ["-rl", "--include", "*.swift", "class \(identifierString)[ |:]", "."] do { - let filelistData = try Shell.execute(program: "/usr/bin/grep", with: arguments, at: url) + let filelistData = try Shell.execute(program: "/usr/bin/grep", with: arguments, at: projectRootUrl) guard let result = String(decoding: filelistData, as: UTF8.self).components(separatedBy: "\n").first, !result.isEmpty else { - return identifierString + return cache(identifierString, for: identifierString) } if result.hasPrefix("./") { - return String(result.dropFirst(2)) + return cache(String(result.dropFirst(2)), for: identifierString) } - return result + return cache(result, for: identifierString) } catch { - return identifierString + return cache(identifierString, for: identifierString) } } + private func cache(_ value: String, for key: String) -> String { + Self.cachedPathnames[key] = value + return value + } + private var statistics: TestMetrics { return TestMetrics(tests: numberOfTests, failures: numberOfFailures) } @@ -385,7 +396,7 @@ private extension ActionTestSummaryGroup { } private extension TestFailureIssueSummary { - func failureXML(projectRoot: String = "") -> XMLElement { + func failureXML(projectRoot: URL? = nil) -> XMLElement { let failure = XMLElement(name: "failure") var value = message if let loc = documentLocationInCreatingWorkspace?.url { @@ -413,11 +424,11 @@ private extension TestFailureIssueSummary { return failure } - private func relativePart(of url: URL, relativeTo projectRoot: String) -> String { - guard !projectRoot.isEmpty else { + private func relativePart(of url: URL, relativeTo projectRoot: URL?) -> String { + guard let projectRoot else { return url.path } - let parts = url.path.components(separatedBy: "/\(projectRoot)") + let parts = url.path.components(separatedBy: "\(projectRoot.path)") guard parts.count > 1 else { return url.path } From 318abe3e41a6d9eb0bb3db9889d4a9705fc54d65 Mon Sep 17 00:00:00 2001 From: Alex da Franca Date: Mon, 2 Dec 2024 09:26:41 +0100 Subject: [PATCH 2/9] Improve Sonar Test output by a landslide by optimizing the path lookup for test classes --- .../xcschemes/xcresultparser.xcscheme | 16 ++- Package.swift | 4 +- README.md | 9 +- .../xcresultparser/CoverageConverter.swift | 6 +- Sources/xcresultparser/JunitXML.swift | 99 ++++++++++++----- .../Services/DependencyFactory.swift | 13 +++ .../Services/FileManaging.swift | 16 +++ Sources/xcresultparser/Services/Shell.swift | 59 ++++++++++ Sources/xcresultparser/Shell.swift | 44 -------- ...arTestExecutionWithProjectRootAbsolute.xml | 14 +++ ...arTestExecutionWithProjectRootRelative.xml | 14 +++ .../XcresultparserTests.swift | 105 ++++++++++++++++++ 12 files changed, 318 insertions(+), 81 deletions(-) create mode 100644 Sources/xcresultparser/Services/DependencyFactory.swift create mode 100644 Sources/xcresultparser/Services/FileManaging.swift create mode 100644 Sources/xcresultparser/Services/Shell.swift delete mode 100644 Sources/xcresultparser/Shell.swift create mode 100644 Tests/XcresultparserTests/TestAssets/sonarTestExecutionWithProjectRootAbsolute.xml create mode 100644 Tests/XcresultparserTests/TestAssets/sonarTestExecutionWithProjectRootRelative.xml diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/xcresultparser.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/xcresultparser.xcscheme index 3b3dee5..c98539d 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/xcresultparser.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/xcresultparser.xcscheme @@ -82,6 +82,14 @@ argument = "/Users/alex/xcodebuild_result.xcresult" isEnabled = "NO"> + + + + @@ -108,7 +116,7 @@ + isEnabled = "YES"> + isEnabled = "NO"> + argument = "/Users/adf/Desktop/Sparkasse2.xcresult" + isEnabled = "YES"> errors.json 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. -If you want to use the test results for sonarqube, there is another twist: the .xcresult bundle only lists the test by testclass, but not by file. However sonarqube expects the file paths of the tests. In this case you must provide a --project-root to *xcresultparser*. Only then *xcresultparser* can convert the classnames to file names, by *grep*-ing for "class NameOfClass". If such a file is found in the directory provided in *--project-root*, then the file path can be detrmined and the *sonar-scanner* happily can scan the files for tests. +If you want to use the test results for sonarqube, there is another twist: the .xcresult bundle only lists the test by testclass, but not by file. However the sonarqube CLI tool expects the file paths of the tests. In this case you must provide a --project-root to *xcresultparser*. Only then *xcresultparser* can convert the classnames to file names, by *egrep*-ing for `^(?:public )?(?:final )?(?:public )?(?:(class|\@implementation) )\w+`. If such a file is found in the directory provided in *--project-root*, then the file path can be detrmined and the *sonar-scanner* happily can scan the files for tests. The pattern matches all swift and objective-c classes. + +Since searching all files in `project-root` takes some time, an index path names for class names is created beforehand and used as lookup table. + +The following egrep expression is used to create the lookup table for the filenames of classes: +``` +egrep -rio --include "*.swift" --include "*.m" "^(?:public )?(?:final )?(?:public )?(?:(class|\@implementation) )\w+" $project-root +``` In cases where the xcresult archive is not created on the same machine and the paths used for *sonar-scanner* differ, the pathnames need to be adjusted. In such a case you can use a relative path for the *sonar.sources* paramater of the *sonar-scanner* CLI tool and convert the output of xcresultparser to also return relative path names. diff --git a/Sources/xcresultparser/CoverageConverter.swift b/Sources/xcresultparser/CoverageConverter.swift index 88eba20..8a5690f 100644 --- a/Sources/xcresultparser/CoverageConverter.swift +++ b/Sources/xcresultparser/CoverageConverter.swift @@ -81,7 +81,7 @@ public class CoverageConverter { } arguments.append("--json") arguments.append(resultFile.url.path) - let coverageData = try Shell.execute(program: "/usr/bin/xcrun", with: arguments) + let coverageData = try DependencyFactory.shell.execute(program: "/usr/bin/xcrun", with: arguments) return try JSONDecoder().decode(FileCoverage.self, from: coverageData) } @@ -107,7 +107,7 @@ public class CoverageConverter { arguments.append("--file") arguments.append(path) arguments.append(resultFile.url.path) - let coverageData = try Shell.execute(program: "/usr/bin/xcrun", with: arguments) + let coverageData = try DependencyFactory.shell.execute(program: "/usr/bin/xcrun", with: arguments) return String(decoding: coverageData, as: UTF8.self) } @@ -121,7 +121,7 @@ public class CoverageConverter { } arguments.append("--file-list") arguments.append(resultFile.url.path) - let filelistData = try Shell.execute(program: "/usr/bin/xcrun", with: arguments) + let filelistData = try DependencyFactory.shell.execute(program: "/usr/bin/xcrun", with: arguments) return String(decoding: filelistData, as: UTF8.self).components(separatedBy: "\n") } } diff --git a/Sources/xcresultparser/JunitXML.swift b/Sources/xcresultparser/JunitXML.swift index 98e6147..d947d83 100644 --- a/Sources/xcresultparser/JunitXML.swift +++ b/Sources/xcresultparser/JunitXML.swift @@ -51,7 +51,8 @@ public struct JunitXML: XmlSerializable { private let projectRoot: URL? private let invocationRecord: ActionsInvocationRecord private let testReportFormat: TestReportFormat - + private let relativePathNames: Bool + private let nodeNames: NodeNames private var numFormatter: NumberFormatter = { @@ -61,10 +62,13 @@ public struct JunitXML: XmlSerializable { return numFormatter }() + // MARK: - Initializer + public init?( with url: URL, projectRoot: String = "", - format: TestReportFormat = .junit + format: TestReportFormat = .junit, + relativePathNames: Bool = true ) { resultFile = XCResultFile(url: url) guard let record = resultFile.getInvocationRecord() else { @@ -72,7 +76,7 @@ public struct JunitXML: XmlSerializable { } var isDirectory: ObjCBool = false - if FileManager.default.fileExists(atPath: projectRoot, isDirectory: &isDirectory), + if DependencyFactory.fileManager.fileExists(atPath: projectRoot, isDirectory: &isDirectory), isDirectory.boolValue == true { self.projectRoot = URL(fileURLWithPath: projectRoot) } else { @@ -86,6 +90,7 @@ public struct JunitXML: XmlSerializable { } else { nodeNames = NodeNames.defaultNodeNames } + self.relativePathNames = relativePathNames } func createRootElement() -> XMLElement { @@ -152,6 +157,13 @@ public struct JunitXML: XmlSerializable { return xml.xmlString(options: [.nodePrettyPrint, .nodeCompactEmptyElement]) } + // only used in unit testing + static func resetCachedPathnames() { + ActionTestSummaryGroup.resetCachedPathnames() + } + + // MARK: - Private interface + // The XMLElement produced by this function is not allowed in the junit XML format and thus unused. // It is kept in case it serves another format. private func runDestinationXML(_ destination: ActionRunDestinationRecord) -> XMLElement { @@ -191,7 +203,7 @@ public struct JunitXML: XmlSerializable { if testReportFormat == .sonar { var nodes = [XMLElement]() for subGroup in group.subtestGroups { - let node = subGroup.sonarFileXML(projectRoot: projectRoot) + let node = subGroup.sonarFileXML(projectRoot: projectRoot, relativePathNames: relativePathNames) let testcases = createTestCases( for: subGroup.nameString, tests: subGroup.subtests, failureSummaries: failureSummaries ) @@ -219,7 +231,7 @@ public struct JunitXML: XmlSerializable { testDirectory: String = "" ) -> XMLElement { let node = testReportFormat == .sonar ? - group.sonarFileXML(projectRoot: projectRoot) : + group.sonarFileXML(projectRoot: projectRoot, relativePathNames: relativePathNames) : group.testSuiteXML(numFormatter: numFormatter) for thisTest in tests { @@ -320,6 +332,8 @@ private extension ActionTestMetadata { } private extension ActionTestSummaryGroup { + private static var cachedPathnames = [String: String]() + struct TestMetrics { let tests: Int let failures: Int @@ -340,39 +354,59 @@ private extension ActionTestSummaryGroup { return testsuite } - func sonarFileXML(projectRoot: URL?) -> XMLElement { + func sonarFileXML(projectRoot: URL?, relativePathNames: Bool = true) -> XMLElement { let testsuite = XMLElement(name: "file") - testsuite.addAttribute(name: "path", stringValue: relativeFilenameGuess(in: projectRoot)) + testsuite.addAttribute(name: "path", stringValue: classPath(in: projectRoot, relativePathNames: relativePathNames)) return testsuite } - private static var cachedPathnames = [String: String]() - private func relativeFilenameGuess(in projectRootUrl: URL?) -> String { + // only used in unit testing + static func resetCachedPathnames() { + cachedPathnames.removeAll() + } + + // MARK: - Private interface + + private func classPath(in projectRootUrl: URL?, relativePathNames: Bool = true) -> String { guard let projectRootUrl else { return identifierString } - if let cachedValue = Self.cachedPathnames[identifierString] { - return cachedValue - } - let arguments = ["-rl", "--include", "*.swift", "class \(identifierString)[ |:]", "."] - do { - let filelistData = try Shell.execute(program: "/usr/bin/grep", with: arguments, at: projectRootUrl) - guard let result = String(decoding: filelistData, as: UTF8.self).components(separatedBy: "\n").first, - !result.isEmpty else { - return cache(identifierString, for: identifierString) - } - if result.hasPrefix("./") { - return cache(String(result.dropFirst(2)), for: identifierString) - } - return cache(result, for: identifierString) - } catch { - return cache(identifierString, for: identifierString) + if Self.cachedPathnames.isEmpty { + cacheAllClassNames(in: projectRootUrl, relativePathNames: relativePathNames) } + return Self.cachedPathnames[identifierString] ?? identifierString } - private func cache(_ value: String, for key: String) -> String { - Self.cachedPathnames[key] = value - return value + private func cacheAllClassNames(in projectRootUrl: URL, relativePathNames: Bool = true) { + let program = "/usr/bin/egrep" + let grepPathArgument = relativePathNames ? "." : projectRootUrl.path + let arguments = [ + "-rio", + "--include", "*.swift", + "--include", "*.m", + "^(?:public )?(?:final )?(?:public )?(?:(class|\\@implementation) )[a-zA-Z0-9_]+", + grepPathArgument + ] + guard let filelistData = try? DependencyFactory.shell.execute(program: program, with: arguments, at: projectRootUrl) else { + return + } + let trimCharacterSet = CharacterSet.whitespacesAndNewlines.union(CharacterSet(charactersIn: ":")) + let result = String(decoding: filelistData, as: UTF8.self).components(separatedBy: "\n") + for match in result { + let items = match.components(separatedBy: ":") + if items.count > 1, + let path = items.first, + !path.isEmpty, + let className = items + .dropFirst() + .joined(separator: ":") + .trimmingCharacters(in: trimCharacterSet) + .components(separatedBy: .whitespaces) + .last, + !className.isEmpty { + Self.cachedPathnames[className] = path.withoutLocalPrefix + } + } } private var statistics: TestMetrics { @@ -395,6 +429,15 @@ private extension ActionTestSummaryGroup { } } +private extension String { + var withoutLocalPrefix: String { + if hasPrefix("./") { + return String(dropFirst(2)) + } + return self + } +} + private extension TestFailureIssueSummary { func failureXML(projectRoot: URL? = nil) -> XMLElement { let failure = XMLElement(name: "failure") diff --git a/Sources/xcresultparser/Services/DependencyFactory.swift b/Sources/xcresultparser/Services/DependencyFactory.swift new file mode 100644 index 0000000..31527f1 --- /dev/null +++ b/Sources/xcresultparser/Services/DependencyFactory.swift @@ -0,0 +1,13 @@ +// +// DependencyFactory.swift +// Xcresultparser +// +// Created by Alex da Franca on 01.12.24. +// + +import Foundation + +class DependencyFactory { + static var shell: Commandline = Shell() + static var fileManager: FileManaging = FileManager.default +} diff --git a/Sources/xcresultparser/Services/FileManaging.swift b/Sources/xcresultparser/Services/FileManaging.swift new file mode 100644 index 0000000..e6f2bfc --- /dev/null +++ b/Sources/xcresultparser/Services/FileManaging.swift @@ -0,0 +1,16 @@ +// +// FileManaging.swift +// Xcresultparser +// +// Created by Alex da Franca on 01.12.24. +// + +import Foundation + +protocol FileManaging { + func fileExists(atPath path: String, isDirectory: UnsafeMutablePointer?) -> Bool +} + +extension FileManager: FileManaging { + // just add protocol conformance to Foundation's FileManager +} diff --git a/Sources/xcresultparser/Services/Shell.swift b/Sources/xcresultparser/Services/Shell.swift new file mode 100644 index 0000000..8e82bf3 --- /dev/null +++ b/Sources/xcresultparser/Services/Shell.swift @@ -0,0 +1,59 @@ +// +// Shell.swift +// +// +// Created by Alex da Franca on 03.04.22. +// + +import Foundation + +protocol Commandline { + func execute(program: String, with arguments: [String], at executionPath: URL?) throws -> Data +} + +extension Commandline { + func execute(program: String, with arguments: [String]) throws -> Data { + return try execute(program: program, with: arguments, at: nil) + } +} + +struct Shell: Commandline { + init() { + // nothing to do here, but required by public struct + } + func execute( + program: String, + with arguments: [String], + at executionPath: URL? = nil + ) throws -> Data { + try autoreleasepool { + let task = Process() + if let directoryUrl = executionPath { + task.currentDirectoryURL = directoryUrl + } + task.executableURL = URL(fileURLWithPath: program) + task.arguments = arguments + let errorPipe = Pipe() + task.standardError = errorPipe + let outPipe = Pipe() + task.standardOutput = outPipe + try task.run() + let fileHandle = outPipe.fileHandleForReading + let data = fileHandle.readDataToEndOfFile() + task.waitUntilExit() + let status = task.terminationStatus + if status != 0 { + let fileHandle = errorPipe.fileHandleForReading + let data = fileHandle.readDataToEndOfFile() + let errorMessage = String(decoding: data, as: UTF8.self) + throw CLIError.executionError(code: Int(status), message: errorMessage) + } else { + return data + } + } + } + + enum CLIError: Error { + case executionError(code: Int, message: String) + } +} diff --git a/Sources/xcresultparser/Shell.swift b/Sources/xcresultparser/Shell.swift deleted file mode 100644 index e6c73c0..0000000 --- a/Sources/xcresultparser/Shell.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// Shell.swift -// -// -// Created by Alex da Franca on 03.04.22. -// - -import Foundation - -enum Shell { - static func execute( - program: String, - with arguments: [String], - at executionPath: URL? = nil - ) throws -> Data { - let task = Process() - if let directoryUrl = executionPath { - task.currentDirectoryURL = directoryUrl - } - task.executableURL = URL(fileURLWithPath: program) - task.arguments = arguments - let errorPipe = Pipe() - task.standardError = errorPipe - let outPipe = Pipe() - task.standardOutput = outPipe - try task.run() - let fileHandle = outPipe.fileHandleForReading - let data = fileHandle.readDataToEndOfFile() - task.waitUntilExit() - let status = task.terminationStatus - if status != 0 { - let fileHandle = errorPipe.fileHandleForReading - let data = fileHandle.readDataToEndOfFile() - let errorMessage = String(decoding: data, as: UTF8.self) - throw CLIError.executionError(code: Int(status), message: errorMessage) - } else { - return data - } - } - - enum CLIError: Error { - case executionError(code: Int, message: String) - } -} diff --git a/Tests/XcresultparserTests/TestAssets/sonarTestExecutionWithProjectRootAbsolute.xml b/Tests/XcresultparserTests/TestAssets/sonarTestExecutionWithProjectRootAbsolute.xml new file mode 100644 index 0000000..541d7ce --- /dev/null +++ b/Tests/XcresultparserTests/TestAssets/sonarTestExecutionWithProjectRootAbsolute.xml @@ -0,0 +1,14 @@ + + + + + + failed - Unable to create CoverageConverter from /Users/fhaeser/Library/Developer/Xcode/DerivedData/xcresultparser-ebyquorsyljyyuchjpndxzpxmxvo/Build/Products/Debug/XcresultparserTests.xctest/Contents/Resources/Xcresultparser_XcresultparserTests.bundle/Contents/Resources/test.xcresult (/Users/fhaeser/code/xcresultparser/Tests/XcresultparserTests/XcresultparserTests.swift:108) + + + + + + + + diff --git a/Tests/XcresultparserTests/TestAssets/sonarTestExecutionWithProjectRootRelative.xml b/Tests/XcresultparserTests/TestAssets/sonarTestExecutionWithProjectRootRelative.xml new file mode 100644 index 0000000..1ce006c --- /dev/null +++ b/Tests/XcresultparserTests/TestAssets/sonarTestExecutionWithProjectRootRelative.xml @@ -0,0 +1,14 @@ + + + + + + failed - Unable to create CoverageConverter from /Users/fhaeser/Library/Developer/Xcode/DerivedData/xcresultparser-ebyquorsyljyyuchjpndxzpxmxvo/Build/Products/Debug/XcresultparserTests.xctest/Contents/Resources/Xcresultparser_XcresultparserTests.bundle/Contents/Resources/test.xcresult (/Users/fhaeser/code/xcresultparser/Tests/XcresultparserTests/XcresultparserTests.swift:108) + + + + + + + + diff --git a/Tests/XcresultparserTests/XcresultparserTests.swift b/Tests/XcresultparserTests/XcresultparserTests.swift index e2aa7c0..b57aeaf 100644 --- a/Tests/XcresultparserTests/XcresultparserTests.swift +++ b/Tests/XcresultparserTests/XcresultparserTests.swift @@ -324,6 +324,7 @@ final class XcresultparserTests: XCTestCase { } func testJunitXMLSonar() throws { + JunitXML.resetCachedPathnames() let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! let projectRoot = "" guard let junitXML = JunitXML( @@ -337,6 +338,71 @@ final class XcresultparserTests: XCTestCase { try assertXmlTestReportsAreEqual(expectedFileName: "sonarTestExecution", actual: junitXML) } + func testJunitXMLSonarRelativePaths() throws { + JunitXML.resetCachedPathnames() + let cliResult = """ +./Tests/XcresultparserTests.swift:class XcresultparserTests +""" + let oldFilemanger = DependencyFactory.fileManager + let oldShell = DependencyFactory.shell + + DependencyFactory.fileManager = MockedFileManager(fileExists: true , isPathDirectory: true) + let mockedShell = MockedShell(response: Data(cliResult.utf8), error: nil) + DependencyFactory.shell = mockedShell + mockedShell.argumentValidation = { arguments in + return arguments.last == "." + } + + let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! + let projectRoot = "/Users/imaginary/project" + guard let junitXML = JunitXML( + with: xcresultFile, + projectRoot: projectRoot, + format: .sonar, + relativePathNames: true + ) else { + XCTFail("Unable to create JunitXML from \(xcresultFile)") + return + } + try assertXmlTestReportsAreEqual(expectedFileName: "sonarTestExecutionWithProjectRootRelative", actual: junitXML) + + DependencyFactory.fileManager = oldFilemanger + DependencyFactory.shell = oldShell + } + + func testJunitXMLSonarAbsolutePaths() throws { + JunitXML.resetCachedPathnames() + let cliResult = """ +/Users/actual/project/Tests/XcresultparserTests.swift:class XcresultparserTests +""" + + let oldFilemanger = DependencyFactory.fileManager + let oldShell = DependencyFactory.shell + + DependencyFactory.fileManager = MockedFileManager(fileExists: true , isPathDirectory: true) + let mockedShell = MockedShell(response: Data(cliResult.utf8), error: nil) + DependencyFactory.shell = mockedShell + mockedShell.argumentValidation = { arguments in + return arguments.last != "." + } + + let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! + let projectRoot = "/Users/imaginary/project" + guard let junitXML = JunitXML( + with: xcresultFile, + projectRoot: projectRoot, + format: .sonar, + relativePathNames: false + ) else { + XCTFail("Unable to create JunitXML from \(xcresultFile)") + return + } + try assertXmlTestReportsAreEqual(expectedFileName: "sonarTestExecutionWithProjectRootAbsolute", actual: junitXML) + + DependencyFactory.fileManager = oldFilemanger + DependencyFactory.shell = oldShell + } + func testJunitXMLJunit() throws { let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! let projectRoot = "" @@ -524,3 +590,42 @@ final class XcresultparserTests: XCTestCase { XCTAssertEqual(actualXMLDocument.xmlString, expectedXMLDocument.xmlString, file: file, line: line) } } + +class MockedFileManager: FileManaging { + var fileExists = true + var isPathDirectory = true + + init(fileExists: Bool, isPathDirectory: Bool) { + self.fileExists = fileExists + self.isPathDirectory = isPathDirectory + } + + func fileExists(atPath path: String, isDirectory: UnsafeMutablePointer?) -> Bool { + if let isDirectory = isDirectory { + isDirectory.pointee = ObjCBool(isPathDirectory) + } + return fileExists + } +} + +class MockedShell: Commandline { + var response = Data() + var error: Error? + var argumentValidation: ([String]) -> Bool = { _ in true } + + init(response: Data, error: Error?) { + self.response = response + self.error = error + } + + func execute(program: String, with arguments: [String], at executionPath: URL?) throws -> Data { + if !argumentValidation(arguments) { + throw NSError(domain: "error", code: 17) + } + if let error { + throw error + } else { + return response + } + } +} From e56603f101e5e50ef5ae7095dc4d21aac441b9ec Mon Sep 17 00:00:00 2001 From: Alex da Franca Date: Mon, 2 Dec 2024 09:41:11 +0100 Subject: [PATCH 3/9] revert changes to scheme --- .../xcshareddata/xcschemes/xcresultparser.xcscheme | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/xcresultparser.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/xcresultparser.xcscheme index c98539d..884a90a 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/xcresultparser.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/xcresultparser.xcscheme @@ -84,10 +84,6 @@ - - + isEnabled = "NO"> - - Date: Mon, 2 Dec 2024 10:30:46 +0100 Subject: [PATCH 4/9] Enhance dependency factory --- .../xcresultparser/CoverageConverter.swift | 10 ++++-- Sources/xcresultparser/JunitXML.swift | 4 +-- .../Services/DependencyFactory.swift | 8 +++-- .../XcresultparserTests.swift | 32 ++++++++++++------- 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/Sources/xcresultparser/CoverageConverter.swift b/Sources/xcresultparser/CoverageConverter.swift index 8a5690f..238bd20 100644 --- a/Sources/xcresultparser/CoverageConverter.swift +++ b/Sources/xcresultparser/CoverageConverter.swift @@ -31,6 +31,10 @@ public class CoverageConverter { let coverageTargets: Set let excludedPaths: Set + // MARK: - Dependencies + + let shell = DependencyFactory.shell() + public init?( with url: URL, projectRoot: String = "", @@ -81,7 +85,7 @@ public class CoverageConverter { } arguments.append("--json") arguments.append(resultFile.url.path) - let coverageData = try DependencyFactory.shell.execute(program: "/usr/bin/xcrun", with: arguments) + let coverageData = try shell.execute(program: "/usr/bin/xcrun", with: arguments) return try JSONDecoder().decode(FileCoverage.self, from: coverageData) } @@ -107,7 +111,7 @@ public class CoverageConverter { arguments.append("--file") arguments.append(path) arguments.append(resultFile.url.path) - let coverageData = try DependencyFactory.shell.execute(program: "/usr/bin/xcrun", with: arguments) + let coverageData = try shell.execute(program: "/usr/bin/xcrun", with: arguments) return String(decoding: coverageData, as: UTF8.self) } @@ -121,7 +125,7 @@ public class CoverageConverter { } arguments.append("--file-list") arguments.append(resultFile.url.path) - let filelistData = try DependencyFactory.shell.execute(program: "/usr/bin/xcrun", with: arguments) + 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/JunitXML.swift b/Sources/xcresultparser/JunitXML.swift index d947d83..d2f31a0 100644 --- a/Sources/xcresultparser/JunitXML.swift +++ b/Sources/xcresultparser/JunitXML.swift @@ -76,7 +76,7 @@ public struct JunitXML: XmlSerializable { } var isDirectory: ObjCBool = false - if DependencyFactory.fileManager.fileExists(atPath: projectRoot, isDirectory: &isDirectory), + if DependencyFactory.fileManager().fileExists(atPath: projectRoot, isDirectory: &isDirectory), isDirectory.boolValue == true { self.projectRoot = URL(fileURLWithPath: projectRoot) } else { @@ -387,7 +387,7 @@ private extension ActionTestSummaryGroup { "^(?:public )?(?:final )?(?:public )?(?:(class|\\@implementation) )[a-zA-Z0-9_]+", grepPathArgument ] - guard let filelistData = try? DependencyFactory.shell.execute(program: program, with: arguments, at: projectRootUrl) else { + guard let filelistData = try? DependencyFactory.shell().execute(program: program, with: arguments, at: projectRootUrl) else { return } let trimCharacterSet = CharacterSet.whitespacesAndNewlines.union(CharacterSet(charactersIn: ":")) diff --git a/Sources/xcresultparser/Services/DependencyFactory.swift b/Sources/xcresultparser/Services/DependencyFactory.swift index 31527f1..5f1c0b1 100644 --- a/Sources/xcresultparser/Services/DependencyFactory.swift +++ b/Sources/xcresultparser/Services/DependencyFactory.swift @@ -8,6 +8,10 @@ import Foundation class DependencyFactory { - static var shell: Commandline = Shell() - static var fileManager: FileManaging = FileManager.default + static var shell: () -> Commandline = { + Shell() + } + static var fileManager: () -> FileManaging = { + FileManager.default + } } diff --git a/Tests/XcresultparserTests/XcresultparserTests.swift b/Tests/XcresultparserTests/XcresultparserTests.swift index b57aeaf..9fc79ad 100644 --- a/Tests/XcresultparserTests/XcresultparserTests.swift +++ b/Tests/XcresultparserTests/XcresultparserTests.swift @@ -343,12 +343,16 @@ final class XcresultparserTests: XCTestCase { let cliResult = """ ./Tests/XcresultparserTests.swift:class XcresultparserTests """ - let oldFilemanger = DependencyFactory.fileManager - let oldShell = DependencyFactory.shell + let savedFilemangerFactory = DependencyFactory.fileManager + let savedShellFactory = DependencyFactory.shell - DependencyFactory.fileManager = MockedFileManager(fileExists: true , isPathDirectory: true) + DependencyFactory.fileManager = { + MockedFileManager(fileExists: true , isPathDirectory: true) + } let mockedShell = MockedShell(response: Data(cliResult.utf8), error: nil) - DependencyFactory.shell = mockedShell + DependencyFactory.shell = { + mockedShell + } mockedShell.argumentValidation = { arguments in return arguments.last == "." } @@ -366,8 +370,8 @@ final class XcresultparserTests: XCTestCase { } try assertXmlTestReportsAreEqual(expectedFileName: "sonarTestExecutionWithProjectRootRelative", actual: junitXML) - DependencyFactory.fileManager = oldFilemanger - DependencyFactory.shell = oldShell + DependencyFactory.fileManager = savedFilemangerFactory + DependencyFactory.shell = savedShellFactory } func testJunitXMLSonarAbsolutePaths() throws { @@ -376,12 +380,16 @@ final class XcresultparserTests: XCTestCase { /Users/actual/project/Tests/XcresultparserTests.swift:class XcresultparserTests """ - let oldFilemanger = DependencyFactory.fileManager - let oldShell = DependencyFactory.shell + let savedFilemangerFactory = DependencyFactory.fileManager + let savedShellFactory = DependencyFactory.shell - DependencyFactory.fileManager = MockedFileManager(fileExists: true , isPathDirectory: true) + DependencyFactory.fileManager = { + MockedFileManager(fileExists: true , isPathDirectory: true) + } let mockedShell = MockedShell(response: Data(cliResult.utf8), error: nil) - DependencyFactory.shell = mockedShell + DependencyFactory.shell = { + mockedShell + } mockedShell.argumentValidation = { arguments in return arguments.last != "." } @@ -399,8 +407,8 @@ final class XcresultparserTests: XCTestCase { } try assertXmlTestReportsAreEqual(expectedFileName: "sonarTestExecutionWithProjectRootAbsolute", actual: junitXML) - DependencyFactory.fileManager = oldFilemanger - DependencyFactory.shell = oldShell + DependencyFactory.fileManager = savedFilemangerFactory + DependencyFactory.shell = savedShellFactory } func testJunitXMLJunit() throws { From 2ff7e5f4c4a2614d5c2765387286c6d864a6d7e7 Mon Sep 17 00:00:00 2001 From: Alex da Franca Date: Mon, 2 Dec 2024 10:48:39 +0100 Subject: [PATCH 5/9] Remove unused code --- Sources/xcresultparser/Services/Shell.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Sources/xcresultparser/Services/Shell.swift b/Sources/xcresultparser/Services/Shell.swift index 8e82bf3..d39798e 100644 --- a/Sources/xcresultparser/Services/Shell.swift +++ b/Sources/xcresultparser/Services/Shell.swift @@ -18,9 +18,6 @@ extension Commandline { } struct Shell: Commandline { - init() { - // nothing to do here, but required by public struct - } func execute( program: String, with arguments: [String], From 6bba6a0b7fe8a720aceb648cbd5b38e58a934c8a Mon Sep 17 00:00:00 2001 From: Alex da Franca Date: Mon, 2 Dec 2024 10:53:02 +0100 Subject: [PATCH 6/9] Better naming --- .../xcresultparser/CoverageConverter.swift | 2 +- Sources/xcresultparser/JunitXML.swift | 4 ++-- .../Services/DependencyFactory.swift | 4 ++-- .../XcresultparserTests.swift | 24 +++++++++---------- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Sources/xcresultparser/CoverageConverter.swift b/Sources/xcresultparser/CoverageConverter.swift index 238bd20..2d21f79 100644 --- a/Sources/xcresultparser/CoverageConverter.swift +++ b/Sources/xcresultparser/CoverageConverter.swift @@ -33,7 +33,7 @@ public class CoverageConverter { // MARK: - Dependencies - let shell = DependencyFactory.shell() + let shell = DependencyFactory.createShell() public init?( with url: URL, diff --git a/Sources/xcresultparser/JunitXML.swift b/Sources/xcresultparser/JunitXML.swift index d2f31a0..cb5fcad 100644 --- a/Sources/xcresultparser/JunitXML.swift +++ b/Sources/xcresultparser/JunitXML.swift @@ -76,7 +76,7 @@ public struct JunitXML: XmlSerializable { } var isDirectory: ObjCBool = false - if DependencyFactory.fileManager().fileExists(atPath: projectRoot, isDirectory: &isDirectory), + if DependencyFactory.createFileManager().fileExists(atPath: projectRoot, isDirectory: &isDirectory), isDirectory.boolValue == true { self.projectRoot = URL(fileURLWithPath: projectRoot) } else { @@ -387,7 +387,7 @@ private extension ActionTestSummaryGroup { "^(?:public )?(?:final )?(?:public )?(?:(class|\\@implementation) )[a-zA-Z0-9_]+", grepPathArgument ] - guard let filelistData = try? DependencyFactory.shell().execute(program: program, with: arguments, at: projectRootUrl) else { + guard let filelistData = try? DependencyFactory.createShell().execute(program: program, with: arguments, at: projectRootUrl) else { return } let trimCharacterSet = CharacterSet.whitespacesAndNewlines.union(CharacterSet(charactersIn: ":")) diff --git a/Sources/xcresultparser/Services/DependencyFactory.swift b/Sources/xcresultparser/Services/DependencyFactory.swift index 5f1c0b1..b20cd63 100644 --- a/Sources/xcresultparser/Services/DependencyFactory.swift +++ b/Sources/xcresultparser/Services/DependencyFactory.swift @@ -8,10 +8,10 @@ import Foundation class DependencyFactory { - static var shell: () -> Commandline = { + static var createShell: () -> Commandline = { Shell() } - static var fileManager: () -> FileManaging = { + static var createFileManager: () -> FileManaging = { FileManager.default } } diff --git a/Tests/XcresultparserTests/XcresultparserTests.swift b/Tests/XcresultparserTests/XcresultparserTests.swift index 9fc79ad..17032c9 100644 --- a/Tests/XcresultparserTests/XcresultparserTests.swift +++ b/Tests/XcresultparserTests/XcresultparserTests.swift @@ -343,14 +343,14 @@ final class XcresultparserTests: XCTestCase { let cliResult = """ ./Tests/XcresultparserTests.swift:class XcresultparserTests """ - let savedFilemangerFactory = DependencyFactory.fileManager - let savedShellFactory = DependencyFactory.shell + let savedFilemangerFactory = DependencyFactory.createFileManager + let savedShellFactory = DependencyFactory.createShell - DependencyFactory.fileManager = { + DependencyFactory.createFileManager = { MockedFileManager(fileExists: true , isPathDirectory: true) } let mockedShell = MockedShell(response: Data(cliResult.utf8), error: nil) - DependencyFactory.shell = { + DependencyFactory.createShell = { mockedShell } mockedShell.argumentValidation = { arguments in @@ -370,8 +370,8 @@ final class XcresultparserTests: XCTestCase { } try assertXmlTestReportsAreEqual(expectedFileName: "sonarTestExecutionWithProjectRootRelative", actual: junitXML) - DependencyFactory.fileManager = savedFilemangerFactory - DependencyFactory.shell = savedShellFactory + DependencyFactory.createFileManager = savedFilemangerFactory + DependencyFactory.createShell = savedShellFactory } func testJunitXMLSonarAbsolutePaths() throws { @@ -380,14 +380,14 @@ final class XcresultparserTests: XCTestCase { /Users/actual/project/Tests/XcresultparserTests.swift:class XcresultparserTests """ - let savedFilemangerFactory = DependencyFactory.fileManager - let savedShellFactory = DependencyFactory.shell + let savedFilemangerFactory = DependencyFactory.createFileManager + let savedShellFactory = DependencyFactory.createShell - DependencyFactory.fileManager = { + DependencyFactory.createFileManager = { MockedFileManager(fileExists: true , isPathDirectory: true) } let mockedShell = MockedShell(response: Data(cliResult.utf8), error: nil) - DependencyFactory.shell = { + DependencyFactory.createShell = { mockedShell } mockedShell.argumentValidation = { arguments in @@ -407,8 +407,8 @@ final class XcresultparserTests: XCTestCase { } try assertXmlTestReportsAreEqual(expectedFileName: "sonarTestExecutionWithProjectRootAbsolute", actual: junitXML) - DependencyFactory.fileManager = savedFilemangerFactory - DependencyFactory.shell = savedShellFactory + DependencyFactory.createFileManager = savedFilemangerFactory + DependencyFactory.createShell = savedShellFactory } func testJunitXMLJunit() throws { From e15e94091e8f2b2f3e12c3c4fa3ef7b260b6b9ae Mon Sep 17 00:00:00 2001 From: Alex da Franca Date: Mon, 2 Dec 2024 13:32:04 +0100 Subject: [PATCH 7/9] More refactoring for more meaningful naming --- Sources/xcresultparser/JunitXML.swift | 2 +- .../DependencyFactory.swift | 5 ----- .../Services/FileManaging.swift | 0 .../{ => SharedTypes}/Services/Shell.swift | 0 .../SharedTypes/SharedInstances.swift | 12 ++++++++++++ .../XcresultparserTests.swift | 18 ++++++++---------- 6 files changed, 21 insertions(+), 16 deletions(-) rename Sources/xcresultparser/{Services => SharedTypes}/DependencyFactory.swift (64%) rename Sources/xcresultparser/{ => SharedTypes}/Services/FileManaging.swift (100%) rename Sources/xcresultparser/{ => SharedTypes}/Services/Shell.swift (100%) create mode 100644 Sources/xcresultparser/SharedTypes/SharedInstances.swift diff --git a/Sources/xcresultparser/JunitXML.swift b/Sources/xcresultparser/JunitXML.swift index cb5fcad..bafe792 100644 --- a/Sources/xcresultparser/JunitXML.swift +++ b/Sources/xcresultparser/JunitXML.swift @@ -76,7 +76,7 @@ public struct JunitXML: XmlSerializable { } var isDirectory: ObjCBool = false - if DependencyFactory.createFileManager().fileExists(atPath: projectRoot, isDirectory: &isDirectory), + if SharedInstances.fileManager.fileExists(atPath: projectRoot, isDirectory: &isDirectory), isDirectory.boolValue == true { self.projectRoot = URL(fileURLWithPath: projectRoot) } else { diff --git a/Sources/xcresultparser/Services/DependencyFactory.swift b/Sources/xcresultparser/SharedTypes/DependencyFactory.swift similarity index 64% rename from Sources/xcresultparser/Services/DependencyFactory.swift rename to Sources/xcresultparser/SharedTypes/DependencyFactory.swift index b20cd63..963fb84 100644 --- a/Sources/xcresultparser/Services/DependencyFactory.swift +++ b/Sources/xcresultparser/SharedTypes/DependencyFactory.swift @@ -5,13 +5,8 @@ // Created by Alex da Franca on 01.12.24. // -import Foundation - class DependencyFactory { static var createShell: () -> Commandline = { Shell() } - static var createFileManager: () -> FileManaging = { - FileManager.default - } } diff --git a/Sources/xcresultparser/Services/FileManaging.swift b/Sources/xcresultparser/SharedTypes/Services/FileManaging.swift similarity index 100% rename from Sources/xcresultparser/Services/FileManaging.swift rename to Sources/xcresultparser/SharedTypes/Services/FileManaging.swift diff --git a/Sources/xcresultparser/Services/Shell.swift b/Sources/xcresultparser/SharedTypes/Services/Shell.swift similarity index 100% rename from Sources/xcresultparser/Services/Shell.swift rename to Sources/xcresultparser/SharedTypes/Services/Shell.swift diff --git a/Sources/xcresultparser/SharedTypes/SharedInstances.swift b/Sources/xcresultparser/SharedTypes/SharedInstances.swift new file mode 100644 index 0000000..d89a39c --- /dev/null +++ b/Sources/xcresultparser/SharedTypes/SharedInstances.swift @@ -0,0 +1,12 @@ +// +// SharedInstances.swift +// Xcresultparser +// +// Created by Alex da Franca on 02.12.24. +// + +import Foundation + +class SharedInstances { + static var fileManager: FileManaging = FileManager.default +} diff --git a/Tests/XcresultparserTests/XcresultparserTests.swift b/Tests/XcresultparserTests/XcresultparserTests.swift index 17032c9..7effa07 100644 --- a/Tests/XcresultparserTests/XcresultparserTests.swift +++ b/Tests/XcresultparserTests/XcresultparserTests.swift @@ -343,12 +343,11 @@ final class XcresultparserTests: XCTestCase { let cliResult = """ ./Tests/XcresultparserTests.swift:class XcresultparserTests """ - let savedFilemangerFactory = DependencyFactory.createFileManager + let savedFilemanger = SharedInstances.fileManager let savedShellFactory = DependencyFactory.createShell - DependencyFactory.createFileManager = { - MockedFileManager(fileExists: true , isPathDirectory: true) - } + SharedInstances.fileManager = MockedFileManager(fileExists: true , isPathDirectory: true) + let mockedShell = MockedShell(response: Data(cliResult.utf8), error: nil) DependencyFactory.createShell = { mockedShell @@ -370,7 +369,7 @@ final class XcresultparserTests: XCTestCase { } try assertXmlTestReportsAreEqual(expectedFileName: "sonarTestExecutionWithProjectRootRelative", actual: junitXML) - DependencyFactory.createFileManager = savedFilemangerFactory + SharedInstances.fileManager = savedFilemanger DependencyFactory.createShell = savedShellFactory } @@ -380,12 +379,11 @@ final class XcresultparserTests: XCTestCase { /Users/actual/project/Tests/XcresultparserTests.swift:class XcresultparserTests """ - let savedFilemangerFactory = DependencyFactory.createFileManager + let savedFilemanger = SharedInstances.fileManager let savedShellFactory = DependencyFactory.createShell - DependencyFactory.createFileManager = { - MockedFileManager(fileExists: true , isPathDirectory: true) - } + SharedInstances.fileManager = MockedFileManager(fileExists: true , isPathDirectory: true) + let mockedShell = MockedShell(response: Data(cliResult.utf8), error: nil) DependencyFactory.createShell = { mockedShell @@ -407,7 +405,7 @@ final class XcresultparserTests: XCTestCase { } try assertXmlTestReportsAreEqual(expectedFileName: "sonarTestExecutionWithProjectRootAbsolute", actual: junitXML) - DependencyFactory.createFileManager = savedFilemangerFactory + SharedInstances.fileManager = savedFilemanger DependencyFactory.createShell = savedShellFactory } From 830838c27be119cbdf4ca70bf4bde9feb792e338 Mon Sep 17 00:00:00 2001 From: Alex da Franca Date: Fri, 6 Dec 2024 20:10:35 +0100 Subject: [PATCH 8/9] Raise version number --- CommandlineTool/main.swift | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CommandlineTool/main.swift b/CommandlineTool/main.swift index 52f4eec..080a861 100644 --- a/CommandlineTool/main.swift +++ b/CommandlineTool/main.swift @@ -9,7 +9,7 @@ import ArgumentParser import Foundation import XcresultparserLib -private let marketingVersion = "1.8.1" +private let marketingVersion = "1.8.2" struct xcresultparser: ParsableCommand { static let configuration = CommandConfiguration( diff --git a/README.md b/README.md index d2185c0..290fbd9 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ You should see the tool respond like this: ``` Error: Missing expected argument '' -OVERVIEW: xcresultparser 1.8.1 +OVERVIEW: xcresultparser 1.8.2 Interpret binary .xcresult files and print summary in different formats: txt, xml, html or colored cli output. From 2fcb440c108f527f741e7f5b577caec8eb7338d3 Mon Sep 17 00:00:00 2001 From: Alex da Franca Date: Fri, 6 Dec 2024 20:16:04 +0100 Subject: [PATCH 9/9] swiftformat --- .swiftformat | 2 + .../CoberturaCoverageConverter.swift | 128 +++++++++--------- Sources/xcresultparser/JunitXML.swift | 25 ++-- .../Formatters/HTML/HTMLResultFormatter.swift | 4 +- .../xcresultparser/XCResultFormatter.swift | 82 +++++------ .../XcresultparserTests.swift | 30 ++-- 6 files changed, 136 insertions(+), 135 deletions(-) diff --git a/.swiftformat b/.swiftformat index 7974dbf..53a1ccf 100644 --- a/.swiftformat +++ b/.swiftformat @@ -1,3 +1,5 @@ +--swiftversion 5.9 + --allman false --assetliterals visual-width --beforemarks diff --git a/Sources/xcresultparser/CoberturaCoverageConverter.swift b/Sources/xcresultparser/CoberturaCoverageConverter.swift index 760d0d8..630ac39 100644 --- a/Sources/xcresultparser/CoberturaCoverageConverter.swift +++ b/Sources/xcresultparser/CoberturaCoverageConverter.swift @@ -48,7 +48,7 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { dtd.name = "coverage" // dtd.systemID = "http://cobertura.sourceforge.net/xml/coverage-04.dtd" dtd.systemID = - "https://github.com/cobertura/cobertura/blob/master/cobertura/src/site/htdocs/xml/coverage-04.dtd" + "https://github.com/cobertura/cobertura/blob/master/cobertura/src/site/htdocs/xml/coverage-04.dtd" let rootElement = makeRootElement() @@ -178,7 +178,7 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { return rootElement } - + // this ised to be fetched online from http://cobertura.sourceforge.net/xml/coverage-04.dtd // that broke, when the URL changed to: // https://github.com/cobertura/cobertura/blob/master/cobertura/src/site/htdocs/xml/coverage-04.dtd @@ -187,68 +187,68 @@ public class CoberturaCoverageConverter: CoverageConverter, XmlSerializable { // IMO all that was overengineered for the followong 60 lines string... // ...which will probably never ever change! private var dtd04 = """ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """ } private struct LineInfo { diff --git a/Sources/xcresultparser/JunitXML.swift b/Sources/xcresultparser/JunitXML.swift index bafe792..5287d49 100644 --- a/Sources/xcresultparser/JunitXML.swift +++ b/Sources/xcresultparser/JunitXML.swift @@ -17,14 +17,14 @@ struct NodeNames { let testcaseName: String let testcaseDurationName: String let testcaseClassNameName: String - + static let defaultNodeNames = NodeNames( testsuitesName: "testsuites", testcaseName: "testcase", testcaseDurationName: "time", testcaseClassNameName: "classname" ) - + static let sonarNodeNames = NodeNames( testsuitesName: "testExecutions", testcaseName: "testCase", @@ -77,7 +77,7 @@ public struct JunitXML: XmlSerializable { var isDirectory: ObjCBool = false if SharedInstances.fileManager.fileExists(atPath: projectRoot, isDirectory: &isDirectory), - isDirectory.boolValue == true { + isDirectory.boolValue == true { self.projectRoot = URL(fileURLWithPath: projectRoot) } else { self.projectRoot = nil @@ -306,11 +306,10 @@ private extension ActionTestMetadata { testcase.addAttribute(name: "name", stringValue: name ?? "No-name") if let time = duration, !nodeNames.testcaseDurationName.isEmpty { - let correctedTime: String - if format == .sonar { - correctedTime = String(max(1, Int(time * 1000))) + let correctedTime: String = if format == .sonar { + String(max(1, Int(time * 1000))) } else { - correctedTime = numFormatter.unwrappedString(for: time) + numFormatter.unwrappedString(for: time) } testcase.addAttribute( name: nodeNames.testcaseDurationName, @@ -322,7 +321,7 @@ private extension ActionTestMetadata { } return testcase } - + func failureSummary(in summaries: [TestFailureIssueSummary]) -> TestFailureIssueSummary? { return summaries.first { summary in return summary.testCaseName == identifier?.replacingOccurrences(of: "/", with: ".") || @@ -398,11 +397,11 @@ private extension ActionTestSummaryGroup { let path = items.first, !path.isEmpty, let className = items - .dropFirst() - .joined(separator: ":") - .trimmingCharacters(in: trimCharacterSet) - .components(separatedBy: .whitespaces) - .last, + .dropFirst() + .joined(separator: ":") + .trimmingCharacters(in: trimCharacterSet) + .components(separatedBy: .whitespaces) + .last, !className.isEmpty { Self.cachedPathnames[className] = path.withoutLocalPrefix } diff --git a/Sources/xcresultparser/OutputFormatting/Formatters/HTML/HTMLResultFormatter.swift b/Sources/xcresultparser/OutputFormatting/Formatters/HTML/HTMLResultFormatter.swift index d8722f4..12aea39 100644 --- a/Sources/xcresultparser/OutputFormatting/Formatters/HTML/HTMLResultFormatter.swift +++ b/Sources/xcresultparser/OutputFormatting/Formatters/HTML/HTMLResultFormatter.swift @@ -129,7 +129,7 @@ public struct HTMLResultFormatter: XCResultFormatting { private func htmlElement(_ nodeName: String, content: String, cssClass: String? = nil) -> XMLElement { let node = XMLElement(name: nodeName, stringValue: content) - if let cssClass = cssClass { + if let cssClass { node.addAttribute(name: "class", stringValue: cssClass) } return node @@ -137,7 +137,7 @@ public struct HTMLResultFormatter: XCResultFormatting { private func htmlElement(_ nodeName: String, content: XMLElement, cssClass: String? = nil) -> XMLElement { let node = XMLElement(name: nodeName) - if let cssClass = cssClass { + if let cssClass { node.addAttribute(name: "class", stringValue: cssClass) } node.addChild(content) diff --git a/Sources/xcresultparser/XCResultFormatter.swift b/Sources/xcresultparser/XCResultFormatter.swift index fdd9f32..a433fcf 100644 --- a/Sources/xcresultparser/XCResultFormatter.swift +++ b/Sources/xcresultparser/XCResultFormatter.swift @@ -12,7 +12,7 @@ public struct XCResultFormatter { private enum SummaryField: String { case errors, warnings, analyzerWarnings, tests, failed, skipped } - + private struct SummaryFields { let enabledFields: Set init(specifiers: String) { @@ -23,9 +23,9 @@ public struct XCResultFormatter { ) } } - + // MARK: - Properties - + private let resultFile: XCResultFile private let invocationRecord: ActionsInvocationRecord private let codeCoverage: CodeCoverage? @@ -34,21 +34,21 @@ public struct XCResultFormatter { private let failedTestsOnly: Bool private let summaryFields: SummaryFields private let coverageReportFormat: CoverageReportFormat - + private var numFormatter: NumberFormatter = { let numFormatter = NumberFormatter() numFormatter.maximumFractionDigits = 4 return numFormatter }() - + private var percentFormatter: NumberFormatter = { let numFormatter = NumberFormatter() numFormatter.maximumFractionDigits = 1 return numFormatter }() - + // MARK: - Initializer - + public init?( with url: URL, formatter: XCResultFormatting, @@ -68,19 +68,19 @@ public struct XCResultFormatter { self.failedTestsOnly = failedTestsOnly self.summaryFields = SummaryFields(specifiers: summaryFields) self.coverageReportFormat = coverageReportFormat - + // if let logsId = invocationRecord?.actions.last?.actionResult.logRef?.id { // let testLogs = resultFile.getLogs(id: logsId) // } // // let testSummary = resultFile.getActionTestSummary(id: "xxx") - + // let payload = resultFile.getPayload(id: "123") // let exportedPath = resultFile.exportPayload(id: "123") } - + // MARK: - Public API - + public var summary: String { if outputFormatter is MDResultFormatter { return createSummaryInOneLine() @@ -88,41 +88,41 @@ public struct XCResultFormatter { return createSummary().joined(separator: "\n") } } - + public var testDetails: String { return createTestDetailsString().joined(separator: "\n") } - + public var divider: String { return outputFormatter.divider } - + public func documentPrefix(title: String) -> String { return outputFormatter.documentPrefix(title: title) } - + public var documentSuffix: String { return outputFormatter.documentSuffix } - + public var coverageDetails: String { return createCoverageReport().joined(separator: "\n") } - + // MARK: - Private API - + private func createSummary() -> [String] { let metrics = invocationRecord.metrics - + let analyzerWarningCount = metrics.analyzerWarningCount ?? 0 let errorCount = metrics.errorCount ?? 0 let testsCount = metrics.testsCount ?? 0 let testsFailedCount = metrics.testsFailedCount ?? 0 let warningCount = metrics.warningCount ?? 0 let testsSkippedCount = metrics.testsSkippedCount ?? 0 - + var lines = [String]() - + lines.append( outputFormatter.testConfiguration("Summary") ) @@ -170,17 +170,17 @@ public struct XCResultFormatter { } return lines } - + private func createSummaryInOneLine() -> String { let metrics = invocationRecord.metrics - + let analyzerWarningCount = metrics.analyzerWarningCount ?? 0 let errorCount = metrics.errorCount ?? 0 let testsCount = metrics.testsCount ?? 0 let testsFailedCount = metrics.testsFailedCount ?? 0 let warningCount = metrics.warningCount ?? 0 let testsSkippedCount = metrics.testsSkippedCount ?? 0 - + var summary = "" if summaryFields.enabledFields.contains(.errors) { summary += "Errors: \(errorCount)" @@ -202,7 +202,7 @@ public struct XCResultFormatter { } return summary } - + private func createTestDetailsString() -> [String] { var lines = [String]() for testAction in invocationRecord.actions where testAction.schemeCommandName == "Test" { @@ -210,7 +210,7 @@ public struct XCResultFormatter { } return lines } - + private func createTestDetailsString(forAction testAction: ActionRecord) -> [String] { var lines = [String]() guard let testsId = testAction.actionResult.testsRef?.id, @@ -220,7 +220,7 @@ public struct XCResultFormatter { let testPlanRunSummaries = testPlanRun.summaries let failureSummaries = invocationRecord.issues.testFailureSummaries let runDestination = testAction.runDestination.displayName - + for thisSummary in testPlanRunSummaries { lines.append( outputFormatter.testConfiguration(thisSummary.name ?? "No-name") @@ -232,7 +232,7 @@ public struct XCResultFormatter { outputFormatter.testConfiguration(targetConfig) ) } - + if failedTestsOnly, outputFormatter is CLIResultFormatter, thisTestableSummary.tests.allSatisfy({ $0.hasNoFailedTests }) { @@ -242,7 +242,7 @@ public struct XCResultFormatter { lines += createTestSummaryInfo(thisTest, level: 0, failureSummaries: failureSummaries) } } - + lines.append( outputFormatter.divider ) @@ -250,7 +250,7 @@ public struct XCResultFormatter { } return lines } - + private func createTestSummaryInfo( _ group: ActionTestSummaryGroup, level: Int, @@ -262,7 +262,7 @@ public struct XCResultFormatter { return lines } let header = "\(group.nameString) (\(numFormatter.unwrappedString(for: group.duration)))" - + switch level { case 0: break @@ -301,7 +301,7 @@ public struct XCResultFormatter { } return lines } - + private func actionTestFileStatusString( for testData: ActionTestMetadata, failureSummaries: [TestFailureIssueSummary] @@ -316,30 +316,30 @@ public struct XCResultFormatter { return outputFormatter.singleTestItem(testTitle, failed: testData.isFailed) } } - + private func actionTestFileStatusStringIcon(testData: ActionTestMetadata) -> String { if testData.isSuccessful { return outputFormatter.testPassIcon } - + if testData.isSkipped { return outputFormatter.testSkipIcon } - + return outputFormatter.testFailIcon } - + private func actionTestFailureStatusString( with header: String, and failure: TestFailureIssueSummary ) -> String { return outputFormatter.failedTestItem(header, message: failure.message) } - + private func createCoverageReport() -> [String] { var lines = [String]() lines.append(outputFormatter.testConfiguration("Coverage report")) - guard let codeCoverage = codeCoverage else { + guard let codeCoverage else { return lines } var executableLines = 0 @@ -359,7 +359,7 @@ public struct XCResultFormatter { lines.insert(line, at: 1) return lines } - + private func createCoverageReportFor(target: CodeCoverageTarget) -> CodeCoverageParseResult { var lines = [String]() var executableLines = 0 @@ -391,7 +391,7 @@ public struct XCResultFormatter { } return CodeCoverageParseResult(lines: lines, executableLines: executableLines, coveredLines: coveredLines) } - + private func createCoverageReportFor(file: CodeCoverageFile) -> [String] { var lines = [String]() let covPercent = percentFormatter.unwrappedString(for: file.lineCoverage * 100) @@ -429,7 +429,7 @@ public struct XCResultFormatter { } return lines } - + struct CodeCoverageParseResult { let lines: [String] let executableLines: Int diff --git a/Tests/XcresultparserTests/XcresultparserTests.swift b/Tests/XcresultparserTests/XcresultparserTests.swift index 7effa07..cdabc25 100644 --- a/Tests/XcresultparserTests/XcresultparserTests.swift +++ b/Tests/XcresultparserTests/XcresultparserTests.swift @@ -167,7 +167,7 @@ final class XcresultparserTests: XCTestCase { XCTAssertTrue(resultParser.coverageDetails.starts(with: "

Coverage report

")) XCTAssertTrue(resultParser.documentSuffix.hasSuffix("")) } - + func testMDResultFormatter() throws { let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! @@ -180,11 +180,11 @@ final class XcresultparserTests: XCTestCase { return } XCTAssertEqual("", resultParser.documentPrefix(title: "XCResults")) - + let expectedSummary = "Errors: 0; Warnings: 3; Analizer Warnings: 0; Tests: 7; Failed: 1; Skipped: 0" XCTAssertEqual(expectedSummary, resultParser.summary) XCTAssertEqual("\n---------------------\n", resultParser.divider) - + let lines = resultParser.testDetails.components(separatedBy: .newlines) XCTAssertTrue(lines[2].starts(with: "### XcresultparserTests.xctest")) XCTAssertTrue(lines[3].starts(with: "### XcresultparserTests")) @@ -197,7 +197,7 @@ final class XcresultparserTests: XCTestCase { XCTAssertEqual("", resultParser.documentSuffix) } - + func testCoverageConverter() throws { let xcresultFile = Bundle.module.url(forResource: "test", withExtension: "xcresult")! let projectRoot = "" @@ -211,7 +211,7 @@ final class XcresultparserTests: XCTestCase { } let info = converter.targetsInfo XCTAssertEqual("\nXcresultparserLib\nXcresultparserTests", info) - + let fileCoverage = try converter.getCoverageDataAsJSON() XCTAssertEqual(13, fileCoverage.files.count) let firstKey = try XCTUnwrap(fileCoverage.files.keys.sorted().first) @@ -226,15 +226,15 @@ final class XcresultparserTests: XCTestCase { XCTAssertEqual(1, firstLineDetail.line) XCTAssertNil(firstLineDetail.executionCount) XCTAssertNil(firstLineDetail.subranges) - + let otherLineDetail = firstItem[50] XCTAssertTrue(otherLineDetail.isExecutable) XCTAssertEqual(51, otherLineDetail.line) XCTAssertEqual(0, otherLineDetail.executionCount) XCTAssertNil(otherLineDetail.subranges) - + // Deprecated methods - + let fileList = try converter.coverageFileList() XCTAssertEqual(14, fileList.count) let firstFile = "/Users/fhaeser/code/xcresultparser/Sources/xcresultparser/CoberturaCoverageConverter.swift" @@ -341,12 +341,12 @@ final class XcresultparserTests: XCTestCase { func testJunitXMLSonarRelativePaths() throws { JunitXML.resetCachedPathnames() let cliResult = """ -./Tests/XcresultparserTests.swift:class XcresultparserTests -""" + ./Tests/XcresultparserTests.swift:class XcresultparserTests + """ let savedFilemanger = SharedInstances.fileManager let savedShellFactory = DependencyFactory.createShell - SharedInstances.fileManager = MockedFileManager(fileExists: true , isPathDirectory: true) + SharedInstances.fileManager = MockedFileManager(fileExists: true, isPathDirectory: true) let mockedShell = MockedShell(response: Data(cliResult.utf8), error: nil) DependencyFactory.createShell = { @@ -376,13 +376,13 @@ final class XcresultparserTests: XCTestCase { func testJunitXMLSonarAbsolutePaths() throws { JunitXML.resetCachedPathnames() let cliResult = """ -/Users/actual/project/Tests/XcresultparserTests.swift:class XcresultparserTests -""" + /Users/actual/project/Tests/XcresultparserTests.swift:class XcresultparserTests + """ let savedFilemanger = SharedInstances.fileManager let savedShellFactory = DependencyFactory.createShell - SharedInstances.fileManager = MockedFileManager(fileExists: true , isPathDirectory: true) + SharedInstances.fileManager = MockedFileManager(fileExists: true, isPathDirectory: true) let mockedShell = MockedShell(response: Data(cliResult.utf8), error: nil) DependencyFactory.createShell = { @@ -607,7 +607,7 @@ class MockedFileManager: FileManaging { } func fileExists(atPath path: String, isDirectory: UnsafeMutablePointer?) -> Bool { - if let isDirectory = isDirectory { + if let isDirectory { isDirectory.pointee = ObjCBool(isPathDirectory) } return fileExists