Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Improved classes search speed #8

Merged
merged 2 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions Sources/DBXCResultParser-Sonar/FSIndex.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import Foundation

struct FSIndex {
let classes: [String: String]

init(path: URL) throws {
self.classes = try Self.classes(in: path)
}
}

extension FSIndex {
private static func classes(in path: URL) throws -> [String: String] {
let fileManager = FileManager.default

var classDictionary: [String: String] = [:]

// Create a DirectoryEnumerator to recursively search for .swift files
let enumerator = fileManager.enumerator(
at: URL(fileURLWithPath: path.relativePath),
includingPropertiesForKeys: [.isRegularFileKey],
options: [.skipsHiddenFiles]
) { (url, error) -> Bool in
DBLogger.logWarning("Directory enumeration error at \(url)")
DBLogger.logWarning(error.localizedDescription)
return true
}

// Regular expression to find class names
let regex = try NSRegularExpression(pattern: "class\\s+([A-Za-z_][A-Za-z_0-9]*)", options: [])

// Iterate over each file found by the enumerator
while let element = enumerator?.nextObject() as? URL {
let isFile = try element.resourceValues(forKeys: [.isRegularFileKey]).isRegularFile ?? false
guard isFile,
element.pathExtension == "swift" else {
continue
}

let fileContent = try String(contentsOf: element, encoding: .utf8)

// Search for class definitions
let nsRange = NSRange(fileContent.startIndex..<fileContent.endIndex, in: fileContent)
let matches = regex.matches(in: fileContent, options: [], range: nsRange)

// Extract class names from the matches and store them in the dictionary
for match in matches {
if let range = Range(match.range(at: 1), in: fileContent) {
let className = String(fileContent[range])
let relativePath = try element.relativePath(from: path) ?! Error.cantGetRelativePath(filePath: element, basePath: path)
classDictionary[className] = relativePath
}
}
}

return classDictionary
}

enum Error: Swift.Error {
case cantGetRelativePath(filePath: URL, basePath: URL)
}
}
7 changes: 0 additions & 7 deletions Sources/DBXCResultParser-Sonar/Logger.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
//
// Logger.swift
//
//
// Created by Aleksey Berezka on 19.12.2023.
//

import Foundation

class DBLogger {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
//
// SonarGenericTestExecutionReportFormatter.swift
//
//
// Created by Aleksey Berezka on 15.12.2023.
//

import Foundation
import DBXCResultParser
import XMLCoder
Expand Down Expand Up @@ -45,12 +38,14 @@ public class SonarGenericTestExecutionReportFormatter: ParsableCommand {

public func sonarTestReport(from report: DBXCReportModel) throws -> String {
let testsPath = URL(fileURLWithPath: testsPath)
let fsIndex = try FSIndex(path: testsPath)
DBLogger.logDebug("Test classes: \(fsIndex.classes)")

let sonarFiles = try report
.modules
.flatMap { $0.files }
.sorted { $0.name < $1.name }
.concurrentMap { try testExecutions.file($0, testsPath: testsPath) }
.concurrentMap { try testExecutions.file($0, index: fsIndex) }

let dto = testExecutions(file: sonarFiles)

Expand Down Expand Up @@ -151,30 +146,23 @@ extension testExecutions.file.testCase {
}

extension testExecutions.file {
init(_ file: DBXCReportModel.Module.File, testsPath: URL) throws {
init(_ file: DBXCReportModel.Module.File, index: FSIndex) throws {
DBLogger.logDebug("Formatting \(file.name)")

let testCases = file.repeatableTests
.sorted { $0.name < $1.name }
.map { testExecutions.file.testCase.init($0) }

let path = try Self.path(toFileWithClass: file.name, in: testsPath)
let path = try index.classes[file.name] ?! Error.missingFile(file.name)

self.init(
path: path,
testCase: testCases
)
}

private static func path(toFileWithClass className: String, in path: URL) throws -> String {
let testsPath = path.relativePath
let command = "find \(testsPath) -name '*.swift' -exec grep -l 'class \(className)' {} + | head -n 1"
let absoluteFilePath = try DBShell.execute(command)
if absoluteFilePath.isEmpty {
DBLogger.logWarning("Can't find file for class \(className)")
}
let relativeFilePath = absoluteFilePath.replacingOccurrences(of: testsPath, with: ".")
return relativeFilePath
enum Error: Swift.Error {
case missingFile(String)
}
}

Expand Down
38 changes: 38 additions & 0 deletions Sources/DBXCResultParser-Sonar/URL+Helpers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Foundation

extension URL {
var isRegularFile: Bool {
get throws {
try resourceValues(forKeys: [.isRegularFileKey]).isRegularFile ?! Error.noResourceValues
}
}

enum Error: Swift.Error {
case noResourceValues
}
}

extension URL {
/// Returns a relative path from a base URL
/// - Parameter baseURL: The base URL to calculate the relative path from.
/// - Returns: A relative path if possible, otherwise nil.
func relativePath(from baseURL: URL) -> String? {
// Check if both URLs are file URLs and that the base URL is a directory
guard self.isFileURL, baseURL.isFileURL, baseURL.hasDirectoryPath else {
return nil
}

// Remove/replace "." and "..", make sure URLs are absolute:
let pathComponents = standardized.pathComponents
let basePathComponents = baseURL.standardized.pathComponents

// Find the number of common path components
let commonPart = zip(pathComponents, basePathComponents).prefix { $0 == $1 }.count

// Build the relative path
let relativeComponents = Array(repeating: "..", count: basePathComponents.count - commonPart) +
pathComponents.dropFirst(commonPart)

return relativeComponents.joined(separator: "/")
}
}
11 changes: 11 additions & 0 deletions Sources/DBXCResultParser-Sonar/UnwrapOrThrow.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Foundation

infix operator ?!: NilCoalescingPrecedence

/// Throws the right hand side error if the left hand side optional is `nil`.
func ?!<T>(value: T?, error: @autoclosure () -> Error) throws -> T {
guard let value = value else {
throw error()
}
return value
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class SonarGenericTestExecutionReportFormatterTests: XCTestCase {
let result = try formatter.sonarTestReport(from: report)
XCTAssertEqual(result, """
<testExecutions version="1">
<file path="./ClassName_a_a.swift">
<file path="ClassName_a_a.swift">
<testCase name="test_expecting_fail" duration="0" />
<testCase name="test_failure" duration="0">
<failure message="Failure message" />
Expand All @@ -42,14 +42,14 @@ class SonarGenericTestExecutionReportFormatterTests: XCTestCase {
</testCase>
<testCase name="test_success" duration="0" />
</file>
<file path="./ClassName_a_b.swift" />
<file path="./ClassName_a_c.swift" />
<file path="./ClassName_b_a.swift" />
<file path="./ClassName_b_b.swift" />
<file path="./ClassName_b_c.swift" />
<file path="./ClassName_c_a.swift" />
<file path="./ClassName_c_b.swift" />
<file path="./ClassName_c_c.swift" />
<file path="ClassName_a_b.swift" />
<file path="ClassName_a_c.swift" />
<file path="ClassName_b_a.swift" />
<file path="ClassName_b_b.swift" />
<file path="ClassName_b_c.swift" />
<file path="ClassName_c_a.swift" />
<file path="ClassName_c_b.swift" />
<file path="ClassName_c_c.swift" />
</testExecutions>
"""
)
Expand Down
Loading