Skip to content

Commit 492e84c

Browse files
committed
Detect when Swift Testing returns EXIT_NO_TESTS_FOUND. (#7777)
This PR detects when a Swift Testing run returns `EXIT_NO_TESTS_FOUND` and treats it as a successful test run. This change handles the change from swiftlang/swift-testing#536. Separately, the refactor in #7766 stops Swift Package Manager from reporting `"No matching test cases were run"` when XCTest has no tests matching passed `--filter` arguments. A third PR after these two have been merged will restore that functionality by collating results from both XCTest and Swift Testing. > [!NOTE] > Test coverage can be added after Swift Testing is in the Swift toolchain. This change partially resolves rdar://131704587.
1 parent 5671cf7 commit 492e84c

File tree

1 file changed

+61
-10
lines changed

1 file changed

+61
-10
lines changed

Sources/Commands/SwiftTestCommand.swift

+61-10
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ import var TSCBasic.stdoutStream
3737
import class TSCBasic.SynchronizedQueue
3838
import class TSCBasic.Thread
3939

40+
#if os(Windows)
41+
import WinSDK // for ERROR_NOT_FOUND
42+
#endif
43+
4044
private enum TestError: Swift.Error {
4145
case invalidListTestJSONData(context: String, underlyingError: Error? = nil)
4246
case testsNotFound
@@ -862,13 +866,40 @@ final class TestRunner {
862866

863867
/// Executes and returns execution status. Prints test output on standard streams if requested
864868
/// - Returns: Boolean indicating if test execution returned code 0, and the output stream result
865-
public func test(outputHandler: @escaping (String) -> Void) -> Bool {
866-
var success = true
869+
func test(outputHandler: @escaping (String) -> Void) -> Bool {
870+
(test(outputHandler: outputHandler) as Result) != .failure
871+
}
872+
873+
/// The result of running the test(s).
874+
enum Result: Equatable {
875+
/// The test(s) ran successfully.
876+
case success
877+
878+
/// The test(s) failed.
879+
case failure
880+
881+
/// There were no matching tests to run.
882+
///
883+
/// XCTest does not report this result. It is used by Swift Testing only.
884+
case noMatchingTests
885+
}
886+
887+
/// Executes and returns execution status. Prints test output on standard streams if requested
888+
/// - Returns: Result of spawning and running the test process, and the output stream result
889+
@_disfavoredOverload
890+
func test(outputHandler: @escaping (String) -> Void) -> Result {
891+
var results = [Result]()
867892
for path in self.bundlePaths {
868893
let testSuccess = self.test(at: path, outputHandler: outputHandler)
869-
success = success && testSuccess
894+
results.append(testSuccess)
895+
}
896+
if results.contains(.failure) {
897+
return .failure
898+
} else if results.isEmpty || results.contains(.success) {
899+
return .success
900+
} else {
901+
return .noMatchingTests
870902
}
871-
return success
872903
}
873904

874905
/// Constructs arguments to execute XCTest.
@@ -892,7 +923,7 @@ final class TestRunner {
892923
return args
893924
}
894925

895-
private func test(at path: AbsolutePath, outputHandler: @escaping (String) -> Void) -> Bool {
926+
private func test(at path: AbsolutePath, outputHandler: @escaping (String) -> Void) -> Result {
896927
let testObservabilityScope = self.observabilityScope.makeChildScope(description: "running test at \(path)")
897928

898929
do {
@@ -907,25 +938,27 @@ final class TestRunner {
907938
)
908939
let process = AsyncProcess(arguments: try args(forTestAt: path), environment: self.testEnv, outputRedirection: outputRedirection)
909940
guard let terminationKey = self.cancellator.register(process) else {
910-
return false // terminating
941+
return .failure // terminating
911942
}
912943
defer { self.cancellator.deregister(terminationKey) }
913944
try process.launch()
914945
let result = try process.waitUntilExit()
915946
switch result.exitStatus {
916947
case .terminated(code: 0):
917-
return true
948+
return .success
949+
case .terminated(code: EXIT_NO_TESTS_FOUND) where library == .swiftTesting:
950+
return .noMatchingTests
918951
#if !os(Windows)
919952
case .signalled(let signal) where ![SIGINT, SIGKILL, SIGTERM].contains(signal):
920953
testObservabilityScope.emit(error: "Exited with unexpected signal code \(signal)")
921-
return false
954+
return .failure
922955
#endif
923956
default:
924-
return false
957+
return .failure
925958
}
926959
} catch {
927960
testObservabilityScope.emit(error)
928-
return false
961+
return .failure
929962
}
930963
}
931964
}
@@ -1392,6 +1425,24 @@ private extension Basics.Diagnostic {
13921425
}
13931426
}
13941427

1428+
/// The exit code returned to Swift Package Manager by Swift Testing when no
1429+
/// tests matched the inputs specified by the developer (or, for the case of
1430+
/// `swift test list`, when no tests were found.)
1431+
///
1432+
/// Because Swift Package Manager does not directly link to the testing library,
1433+
/// it duplicates the definition of this constant in its own source. Any changes
1434+
/// to this constant in either package must be mirrored in the other.
1435+
private var EXIT_NO_TESTS_FOUND: CInt {
1436+
#if os(macOS) || os(Linux)
1437+
EX_UNAVAILABLE
1438+
#elseif os(Windows)
1439+
ERROR_NOT_FOUND
1440+
#else
1441+
#warning("Platform-specific implementation missing: value for EXIT_NO_TESTS_FOUND unavailable")
1442+
return 2 // We're assuming that EXIT_SUCCESS = 0 and EXIT_FAILURE = 1.
1443+
#endif
1444+
}
1445+
13951446
/// Builds the "test" target if enabled in options.
13961447
///
13971448
/// - Returns: The paths to the build test products.

0 commit comments

Comments
 (0)