diff --git a/Sources/XcbeautifyLib/JunitReporter.swift b/Sources/XcbeautifyLib/JunitReporter.swift index fe1facb0..314111ee 100644 --- a/Sources/XcbeautifyLib/JunitReporter.swift +++ b/Sources/XcbeautifyLib/JunitReporter.swift @@ -23,29 +23,29 @@ package final class JunitReporter { // Remove any preceding or excessive spaces let line = line.trimmingCharacters(in: .whitespacesAndNewlines) - if FailingTestCaptureGroup.regex.match(string: line) { - guard let testCase = generateFailingTest(line: line) else { return } + if let groups = FailingTestCaptureGroup.regex.captureGroups(for: line) { + guard let testCase = generateFailingTest(groups: groups) else { return } components.append(.failingTest(testCase)) - } else if RestartingTestCaptureGroup.regex.match(string: line) { - guard let testCase = generateRestartingTest(line: line) else { return } + } else if let groups = RestartingTestCaptureGroup.regex.captureGroups(for: line) { + guard let testCase = generateRestartingTest(groups: groups, line: line) else { return } components.append(.failingTest(testCase)) - } else if TestCasePassedCaptureGroup.regex.match(string: line) { - guard let testCase = generatePassingTest(line: line) else { return } + } else if let groups = TestCasePassedCaptureGroup.regex.captureGroups(for: line) { + guard let testCase = generatePassingTest(groups: groups) else { return } components.append(.testCasePassed(testCase)) - } else if TestCaseSkippedCaptureGroup.regex.match(string: line) { - guard let testCase = generateSkippedTest(line: line) else { return } + } else if let groups = TestCaseSkippedCaptureGroup.regex.captureGroups(for: line) { + guard let testCase = generateSkippedTest(groups: groups) else { return } components.append(.skippedTest(testCase)) - } else if TestSuiteStartCaptureGroup.regex.match(string: line) { - guard let testStart = generateSuiteStart(line: line) else { return } + } else if let groups = TestSuiteStartCaptureGroup.regex.captureGroups(for: line) { + guard let testStart = generateSuiteStart(groups: groups) else { return } components.append(.testSuiteStart(testStart)) - } else if ParallelTestCaseFailedCaptureGroup.regex.match(string: line) { - guard let testCase = generateParallelFailingTest(line: line) else { return } + } else if let groups = ParallelTestCaseFailedCaptureGroup.regex.captureGroups(for: line) { + guard let testCase = generateParallelFailingTest(groups: groups) else { return } parallelComponents.append(.failingTest(testCase)) - } else if ParallelTestCasePassedCaptureGroup.regex.match(string: line) { - guard let testCase = generatePassingParallelTest(line: line) else { return } + } else if let groups = ParallelTestCasePassedCaptureGroup.regex.captureGroups(for: line) { + guard let testCase = generatePassingParallelTest(groups: groups) else { return } parallelComponents.append(.testCasePassed(testCase)) - } else if ParallelTestCaseSkippedCaptureGroup.regex.match(string: line) { - guard let testCase = generateSkippedParallelTest(line: line) else { return } + } else if let groups = ParallelTestCaseSkippedCaptureGroup.regex.captureGroups(for: line) { + guard let testCase = generateSkippedParallelTest(groups: groups) else { return } parallelComponents.append(.testCasePassed(testCase)) } else { // Not needed for generating a junit report @@ -53,51 +53,44 @@ package final class JunitReporter { } } - private func generateFailingTest(line: String) -> TestCase? { - let groups = FailingTestCaptureGroup.regex.captureGroups(for: line) + private func generateFailingTest(groups: [String]) -> TestCase? { guard let group = FailingTestCaptureGroup(groups: groups) else { return nil } return TestCase(classname: group.testSuite, name: group.testCase, time: nil, failure: .init(message: "\(group.file) - \(group.reason)")) } - private func generateRestartingTest(line: String) -> TestCase? { - let groups = RestartingTestCaptureGroup.regex.captureGroups(for: line) + // TODO: Delete `line` parameter + private func generateRestartingTest(groups: [String], line: String) -> TestCase? { guard let group = RestartingTestCaptureGroup(groups: groups) else { return nil } return TestCase(classname: group.testSuite, name: group.testCase, time: nil, failure: .init(message: line)) } - private func generateParallelFailingTest(line: String) -> TestCase? { + private func generateParallelFailingTest(groups: [String]) -> TestCase? { // Parallel tests do not provide meaningful failure messages - let groups = ParallelTestCaseFailedCaptureGroup.regex.captureGroups(for: line) guard let group = ParallelTestCaseFailedCaptureGroup(groups: groups) else { return nil } return TestCase(classname: group.suite, name: group.testCase, time: nil, failure: .init(message: "Parallel test failed")) } - private func generatePassingTest(line: String) -> TestCase? { - let groups = TestCasePassedCaptureGroup.regex.captureGroups(for: line) + private func generatePassingTest(groups: [String]) -> TestCase? { guard let group = TestCasePassedCaptureGroup(groups: groups) else { return nil } return TestCase(classname: group.suite, name: group.testCase, time: group.time) } - private func generateSkippedTest(line: String) -> TestCase? { - let groups = TestCaseSkippedCaptureGroup.regex.captureGroups(for: line) + private func generateSkippedTest(groups: [String]) -> TestCase? { guard let group = TestCaseSkippedCaptureGroup(groups: groups) else { return nil } return TestCase(classname: group.suite, name: group.testCase, time: group.time, skipped: .init(message: nil)) } - private func generatePassingParallelTest(line: String) -> TestCase? { - let groups = ParallelTestCasePassedCaptureGroup.regex.captureGroups(for: line) + private func generatePassingParallelTest(groups: [String]) -> TestCase? { guard let group = ParallelTestCasePassedCaptureGroup(groups: groups) else { return nil } return TestCase(classname: group.suite, name: group.testCase, time: group.time) } - private func generateSkippedParallelTest(line: String) -> TestCase? { - let groups = ParallelTestCaseSkippedCaptureGroup.regex.captureGroups(for: line) + private func generateSkippedParallelTest(groups: [String]) -> TestCase? { guard let group = ParallelTestCaseSkippedCaptureGroup(groups: groups) else { return nil } return TestCase(classname: group.suite, name: group.testCase, time: group.time, skipped: .init(message: nil)) } - private func generateSuiteStart(line: String) -> String? { - let groups = TestSuiteStartCaptureGroup.regex.captureGroups(for: line) + private func generateSuiteStart(groups: [String]) -> String? { guard let group = TestSuiteStartCaptureGroup(groups: groups) else { return nil } return group.testSuiteName } diff --git a/Sources/XcbeautifyLib/Parser.swift b/Sources/XcbeautifyLib/Parser.swift index 09a501c4..2bf24b0f 100644 --- a/Sources/XcbeautifyLib/Parser.swift +++ b/Sources/XcbeautifyLib/Parser.swift @@ -134,25 +134,20 @@ package final class Parser { return nil } - // Find first parser that can parse specified string - guard let idx = captureGroupTypes.firstIndex(where: { $0.regex.match(string: line) }) else { - return nil - } + for (index, captureGroupType) in captureGroupTypes.enumerated() { + guard let groups = captureGroupType.regex.captureGroups(for: line) else { continue } - guard let captureGroupType = captureGroupTypes[safe: idx] else { - assertionFailure() - return nil - } + guard let captureGroup = captureGroupType.init(groups: groups) else { + assertionFailure() + return nil + } - let groups: [String] = captureGroupType.regex.captureGroups(for: line) - guard let captureGroup = captureGroupType.init(groups: groups) else { - assertionFailure() - return nil - } + // Move found parser to the top, so next time it will be checked first + captureGroupTypes.insert(captureGroupTypes.remove(at: index), at: 0) - // Move found parser to the top, so next time it will be checked first - captureGroupTypes.insert(captureGroupTypes.remove(at: idx), at: 0) + return captureGroup + } - return captureGroup + return nil } } diff --git a/Sources/XcbeautifyLib/XCRegex.swift b/Sources/XcbeautifyLib/XCRegex.swift index bcd57e2e..9aee14ab 100644 --- a/Sources/XcbeautifyLib/XCRegex.swift +++ b/Sources/XcbeautifyLib/XCRegex.swift @@ -15,14 +15,15 @@ package final class XCRegex: @unchecked Sendable { self.pattern = pattern } - func match(string: String) -> Bool { - let fullRange = NSRange(string.startIndex..., in: string) - return regex?.rangeOfFirstMatch(in: string, range: fullRange).location != NSNotFound - } + func captureGroups(for line: String) -> [String]? { + assert(regex != nil) - func captureGroups(for line: String) -> [String] { - let matches = regex?.matches(in: line, range: NSRange(location: 0, length: line.utf16.count)) - guard let match = matches?.first else { return [] } + guard + let matches = regex?.matches(in: line, range: NSRange(location: 0, length: line.utf16.count)), + let match = matches.first + else { + return nil + } let lastRangeIndex = match.numberOfRanges - 1 guard lastRangeIndex >= 1 else { return [] } diff --git a/Tests/XcbeautifyLibTests/CaptureGroupTests.swift b/Tests/XcbeautifyLibTests/CaptureGroupTests.swift index 6f833092..10c8d9da 100644 --- a/Tests/XcbeautifyLibTests/CaptureGroupTests.swift +++ b/Tests/XcbeautifyLibTests/CaptureGroupTests.swift @@ -18,22 +18,22 @@ final class CaptureGroupTests: XCTestCase { ] for input in inputs { - XCTAssertTrue(SwiftCompilingCaptureGroup.regex.match(string: input)) + XCTAssertNotNil(SwiftCompilingCaptureGroup.regex.captureGroups(for: input)) } } func testMatchCompilationResults() { let input = #"/* com.apple.actool.compilation-results */"# - XCTAssertTrue(CompilationResultCaptureGroup.regex.match(string: input)) + XCTAssertNotNil(CompilationResultCaptureGroup.regex.captureGroups(for: input)) } func testMatchSwiftDriverJobDiscoveryEmittingModule() { let input = #"SwiftDriverJobDiscovery normal arm64 Emitting module for Widgets (in target 'Widgets' from project 'Backyard Birds')"# - XCTAssertTrue(SwiftDriverJobDiscoveryEmittingModuleCaptureGroup.regex.match(string: input)) + XCTAssertNotNil(SwiftDriverJobDiscoveryEmittingModuleCaptureGroup.regex.captureGroups(for: input)) } func testMkDirCaptureGroup() throws { let input = "MkDir /Backyard-Birds/Build/Products/Debug/Widgets.appex/Contents (in target \'Widgets\' from project \'Backyard Birds\')" - XCTAssertTrue(MkDirCaptureGroup.regex.match(string: input)) + XCTAssertNotNil(MkDirCaptureGroup.regex.captureGroups(for: input)) } }