Skip to content

Commit 736845d

Browse files
authored
Merge pull request #30 from alexey1312/alexey1312/fix-parallel-tests-total-count
Fix parallel test count parsing for Swift Testing
2 parents 5b0b28c + 9c37104 commit 736845d

File tree

2 files changed

+187
-1
lines changed

2 files changed

+187
-1
lines changed

Sources/OutputParser.swift

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class OutputParser {
1212
private var summaryFailedTestsCount: Int?
1313
private var passedTestsCount: Int = 0
1414
private var seenPassedTestNames: Set<String> = []
15+
private var parallelTestsTotalCount: Int?
1516

1617
// Linker error parsing state
1718
private var currentLinkerArchitecture: String?
@@ -302,6 +303,17 @@ class OutputParser {
302303
".xctest'"
303304
}
304305

306+
// Parallel test scheduling pattern: [N/TOTAL] Testing Module.Class/method
307+
nonisolated(unsafe) private static let parallelTestSchedulingRegex = Regex {
308+
"["
309+
Capture(OneOrMore(.digit))
310+
"/"
311+
Capture(OneOrMore(.digit))
312+
"] Testing "
313+
Capture(OneOrMore(.any, .reluctant))
314+
Anchor.endOfSubject
315+
}
316+
305317
func parse(
306318
input: String,
307319
printWarnings: Bool = false,
@@ -339,9 +351,16 @@ class OutputParser {
339351

340352
let summaryFailedCount = summaryFailedTestsCount ?? failedTests.count
341353
let computedPassedTests: Int? = {
354+
// Priority 1: Use parallel test total count if available
355+
// This is the authoritative count from [N/TOTAL] Testing lines
356+
if let parallelTotal = parallelTestsTotalCount {
357+
return max(parallelTotal - summaryFailedCount, 0)
358+
}
359+
// Priority 2: Use executed tests count from summary line
342360
if let executed = executedTestsCount {
343361
return max(executed - summaryFailedCount, 0)
344362
}
363+
// Priority 3: Use counted passed tests
345364
if passedTestsCount > 0 {
346365
return passedTestsCount
347366
}
@@ -407,6 +426,7 @@ class OutputParser {
407426
pendingLinkerSymbol = nil
408427
pendingDuplicateSymbol = nil
409428
pendingConflictingFiles = []
429+
parallelTestsTotalCount = nil
410430
}
411431

412432
private func parseLine(_ line: String) {
@@ -424,12 +444,23 @@ class OutputParser {
424444
let containsRelevant =
425445
line.contains("error:") || line.contains("warning:") || line.contains("failed") || line.contains("passed")
426446
|| line.contains("") || line.contains("") || line.contains("") || line.contains("Build succeeded")
427-
|| line.contains("Build failed") || line.contains("Executed")
447+
|| line.contains("Build failed") || line.contains("Executed") || line.contains("] Testing ")
428448

429449
if !containsRelevant {
430450
return
431451
}
432452

453+
// Parse parallel test scheduling lines: [N/TOTAL] Testing Module.Class/method
454+
if line.contains("] Testing "), let match = line.firstMatch(of: Self.parallelTestSchedulingRegex) {
455+
if let _ = Int(match.1), let total = Int(match.2) {
456+
// Only set on first match (total should be consistent across all lines)
457+
if parallelTestsTotalCount == nil {
458+
parallelTestsTotalCount = total
459+
}
460+
}
461+
return
462+
}
463+
433464
if let failedTest = parseFailedTest(line) {
434465
let normalizedTestName = normalizeTestName(failedTest.test)
435466

@@ -914,6 +945,36 @@ class OutputParser {
914945
}
915946
}
916947

948+
// Pattern: ✘ Test run with N test(s) failed, N test(s) passed after X seconds.
949+
// Swift Testing failure summary format (check this BEFORE the passed-only pattern)
950+
if let testRunRange = line.range(of: "Test run with "),
951+
let failedRange = line.range(of: " failed, ", range: testRunRange.upperBound ..< line.endIndex),
952+
let passedRange = line.range(of: " passed after ", range: failedRange.upperBound ..< line.endIndex)
953+
{
954+
// Extract failed count
955+
let beforeFailed = line[testRunRange.upperBound ..< failedRange.lowerBound]
956+
let failedCountStr = beforeFailed.split(separator: " ").first
957+
if let failedCountStr = failedCountStr, let failedCount = Int(failedCountStr) {
958+
summaryFailedTestsCount = failedCount
959+
}
960+
961+
// Extract passed count (for executedTestsCount calculation)
962+
let beforePassed = line[failedRange.upperBound ..< passedRange.lowerBound]
963+
let passedCountStr = beforePassed.split(separator: " ").first
964+
if let passedCountStr = passedCountStr, let passedCount = Int(passedCountStr),
965+
let failedCount = summaryFailedTestsCount
966+
{
967+
executedTestsCount = passedCount + failedCount
968+
}
969+
970+
// Extract time
971+
let afterPassed = line[passedRange.upperBound...]
972+
if let secondsRange = afterPassed.range(of: " seconds", options: .backwards) {
973+
return String(afterPassed[..<secondsRange.lowerBound])
974+
}
975+
return String(afterPassed).trimmingCharacters(in: CharacterSet(charactersIn: "."))
976+
}
977+
917978
// Pattern: Test run with N tests in N suites passed after X seconds.
918979
// Note: Swift Testing output may have a Unicode checkmark prefix (e.g., "􁁛 Test run with...")
919980
if let testRunRange = line.range(of: "Test run with "),

Tests/ParsingTests.swift

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,4 +380,129 @@ final class ParsingTests: XCTestCase {
380380
XCTAssertEqual(result.errors[0].line, 15)
381381
XCTAssertEqual(result.errors[0].message, "use of undeclared identifier 'unknown'")
382382
}
383+
384+
// MARK: - Swift Test Parallel Tests
385+
386+
func testSwiftTestParallelAllPassed() {
387+
let parser = OutputParser()
388+
let input = """
389+
Building for debugging...
390+
Build complete! (5.00s)
391+
[1/20] Testing ModuleA.TestClassA/testMethod1
392+
[2/20] Testing ModuleA.TestClassA/testMethod2
393+
[3/20] Testing ModuleA.TestClassA/testMethod3
394+
[4/20] Testing ModuleA.TestClassB/testMethod1
395+
[5/20] Testing ModuleA.TestClassB/testMethod2
396+
[6/20] Testing ModuleB.TestClassC/testMethod1
397+
[7/20] Testing ModuleB.TestClassC/testMethod2
398+
[8/20] Testing ModuleB.TestClassC/testMethod3
399+
[9/20] Testing ModuleB.TestClassD/testMethod1
400+
[10/20] Testing ModuleB.TestClassD/testMethod2
401+
[11/20] Testing ModuleC.TestClassE/testMethod1
402+
[12/20] Testing ModuleC.TestClassE/testMethod2
403+
[13/20] Testing ModuleC.TestClassE/testMethod3
404+
[14/20] Testing ModuleC.TestClassE/testMethod4
405+
[15/20] Testing ModuleC.TestClassF/testMethod1
406+
[16/20] Testing ModuleC.TestClassF/testMethod2
407+
[17/20] Testing ModuleD.TestClassG/testMethod1
408+
[18/20] Testing ModuleD.TestClassG/testMethod2
409+
[19/20] Testing ModuleD.TestClassG/testMethod3
410+
[20/20] Testing ModuleD.TestClassH/testMethod1
411+
◇ Test run started.
412+
↳ Testing Library Version: 6.0.3
413+
◇ Suite "TestClassG" started.
414+
✔ Test "testMethod1" passed after 0.005 seconds.
415+
✔ Test "testMethod2" passed after 0.004 seconds.
416+
✔ Test "testMethod3" passed after 0.003 seconds.
417+
✔ Suite "TestClassG" passed after 0.010 seconds.
418+
✔ Test run with 4 tests passed after 0.015 seconds.
419+
"""
420+
421+
let result = parser.parse(input: input)
422+
423+
XCTAssertEqual(result.status, "success")
424+
XCTAssertEqual(result.summary.passedTests, 20)
425+
XCTAssertEqual(result.summary.failedTests, 0)
426+
}
427+
428+
func testSwiftTestParallelWithFailure() {
429+
let parser = OutputParser()
430+
let input = """
431+
Building for debugging...
432+
Build complete! (5.00s)
433+
[1/10] Testing ModuleA.TestClassA/testMethod1
434+
[2/10] Testing ModuleA.TestClassA/testMethod2
435+
[3/10] Testing ModuleA.TestClassA/testMethod3
436+
[4/10] Testing ModuleA.TestClassB/testMethod1
437+
[5/10] Testing ModuleA.TestClassB/testMethod2
438+
[6/10] Testing ModuleB.TestClassC/testMethod1
439+
[7/10] Testing ModuleB.TestClassC/testMethod2
440+
[8/10] Testing ModuleB.TestClassC/testMethod3
441+
[9/10] Testing ModuleB.TestClassD/testMethod1
442+
[10/10] Testing ModuleB.TestClassD/testMethod2
443+
◇ Test run started.
444+
↳ Testing Library Version: 6.0.3
445+
◇ Suite "TestClassD" started.
446+
✔ Test "testMethod1" passed after 0.005 seconds.
447+
✘ Test "testMethod2" failed after 0.010 seconds.
448+
✘ Test run with 1 test failed, 1 test passed after 0.020 seconds.
449+
"""
450+
451+
let result = parser.parse(input: input)
452+
453+
XCTAssertEqual(result.status, "failed")
454+
XCTAssertEqual(result.summary.passedTests, 9)
455+
XCTAssertEqual(result.summary.buildTime, "0.020")
456+
}
457+
458+
func testSwiftTestParallelLargeCount() {
459+
let parser = OutputParser()
460+
// Simulate a large test run with 1306 tests
461+
var input = "Building for debugging...\nBuild complete! (5.00s)\n"
462+
for i in 1 ... 1306 {
463+
input += "[\(i)/1306] Testing Module.TestClass/testMethod\(i)\n"
464+
}
465+
input += "◇ Test run started.\n"
466+
input += "✔ Test run with 82 tests passed after 0.170 seconds.\n"
467+
468+
let result = parser.parse(input: input)
469+
470+
XCTAssertEqual(result.status, "success")
471+
XCTAssertEqual(result.summary.passedTests, 1306)
472+
XCTAssertEqual(result.summary.failedTests, 0)
473+
}
474+
475+
func testSwiftTestParallelPrioritizesSchedulingCount() {
476+
let parser = OutputParser()
477+
// When both [N/TOTAL] and "Test run with X tests passed" are present,
478+
// the [N/TOTAL] count should take priority
479+
let input = """
480+
[1/100] Testing Module.TestClass/testMethod1
481+
[100/100] Testing Module.TestClass/testMethod100
482+
◇ Test run started.
483+
✔ Test run with 5 tests passed after 0.015 seconds.
484+
"""
485+
486+
let result = parser.parse(input: input)
487+
488+
// Should use 100 from [N/TOTAL], not 5 from summary
489+
XCTAssertEqual(result.summary.passedTests, 100)
490+
}
491+
492+
func testSwiftTestingFailureSummaryParsing() {
493+
let parser = OutputParser()
494+
let input = """
495+
◇ Test run started.
496+
✘ Test "testMethod" failed after 0.010 seconds.
497+
✘ Test run with 3 tests failed, 7 tests passed after 0.050 seconds.
498+
"""
499+
500+
let result = parser.parse(input: input)
501+
502+
XCTAssertEqual(result.status, "failed")
503+
// Without [N/TOTAL] lines, should use summary: 3 failed + 7 passed = 10 total
504+
// passed = 10 - 3 = 7
505+
XCTAssertEqual(result.summary.passedTests, 7)
506+
XCTAssertEqual(result.summary.buildTime, "0.050")
507+
}
383508
}

0 commit comments

Comments
 (0)