diff --git a/Sources/OutputParser.swift b/Sources/OutputParser.swift index edaec18..568f136 100644 --- a/Sources/OutputParser.swift +++ b/Sources/OutputParser.swift @@ -696,7 +696,8 @@ class OutputParser { || line.contains("✘") || line.contains("✓") || line.contains("❌") || line.contains("Build succeeded") || line.contains("Build failed") || line.contains("Executed") || line.contains("] Testing ") || line.contains("BUILD SUCCEEDED") || line.contains("BUILD FAILED") || line.contains("TEST FAILED") - || line.contains("Build complete!") || line.hasPrefix("RegisterWithLaunchServices") + || line.contains("Build complete!") || line.contains("recorded an issue") + || line.hasPrefix("RegisterWithLaunchServices") || line.hasPrefix("Validate") || line.contains("Fatal error") || (line.hasPrefix("/") && line.contains(".swift:")) // runtime warnings @@ -896,6 +897,43 @@ class OutputParser { return testName } + // MARK: - Swift Testing Name Extraction + + /// Extracts test name from Swift Testing output line. + /// Handles both formats: + /// - With displayName: `Test "Human readable name" ...` + /// - Without displayName: `Test functionName() ...` + /// + /// Returns: (testName, endIndex) or nil if not found + private func extractSwiftTestingName( + from line: String, + after startIndex: String.Index + ) -> (name: String, endIndex: String.Index)? { + let afterTest = line[startIndex...] + + // Format 1: With quotes (displayName) + if afterTest.hasPrefix("\"") { + let nameStart = line.index(after: startIndex) + if let quoteEnd = line[nameStart...].firstIndex(of: "\"") { + let name = String(line[nameStart ..< quoteEnd]) + return (name, line.index(after: quoteEnd)) + } + } + + // Format 2: Without quotes (function name) + // Find end markers: " recorded", " failed", " passed", " started" + let endMarkers = [" recorded", " failed", " passed", " started"] + for marker in endMarkers { + if let markerRange = afterTest.range(of: marker) { + let name = String(line[startIndex ..< markerRange.lowerBound]) + .trimmingCharacters(in: .whitespaces) + return (name, markerRange.lowerBound) + } + } + + return nil + } + private func hasSeenSimilarTest(_ normalizedTestName: String) -> Bool { return seenTestNames.contains(normalizedTestName) } @@ -1310,41 +1348,60 @@ class OutputParser { return FailedTest(test: test, message: message, file: nil, line: nil, duration: duration) } - // Pattern: ✘ Test "name" recorded an issue at file:line:column: message - if line.hasPrefix("✘ Test \""), let issueAt = line.range(of: "\" recorded an issue at ") { - let startIndex = line.index(line.startIndex, offsetBy: 8) - let test = String(line[startIndex ..< issueAt.lowerBound]) - let afterIssue = String(line[issueAt.upperBound...]) - - // Parse file:line:column: message - let parts = afterIssue.split(separator: ":", maxSplits: 3, omittingEmptySubsequences: false) - if parts.count >= 4, let lineNum = Int(parts[1]) { - let file = String(parts[0]) - let message = String(parts[3]).trimmingCharacters(in: .whitespaces) - return FailedTest(test: test, message: message, file: file, line: lineNum) - } - } - - // Pattern: ✘ Test "name" failed after 0.123 seconds with N issues. - if line.hasPrefix("✘ Test \""), let failedAfter = line.range(of: "\" failed after ") { - let startIndex = line.index(line.startIndex, offsetBy: 8) - let test = String(line[startIndex ..< failedAfter.lowerBound]) - - // Extract duration from "failed after X.XXX seconds" - var duration: Double? = nil - let afterStr = line[failedAfter.upperBound...] - if let secondsRange = afterStr.range(of: " seconds") { - let durationStr = String(afterStr[..= 4, let lineNum = Int(parts[1]) { + let file = String(parts[0]) + let message = String(parts[3]).trimmingCharacters(in: .whitespaces) + return FailedTest(test: testName, message: message, file: file, line: lineNum) + } + } - // Track duration for slow test detection - let normalizedTest = normalizeTestName(test) - if let dur = duration { - failedTestDurations[normalizedTest] = dur + // Pattern: failed after X.XXX seconds [with N issue(s)] + if let failedAfter = afterName.range(of: " failed after ") { + var duration: Double? = nil + let afterFailed = line[failedAfter.upperBound...] + if let secondsRange = afterFailed.range(of: " seconds") { + let durationStr = String(afterFailed[..