From bb7191bd4824145fe798491cbf003fc62d733c38 Mon Sep 17 00:00:00 2001 From: alexey1312 Date: Tue, 18 Aug 2026 12:42:15 +0500 Subject: [PATCH] fix: count XCTest suite totals once per run XCTest prints the same "Executed N tests" line at each suite level: the nested suite, the .xctest bundle, and the "Selected tests"/"All tests" wrapper. The parser added the wrapper count to the bundle count. A filtered run thus reported twice the tests and twice the failures. The parser also added every duration line. It reported three times the real test time. Count the .xctest bundles only. Read the run duration from the same suite level as the counts. Fixes #83 --- Sources/XCSiftCore/OutputParser.swift | 21 ++++-- Sources/XCSiftCore/XcodebuildSymbols.swift | 1 - Tests/XCSiftCoreTests/ParsingTests.swift | 74 ++++++++++++++++++++++ 3 files changed, 89 insertions(+), 7 deletions(-) diff --git a/Sources/XCSiftCore/OutputParser.swift b/Sources/XCSiftCore/OutputParser.swift index 01fdf5d..a402d02 100644 --- a/Sources/XCSiftCore/OutputParser.swift +++ b/Sources/XCSiftCore/OutputParser.swift @@ -28,7 +28,7 @@ public struct StreamingOutputParser { var executables: [Executable] = [] var seenExecutablePaths: Set = [] var buildTime: String? - var testTimeAccumulator: Double = 0 + var swiftTestingTimeAccumulator: Double = 0 var seenTestNames: Set = [] var seenWarnings: Set = [] var warningCount = 0 @@ -37,8 +37,10 @@ public struct StreamingOutputParser { var seenPassedTestNames: Set = [] var xctestBundleExecutedCount: Int = 0 var xctestBundleFailedCount: Int = 0 + var xctestBundleDuration: Double = 0 var xctestFallbackExecutedCount: Int? var xctestFallbackFailedCount: Int? + var xctestFallbackDuration: Double = 0 var sawBundleLevelXCTestSummary: Bool = false var swiftTestingExecutedCount: Int? var swiftTestingFailedCount: Int? @@ -228,9 +230,10 @@ public struct StreamingOutputParser { let flakyTests = detectFlakyTests() + let totalTestTime = state.swiftTestingTimeAccumulator + resolvedXCTestDuration() let formattedTestTime: String? = - state.testTimeAccumulator > 0 - ? String(format: "%.3fs", state.testTimeAccumulator) + totalTestTime > 0 + ? String(format: "%.3fs", totalTestTime) : nil let summary = BuildSummary( @@ -345,20 +348,22 @@ public struct StreamingOutputParser { } case .testSuiteCompleted(let suiteName, let executed, let failed, let duration): - if suiteName.hasSuffix(".xctest") || suiteName == XcodebuildSymbols.selectedTestsSuite { + // "Selected tests"/"All tests" wrap the bundles, so they repeat totals instead of adding to them + if suiteName.hasSuffix(".xctest") { state.xctestBundleExecutedCount += executed state.xctestBundleFailedCount += failed + state.xctestBundleDuration += duration state.sawBundleLevelXCTestSummary = true } else { state.xctestFallbackExecutedCount = executed state.xctestFallbackFailedCount = failed + state.xctestFallbackDuration = duration } - state.testTimeAccumulator += duration case .swiftTestingCompleted(let executed, let failed, let duration): state.swiftTestingExecutedCount = (state.swiftTestingExecutedCount ?? 0) + executed state.swiftTestingFailedCount = (state.swiftTestingFailedCount ?? 0) + failed - state.testTimeAccumulator += duration + state.swiftTestingTimeAccumulator += duration case .parallelTestScheduled(let index, let total): if let previousIndex = state.lastParallelTestSchedulingIndex { @@ -476,6 +481,10 @@ public struct StreamingOutputParser { private func resolvedXCTestFailedCount() -> Int? { state.sawBundleLevelXCTestSummary ? state.xctestBundleFailedCount : state.xctestFallbackFailedCount } + + private func resolvedXCTestDuration() -> Double { + state.sawBundleLevelXCTestSummary ? state.xctestBundleDuration : state.xctestFallbackDuration + } } /// Parses a complete xcodebuild or SPM output string into a structured ``BuildResult``. diff --git a/Sources/XCSiftCore/XcodebuildSymbols.swift b/Sources/XCSiftCore/XcodebuildSymbols.swift index 4f63ca2..b35cdfa 100644 --- a/Sources/XCSiftCore/XcodebuildSymbols.swift +++ b/Sources/XCSiftCore/XcodebuildSymbols.swift @@ -35,7 +35,6 @@ enum XcodebuildSymbols { static let testSuiteStartedSuffix = "' started" static let testSuitePassedMarker = " passed" static let testSuiteFailedMarker = " failed" - static let selectedTestsSuite = "Selected tests" // Swift Testing symbols (macOS Private Use Area + Linux fallback) static let swiftTestingPass = "✓" diff --git a/Tests/XCSiftCoreTests/ParsingTests.swift b/Tests/XCSiftCoreTests/ParsingTests.swift index 8ed82c4..f078903 100644 --- a/Tests/XCSiftCoreTests/ParsingTests.swift +++ b/Tests/XCSiftCoreTests/ParsingTests.swift @@ -284,6 +284,80 @@ final class ParsingTests: XCTestCase { XCTAssertEqual(result.summary.failedTests, 0) } + /// Issue #83: `swift test --filter` prints the same totals for the nested suite, the `.xctest` + /// bundle and the `Selected tests` wrapper. Only one of them may count. + func testSelectedTestsWrapperDoesNotDoubleCountBundleTotals() { + let parser = OutputParser() + let input = """ + Test Suite 'Selected tests' started at 2026-08-18 11:16:21.129. + Test Suite 'xcsiftPackageTests.xctest' started at 2026-08-18 11:16:21.130. + Test Suite 'PluginFilesTests' started at 2026-08-18 11:16:21.130. + Test Suite 'PluginFilesTests' passed at 2026-08-18 11:16:21.132. + Executed 4 tests, with 0 failures (0 unexpected) in 0.002 (0.002) seconds + Test Suite 'xcsiftPackageTests.xctest' passed at 2026-08-18 11:16:21.132. + Executed 4 tests, with 0 failures (0 unexpected) in 0.002 (0.002) seconds + Test Suite 'Selected tests' passed at 2026-08-18 11:16:21.132. + Executed 4 tests, with 0 failures (0 unexpected) in 0.002 (0.003) seconds + """ + + let result = parser.parse(input: input) + + XCTAssertEqual(result.summary.passedTests, 4) + XCTAssertEqual(result.summary.failedTests, 0) + } + + /// Issue #83: the wrapper repeats the failure count too, so a filtered run with failures + /// must not report more failures than XCTest ran. + func testSelectedTestsWrapperDoesNotDoubleCountFailures() { + let parser = OutputParser() + let input = """ + Test Suite 'FeatureTests' failed at 2026-08-18 11:16:21.132. + Executed 4 tests, with 1 failure (0 unexpected) in 0.002 (0.002) seconds + Test Suite 'MyPackageTests.xctest' failed at 2026-08-18 11:16:21.132. + Executed 4 tests, with 1 failure (0 unexpected) in 0.002 (0.002) seconds + Test Suite 'Selected tests' failed at 2026-08-18 11:16:21.132. + Executed 4 tests, with 1 failure (0 unexpected) in 0.002 (0.003) seconds + """ + + let result = parser.parse(input: input) + + XCTAssertEqual(result.summary.failedTests, 1) + XCTAssertEqual(result.summary.passedTests, 3) + } + + /// Issue #83: every suite level repeats the same duration, so test_time must count one level only. + func testXCTestSuiteDurationsAreNotDoubleCountedAcrossSuiteLevels() { + let parser = OutputParser() + let input = """ + Test Suite 'PluginFilesTests' passed at 2026-08-18 11:16:21.132. + Executed 4 tests, with 0 failures (0 unexpected) in 1.500 (1.510) seconds + Test Suite 'xcsiftPackageTests.xctest' passed at 2026-08-18 11:16:21.132. + Executed 4 tests, with 0 failures (0 unexpected) in 1.500 (1.510) seconds + Test Suite 'Selected tests' passed at 2026-08-18 11:16:21.132. + Executed 4 tests, with 0 failures (0 unexpected) in 1.500 (1.520) seconds + """ + + let result = parser.parse(input: input) + + XCTAssertEqual(result.summary.testTime, "1.500s") + } + + /// Separate bundles hold disjoint tests, so their durations still add up. + func testXCTestBundleDurationsAccumulateAcrossBundles() { + let parser = OutputParser() + let input = """ + Test Suite 'UnitTests.xctest' passed at 2026-01-15 12:00:00.001. + Executed 2 tests, with 0 failures in 0.100 seconds + Test Suite 'UITests.xctest' passed at 2026-01-15 12:00:00.002. + Executed 3 tests, with 0 failures in 0.200 seconds + """ + + let result = parser.parse(input: input) + + XCTAssertEqual(result.summary.testTime, "0.300s") + XCTAssertEqual(result.summary.passedTests, 5) + } + /// Tests that test_time is accumulated correctly when both XCTest and Swift Testing are present /// Regression test for fix where test times are summed across multiple test bundles func testCombinedTestTimeAccumulation() {