Skip to content

Commit

Permalink
Improved classes search speed (#8)
Browse files Browse the repository at this point in the history
* Improved classes search speed

* Removed comments
  • Loading branch information
AllDmeat authored May 20, 2024
1 parent 82bc4dd commit 9779137
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 35 deletions.
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

0 comments on commit 9779137

Please sign in to comment.