diff --git a/Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift b/Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift index b18e51e50..81988b126 100644 --- a/Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift @@ -118,6 +118,11 @@ private func entryPoint( let eventHandler = try eventHandlerForStreamingEvents(version: args?.eventStreamVersion, forwardingTo: recordHandler) let exitCode = await entryPoint(passing: args, eventHandler: eventHandler) + + // To maintain compatibility with Xcode 16 Beta 1, suppress custom exit codes. + if exitCode == EXIT_NO_TESTS_FOUND { + return EXIT_SUCCESS + } return exitCode } #endif diff --git a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift index b10f31c8e..6ed1bbe87 100644 --- a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift @@ -74,8 +74,13 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha } } + // The set of matching tests (or, in the case of `swift test list`, the set + // of all tests.) + let tests: [Test] + if args.listTests ?? false { - let tests = await Test.all + tests = await Array(Test.all) + if args.verbosity > .min { for testID in listTestsForEntryPoint(tests) { // Print the test ID to stdout (classical CLI behavior.) @@ -95,8 +100,20 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha } else { // Run the tests. let runner = await Runner(configuration: configuration) + tests = runner.tests await runner.run() } + + // If there were no matching tests, exit with a dedicated exit code so that + // the caller (assumed to be Swift Package Manager) can implement special + // handling. + if tests.isEmpty { + exitCode.withLock { exitCode in + if exitCode == EXIT_SUCCESS { + exitCode = EXIT_NO_TESTS_FOUND + } + } + } } catch { #if !SWT_NO_FILE_IO try? FileHandle.stderr.write(String(describing: error)) diff --git a/Sources/Testing/ABI/EntryPoints/SwiftPMEntryPoint.swift b/Sources/Testing/ABI/EntryPoints/SwiftPMEntryPoint.swift index 118d3a335..7237b95d4 100644 --- a/Sources/Testing/ABI/EntryPoints/SwiftPMEntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/SwiftPMEntryPoint.swift @@ -14,6 +14,30 @@ private import _TestingInternals #endif +/// The exit code returned to Swift Package Manager by Swift Testing when no +/// tests matched the inputs specified by the developer (or, for the case of +/// `swift test list`, when no tests were found.) +/// +/// Because Swift Package Manager does not directly link to the testing library, +/// it duplicates the definition of this constant in its own source. Any changes +/// to this constant in either package must be mirrored in the other. +/// +/// Tools authors using the ABI entry point function can determine if no tests +/// matched the developer's inputs by counting the number of test records passed +/// to the event handler or written to the event stream output path. +/// +/// This constant is not part of the public interface of the testing library. +var EXIT_NO_TESTS_FOUND: CInt { +#if SWT_TARGET_OS_APPLE || os(Linux) + EX_UNAVAILABLE +#elseif os(Windows) + ERROR_NOT_FOUND +#else +#warning("Platform-specific implementation missing: value for EXIT_NO_TESTS_FOUND unavailable") + return 2 // We're assuming that EXIT_SUCCESS = 0 and EXIT_FAILURE = 1. +#endif +} + /// The entry point to the testing library used by Swift Package Manager. /// /// - Parameters: diff --git a/Sources/_TestingInternals/include/Includes.h b/Sources/_TestingInternals/include/Includes.h index c856d4155..94015f47a 100644 --- a/Sources/_TestingInternals/include/Includes.h +++ b/Sources/_TestingInternals/include/Includes.h @@ -103,6 +103,10 @@ #include #endif +#if __has_include() +#include +#endif + #if defined(_WIN32) #define WIN32_LEAN_AND_MEAN #define NOMINMAX diff --git a/Tests/TestingTests/SwiftPMTests.swift b/Tests/TestingTests/SwiftPMTests.swift index 4c4547179..6a4ccbc37 100644 --- a/Tests/TestingTests/SwiftPMTests.swift +++ b/Tests/TestingTests/SwiftPMTests.swift @@ -29,6 +29,12 @@ struct SwiftPMTests { #expect(!CommandLine.arguments.isEmpty) } + @Test("EXIT_NO_TESTS_FOUND is unique") + func valueOfEXIT_NO_TESTS_FOUND() { + #expect(EXIT_NO_TESTS_FOUND != EXIT_SUCCESS) + #expect(EXIT_NO_TESTS_FOUND != EXIT_FAILURE) + } + @Test("--parallel/--no-parallel argument") func parallel() throws { var configuration = try configurationForEntryPoint(withArguments: ["PATH"]) @@ -88,6 +94,14 @@ struct SwiftPMTests { } } + @Test("--filter with no matches") + func filterWithNoMatches() async { + var args = __CommandLineArguments_v0() + args.filter = ["NOTHING_MATCHES_THIS_TEST_NAME_HOPEFULLY"] + let exitCode = await __swiftPMEntryPoint(passing: args) as CInt + #expect(exitCode == EXIT_NO_TESTS_FOUND) + } + @Test("--skip argument") @available(_regexAPI, *) func skip() async throws {