Skip to content

Commit 1be3357

Browse files
authored
Emit a specific exit code when no tests match inputs to swift test. (#536)
This PR modifies the entry point function used by Swift Package Manager so that when no tests match the inputs (or, in the case of `swift test list`, when there are no Swift Testing tests in the test target), Swift Package Manager can detect it and respond appropriately. Because exit codes exist in a flat namespace, and because on POSIX-y systems only the low 8 bits of an exit code are reliable, our options here are limited. I have selected the `EX_UNAVAILABLE` code from sysexits.h to represent this state. On Windows, all 32 bits of the exit code are propagated, but there is no `EX_UNAVAILABLE` value, so I've used the Win32 `ERROR_NOT_FOUND` error code instead. My assumption is that neither of these codes is likely to _actually_ be passed to `exit()` by test code (because why would they be?) A separate PR is required in Swift Package Manager to correctly handle this exit code and emit the `noMatchingTests` diagnostic that's currently emitted only for XCTest. Resolves rdar://131704539. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent ffa4d0f commit 1be3357

File tree

5 files changed

+65
-1
lines changed

5 files changed

+65
-1
lines changed

Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ private func entryPoint(
118118

119119
let eventHandler = try eventHandlerForStreamingEvents(version: args?.eventStreamVersion, forwardingTo: recordHandler)
120120
let exitCode = await entryPoint(passing: args, eventHandler: eventHandler)
121+
122+
// To maintain compatibility with Xcode 16 Beta 1, suppress custom exit codes.
123+
if exitCode == EXIT_NO_TESTS_FOUND {
124+
return EXIT_SUCCESS
125+
}
121126
return exitCode
122127
}
123128
#endif

Sources/Testing/ABI/EntryPoints/EntryPoint.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,13 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha
7474
}
7575
}
7676

77+
// The set of matching tests (or, in the case of `swift test list`, the set
78+
// of all tests.)
79+
let tests: [Test]
80+
7781
if args.listTests ?? false {
78-
let tests = await Test.all
82+
tests = await Array(Test.all)
83+
7984
if args.verbosity > .min {
8085
for testID in listTestsForEntryPoint(tests) {
8186
// Print the test ID to stdout (classical CLI behavior.)
@@ -95,8 +100,20 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha
95100
} else {
96101
// Run the tests.
97102
let runner = await Runner(configuration: configuration)
103+
tests = runner.tests
98104
await runner.run()
99105
}
106+
107+
// If there were no matching tests, exit with a dedicated exit code so that
108+
// the caller (assumed to be Swift Package Manager) can implement special
109+
// handling.
110+
if tests.isEmpty {
111+
exitCode.withLock { exitCode in
112+
if exitCode == EXIT_SUCCESS {
113+
exitCode = EXIT_NO_TESTS_FOUND
114+
}
115+
}
116+
}
100117
} catch {
101118
#if !SWT_NO_FILE_IO
102119
try? FileHandle.stderr.write(String(describing: error))

Sources/Testing/ABI/EntryPoints/SwiftPMEntryPoint.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,30 @@
1414
private import _TestingInternals
1515
#endif
1616

17+
/// The exit code returned to Swift Package Manager by Swift Testing when no
18+
/// tests matched the inputs specified by the developer (or, for the case of
19+
/// `swift test list`, when no tests were found.)
20+
///
21+
/// Because Swift Package Manager does not directly link to the testing library,
22+
/// it duplicates the definition of this constant in its own source. Any changes
23+
/// to this constant in either package must be mirrored in the other.
24+
///
25+
/// Tools authors using the ABI entry point function can determine if no tests
26+
/// matched the developer's inputs by counting the number of test records passed
27+
/// to the event handler or written to the event stream output path.
28+
///
29+
/// This constant is not part of the public interface of the testing library.
30+
var EXIT_NO_TESTS_FOUND: CInt {
31+
#if SWT_TARGET_OS_APPLE || os(Linux)
32+
EX_UNAVAILABLE
33+
#elseif os(Windows)
34+
ERROR_NOT_FOUND
35+
#else
36+
#warning("Platform-specific implementation missing: value for EXIT_NO_TESTS_FOUND unavailable")
37+
return 2 // We're assuming that EXIT_SUCCESS = 0 and EXIT_FAILURE = 1.
38+
#endif
39+
}
40+
1741
/// The entry point to the testing library used by Swift Package Manager.
1842
///
1943
/// - Parameters:

Sources/_TestingInternals/include/Includes.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@
103103
#include <dlfcn.h>
104104
#endif
105105

106+
#if __has_include(<sysexits.h>)
107+
#include <sysexits.h>
108+
#endif
109+
106110
#if defined(_WIN32)
107111
#define WIN32_LEAN_AND_MEAN
108112
#define NOMINMAX

Tests/TestingTests/SwiftPMTests.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ struct SwiftPMTests {
2929
#expect(!CommandLine.arguments.isEmpty)
3030
}
3131

32+
@Test("EXIT_NO_TESTS_FOUND is unique")
33+
func valueOfEXIT_NO_TESTS_FOUND() {
34+
#expect(EXIT_NO_TESTS_FOUND != EXIT_SUCCESS)
35+
#expect(EXIT_NO_TESTS_FOUND != EXIT_FAILURE)
36+
}
37+
3238
@Test("--parallel/--no-parallel argument")
3339
func parallel() throws {
3440
var configuration = try configurationForEntryPoint(withArguments: ["PATH"])
@@ -88,6 +94,14 @@ struct SwiftPMTests {
8894
}
8995
}
9096

97+
@Test("--filter with no matches")
98+
func filterWithNoMatches() async {
99+
var args = __CommandLineArguments_v0()
100+
args.filter = ["NOTHING_MATCHES_THIS_TEST_NAME_HOPEFULLY"]
101+
let exitCode = await __swiftPMEntryPoint(passing: args) as CInt
102+
#expect(exitCode == EXIT_NO_TESTS_FOUND)
103+
}
104+
91105
@Test("--skip argument")
92106
@available(_regexAPI, *)
93107
func skip() async throws {

0 commit comments

Comments
 (0)