@@ -696,7 +696,8 @@ class OutputParser {
696696 || line. contains ( " ✘ " ) || line. contains ( " ✓ " ) || line. contains ( " ❌ " ) || line. contains ( " Build succeeded " )
697697 || line. contains ( " Build failed " ) || line. contains ( " Executed " ) || line. contains ( " ] Testing " )
698698 || line. contains ( " BUILD SUCCEEDED " ) || line. contains ( " BUILD FAILED " ) || line. contains ( " TEST FAILED " )
699- || line. contains ( " Build complete! " ) || line. hasPrefix ( " RegisterWithLaunchServices " )
699+ || line. contains ( " Build complete! " ) || line. contains ( " recorded an issue " )
700+ || line. hasPrefix ( " RegisterWithLaunchServices " )
700701 || line. hasPrefix ( " Validate " ) || line. contains ( " Fatal error " )
701702 || ( line. hasPrefix ( " / " ) && line. contains ( " .swift: " ) ) // runtime warnings
702703
@@ -896,6 +897,43 @@ class OutputParser {
896897 return testName
897898 }
898899
900+ // MARK: - Swift Testing Name Extraction
901+
902+ /// Extracts test name from Swift Testing output line.
903+ /// Handles both formats:
904+ /// - With displayName: `Test "Human readable name" ...`
905+ /// - Without displayName: `Test functionName() ...`
906+ ///
907+ /// Returns: (testName, endIndex) or nil if not found
908+ private func extractSwiftTestingName(
909+ from line: String ,
910+ after startIndex: String . Index
911+ ) -> ( name: String , endIndex: String . Index ) ? {
912+ let afterTest = line [ startIndex... ]
913+
914+ // Format 1: With quotes (displayName)
915+ if afterTest. hasPrefix ( " \" " ) {
916+ let nameStart = line. index ( after: startIndex)
917+ if let quoteEnd = line [ nameStart... ] . firstIndex ( of: " \" " ) {
918+ let name = String ( line [ nameStart ..< quoteEnd] )
919+ return ( name, line. index ( after: quoteEnd) )
920+ }
921+ }
922+
923+ // Format 2: Without quotes (function name)
924+ // Find end markers: " recorded", " failed", " passed", " started"
925+ let endMarkers = [ " recorded " , " failed " , " passed " , " started " ]
926+ for marker in endMarkers {
927+ if let markerRange = afterTest. range ( of: marker) {
928+ let name = String ( line [ startIndex ..< markerRange. lowerBound] )
929+ . trimmingCharacters ( in: . whitespaces)
930+ return ( name, markerRange. lowerBound)
931+ }
932+ }
933+
934+ return nil
935+ }
936+
899937 private func hasSeenSimilarTest( _ normalizedTestName: String ) -> Bool {
900938 return seenTestNames. contains ( normalizedTestName)
901939 }
@@ -1310,41 +1348,60 @@ class OutputParser {
13101348 return FailedTest ( test: test, message: message, file: nil , line: nil , duration: duration)
13111349 }
13121350
1313- // Pattern: ✘ Test "name" recorded an issue at file:line:column: message
1314- if line. hasPrefix ( " ✘ Test \" " ) , let issueAt = line. range ( of: " \" recorded an issue at " ) {
1315- let startIndex = line. index ( line. startIndex, offsetBy: 8 )
1316- let test = String ( line [ startIndex ..< issueAt. lowerBound] )
1317- let afterIssue = String ( line [ issueAt. upperBound... ] )
1318-
1319- // Parse file:line:column: message
1320- let parts = afterIssue. split ( separator: " : " , maxSplits: 3 , omittingEmptySubsequences: false )
1321- if parts. count >= 4 , let lineNum = Int ( parts [ 1 ] ) {
1322- let file = String ( parts [ 0 ] )
1323- let message = String ( parts [ 3 ] ) . trimmingCharacters ( in: . whitespaces)
1324- return FailedTest ( test: test, message: message, file: file, line: lineNum)
1325- }
1326- }
1327-
1328- // Pattern: ✘ Test "name" failed after 0.123 seconds with N issues.
1329- if line. hasPrefix ( " ✘ Test \" " ) , let failedAfter = line. range ( of: " \" failed after " ) {
1330- let startIndex = line. index ( line. startIndex, offsetBy: 8 )
1331- let test = String ( line [ startIndex ..< failedAfter. lowerBound] )
1332-
1333- // Extract duration from "failed after X.XXX seconds"
1334- var duration : Double ? = nil
1335- let afterStr = line [ failedAfter. upperBound... ]
1336- if let secondsRange = afterStr. range ( of: " seconds " ) {
1337- let durationStr = String ( afterStr [ ..< secondsRange. lowerBound] )
1338- duration = Double ( durationStr)
1339- }
1351+ // Swift Testing: Test {name} recorded an issue / failed after
1352+ // Supports: any leading symbol (✘, , ◇, etc.), with or without quotes
1353+ // Formats:
1354+ // - Test "Human readable name" recorded an issue at file:line:col: message
1355+ // - Test functionName() recorded an issue at file:line:col: message
1356+ // - Test "name" failed after 0.123 seconds [with N issues]
1357+ // - Test name() failed after 0.123 seconds [with N issues]
1358+ if let testStart = line. range ( of: " Test " ) {
1359+ // Skip "Test run with" (summary line) and "Test Case" (XCTest format)
1360+ let beforeTest = line [ ..< testStart. lowerBound]
1361+ let afterTestStr = line [ testStart. upperBound... ]
1362+
1363+ if !afterTestStr. hasPrefix ( " run with " ) && !afterTestStr. hasPrefix ( " Case " ) {
1364+ if let ( testName, nameEnd) = extractSwiftTestingName ( from: line, after: testStart. upperBound) {
1365+ let afterName = line [ nameEnd... ]
1366+
1367+ // Pattern: recorded an issue at file:line:col: message
1368+ if let issueAt = afterName. range ( of: " recorded an issue at " ) {
1369+ let afterIssue = String ( line [ issueAt. upperBound... ] )
1370+
1371+ // Parse file:line:column: message
1372+ let parts = afterIssue. split ( separator: " : " , maxSplits: 3 , omittingEmptySubsequences: false )
1373+ if parts. count >= 4 , let lineNum = Int ( parts [ 1 ] ) {
1374+ let file = String ( parts [ 0 ] )
1375+ let message = String ( parts [ 3 ] ) . trimmingCharacters ( in: . whitespaces)
1376+ return FailedTest ( test: testName, message: message, file: file, line: lineNum)
1377+ }
1378+ }
13401379
1341- // Track duration for slow test detection
1342- let normalizedTest = normalizeTestName ( test)
1343- if let dur = duration {
1344- failedTestDurations [ normalizedTest] = dur
1380+ // Pattern: failed after X.XXX seconds [with N issue(s)]
1381+ if let failedAfter = afterName. range ( of: " failed after " ) {
1382+ var duration : Double ? = nil
1383+ let afterFailed = line [ failedAfter. upperBound... ]
1384+ if let secondsRange = afterFailed. range ( of: " seconds " ) {
1385+ let durationStr = String ( afterFailed [ ..< secondsRange. lowerBound] )
1386+ duration = Double ( durationStr)
1387+ }
1388+
1389+ // Track duration for slow test detection
1390+ let normalizedTest = normalizeTestName ( testName)
1391+ if let dur = duration {
1392+ failedTestDurations [ normalizedTest] = dur
1393+ }
1394+
1395+ return FailedTest (
1396+ test: testName,
1397+ message: " Test failed " ,
1398+ file: nil ,
1399+ line: nil ,
1400+ duration: duration
1401+ )
1402+ }
1403+ }
13451404 }
1346-
1347- return FailedTest ( test: test, message: " Test failed " , file: nil , line: nil , duration: duration)
13481405 }
13491406
13501407 // Pattern: ❌ testname (message)
@@ -1476,6 +1533,45 @@ class OutputParser {
14761533 return
14771534 }
14781535
1536+ // Pattern: Test run with N test(s) in M suite(s) failed after X seconds with Y issue(s).
1537+ // Swift Testing failure summary format - TEST time
1538+ if let testRunRange = line. range ( of: " Test run with " ) ,
1539+ let failedAfterRange = line. range ( of: " failed after " , range: testRunRange. upperBound ..< line. endIndex)
1540+ {
1541+ let beforeFailed = line [ testRunRange. upperBound ..< failedAfterRange. lowerBound]
1542+ let totalCountStr = beforeFailed. split ( separator: " " ) . first
1543+ if let totalCountStr = totalCountStr, let totalCount = Int ( totalCountStr) {
1544+ swiftTestingExecutedCount = totalCount
1545+ }
1546+
1547+ // Extract TEST time and accumulate
1548+ let afterFailed = line [ failedAfterRange. upperBound... ]
1549+ if let secondsRange = afterFailed. range ( of: " seconds " , options: . backwards) {
1550+ accumulateTestTime ( String ( afterFailed [ ..< secondsRange. lowerBound] ) )
1551+
1552+ // Extract issue count (if present) from the trailing "with Y issue(s)"
1553+ let afterSeconds = afterFailed [ secondsRange. upperBound... ]
1554+ if let withRange = afterSeconds. range ( of: " with " ) ,
1555+ let issueRange = afterSeconds. range (
1556+ of: " issue " ,
1557+ range: withRange. upperBound ..< afterSeconds. endIndex
1558+ )
1559+ {
1560+ let issueCountStr = afterSeconds [ withRange. upperBound ..< issueRange. lowerBound]
1561+ if let issueCount = Int ( issueCountStr. trimmingCharacters ( in: . whitespaces) ) {
1562+ if let totalCount = swiftTestingExecutedCount {
1563+ swiftTestingFailedCount = min ( issueCount, totalCount)
1564+ } else {
1565+ swiftTestingFailedCount = issueCount
1566+ }
1567+ }
1568+ }
1569+ } else {
1570+ accumulateTestTime ( String ( afterFailed) )
1571+ }
1572+ return
1573+ }
1574+
14791575 // Pattern: Test run with N tests in N suites passed after X seconds.
14801576 // Swift Testing output format (passed-only case) - TEST time
14811577 if let testRunRange = line. range ( of: " Test run with " ) ,
0 commit comments