Skip to content

Commit 81fdf8b

Browse files
authored
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 125b3eb commit 81fdf8b

File tree

1 file changed

+61
-10
lines changed

1 file changed

+61
-10
lines changed

Sources/Commands/SwiftTestCommand.swift

Lines changed: 61 additions & 10 deletions
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
@@ -869,13 +873,40 @@ final class TestRunner {
869873

870874
/// Executes and returns execution status. Prints test output on standard streams if requested
871875
/// - Returns: Boolean indicating if test execution returned code 0, and the output stream result
872-
public func test(outputHandler: @escaping (String) -> Void) -> Bool {
873-
var success = true
876+
func test(outputHandler: @escaping (String) -> Void) -> Bool {
877+
(test(outputHandler: outputHandler) as Result) != .failure
878+
}
879+
880+
/// The result of running the test(s).
881+
enum Result: Equatable {
882+
/// The test(s) ran successfully.
883+
case success
884+
885+
/// The test(s) failed.
886+
case failure
887+
888+
/// There were no matching tests to run.
889+
///
890+
/// XCTest does not report this result. It is used by Swift Testing only.
891+
case noMatchingTests
892+
}
893+
894+
/// Executes and returns execution status. Prints test output on standard streams if requested
895+
/// - Returns: Result of spawning and running the test process, and the output stream result
896+
@_disfavoredOverload
897+
func test(outputHandler: @escaping (String) -> Void) -> Result {
898+
var results = [Result]()
874899
for path in self.bundlePaths {
875900
let testSuccess = self.test(at: path, outputHandler: outputHandler)
876-
success = success && testSuccess
901+
results.append(testSuccess)
902+
}
903+
if results.contains(.failure) {
904+
return .failure
905+
} else if results.isEmpty || results.contains(.success) {
906+
return .success
907+
} else {
908+
return .noMatchingTests
877909
}
878-
return success
879910
}
880911

881912
/// Constructs arguments to execute XCTest.
@@ -899,7 +930,7 @@ final class TestRunner {
899930
return args
900931
}
901932

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

905936
do {
@@ -914,25 +945,27 @@ final class TestRunner {
914945
)
915946
let process = AsyncProcess(arguments: try args(forTestAt: path), environment: self.testEnv, outputRedirection: outputRedirection)
916947
guard let terminationKey = self.cancellator.register(process) else {
917-
return false // terminating
948+
return .failure // terminating
918949
}
919950
defer { self.cancellator.deregister(terminationKey) }
920951
try process.launch()
921952
let result = try process.waitUntilExit()
922953
switch result.exitStatus {
923954
case .terminated(code: 0):
924-
return true
955+
return .success
956+
case .terminated(code: EXIT_NO_TESTS_FOUND) where library == .swiftTesting:
957+
return .noMatchingTests
925958
#if !os(Windows)
926959
case .signalled(let signal) where ![SIGINT, SIGKILL, SIGTERM].contains(signal):
927960
testObservabilityScope.emit(error: "Exited with unexpected signal code \(signal)")
928-
return false
961+
return .failure
929962
#endif
930963
default:
931-
return false
964+
return .failure
932965
}
933966
} catch {
934967
testObservabilityScope.emit(error)
935-
return false
968+
return .failure
936969
}
937970
}
938971
}
@@ -1399,6 +1432,24 @@ private extension Basics.Diagnostic {
13991432
}
14001433
}
14011434

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

0 commit comments

Comments
 (0)