From ded1894210b66c27f08aa6a76a1de0648e22aeec Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 27 Oct 2025 11:46:51 -0400 Subject: [PATCH 01/22] [WIP] Enable exit tests on Android API level 28 and newer. --- Package.swift | 4 ++-- .../Attachments/Attachment+URL.swift | 17 +++++++++++++---- Sources/Testing/ExitTests/ExitStatus.swift | 15 ++++++++++++--- .../ExitTests/ExitTest.CapturedValue.swift | 4 +++- .../Testing/ExitTests/ExitTest.Condition.swift | 16 ++++++++++++---- Sources/Testing/ExitTests/ExitTest.Result.swift | 4 +++- Sources/Testing/ExitTests/ExitTest.swift | 8 ++++++-- Sources/Testing/ExitTests/SpawnProcess.swift | 11 ++++++++--- Sources/Testing/ExitTests/WaitFor.swift | 12 ++++++++---- .../Expectations/Expectation+Macro.swift | 8 ++++++-- cmake/modules/shared/CompilerSettings.cmake | 4 ++-- 11 files changed, 75 insertions(+), 28 deletions(-) diff --git a/Package.swift b/Package.swift index 2788502c3..c64c77ca3 100644 --- a/Package.swift +++ b/Package.swift @@ -377,7 +377,7 @@ extension Array where Element == PackageDescription.SwiftSetting { .define("SWT_TARGET_OS_APPLE", .whenApple()), - .define("SWT_NO_EXIT_TESTS", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android]))), + .define("SWT_NO_EXIT_TESTS", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi]))), .define("SWT_NO_PROCESS_SPAWNING", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android]))), .define("SWT_NO_SNAPSHOT_TYPES", .whenEmbedded(or: .whenApple(false))), .define("SWT_NO_DYNAMIC_LINKING", .whenEmbedded(or: .when(platforms: [.wasi]))), @@ -437,7 +437,7 @@ extension Array where Element == PackageDescription.CXXSetting { result += [ .define("SWT_NO_EXIT_TESTS", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android]))), - .define("SWT_NO_PROCESS_SPAWNING", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android]))), + .define("SWT_NO_PROCESS_SPAWNING", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi]))), .define("SWT_NO_SNAPSHOT_TYPES", .whenEmbedded(or: .whenApple(false))), .define("SWT_NO_DYNAMIC_LINKING", .whenEmbedded(or: .when(platforms: [.wasi]))), .define("SWT_NO_PIPES", .whenEmbedded(or: .when(platforms: [.wasi]))), diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift index 3ca05b8d1..a7fec71ea 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift @@ -160,6 +160,13 @@ private let _archiverPath: String? = { /// an archive (currently of `.zip` format, although this is subject to change.) private func _compressContentsOfDirectory(at directoryURL: URL) async throws -> Data { #if !SWT_NO_PROCESS_SPAWNING +#if os(Android) + guard #available(Android 28, *) else { + // API level 28 corresponds to Android 9 Pie. + throw CocoaError(.featureUnsupported, userInfo: [NSLocalizedDescriptionKey: "Attaching directories to tests requires Android 9 (API level 28) or newer."]) + } +#endif + let temporaryName = "\(UUID().uuidString).zip" let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(temporaryName) defer { @@ -180,12 +187,15 @@ private func _compressContentsOfDirectory(at directoryURL: URL) async throws -> // OpenBSD's tar(1) does not support writing PKZIP archives, and /usr/bin/zip // tool is an optional install, so we check if it's present before trying to // execute it. -#if os(Linux) || os(OpenBSD) + // + // TODO: figure out whether tar or zip is available on Android and where it's stored +#if os(Linux) || os(OpenBSD) || os(Android) let archiverPath = "/bin/sh" -#if os(Linux) +#if os(Linux) || os(Android) let trueArchiverPath = "/usr/bin/zip" #else let trueArchiverPath = "/usr/local/bin/zip" +#endif var isDirectory = false if !FileManager.default.fileExists(atPath: trueArchiverPath, isDirectory: &isDirectory) || isDirectory { throw CocoaError(.fileNoSuchFile, userInfo: [ @@ -193,7 +203,6 @@ private func _compressContentsOfDirectory(at directoryURL: URL) async throws -> NSFilePathErrorKey: trueArchiverPath ]) } -#endif #elseif SWT_TARGET_OS_APPLE || os(FreeBSD) let archiverPath = "/usr/bin/tar" #elseif os(Windows) @@ -211,7 +220,7 @@ private func _compressContentsOfDirectory(at directoryURL: URL) async throws -> let sourcePath = directoryURL.path let destinationPath = temporaryURL.path let arguments = { -#if os(Linux) || os(OpenBSD) +#if os(Linux) || os(OpenBSD) || os(Android) // The zip command constructs relative paths from the current working // directory rather than from command-line arguments. ["-c", #"cd "$0" && "$1" "$2" --recurse-paths ."#, sourcePath, trueArchiverPath, destinationPath] diff --git a/Sources/Testing/ExitTests/ExitStatus.swift b/Sources/Testing/ExitTests/ExitStatus.swift index 21fa2335e..031348681 100644 --- a/Sources/Testing/ExitTests/ExitStatus.swift +++ b/Sources/Testing/ExitTests/ExitStatus.swift @@ -23,7 +23,10 @@ private import _TestingInternals /// @Available(Swift, introduced: 6.2) /// @Available(Xcode, introduced: 26.0) /// } -#if SWT_NO_PROCESS_SPAWNING +#if !SWT_NO_PROCESS_SPAWNING +@available(Android 28, *) +#else +@_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif public enum ExitStatus: Sendable { @@ -90,7 +93,10 @@ public enum ExitStatus: Sendable { // MARK: - Equatable -#if SWT_NO_PROCESS_SPAWNING +#if !SWT_NO_PROCESS_SPAWNING +@available(Android 28, *) +#else +@_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif extension ExitStatus: Equatable {} @@ -109,7 +115,10 @@ private let _sigabbrev_np = symbol(named: "sigabbrev_np").map { } #endif -#if SWT_NO_PROCESS_SPAWNING +#if !SWT_NO_PROCESS_SPAWNING +@available(Android 28, *) +#else +@_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif extension ExitStatus: CustomStringConvertible { diff --git a/Sources/Testing/ExitTests/ExitTest.CapturedValue.swift b/Sources/Testing/ExitTests/ExitTest.CapturedValue.swift index 556fc0cf6..28ce5d4d5 100644 --- a/Sources/Testing/ExitTests/ExitTest.CapturedValue.swift +++ b/Sources/Testing/ExitTests/ExitTest.CapturedValue.swift @@ -11,7 +11,9 @@ private import _TestingInternals @_spi(ForToolsIntegrationOnly) -#if SWT_NO_EXIT_TESTS +#if !SWT_NO_EXIT_TESTS +@available(Android 28, *) +#else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif diff --git a/Sources/Testing/ExitTests/ExitTest.Condition.swift b/Sources/Testing/ExitTests/ExitTest.Condition.swift index edd94193b..1d1494fbc 100644 --- a/Sources/Testing/ExitTests/ExitTest.Condition.swift +++ b/Sources/Testing/ExitTests/ExitTest.Condition.swift @@ -10,7 +10,9 @@ private import _TestingInternals -#if SWT_NO_EXIT_TESTS +#if !SWT_NO_EXIT_TESTS +@available(Android 28, *) +#else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif @@ -58,7 +60,9 @@ extension ExitTest { // MARK: - -#if SWT_NO_EXIT_TESTS +#if !SWT_NO_EXIT_TESTS +@available(Android 28, *) +#else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif @@ -178,7 +182,9 @@ extension ExitTest.Condition { // MARK: - CustomStringConvertible -#if SWT_NO_EXIT_TESTS +#if !SWT_NO_EXIT_TESTS +@available(Android 28, *) +#else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif @@ -201,7 +207,9 @@ extension ExitTest.Condition: CustomStringConvertible { // MARK: - Comparison -#if SWT_NO_EXIT_TESTS +#if !SWT_NO_EXIT_TESTS +@available(Android 28, *) +#else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif diff --git a/Sources/Testing/ExitTests/ExitTest.Result.swift b/Sources/Testing/ExitTests/ExitTest.Result.swift index 53d816c85..3c72e43ed 100644 --- a/Sources/Testing/ExitTests/ExitTest.Result.swift +++ b/Sources/Testing/ExitTests/ExitTest.Result.swift @@ -8,7 +8,9 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -#if SWT_NO_EXIT_TESTS +#if !SWT_NO_EXIT_TESTS +@available(Android 28, *) +#else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 56096906f..15bdfc2d0 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -34,7 +34,9 @@ private import _TestingInternals /// @Available(Swift, introduced: 6.2) /// @Available(Xcode, introduced: 26.0) /// } -#if SWT_NO_EXIT_TESTS +#if !SWT_NO_EXIT_TESTS +@available(Android 28, *) +#else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif @@ -223,6 +225,8 @@ extension ExitTest { // as I can tell, special-case RLIMIT_CORE=1. var rl = rlimit(rlim_cur: 0, rlim_max: 0) _ = setrlimit(RLIMIT_CORE, &rl) +#elseif os(Android) + // TODO: "tombstoned_intercept"? #elseif os(Windows) // On Windows, similarly disable Windows Error Reporting and the Windows // Error Reporting UI. Note we expect to be the first component to call @@ -698,7 +702,7 @@ extension ExitTest { /// back to a (new) file handle with `_makeFileHandle()`, or `nil` if the /// file handle could not be converted to a string. private static func _makeEnvironmentVariable(for fileHandle: borrowing FileHandle) -> String? { -#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) +#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) return fileHandle.withUnsafePOSIXFileDescriptor { fd in fd.map(String.init(describing:)) } diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 6114566f1..4fb6e50e2 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -17,7 +17,7 @@ internal import _TestingInternals /// A platform-specific value identifying a process running on the current /// system. -#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) +#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) typealias ProcessID = pid_t #elseif os(Windows) typealias ProcessID = HANDLE @@ -62,6 +62,7 @@ private let _posix_spawn_file_actions_addclosefrom_np = symbol(named: "posix_spa /// resources. /// /// - Throws: Any error that prevented the process from spawning. +@available(Android 28, *) func spawnExecutable( atPath executablePath: String, arguments: [String], @@ -75,11 +76,11 @@ func spawnExecutable( // use, so use this typealias to paper over the differences. #if SWT_TARGET_OS_APPLE || os(FreeBSD) || os(OpenBSD) typealias P = T? -#elseif os(Linux) +#elseif os(Linux) || os(Android) typealias P = T #endif -#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) +#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) return try withUnsafeTemporaryAllocation(of: P.self, capacity: 1) { fileActions in let fileActions = fileActions.baseAddress! let fileActionsInitialized = posix_spawn_file_actions_init(fileActions) @@ -194,6 +195,9 @@ func spawnExecutable( // spawned child process if we control its execution. var environment = environment environment["SWT_CLOSEFROM"] = String(describing: highestFD + 1) +#elseif os(Android) + // Android does not have posix_spawn_file_actions_addclosefrom_np() nor + // closefrom(2), so we don't attempt this operation there. #else #warning("Platform-specific implementation missing: cannot close unused file descriptors") #endif @@ -463,6 +467,7 @@ private func _escapeCommandLine(_ arguments: [String]) -> String { /// This function is a convenience that spawns the given process and waits for /// it to terminate. It is primarily for use by other targets in this package /// such as its cross-import overlays. +@available(Android 28, *) package func spawnExecutableAtPathAndWait( _ executablePath: String, arguments: [String] = [], diff --git a/Sources/Testing/ExitTests/WaitFor.swift b/Sources/Testing/ExitTests/WaitFor.swift index f0326ff3c..dc6f74ee9 100644 --- a/Sources/Testing/ExitTests/WaitFor.swift +++ b/Sources/Testing/ExitTests/WaitFor.swift @@ -11,7 +11,7 @@ #if !SWT_NO_PROCESS_SPAWNING internal import _TestingInternals -#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) +#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) /// Block the calling thread, wait for the target process to exit, and return /// a value describing the conditions under which it exited. /// @@ -78,7 +78,7 @@ func wait(for pid: consuming pid_t) async throws -> ExitStatus { return try _blockAndWait(for: pid) } -#elseif SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) +#elseif SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) /// A mapping of awaited child PIDs to their corresponding Swift continuations. private nonisolated(unsafe) let _childProcessContinuations = { let result = ManagedBuffer<[pid_t: CheckedContinuation], pthread_mutex_t>.create( @@ -189,8 +189,9 @@ private let _createWaitThread: Void = { { _ in // Set the thread name to help with diagnostics. Note that different // platforms support different thread name lengths. See MAXTHREADNAMESIZE - // on Darwin, TASK_COMM_LEN on Linux, MAXCOMLEN on FreeBSD, and _MAXCOMLEN - // on OpenBSD. We try to maximize legibility in the available space. + // on Darwin, TASK_COMM_LEN on Linux, MAXCOMLEN on FreeBSD, _MAXCOMLEN on + // OpenBSD, and MAX_TASK_COMM_LEN on Android. We try to maximize + // legibility in the available space. #if SWT_TARGET_OS_APPLE _ = pthread_setname_np("Swift Testing exit test monitor") #elseif os(Linux) @@ -201,6 +202,8 @@ private let _createWaitThread: Void = { pthread_set_name_np(pthread_self(), "SWT ex test monitor") #elseif os(OpenBSD) pthread_set_name_np(pthread_self(), "SWT exit test monitor") +#elseif os(Android) + _ = pthread_setname_np(pthread_self(), "SWT ExT monitor") #else #warning("Platform-specific implementation missing: thread naming unavailable") #endif @@ -233,6 +236,7 @@ private let _createWaitThread: Void = { /// /// On Apple platforms, the libdispatch-based implementation above is more /// efficient because it does not need to permanently reserve a thread. +@available(Android 28, *) func wait(for pid: consuming pid_t) async throws -> ExitStatus { let pid = consume pid diff --git a/Sources/Testing/Expectations/Expectation+Macro.swift b/Sources/Testing/Expectations/Expectation+Macro.swift index ea007f667..281f8259d 100644 --- a/Sources/Testing/Expectations/Expectation+Macro.swift +++ b/Sources/Testing/Expectations/Expectation+Macro.swift @@ -876,7 +876,9 @@ public macro require( /// } @freestanding(expression) @discardableResult -#if SWT_NO_EXIT_TESTS +#if !SWT_NO_EXIT_TESTS +@available(Android 28, *) +#else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif @@ -924,7 +926,9 @@ public macro expect( /// } @freestanding(expression) @discardableResult -#if SWT_NO_EXIT_TESTS +#if !SWT_NO_EXIT_TESTS +@available(Android 28, *) +#else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif diff --git a/cmake/modules/shared/CompilerSettings.cmake b/cmake/modules/shared/CompilerSettings.cmake index b3c0fe3aa..cd961f6de 100644 --- a/cmake/modules/shared/CompilerSettings.cmake +++ b/cmake/modules/shared/CompilerSettings.cmake @@ -26,11 +26,11 @@ add_compile_options( if(APPLE) add_compile_definitions("SWT_TARGET_OS_APPLE") endif() -set(SWT_NO_EXIT_TESTS_LIST "iOS" "watchOS" "tvOS" "visionOS" "WASI" "Android") +set(SWT_NO_EXIT_TESTS_LIST "iOS" "watchOS" "tvOS" "visionOS" "WASI") if(CMAKE_SYSTEM_NAME IN_LIST SWT_NO_EXIT_TESTS_LIST) add_compile_definitions("SWT_NO_EXIT_TESTS") endif() -set(SWT_NO_PROCESS_SPAWNING_LIST "iOS" "watchOS" "tvOS" "visionOS" "WASI" "Android") +set(SWT_NO_PROCESS_SPAWNING_LIST "iOS" "watchOS" "tvOS" "visionOS" "WASI") if(CMAKE_SYSTEM_NAME IN_LIST SWT_NO_PROCESS_SPAWNING_LIST) add_compile_definitions("SWT_NO_PROCESS_SPAWNING") endif() From 670997c4ebe82a26ff43b66d3c00785da64ec3c9 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 27 Oct 2025 14:38:07 -0400 Subject: [PATCH 02/22] Fix typos --- Package.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index c64c77ca3..96d80c388 100644 --- a/Package.swift +++ b/Package.swift @@ -378,7 +378,7 @@ extension Array where Element == PackageDescription.SwiftSetting { .define("SWT_TARGET_OS_APPLE", .whenApple()), .define("SWT_NO_EXIT_TESTS", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi]))), - .define("SWT_NO_PROCESS_SPAWNING", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android]))), + .define("SWT_NO_PROCESS_SPAWNING", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi]))), .define("SWT_NO_SNAPSHOT_TYPES", .whenEmbedded(or: .whenApple(false))), .define("SWT_NO_DYNAMIC_LINKING", .whenEmbedded(or: .when(platforms: [.wasi]))), .define("SWT_NO_PIPES", .whenEmbedded(or: .when(platforms: [.wasi]))), @@ -436,7 +436,7 @@ extension Array where Element == PackageDescription.CXXSetting { var result = Self() result += [ - .define("SWT_NO_EXIT_TESTS", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android]))), + .define("SWT_NO_EXIT_TESTS", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi]))), .define("SWT_NO_PROCESS_SPAWNING", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi]))), .define("SWT_NO_SNAPSHOT_TYPES", .whenEmbedded(or: .whenApple(false))), .define("SWT_NO_DYNAMIC_LINKING", .whenEmbedded(or: .when(platforms: [.wasi]))), From 7e4bb7fe45f40c66cf4ca263ade8fa4acfdd905c Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 27 Oct 2025 14:39:52 -0400 Subject: [PATCH 03/22] Fix errors --- Sources/Testing/ExitTests/SpawnProcess.swift | 4 ++-- Sources/_TestingInternals/include/Stubs.h | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 4fb6e50e2..f38039896 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -74,9 +74,9 @@ func spawnExecutable( ) throws -> ProcessID { // Darwin and Linux differ in their optionality for the posix_spawn types we // use, so use this typealias to paper over the differences. -#if SWT_TARGET_OS_APPLE || os(FreeBSD) || os(OpenBSD) +#if SWT_TARGET_OS_APPLE || os(FreeBSD) || os(OpenBSD) || os(Android) typealias P = T? -#elseif os(Linux) || os(Android) +#elseif os(Linux) typealias P = T #endif diff --git a/Sources/_TestingInternals/include/Stubs.h b/Sources/_TestingInternals/include/Stubs.h index 636ea9aff..1e743f2f2 100644 --- a/Sources/_TestingInternals/include/Stubs.h +++ b/Sources/_TestingInternals/include/Stubs.h @@ -126,7 +126,6 @@ static char *_Nullable *_Null_unspecified swt_environ(void) { } #endif -#if !defined(__ANDROID__) #if __has_include() && defined(si_pid) /// Get the value of the `si_pid` field of a `siginfo_t` structure. /// @@ -150,7 +149,6 @@ static int swt_siginfo_t_si_status(const siginfo_t *siginfo) { return siginfo->si_status; } #endif -#endif /// Get the value of `EEXIST`. /// From 7e54064d67a1a7cbf4c712ffc15768c0ca4bac27 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 27 Oct 2025 14:43:20 -0400 Subject: [PATCH 04/22] Differing optionality --- Sources/Testing/ExitTests/SpawnProcess.swift | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index f38039896..6d970967a 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -74,10 +74,15 @@ func spawnExecutable( ) throws -> ProcessID { // Darwin and Linux differ in their optionality for the posix_spawn types we // use, so use this typealias to paper over the differences. -#if SWT_TARGET_OS_APPLE || os(FreeBSD) || os(OpenBSD) || os(Android) - typealias P = T? +#if SWT_TARGET_OS_APPLE || os(FreeBSD) || os(OpenBSD) + typealias posix_spawn_file_actions_p = posix_spawn_file_actions_t? + typealias posix_spawnattr_p = posix_spawnattr_t? #elseif os(Linux) - typealias P = T + typealias posix_spawn_file_actions_p = posix_spawn_file_actions_t + typealias posix_spawnattr_p = posix_spawnattr_t +#elseif os(Android) + typealias posix_spawn_file_actions_p = posix_spawn_file_actions_t? + typealias posix_spawnattr_p = posix_spawnattr_t #endif #if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) From d3ebaa0a10ad7d89a217db6d4019905a1fa48c90 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 27 Oct 2025 14:44:40 -0400 Subject: [PATCH 05/22] Seriously Jonathan --- Sources/Testing/ExitTests/SpawnProcess.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 6d970967a..8c05b2393 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -86,7 +86,7 @@ func spawnExecutable( #endif #if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) - return try withUnsafeTemporaryAllocation(of: P.self, capacity: 1) { fileActions in + return try withUnsafeTemporaryAllocation(of: posix_spawn_file_actions_p.self, capacity: 1) { fileActions in let fileActions = fileActions.baseAddress! let fileActionsInitialized = posix_spawn_file_actions_init(fileActions) guard 0 == fileActionsInitialized else { @@ -96,7 +96,7 @@ func spawnExecutable( _ = posix_spawn_file_actions_destroy(fileActions) } - return try withUnsafeTemporaryAllocation(of: P.self, capacity: 1) { attrs in + return try withUnsafeTemporaryAllocation(of: posix_spawnattr_p.self, capacity: 1) { attrs in let attrs = attrs.baseAddress! let attrsInitialized = posix_spawnattr_init(attrs) guard 0 == attrsInitialized else { From da399c4a17167d7bc2e0ffec6cac83f016bd094c Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 27 Oct 2025 15:20:33 -0400 Subject: [PATCH 06/22] Type differs after init, sigh --- Sources/Testing/ExitTests/SpawnProcess.swift | 33 +++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 8c05b2393..088dfa604 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -72,23 +72,26 @@ func spawnExecutable( standardError: borrowing FileHandle? = nil, additionalFileHandles: [UnsafePointer] = [] ) throws -> ProcessID { - // Darwin and Linux differ in their optionality for the posix_spawn types we - // use, so use this typealias to paper over the differences. + // Darwin, the BSDs, Linux, and Android all differ in their optionality for + // the posix_spawn types we use, so use this typealias and helper function to + // paper over the differences. #if SWT_TARGET_OS_APPLE || os(FreeBSD) || os(OpenBSD) - typealias posix_spawn_file_actions_p = posix_spawn_file_actions_t? - typealias posix_spawnattr_p = posix_spawnattr_t? + typealias P = T? + func asArgument(_ p: UnsafeMutableBufferPointer) -> UnsafeMutablePointer { p.baseAddress! } #elseif os(Linux) - typealias posix_spawn_file_actions_p = posix_spawn_file_actions_t - typealias posix_spawnattr_p = posix_spawnattr_t + typealias P = T + func asArgument(_ p: UnsafeMutableBufferPointer) -> UnsafeMutablePointer { p.baseAddress! } #elseif os(Android) - typealias posix_spawn_file_actions_p = posix_spawn_file_actions_t? - typealias posix_spawnattr_p = posix_spawnattr_t + typealias P = T? + func asArgument(_ p: UnsafeMutableBufferPointer) -> UnsafeMutablePointer { + UnsafeMutableRawPointer(p.baseAddress!).bindMemory(to: T.self, capacity: 1) + } #endif -#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) - return try withUnsafeTemporaryAllocation(of: posix_spawn_file_actions_p.self, capacity: 1) { fileActions in - let fileActions = fileActions.baseAddress! - let fileActionsInitialized = posix_spawn_file_actions_init(fileActions) +#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) + return try withUnsafeTemporaryAllocation(of: P.self, capacity: 1) { fileActions in + let fileActionsInitialized = posix_spawn_file_actions_init(fileActions.baseAddress!) + let fileActions = asArgument(fileActions) guard 0 == fileActionsInitialized else { throw CError(rawValue: fileActionsInitialized) } @@ -96,9 +99,9 @@ func spawnExecutable( _ = posix_spawn_file_actions_destroy(fileActions) } - return try withUnsafeTemporaryAllocation(of: posix_spawnattr_p.self, capacity: 1) { attrs in - let attrs = attrs.baseAddress! - let attrsInitialized = posix_spawnattr_init(attrs) + return try withUnsafeTemporaryAllocation(of: P.self, capacity: 1) { attrs in + let attrsInitialized = posix_spawnattr_init(attrs.baseAddress!) + let attrs = asArgument(attrs) guard 0 == attrsInitialized else { throw CError(rawValue: attrsInitialized) } From 0eecc00e45cf031262b9c3e54fc25654a8d16943 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 27 Oct 2025 15:20:33 -0400 Subject: [PATCH 07/22] Expose si_pid and si_status (renaming in Swift doesn't seem to want to work on Android, cut the knot) --- Sources/Testing/ExitTests/WaitFor.swift | 6 +++--- Sources/_TestingInternals/include/Stubs.h | 10 ++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/Sources/Testing/ExitTests/WaitFor.swift b/Sources/Testing/ExitTests/WaitFor.swift index dc6f74ee9..cdf0e500d 100644 --- a/Sources/Testing/ExitTests/WaitFor.swift +++ b/Sources/Testing/ExitTests/WaitFor.swift @@ -29,9 +29,9 @@ private func _blockAndWait(for pid: consuming pid_t) throws -> ExitStatus { if 0 == waitid(P_PID, id_t(pid), &siginfo, WEXITED) { switch siginfo.si_code { case .init(CLD_EXITED): - return .exitCode(siginfo.si_status) + return .exitCode(swt_siginfo_t_si_status(siginfo)) case .init(CLD_KILLED), .init(CLD_DUMPED): - return .signal(siginfo.si_status) + return .signal(swt_siginfo_t_si_status(siginfo)) default: throw SystemError(description: "Unexpected siginfo_t value. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new and include this information: \(String(reflecting: siginfo))") } @@ -146,7 +146,7 @@ private let _createWaitThread: Void = { // continuation (if available) before reaping. var siginfo = siginfo_t() if 0 == waitid(P_ALL, 0, &siginfo, WEXITED | WNOWAIT) { - if case let pid = siginfo.si_pid, pid != 0 { + if case let pid = swt_siginfo_t_si_pid(siginfo), pid != 0 { let continuation = _withLockedChildProcessContinuations { childProcessContinuations, _ in childProcessContinuations.removeValue(forKey: pid) } diff --git a/Sources/_TestingInternals/include/Stubs.h b/Sources/_TestingInternals/include/Stubs.h index 1e743f2f2..ad96a85f4 100644 --- a/Sources/_TestingInternals/include/Stubs.h +++ b/Sources/_TestingInternals/include/Stubs.h @@ -132,9 +132,8 @@ static char *_Nullable *_Null_unspecified swt_environ(void) { /// This function is provided because `si_pid` is a complex macro on some /// platforms and cannot be imported directly into Swift. It is renamed back to /// `siginfo_t.si_pid` in Swift. -SWT_SWIFT_NAME(getter:siginfo_t.si_pid(self:)) -static pid_t swt_siginfo_t_si_pid(const siginfo_t *siginfo) { - return siginfo->si_pid; +static pid_t swt_siginfo_t_si_pid(siginfo_t siginfo) { + return siginfo.si_pid; } #endif @@ -144,9 +143,8 @@ static pid_t swt_siginfo_t_si_pid(const siginfo_t *siginfo) { /// This function is provided because `si_status` is a complex macro on some /// platforms and cannot be imported directly into Swift. It is renamed back to /// `siginfo_t.si_status` in Swift. -SWT_SWIFT_NAME(getter:siginfo_t.si_status(self:)) -static int swt_siginfo_t_si_status(const siginfo_t *siginfo) { - return siginfo->si_status; +static int swt_siginfo_t_si_status(siginfo_t siginfo) { + return siginfo.si_status; } #endif From b665223d5f1ffb03e7566c8c412fe494b263a566 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 27 Oct 2025 15:52:19 -0400 Subject: [PATCH 08/22] si_status and si_pid aren't macros on Darwin --- Sources/_TestingInternals/include/Stubs.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/_TestingInternals/include/Stubs.h b/Sources/_TestingInternals/include/Stubs.h index ad96a85f4..a37f61089 100644 --- a/Sources/_TestingInternals/include/Stubs.h +++ b/Sources/_TestingInternals/include/Stubs.h @@ -126,7 +126,8 @@ static char *_Nullable *_Null_unspecified swt_environ(void) { } #endif -#if __has_include() && defined(si_pid) +#if !SWT_NO_PROCESS_SPAWNING && __has_include() +#if defined(__APPLE__) || defined(si_pid) /// Get the value of the `si_pid` field of a `siginfo_t` structure. /// /// This function is provided because `si_pid` is a complex macro on some @@ -137,7 +138,7 @@ static pid_t swt_siginfo_t_si_pid(siginfo_t siginfo) { } #endif -#if __has_include() && defined(si_status) +#if defined(__APPLE__) || defined(si_status) /// Get the value of the `si_status` field of a `siginfo_t` structure. /// /// This function is provided because `si_status` is a complex macro on some @@ -147,6 +148,7 @@ static int swt_siginfo_t_si_status(siginfo_t siginfo) { return siginfo.si_status; } #endif +#endif /// Get the value of `EEXIST`. /// From 7c6a47fcbd43a92bd2745260a627f602576dddcd Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 30 Oct 2025 14:42:22 -0400 Subject: [PATCH 09/22] Disable core files and tombstones --- Sources/Testing/ExitTests/ExitTest.swift | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 56d93a989..f7af55812 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -226,7 +226,20 @@ extension ExitTest { var rl = rlimit(rlim_cur: 0, rlim_max: 0) _ = setrlimit(RLIMIT_CORE, &rl) #elseif os(Android) - // TODO: "tombstoned_intercept"? + // Android inherits the RLIMIT_CORE=1 special case from Linux. + // SEE: https://android.googlesource.com/kernel/common/+/refs/heads/android-mainline/fs/coredump.c#978 + var rl = rlimit(rlim_cur: 1, rlim_max: 1) + _ = setrlimit(CInt(RLIMIT_CORE.rawValue), &rl) + + // In addition, Android installs signal handlers in native processes that + // cause the system to generate "tombstone" files. Suppress those too by + // resetting all signal handlers to SIG_DFL. debuggerd_register_handlers() + // is not exported, so we must manually walk all the signals it handles. + // SEE: https://android.googlesource.com/platform/system/core/+/main/debuggerd/include/debuggerd/handler.h#81 + let BIONIC_SIGNAL_DEBUGGER = __SIGRTMIN + 3 + for sig in [SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGSEGV, SIGSTKFLT, SIGSYS, SIGTRAP, BIONIC_SIGNAL_DEBUGGER] { + _ = signal(sig, SIG_DFL) + } #elseif os(Windows) // On Windows, similarly disable Windows Error Reporting and the Windows // Error Reporting UI. Note we expect to be the first component to call From d64048d425b542749cae461bc7f9e2848ea93264 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 30 Oct 2025 15:08:48 -0400 Subject: [PATCH 10/22] Fixups --- Sources/Testing/ExitTests/ExitTest.swift | 4 ++-- Sources/_TestingInternals/include/Stubs.h | 15 +++++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index f7af55812..325661f10 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -229,7 +229,7 @@ extension ExitTest { // Android inherits the RLIMIT_CORE=1 special case from Linux. // SEE: https://android.googlesource.com/kernel/common/+/refs/heads/android-mainline/fs/coredump.c#978 var rl = rlimit(rlim_cur: 1, rlim_max: 1) - _ = setrlimit(CInt(RLIMIT_CORE.rawValue), &rl) + _ = setrlimit(RLIMIT_CORE, &rl) // In addition, Android installs signal handlers in native processes that // cause the system to generate "tombstone" files. Suppress those too by @@ -238,7 +238,7 @@ extension ExitTest { // SEE: https://android.googlesource.com/platform/system/core/+/main/debuggerd/include/debuggerd/handler.h#81 let BIONIC_SIGNAL_DEBUGGER = __SIGRTMIN + 3 for sig in [SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGSEGV, SIGSTKFLT, SIGSYS, SIGTRAP, BIONIC_SIGNAL_DEBUGGER] { - _ = signal(sig, SIG_DFL) + _ = signal(sig, swt_SIG_DFL()) } #elseif os(Windows) // On Windows, similarly disable Windows Error Reporting and the Windows diff --git a/Sources/_TestingInternals/include/Stubs.h b/Sources/_TestingInternals/include/Stubs.h index a37f61089..e09114458 100644 --- a/Sources/_TestingInternals/include/Stubs.h +++ b/Sources/_TestingInternals/include/Stubs.h @@ -131,8 +131,7 @@ static char *_Nullable *_Null_unspecified swt_environ(void) { /// Get the value of the `si_pid` field of a `siginfo_t` structure. /// /// This function is provided because `si_pid` is a complex macro on some -/// platforms and cannot be imported directly into Swift. It is renamed back to -/// `siginfo_t.si_pid` in Swift. +/// platforms and cannot be imported directly into Swift. static pid_t swt_siginfo_t_si_pid(siginfo_t siginfo) { return siginfo.si_pid; } @@ -142,12 +141,20 @@ static pid_t swt_siginfo_t_si_pid(siginfo_t siginfo) { /// Get the value of the `si_status` field of a `siginfo_t` structure. /// /// This function is provided because `si_status` is a complex macro on some -/// platforms and cannot be imported directly into Swift. It is renamed back to -/// `siginfo_t.si_status` in Swift. +/// platforms and cannot be imported directly into Swift. static int swt_siginfo_t_si_status(siginfo_t siginfo) { return siginfo.si_status; } #endif + +/// Get the default signal handler. +/// +/// This function is provided because `SIG_DFL` is a complex macro on some +/// platforms and cannot be imported directly into Swift. +static sig_t swt_SIG_DFL(void) { + return SIG_DFL; +} + #endif /// Get the value of `EEXIST`. From c3f216367189d52f47fa4374951572a00dd4e7d8 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 30 Oct 2025 15:11:20 -0400 Subject: [PATCH 11/22] Fix platform-specific missing bits --- Sources/Testing/ExitTests/ExitTest.swift | 2 +- Sources/Testing/ExitTests/SpawnProcess.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 325661f10..5d7891d7b 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -679,7 +679,7 @@ extension ExitTest { Environment.setVariable(nil, named: name) var fd: CInt? -#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) +#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) fd = CInt(environmentVariable) #elseif os(Windows) if let handle = UInt(environmentVariable).flatMap(HANDLE.init(bitPattern:)) { diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 088dfa604..8c0241a28 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -88,7 +88,7 @@ func spawnExecutable( } #endif -#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) +#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) return try withUnsafeTemporaryAllocation(of: P.self, capacity: 1) { fileActions in let fileActionsInitialized = posix_spawn_file_actions_init(fileActions.baseAddress!) let fileActions = asArgument(fileActions) From e297fd4fe4bc0790b87bbd277ed6f084733c712e Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 30 Oct 2025 15:33:08 -0400 Subject: [PATCH 12/22] More fixups --- Sources/Testing/ExitTests/SpawnProcess.swift | 35 +++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 8c0241a28..14cc36572 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -77,21 +77,14 @@ func spawnExecutable( // paper over the differences. #if SWT_TARGET_OS_APPLE || os(FreeBSD) || os(OpenBSD) typealias P = T? - func asArgument(_ p: UnsafeMutableBufferPointer) -> UnsafeMutablePointer { p.baseAddress! } -#elseif os(Linux) +#elseif os(Linux) || os(Android) typealias P = T - func asArgument(_ p: UnsafeMutableBufferPointer) -> UnsafeMutablePointer { p.baseAddress! } -#elseif os(Android) - typealias P = T? - func asArgument(_ p: UnsafeMutableBufferPointer) -> UnsafeMutablePointer { - UnsafeMutableRawPointer(p.baseAddress!).bindMemory(to: T.self, capacity: 1) - } #endif #if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) return try withUnsafeTemporaryAllocation(of: P.self, capacity: 1) { fileActions in - let fileActionsInitialized = posix_spawn_file_actions_init(fileActions.baseAddress!) - let fileActions = asArgument(fileActions) + let fileActions = fileActions.baseAddress! + let fileActionsInitialized = posix_spawn_file_actions_init(fileActions) guard 0 == fileActionsInitialized else { throw CError(rawValue: fileActionsInitialized) } @@ -100,8 +93,14 @@ func spawnExecutable( } return try withUnsafeTemporaryAllocation(of: P.self, capacity: 1) { attrs in - let attrsInitialized = posix_spawnattr_init(attrs.baseAddress!) - let attrs = asArgument(attrs) + let attrs = attrs.baseAddress! +#if os(Android) + let attrsInitialized = attrs.withMemoryRebound(to: Optional.self, capacity: 1) { attrs in + posix_spawnattr_init(attrs) + } +#else + let attrsInitialized = posix_spawnattr_init(attrs) +#endif guard 0 == attrsInitialized else { throw CError(rawValue: attrsInitialized) } @@ -237,7 +236,19 @@ func spawnExecutable( } var pid = pid_t() +#if os(Android) + let processSpawned = fileActions.withMemoryRebound(to: posix_spawn_file_actions_t?.self, capacity: 1) { fileActions in + attrs.withMemoryRebound(to: posix_spawnattr_t?.self, capacity: 1) { attrs in + argv.withUnsafeBufferPointer { argv in + argv.withMemoryRebound(to: UnsafeMutablePointer.self) { argv in + posix_spawn(&pid, executablePath, fileActions, attrs, argv.baseAddress!, environ) + } + } + } + } +#else let processSpawned = posix_spawn(&pid, executablePath, fileActions, attrs, argv, environ) +#endif guard 0 == processSpawned else { throw CError(rawValue: processSpawned) } From 8a1c30bf39ea8594ad3d5a1e9f45ed5df5c92614 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 30 Oct 2025 16:12:12 -0400 Subject: [PATCH 13/22] Work around posix_spawn nullability problems --- Package.swift | 17 +++++++++++++++-- Sources/Testing/ExitTests/SpawnProcess.swift | 10 +--------- Sources/_TestingInternals/include/Stubs.h | 14 ++++++++++++++ 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/Package.swift b/Package.swift index 92b4c011f..a858b88d8 100644 --- a/Package.swift +++ b/Package.swift @@ -363,6 +363,19 @@ extension BuildSettingCondition { } } +/// A constant set of platforms including Android when the compiler is 6.3 or +/// newer. +/// +/// This constant is used to conditionally enable features on Android only on +/// newer compilers. +static let androidIfCompiler6_3: some Collection = { +#if compiler(>=6.3) + CollectionOfOne(.android) +#else + EmptyCollection() +#endif +}() + extension Array where Element == PackageDescription.SwiftSetting { /// Settings intended to be applied to every Swift target in this package. /// Analogous to project-level build settings in an Xcode project. @@ -398,8 +411,8 @@ extension Array where Element == PackageDescription.SwiftSetting { .define("SWT_TARGET_OS_APPLE", .whenApple()), - .define("SWT_NO_EXIT_TESTS", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi]))), - .define("SWT_NO_PROCESS_SPAWNING", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi]))), + .define("SWT_NO_EXIT_TESTS", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi] + androidIfCompiler6_3))), + .define("SWT_NO_PROCESS_SPAWNING", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi] + androidIfCompiler6_3))), .define("SWT_NO_SNAPSHOT_TYPES", .whenEmbedded(or: .whenApple(false))), .define("SWT_NO_DYNAMIC_LINKING", .whenEmbedded(or: .when(platforms: [.wasi]))), .define("SWT_NO_PIPES", .whenEmbedded(or: .when(platforms: [.wasi]))), diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 14cc36572..063872336 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -237,15 +237,7 @@ func spawnExecutable( var pid = pid_t() #if os(Android) - let processSpawned = fileActions.withMemoryRebound(to: posix_spawn_file_actions_t?.self, capacity: 1) { fileActions in - attrs.withMemoryRebound(to: posix_spawnattr_t?.self, capacity: 1) { attrs in - argv.withUnsafeBufferPointer { argv in - argv.withMemoryRebound(to: UnsafeMutablePointer.self) { argv in - posix_spawn(&pid, executablePath, fileActions, attrs, argv.baseAddress!, environ) - } - } - } - } + let processSpawned = swt_posix_spawn(&pid, executablePath, fileActions, attrs, argv, environ) #else let processSpawned = posix_spawn(&pid, executablePath, fileActions, attrs, argv, environ) #endif diff --git a/Sources/_TestingInternals/include/Stubs.h b/Sources/_TestingInternals/include/Stubs.h index e09114458..51731bc8c 100644 --- a/Sources/_TestingInternals/include/Stubs.h +++ b/Sources/_TestingInternals/include/Stubs.h @@ -155,6 +155,20 @@ static sig_t swt_SIG_DFL(void) { return SIG_DFL; } +#if defined(__ANDROID__) +/// Call `posix_spawn(3)`. +/// +/// This function is provided because the nullability for `posix_spawn(3)` is +/// incorrectly specified in the Android NDK. +static int swt_posix_spawn( + pid_t *pid, const char *path, + const posix_spawn_file_actions_t _Nonnull *_Nullable fileActions, + const posix_spawnattr_t _Nonnull *_Nullable attrs, + char *const _Nullable argv[_Nonnull], char *const _Nullable env[_Nonnull] +) { + return posix_spawn(pid, path, fileActions, attrs, argv, env); +} +#endif #endif /// Get the value of `EEXIST`. From 947ad22f5ae65ddaccd32191621e94154110cb5d Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 30 Oct 2025 16:13:19 -0400 Subject: [PATCH 14/22] Optional -> ? --- Sources/Testing/ExitTests/SpawnProcess.swift | 2 +- foo.txt | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 foo.txt diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 14cc36572..b4ba73c16 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -95,7 +95,7 @@ func spawnExecutable( return try withUnsafeTemporaryAllocation(of: P.self, capacity: 1) { attrs in let attrs = attrs.baseAddress! #if os(Android) - let attrsInitialized = attrs.withMemoryRebound(to: Optional.self, capacity: 1) { attrs in + let attrsInitialized = attrs.withMemoryRebound(to: posix_spawnattr_t?.self, capacity: 1) { attrs in posix_spawnattr_init(attrs) } #else diff --git a/foo.txt b/foo.txt new file mode 100644 index 000000000..e5e3b6e74 --- /dev/null +++ b/foo.txt @@ -0,0 +1,2 @@ +int posix_spawn(pid_t* _Nullable __pid, const char* _Nonnull __path, const posix_spawn_file_actions_t _Nullable * _Nullable __actions, const posix_spawnattr_t _Nullable * _Nullable __attr, char* const _Nonnull __argv[_Nonnull], char* const _Nullable __env[_Nullable]) __INTRODUCED_IN(28); +int posix_spawnp(pid_t* _Nullable __pid, const char* _Nonnull __file, const posix_spawn_file_actions_t _Nullable * _Nullable __actions, const posix_spawnattr_t _Nullable * _Nullable __attr, char* const _Nonnull __argv[_Nonnull], char* const _Nullable __env[_Nullable]) __INTRODUCED_IN(28); From c6fe393b63fa504da78b57ea7dcad20b75c78a17 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 30 Oct 2025 16:14:24 -0400 Subject: [PATCH 15/22] Fix androidIfCompiler6_3 sigh --- Package.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index a858b88d8..6cde28ea4 100644 --- a/Package.swift +++ b/Package.swift @@ -368,13 +368,13 @@ extension BuildSettingCondition { /// /// This constant is used to conditionally enable features on Android only on /// newer compilers. -static let androidIfCompiler6_3: some Collection = { +nonisolated(unsafe) var androidIfCompiler6_3: some Collection { #if compiler(>=6.3) CollectionOfOne(.android) #else EmptyCollection() #endif -}() +} extension Array where Element == PackageDescription.SwiftSetting { /// Settings intended to be applied to every Swift target in this package. From e9fc4539ca1eece7da1ea5879ac388b142d8e8b5 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 30 Oct 2025 16:17:52 -0400 Subject: [PATCH 16/22] Add nullable --- Sources/_TestingInternals/include/Stubs.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/_TestingInternals/include/Stubs.h b/Sources/_TestingInternals/include/Stubs.h index 51731bc8c..afbc5204d 100644 --- a/Sources/_TestingInternals/include/Stubs.h +++ b/Sources/_TestingInternals/include/Stubs.h @@ -161,7 +161,7 @@ static sig_t swt_SIG_DFL(void) { /// This function is provided because the nullability for `posix_spawn(3)` is /// incorrectly specified in the Android NDK. static int swt_posix_spawn( - pid_t *pid, const char *path, + pid_t *_Nullable pid, const char *path, const posix_spawn_file_actions_t _Nonnull *_Nullable fileActions, const posix_spawnattr_t _Nonnull *_Nullable attrs, char *const _Nullable argv[_Nonnull], char *const _Nullable env[_Nonnull] From fc7023c85ee6e1e93fb606674d4c07b269144b57 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 30 Oct 2025 16:18:43 -0400 Subject: [PATCH 17/22] Add _Null_unspecified --- Sources/_TestingInternals/include/Stubs.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/_TestingInternals/include/Stubs.h b/Sources/_TestingInternals/include/Stubs.h index afbc5204d..130cde4fb 100644 --- a/Sources/_TestingInternals/include/Stubs.h +++ b/Sources/_TestingInternals/include/Stubs.h @@ -151,7 +151,7 @@ static int swt_siginfo_t_si_status(siginfo_t siginfo) { /// /// This function is provided because `SIG_DFL` is a complex macro on some /// platforms and cannot be imported directly into Swift. -static sig_t swt_SIG_DFL(void) { +static sig_t _Null_unspecified swt_SIG_DFL(void) { return SIG_DFL; } From d548176bcaf65b72846c16a40ef24277de2d30a3 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 5 Nov 2025 23:08:41 -0500 Subject: [PATCH 18/22] Avoid non-portable sig_t in SIG_DFL stub --- Sources/_TestingInternals/include/Stubs.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/_TestingInternals/include/Stubs.h b/Sources/_TestingInternals/include/Stubs.h index 130cde4fb..1f02128a3 100644 --- a/Sources/_TestingInternals/include/Stubs.h +++ b/Sources/_TestingInternals/include/Stubs.h @@ -147,13 +147,15 @@ static int swt_siginfo_t_si_status(siginfo_t siginfo) { } #endif +#if __has_include() && !defined(__wasi__) /// Get the default signal handler. /// /// This function is provided because `SIG_DFL` is a complex macro on some /// platforms and cannot be imported directly into Swift. -static sig_t _Null_unspecified swt_SIG_DFL(void) { +static __typeof__(SIG_DFL) _Null_unspecified swt_SIG_DFL(void) { return SIG_DFL; } +#endif #if defined(__ANDROID__) /// Call `posix_spawn(3)`. From 16e5804c0f9e5151872ea89d9039ac027ae22fb1 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 21 Nov 2025 09:40:02 -0500 Subject: [PATCH 19/22] Use close_range(2) --- Documentation/EnvironmentVariables.md | 2 +- Sources/Testing/ExitTests/ExitTest.swift | 18 +++++++++++++++++- Sources/Testing/ExitTests/SpawnProcess.swift | 8 +++----- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/Documentation/EnvironmentVariables.md b/Documentation/EnvironmentVariables.md index a73ffdc65..f707c0ada 100644 --- a/Documentation/EnvironmentVariables.md +++ b/Documentation/EnvironmentVariables.md @@ -51,7 +51,7 @@ names prefixed with `SWT_`. |-|:-:|-| | `SWT_BACKCHANNEL` | `CInt`/`HANDLE` | A file descriptor (handle on Windows) to which the exit test's events are written. | | `SWT_CAPTURED_VALUES` | `CInt`/`HANDLE` | A file descriptor (handle on Windows) containing captured values passed to the exit test. | -| `SWT_CLOSEFROM` | `CInt` | Used on OpenBSD to emulate `posix_spawn_file_actions_addclosefrom_np()`. | +| `SWT_CLOSEFROM` | `CInt` | Used on OpenBSD and Android to emulate `posix_spawn_file_actions_addclosefrom_np()`. | | `SWT_EXIT_TEST_ID` | `String` (JSON) | Specifies which exit test to run. | | `XCTestBundlePath`\* | `String` | Used on Apple platforms to determine if Xcode is hosting the test run. | diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 5d7891d7b..9f7ddfacf 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -191,6 +191,17 @@ extension ExitTest { // MARK: - Invocation +#if os(Android) && !SWT_NO_DYNAMIC_LINKING +/// Close a range of file descriptors. +/// +/// This function declaration is provided because `close_range()` is only +/// declared if `_GNU_SOURCE` is set, but setting it causes build errors due to +/// conflicts with Swift's Glibc module. +private let _close_range = symbol(named: "close_range").map { + castCFunction(at: $0, to: (@convention(c) (CUnsignedInt, CUnsignedInt, CInt) -> CInt).self) +} +#endif + @_spi(ForToolsIntegrationOnly) extension ExitTest { /// Disable crash reporting, crash logging, or core dumps for the current @@ -295,11 +306,16 @@ extension ExitTest { } #endif -#if os(OpenBSD) +#if os(OpenBSD) || (os(Android) && !SWT_NO_DYNAMIC_LINKING) // OpenBSD does not have posix_spawn_file_actions_addclosefrom_np(). // However, it does have closefrom(2), which we call here as a best effort. + // Android has close_range(2) which serves the same purpose. if let from = Environment.variable(named: "SWT_CLOSEFROM").flatMap(CInt.init) { +#if os(OpenBSD) _ = closefrom(from) +#else + _ = _close_range?(CUnsignedInt(bitPattern: from), .max, 0) +#endif } #endif diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 04d0ec358..2a3a3a3ef 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -196,15 +196,13 @@ func spawnExecutable( // `posix_spawn_file_actions_addclosefrom_np`, and FreeBSD does not use // glibc nor guard symbols behind `_DEFAULT_SOURCE`. _ = posix_spawn_file_actions_addclosefrom_np(fileActions, highestFD + 1) -#elseif os(OpenBSD) +#elseif os(OpenBSD) || (os(Android) && !SWT_NO_DYNAMIC_LINKING) // OpenBSD does not have posix_spawn_file_actions_addclosefrom_np(). // However, it does have closefrom(2), which we can call from within the - // spawned child process if we control its execution. + // spawned child process if we control its execution. Android has + // close_range(2) which serves the same purpose. var environment = environment environment["SWT_CLOSEFROM"] = String(describing: highestFD + 1) -#elseif os(Android) - // Android does not have posix_spawn_file_actions_addclosefrom_np() nor - // closefrom(2), so we don't attempt this operation there. #else #warning("Platform-specific implementation missing: cannot close unused file descriptors") #endif From e83b9e137d148eae059faf015239e52fc80e2e83 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 21 Nov 2025 09:44:32 -0500 Subject: [PATCH 20/22] More availability annotations --- Sources/Testing/ExitTests/WaitFor.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/Testing/ExitTests/WaitFor.swift b/Sources/Testing/ExitTests/WaitFor.swift index cdf0e500d..76c36eba4 100644 --- a/Sources/Testing/ExitTests/WaitFor.swift +++ b/Sources/Testing/ExitTests/WaitFor.swift @@ -20,6 +20,7 @@ internal import _TestingInternals /// /// - Throws: If the exit status of the process with ID `pid` cannot be /// determined (i.e. it does not represent an exit condition.) +@available(Android 28, *) private func _blockAndWait(for pid: consuming pid_t) throws -> ExitStatus { let pid = consume pid @@ -80,6 +81,7 @@ func wait(for pid: consuming pid_t) async throws -> ExitStatus { } #elseif SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) /// A mapping of awaited child PIDs to their corresponding Swift continuations. +@available(Android 28, *) private nonisolated(unsafe) let _childProcessContinuations = { let result = ManagedBuffer<[pid_t: CheckedContinuation], pthread_mutex_t>.create( minimumCapacity: 1, @@ -101,6 +103,7 @@ private nonisolated(unsafe) let _childProcessContinuations = { /// - Returns: Whatever is returned by `body`. /// /// - Throws: Whatever is thrown by `body`. +@available(Android 28, *) private func _withLockedChildProcessContinuations( _ body: ( _ childProcessContinuations: inout [pid_t: CheckedContinuation], @@ -119,6 +122,7 @@ private func _withLockedChildProcessContinuations( /// A condition variable used to suspend the waiter thread created by /// `_createWaitThread()` when there are no child processes to await. +@available(Android 28, *) private nonisolated(unsafe) let _waitThreadNoChildrenCondition = { let result = UnsafeMutablePointer.allocate(capacity: 1) _ = pthread_cond_init(result, nil) @@ -138,6 +142,7 @@ private let _pthread_setname_np = symbol(named: "pthread_setname_np").map { /// Create a waiter thread that is responsible for waiting for child processes /// to exit. +@available(Android 28, *) private let _createWaitThread: Void = { // The body of the thread's run loop. func waitForAnyChild() { From 9805a8ae739fb47caf061e0a86afd28c60a949b7 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 21 Nov 2025 09:47:05 -0500 Subject: [PATCH 21/22] More availability annotations --- .../Testing/ABI/EntryPoints/EntryPoint.swift | 7 +++++-- .../ExitTests/ExitTest.CapturedValue.swift | 2 ++ Sources/Testing/ExitTests/ExitTest.swift | 6 ++++++ Sources/Testing/ExitTests/WaitFor.swift | 1 + .../ExpectationChecking+Macro.swift | 2 ++ Sources/Testing/Running/Configuration.swift | 20 +++++++++++++++++-- Sources/Testing/Test+Cancellation.swift | 4 +++- 7 files changed, 37 insertions(+), 5 deletions(-) diff --git a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift index 6163e7bd1..107372291 100644 --- a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift @@ -31,7 +31,8 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha do { #if !SWT_NO_EXIT_TESTS // If an exit test was specified, run it. `exitTest` returns `Never`. - if let exitTest = ExitTest.findInEnvironmentForEntryPoint() { + if #available(Android 28, *), + let exitTest = ExitTest.findInEnvironmentForEntryPoint() { await exitTest() } #endif @@ -674,7 +675,9 @@ public func configurationForEntryPoint(from args: __CommandLineArguments_v0) thr #if !SWT_NO_EXIT_TESTS // Enable exit test handling via __swiftPMEntryPoint(). - configuration.exitTestHandler = ExitTest.handlerForEntryPoint() + if #available(Android 28, *) { + configuration.exitTestHandler = ExitTest.handlerForEntryPoint() + } #endif // Warning issues (experimental). diff --git a/Sources/Testing/ExitTests/ExitTest.CapturedValue.swift b/Sources/Testing/ExitTests/ExitTest.CapturedValue.swift index 28ce5d4d5..54651a3b6 100644 --- a/Sources/Testing/ExitTests/ExitTest.CapturedValue.swift +++ b/Sources/Testing/ExitTests/ExitTest.CapturedValue.swift @@ -133,6 +133,7 @@ extension ExitTest { #if !SWT_NO_EXIT_TESTS // MARK: - Collection conveniences +@available(Android 28, *) extension Array where Element == ExitTest.CapturedValue { init(_ wrappedValues: repeat each T) where repeat each T: Codable & Sendable { self.init() @@ -145,6 +146,7 @@ extension Array where Element == ExitTest.CapturedValue { } } +@available(Android 28, *) extension Collection where Element == ExitTest.CapturedValue { /// Cast the elements in this collection to a tuple of their wrapped values. /// diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 9f7ddfacf..057c79660 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -151,6 +151,7 @@ public struct ExitTest: Sendable, ~Copyable { #if !SWT_NO_EXIT_TESTS // MARK: - Current +@available(Android 28, *) extension ExitTest { /// Storage for ``current``. /// @@ -203,6 +204,7 @@ private let _close_range = symbol(named: "close_range").map { #endif @_spi(ForToolsIntegrationOnly) +@available(Android 28, *) extension ExitTest { /// Disable crash reporting, crash logging, or core dumps for the current /// process. @@ -340,6 +342,7 @@ extension ExitTest { // MARK: - Discovery +@available(Android 28, *) extension ExitTest { /// A type representing an exit test as a test content record. fileprivate struct Record: Sendable, DiscoverableAsTestContent { @@ -438,6 +441,7 @@ extension ExitTest { } @_spi(ForToolsIntegrationOnly) +@available(Android 28, *) extension ExitTest { /// Find the exit test function at the given source location. /// @@ -491,6 +495,7 @@ extension ExitTest { /// This function contains the common implementation for all /// `await #expect(processExitsWith:) { }` invocations regardless of calling /// convention. +@available(Android 28, *) func callExitTest( identifiedBy exitTestID: (UInt64, UInt64, UInt64, UInt64), encodingCapturedValues capturedValues: [ExitTest.CapturedValue], @@ -577,6 +582,7 @@ extension ABI { } @_spi(ForToolsIntegrationOnly) +@available(Android 28, *) extension ExitTest { /// A barrier value to insert into the standard output and standard error /// streams immediately before and after the body of an exit test runs in diff --git a/Sources/Testing/ExitTests/WaitFor.swift b/Sources/Testing/ExitTests/WaitFor.swift index 76c36eba4..d9d647f34 100644 --- a/Sources/Testing/ExitTests/WaitFor.swift +++ b/Sources/Testing/ExitTests/WaitFor.swift @@ -62,6 +62,7 @@ private func _blockAndWait(for pid: consuming pid_t) throws -> ExitStatus { /// - Note: The open-source implementation of libdispatch available on Linux /// and other platforms does not support `DispatchSourceProcess`. Those /// platforms use an alternate implementation below. +@available(Android 28, *) func wait(for pid: consuming pid_t) async throws -> ExitStatus { let pid = consume pid diff --git a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift index ed81d1f59..155def51a 100644 --- a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift +++ b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift @@ -1146,6 +1146,7 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. +@available(Android 28, *) public func __checkClosureCall( identifiedBy exitTestID: (UInt64, UInt64, UInt64, UInt64), processExitsWith expectedExitCondition: ExitTest.Condition, @@ -1177,6 +1178,7 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. +@available(Android 28, *) public func __checkClosureCall( identifiedBy exitTestID: (UInt64, UInt64, UInt64, UInt64), encodingCapturedValues capturedValues: (repeat each T), diff --git a/Sources/Testing/Running/Configuration.swift b/Sources/Testing/Running/Configuration.swift index e0fe009ba..682227ffe 100644 --- a/Sources/Testing/Running/Configuration.swift +++ b/Sources/Testing/Running/Configuration.swift @@ -232,6 +232,16 @@ public struct Configuration: Sendable { public var eventHandler: Event.Handler = { _, _ in } #if !SWT_NO_EXIT_TESTS + /// Storage for ``exitTestHandler``. + private var _exitTestHandler: (any Sendable)? = { + if #available(Android 28, *) { + return { exitTest in + throw SystemError(description: "Exit test support has not been implemented by the current testing infrastructure.") + } as ExitTest.Handler + } + return nil + }() + /// A handler that is invoked when an exit test starts. /// /// For an explanation of how this property is used, see ``ExitTest/Handler``. @@ -239,8 +249,14 @@ public struct Configuration: Sendable { /// When using the `swift test` command from Swift Package Manager, this /// property is pre-configured. Otherwise, the default value of this property /// records an issue indicating that it has not been configured. - public var exitTestHandler: ExitTest.Handler = { exitTest in - throw SystemError(description: "Exit test support has not been implemented by the current testing infrastructure.") + @available(Android 28, *) + public var exitTestHandler: ExitTest.Handler { + get { + _exitTestHandler as! ExitTest.Handler + } + set { + _exitTestHandler = newValue + } } #endif diff --git a/Sources/Testing/Test+Cancellation.swift b/Sources/Testing/Test+Cancellation.swift index 694fdd1db..0b11e354c 100644 --- a/Sources/Testing/Test+Cancellation.swift +++ b/Sources/Testing/Test+Cancellation.swift @@ -160,7 +160,9 @@ private func _cancel(_ cancellableValue: T?, for testAndTestCase: (Test?, Tes var inExitTest = false #if !SWT_NO_EXIT_TESTS - inExitTest = (ExitTest.current != nil) + if #available(Android 28, *) { + inExitTest = (ExitTest.current != nil) + } #endif if Bool(inExitTest) { // This code is running in an exit test. We don't have a "current test" or From 94d54dd91886ea8a3e332a8c30cd88721a0bad63 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 21 Nov 2025 10:06:53 -0500 Subject: [PATCH 22/22] Use an availability flag instead of explicitly writing 'Android 28' --- Package.swift | 1 + .../Attachments/Attachment+URL.swift | 2 +- Sources/Testing/ABI/EntryPoints/EntryPoint.swift | 4 ++-- Sources/Testing/ExitTests/ExitStatus.swift | 6 +++--- .../Testing/ExitTests/ExitTest.CapturedValue.swift | 6 +++--- Sources/Testing/ExitTests/ExitTest.Condition.swift | 8 ++++---- Sources/Testing/ExitTests/ExitTest.Result.swift | 2 +- Sources/Testing/ExitTests/ExitTest.swift | 14 +++++++------- Sources/Testing/ExitTests/SpawnProcess.swift | 4 ++-- Sources/Testing/ExitTests/WaitFor.swift | 14 +++++++------- .../Testing/Expectations/Expectation+Macro.swift | 4 ++-- .../Expectations/ExpectationChecking+Macro.swift | 4 ++-- Sources/Testing/Running/Configuration.swift | 4 ++-- Sources/Testing/Test+Cancellation.swift | 2 +- cmake/modules/shared/AvailabilityDefinitions.cmake | 1 + 15 files changed, 39 insertions(+), 37 deletions(-) diff --git a/Package.swift b/Package.swift index 6cde28ea4..0b5a3cc59 100644 --- a/Package.swift +++ b/Package.swift @@ -441,6 +441,7 @@ extension Array where Element == PackageDescription.SwiftSetting { .enableExperimentalFeature("AvailabilityMacro=_swiftVersionAPI:macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0"), .enableExperimentalFeature("AvailabilityMacro=_typedThrowsAPI:macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0"), .enableExperimentalFeature("AvailabilityMacro=_castingWithNonCopyableGenerics:macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0"), + .enableExperimentalFeature("AvailabilityMacro=_posixSpawnAPI:Android 28.0"), .enableExperimentalFeature("AvailabilityMacro=_distantFuture:macOS 99.0, iOS 99.0, watchOS 99.0, tvOS 99.0, visionOS 99.0"), ] diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift index 6eed14e87..0c2fc7b50 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift @@ -161,7 +161,7 @@ private let _archiverPath: String? = { private func _compressContentsOfDirectory(at directoryURL: URL) async throws -> Data { #if !SWT_NO_PROCESS_SPAWNING #if os(Android) - guard #available(Android 28, *) else { + guard #available(_posixSpawnAPI, *) else { // API level 28 corresponds to Android 9 Pie. throw CocoaError(.featureUnsupported, userInfo: [NSLocalizedDescriptionKey: "Attaching directories to tests requires Android 9 (API level 28) or newer."]) } diff --git a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift index 107372291..fb82df7f7 100644 --- a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift @@ -31,7 +31,7 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha do { #if !SWT_NO_EXIT_TESTS // If an exit test was specified, run it. `exitTest` returns `Never`. - if #available(Android 28, *), + if #available(_posixSpawnAPI, *), let exitTest = ExitTest.findInEnvironmentForEntryPoint() { await exitTest() } @@ -675,7 +675,7 @@ public func configurationForEntryPoint(from args: __CommandLineArguments_v0) thr #if !SWT_NO_EXIT_TESTS // Enable exit test handling via __swiftPMEntryPoint(). - if #available(Android 28, *) { + if #available(_posixSpawnAPI, *) { configuration.exitTestHandler = ExitTest.handlerForEntryPoint() } #endif diff --git a/Sources/Testing/ExitTests/ExitStatus.swift b/Sources/Testing/ExitTests/ExitStatus.swift index 031348681..121eb5d93 100644 --- a/Sources/Testing/ExitTests/ExitStatus.swift +++ b/Sources/Testing/ExitTests/ExitStatus.swift @@ -24,7 +24,7 @@ private import _TestingInternals /// @Available(Xcode, introduced: 26.0) /// } #if !SWT_NO_PROCESS_SPAWNING -@available(Android 28, *) +@available(_posixSpawnAPI, *) #else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") @@ -94,7 +94,7 @@ public enum ExitStatus: Sendable { // MARK: - Equatable #if !SWT_NO_PROCESS_SPAWNING -@available(Android 28, *) +@available(_posixSpawnAPI, *) #else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") @@ -116,7 +116,7 @@ private let _sigabbrev_np = symbol(named: "sigabbrev_np").map { #endif #if !SWT_NO_PROCESS_SPAWNING -@available(Android 28, *) +@available(_posixSpawnAPI, *) #else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") diff --git a/Sources/Testing/ExitTests/ExitTest.CapturedValue.swift b/Sources/Testing/ExitTests/ExitTest.CapturedValue.swift index 54651a3b6..e5ca49854 100644 --- a/Sources/Testing/ExitTests/ExitTest.CapturedValue.swift +++ b/Sources/Testing/ExitTests/ExitTest.CapturedValue.swift @@ -12,7 +12,7 @@ private import _TestingInternals @_spi(ForToolsIntegrationOnly) #if !SWT_NO_EXIT_TESTS -@available(Android 28, *) +@available(_posixSpawnAPI, *) #else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") @@ -133,7 +133,7 @@ extension ExitTest { #if !SWT_NO_EXIT_TESTS // MARK: - Collection conveniences -@available(Android 28, *) +@available(_posixSpawnAPI, *) extension Array where Element == ExitTest.CapturedValue { init(_ wrappedValues: repeat each T) where repeat each T: Codable & Sendable { self.init() @@ -146,7 +146,7 @@ extension Array where Element == ExitTest.CapturedValue { } } -@available(Android 28, *) +@available(_posixSpawnAPI, *) extension Collection where Element == ExitTest.CapturedValue { /// Cast the elements in this collection to a tuple of their wrapped values. /// diff --git a/Sources/Testing/ExitTests/ExitTest.Condition.swift b/Sources/Testing/ExitTests/ExitTest.Condition.swift index 1d1494fbc..8c70cf188 100644 --- a/Sources/Testing/ExitTests/ExitTest.Condition.swift +++ b/Sources/Testing/ExitTests/ExitTest.Condition.swift @@ -11,7 +11,7 @@ private import _TestingInternals #if !SWT_NO_EXIT_TESTS -@available(Android 28, *) +@available(_posixSpawnAPI, *) #else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") @@ -61,7 +61,7 @@ extension ExitTest { // MARK: - #if !SWT_NO_EXIT_TESTS -@available(Android 28, *) +@available(_posixSpawnAPI, *) #else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") @@ -183,7 +183,7 @@ extension ExitTest.Condition { // MARK: - CustomStringConvertible #if !SWT_NO_EXIT_TESTS -@available(Android 28, *) +@available(_posixSpawnAPI, *) #else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") @@ -208,7 +208,7 @@ extension ExitTest.Condition: CustomStringConvertible { // MARK: - Comparison #if !SWT_NO_EXIT_TESTS -@available(Android 28, *) +@available(_posixSpawnAPI, *) #else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") diff --git a/Sources/Testing/ExitTests/ExitTest.Result.swift b/Sources/Testing/ExitTests/ExitTest.Result.swift index 3c72e43ed..0c32b6086 100644 --- a/Sources/Testing/ExitTests/ExitTest.Result.swift +++ b/Sources/Testing/ExitTests/ExitTest.Result.swift @@ -9,7 +9,7 @@ // #if !SWT_NO_EXIT_TESTS -@available(Android 28, *) +@available(_posixSpawnAPI, *) #else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 057c79660..d6acc4b54 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -35,7 +35,7 @@ private import _TestingInternals /// @Available(Xcode, introduced: 26.0) /// } #if !SWT_NO_EXIT_TESTS -@available(Android 28, *) +@available(_posixSpawnAPI, *) #else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") @@ -151,7 +151,7 @@ public struct ExitTest: Sendable, ~Copyable { #if !SWT_NO_EXIT_TESTS // MARK: - Current -@available(Android 28, *) +@available(_posixSpawnAPI, *) extension ExitTest { /// Storage for ``current``. /// @@ -204,7 +204,7 @@ private let _close_range = symbol(named: "close_range").map { #endif @_spi(ForToolsIntegrationOnly) -@available(Android 28, *) +@available(_posixSpawnAPI, *) extension ExitTest { /// Disable crash reporting, crash logging, or core dumps for the current /// process. @@ -342,7 +342,7 @@ extension ExitTest { // MARK: - Discovery -@available(Android 28, *) +@available(_posixSpawnAPI, *) extension ExitTest { /// A type representing an exit test as a test content record. fileprivate struct Record: Sendable, DiscoverableAsTestContent { @@ -441,7 +441,7 @@ extension ExitTest { } @_spi(ForToolsIntegrationOnly) -@available(Android 28, *) +@available(_posixSpawnAPI, *) extension ExitTest { /// Find the exit test function at the given source location. /// @@ -495,7 +495,7 @@ extension ExitTest { /// This function contains the common implementation for all /// `await #expect(processExitsWith:) { }` invocations regardless of calling /// convention. -@available(Android 28, *) +@available(_posixSpawnAPI, *) func callExitTest( identifiedBy exitTestID: (UInt64, UInt64, UInt64, UInt64), encodingCapturedValues capturedValues: [ExitTest.CapturedValue], @@ -582,7 +582,7 @@ extension ABI { } @_spi(ForToolsIntegrationOnly) -@available(Android 28, *) +@available(_posixSpawnAPI, *) extension ExitTest { /// A barrier value to insert into the standard output and standard error /// streams immediately before and after the body of an exit test runs in diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 2a3a3a3ef..2f04b15b7 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -62,7 +62,7 @@ private let _posix_spawn_file_actions_addclosefrom_np = symbol(named: "posix_spa /// resources. /// /// - Throws: Any error that prevented the process from spawning. -@available(Android 28, *) +@available(_posixSpawnAPI, *) func spawnExecutable( atPath executablePath: String, arguments: [String], @@ -476,7 +476,7 @@ private func _escapeCommandLine(_ arguments: [String]) -> String { /// This function is a convenience that spawns the given process and waits for /// it to terminate. It is primarily for use by other targets in this package /// such as its cross-import overlays. -@available(Android 28, *) +@available(_posixSpawnAPI, *) package func spawnExecutableAtPathAndWait( _ executablePath: String, arguments: [String] = [], diff --git a/Sources/Testing/ExitTests/WaitFor.swift b/Sources/Testing/ExitTests/WaitFor.swift index d9d647f34..69920a774 100644 --- a/Sources/Testing/ExitTests/WaitFor.swift +++ b/Sources/Testing/ExitTests/WaitFor.swift @@ -20,7 +20,7 @@ internal import _TestingInternals /// /// - Throws: If the exit status of the process with ID `pid` cannot be /// determined (i.e. it does not represent an exit condition.) -@available(Android 28, *) +@available(_posixSpawnAPI, *) private func _blockAndWait(for pid: consuming pid_t) throws -> ExitStatus { let pid = consume pid @@ -62,7 +62,7 @@ private func _blockAndWait(for pid: consuming pid_t) throws -> ExitStatus { /// - Note: The open-source implementation of libdispatch available on Linux /// and other platforms does not support `DispatchSourceProcess`. Those /// platforms use an alternate implementation below. -@available(Android 28, *) +@available(_posixSpawnAPI, *) func wait(for pid: consuming pid_t) async throws -> ExitStatus { let pid = consume pid @@ -82,7 +82,7 @@ func wait(for pid: consuming pid_t) async throws -> ExitStatus { } #elseif SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) /// A mapping of awaited child PIDs to their corresponding Swift continuations. -@available(Android 28, *) +@available(_posixSpawnAPI, *) private nonisolated(unsafe) let _childProcessContinuations = { let result = ManagedBuffer<[pid_t: CheckedContinuation], pthread_mutex_t>.create( minimumCapacity: 1, @@ -104,7 +104,7 @@ private nonisolated(unsafe) let _childProcessContinuations = { /// - Returns: Whatever is returned by `body`. /// /// - Throws: Whatever is thrown by `body`. -@available(Android 28, *) +@available(_posixSpawnAPI, *) private func _withLockedChildProcessContinuations( _ body: ( _ childProcessContinuations: inout [pid_t: CheckedContinuation], @@ -123,7 +123,7 @@ private func _withLockedChildProcessContinuations( /// A condition variable used to suspend the waiter thread created by /// `_createWaitThread()` when there are no child processes to await. -@available(Android 28, *) +@available(_posixSpawnAPI, *) private nonisolated(unsafe) let _waitThreadNoChildrenCondition = { let result = UnsafeMutablePointer.allocate(capacity: 1) _ = pthread_cond_init(result, nil) @@ -143,7 +143,7 @@ private let _pthread_setname_np = symbol(named: "pthread_setname_np").map { /// Create a waiter thread that is responsible for waiting for child processes /// to exit. -@available(Android 28, *) +@available(_posixSpawnAPI, *) private let _createWaitThread: Void = { // The body of the thread's run loop. func waitForAnyChild() { @@ -242,7 +242,7 @@ private let _createWaitThread: Void = { /// /// On Apple platforms, the libdispatch-based implementation above is more /// efficient because it does not need to permanently reserve a thread. -@available(Android 28, *) +@available(_posixSpawnAPI, *) func wait(for pid: consuming pid_t) async throws -> ExitStatus { let pid = consume pid diff --git a/Sources/Testing/Expectations/Expectation+Macro.swift b/Sources/Testing/Expectations/Expectation+Macro.swift index 281f8259d..8da7755b5 100644 --- a/Sources/Testing/Expectations/Expectation+Macro.swift +++ b/Sources/Testing/Expectations/Expectation+Macro.swift @@ -877,7 +877,7 @@ public macro require( @freestanding(expression) @discardableResult #if !SWT_NO_EXIT_TESTS -@available(Android 28, *) +@available(_posixSpawnAPI, *) #else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") @@ -927,7 +927,7 @@ public macro expect( @freestanding(expression) @discardableResult #if !SWT_NO_EXIT_TESTS -@available(Android 28, *) +@available(_posixSpawnAPI, *) #else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") diff --git a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift index 155def51a..6d1c9d845 100644 --- a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift +++ b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift @@ -1146,7 +1146,7 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -@available(Android 28, *) +@available(_posixSpawnAPI, *) public func __checkClosureCall( identifiedBy exitTestID: (UInt64, UInt64, UInt64, UInt64), processExitsWith expectedExitCondition: ExitTest.Condition, @@ -1178,7 +1178,7 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -@available(Android 28, *) +@available(_posixSpawnAPI, *) public func __checkClosureCall( identifiedBy exitTestID: (UInt64, UInt64, UInt64, UInt64), encodingCapturedValues capturedValues: (repeat each T), diff --git a/Sources/Testing/Running/Configuration.swift b/Sources/Testing/Running/Configuration.swift index 682227ffe..6061cb9cc 100644 --- a/Sources/Testing/Running/Configuration.swift +++ b/Sources/Testing/Running/Configuration.swift @@ -234,7 +234,7 @@ public struct Configuration: Sendable { #if !SWT_NO_EXIT_TESTS /// Storage for ``exitTestHandler``. private var _exitTestHandler: (any Sendable)? = { - if #available(Android 28, *) { + if #available(_posixSpawnAPI, *) { return { exitTest in throw SystemError(description: "Exit test support has not been implemented by the current testing infrastructure.") } as ExitTest.Handler @@ -249,7 +249,7 @@ public struct Configuration: Sendable { /// When using the `swift test` command from Swift Package Manager, this /// property is pre-configured. Otherwise, the default value of this property /// records an issue indicating that it has not been configured. - @available(Android 28, *) + @available(_posixSpawnAPI, *) public var exitTestHandler: ExitTest.Handler { get { _exitTestHandler as! ExitTest.Handler diff --git a/Sources/Testing/Test+Cancellation.swift b/Sources/Testing/Test+Cancellation.swift index 0b11e354c..dbc7f6b0e 100644 --- a/Sources/Testing/Test+Cancellation.swift +++ b/Sources/Testing/Test+Cancellation.swift @@ -160,7 +160,7 @@ private func _cancel(_ cancellableValue: T?, for testAndTestCase: (Test?, Tes var inExitTest = false #if !SWT_NO_EXIT_TESTS - if #available(Android 28, *) { + if #available(_posixSpawnAPI, *) { inExitTest = (ExitTest.current != nil) } #endif diff --git a/cmake/modules/shared/AvailabilityDefinitions.cmake b/cmake/modules/shared/AvailabilityDefinitions.cmake index e6b716657..bdb2b97f7 100644 --- a/cmake/modules/shared/AvailabilityDefinitions.cmake +++ b/cmake/modules/shared/AvailabilityDefinitions.cmake @@ -18,4 +18,5 @@ add_compile_options( "SHELL:$<$:-Xfrontend -define-availability -Xfrontend \"_swiftVersionAPI:macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0\">" "SHELL:$<$:-Xfrontend -define-availability -Xfrontend \"_typedThrowsAPI:macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0\">" "SHELL:$<$:-Xfrontend -define-availability -Xfrontend \"_castingWithNonCopyableGenerics:macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0\">" + "SHELL:$<$:-Xfrontend -define-availability -Xfrontend \"_posixSpawnAPI:Android 28.0\">" "SHELL:$<$:-Xfrontend -define-availability -Xfrontend \"_distantFuture:macOS 99.0, iOS 99.0, watchOS 99.0, tvOS 99.0, visionOS 99.0\">")