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/Package.swift b/Package.swift index a82241527..0b5a3cc59 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. +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. /// 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, .android]))), - .define("SWT_NO_PROCESS_SPAWNING", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android]))), + .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]))), @@ -428,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"), ] @@ -457,8 +471,8 @@ 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_PROCESS_SPAWNING", .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]))), .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 e072bf76d..0c2fc7b50 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(_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."]) + } +#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/ABI/EntryPoints/EntryPoint.swift b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift index 6163e7bd1..fb82df7f7 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(_posixSpawnAPI, *), + 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(_posixSpawnAPI, *) { + configuration.exitTestHandler = ExitTest.handlerForEntryPoint() + } #endif // Warning issues (experimental). diff --git a/Sources/Testing/ExitTests/ExitStatus.swift b/Sources/Testing/ExitTests/ExitStatus.swift index 21fa2335e..121eb5d93 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(_posixSpawnAPI, *) +#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(_posixSpawnAPI, *) +#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(_posixSpawnAPI, *) +#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..e5ca49854 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(_posixSpawnAPI, *) +#else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif @@ -131,6 +133,7 @@ extension ExitTest { #if !SWT_NO_EXIT_TESTS // MARK: - Collection conveniences +@available(_posixSpawnAPI, *) extension Array where Element == ExitTest.CapturedValue { init(_ wrappedValues: repeat each T) where repeat each T: Codable & Sendable { self.init() @@ -143,6 +146,7 @@ extension Array where Element == ExitTest.CapturedValue { } } +@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 edd94193b..8c70cf188 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(_posixSpawnAPI, *) +#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(_posixSpawnAPI, *) +#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(_posixSpawnAPI, *) +#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(_posixSpawnAPI, *) +#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..0c32b6086 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(_posixSpawnAPI, *) +#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 da4da4c91..d6acc4b54 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(_posixSpawnAPI, *) +#else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif @@ -149,6 +151,7 @@ public struct ExitTest: Sendable, ~Copyable { #if !SWT_NO_EXIT_TESTS // MARK: - Current +@available(_posixSpawnAPI, *) extension ExitTest { /// Storage for ``current``. /// @@ -189,7 +192,19 @@ 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) +@available(_posixSpawnAPI, *) extension ExitTest { /// Disable crash reporting, crash logging, or core dumps for the current /// process. @@ -223,6 +238,21 @@ 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) + // 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(RLIMIT_CORE, &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, swt_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 @@ -278,11 +308,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 @@ -307,6 +342,7 @@ extension ExitTest { // MARK: - Discovery +@available(_posixSpawnAPI, *) extension ExitTest { /// A type representing an exit test as a test content record. fileprivate struct Record: Sendable, DiscoverableAsTestContent { @@ -405,6 +441,7 @@ extension ExitTest { } @_spi(ForToolsIntegrationOnly) +@available(_posixSpawnAPI, *) extension ExitTest { /// Find the exit test function at the given source location. /// @@ -458,6 +495,7 @@ extension ExitTest { /// This function contains the common implementation for all /// `await #expect(processExitsWith:) { }` invocations regardless of calling /// convention. +@available(_posixSpawnAPI, *) func callExitTest( identifiedBy exitTestID: (UInt64, UInt64, UInt64, UInt64), encodingCapturedValues capturedValues: [ExitTest.CapturedValue], @@ -544,6 +582,7 @@ extension ABI { } @_spi(ForToolsIntegrationOnly) +@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 @@ -662,7 +701,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:)) { @@ -699,7 +738,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..2f04b15b7 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(_posixSpawnAPI, *) func spawnExecutable( atPath executablePath: String, arguments: [String], @@ -71,15 +72,16 @@ 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 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) @@ -92,7 +94,13 @@ func spawnExecutable( return try withUnsafeTemporaryAllocation(of: P.self, capacity: 1) { attrs in let attrs = attrs.baseAddress! +#if os(Android) + let attrsInitialized = attrs.withMemoryRebound(to: posix_spawnattr_t?.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) } @@ -188,10 +196,11 @@ 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) #else @@ -225,7 +234,11 @@ func spawnExecutable( } var pid = pid_t() +#if os(Android) + let processSpawned = swt_posix_spawn(&pid, executablePath, fileActions, attrs, argv, environ) +#else let processSpawned = posix_spawn(&pid, executablePath, fileActions, attrs, argv, environ) +#endif guard 0 == processSpawned else { throw CError(rawValue: processSpawned) } @@ -463,6 +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(_posixSpawnAPI, *) package func spawnExecutableAtPathAndWait( _ executablePath: String, arguments: [String] = [], diff --git a/Sources/Testing/ExitTests/WaitFor.swift b/Sources/Testing/ExitTests/WaitFor.swift index f0326ff3c..69920a774 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. /// @@ -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(_posixSpawnAPI, *) private func _blockAndWait(for pid: consuming pid_t) throws -> ExitStatus { let pid = consume pid @@ -29,9 +30,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))") } @@ -61,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(_posixSpawnAPI, *) func wait(for pid: consuming pid_t) async throws -> ExitStatus { let pid = consume pid @@ -78,8 +80,9 @@ 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. +@available(_posixSpawnAPI, *) private nonisolated(unsafe) let _childProcessContinuations = { let result = ManagedBuffer<[pid_t: CheckedContinuation], pthread_mutex_t>.create( minimumCapacity: 1, @@ -101,6 +104,7 @@ private nonisolated(unsafe) let _childProcessContinuations = { /// - Returns: Whatever is returned by `body`. /// /// - Throws: Whatever is thrown by `body`. +@available(_posixSpawnAPI, *) private func _withLockedChildProcessContinuations( _ body: ( _ childProcessContinuations: inout [pid_t: CheckedContinuation], @@ -119,6 +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(_posixSpawnAPI, *) private nonisolated(unsafe) let _waitThreadNoChildrenCondition = { let result = UnsafeMutablePointer.allocate(capacity: 1) _ = pthread_cond_init(result, nil) @@ -138,6 +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(_posixSpawnAPI, *) private let _createWaitThread: Void = { // The body of the thread's run loop. func waitForAnyChild() { @@ -146,7 +152,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) } @@ -189,8 +195,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 +208,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 +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(_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 ea007f667..8da7755b5 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(_posixSpawnAPI, *) +#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(_posixSpawnAPI, *) +#else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif diff --git a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift index ed81d1f59..6d1c9d845 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(_posixSpawnAPI, *) 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(_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 e0fe009ba..6061cb9cc 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(_posixSpawnAPI, *) { + 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(_posixSpawnAPI, *) + 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..dbc7f6b0e 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(_posixSpawnAPI, *) { + inExitTest = (ExitTest.current != nil) + } #endif if Bool(inExitTest) { // This code is running in an exit test. We don't have a "current test" or diff --git a/Sources/_TestingInternals/include/Stubs.h b/Sources/_TestingInternals/include/Stubs.h index 636ea9aff..1f02128a3 100644 --- a/Sources/_TestingInternals/include/Stubs.h +++ b/Sources/_TestingInternals/include/Stubs.h @@ -126,28 +126,49 @@ static char *_Nullable *_Null_unspecified swt_environ(void) { } #endif -#if !defined(__ANDROID__) -#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 -/// 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; +/// platforms and cannot be imported directly into Swift. +static pid_t swt_siginfo_t_si_pid(siginfo_t siginfo) { + return siginfo.si_pid; } #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 -/// 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; +/// platforms and cannot be imported directly into Swift. +static int swt_siginfo_t_si_status(siginfo_t siginfo) { + return siginfo.si_status; +} +#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 __typeof__(SIG_DFL) _Null_unspecified swt_SIG_DFL(void) { + return SIG_DFL; +} +#endif + +#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 *_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] +) { + return posix_spawn(pid, path, fileActions, attrs, argv, env); } #endif #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\">") 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() 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);