Skip to content
Draft
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ded1894
[WIP] Enable exit tests on Android API level 28 and newer.
grynspan Oct 27, 2025
670997c
Fix typos
grynspan Oct 27, 2025
7e4bb7f
Fix errors
grynspan Oct 27, 2025
7e54064
Differing optionality
grynspan Oct 27, 2025
d3ebaa0
Seriously Jonathan
grynspan Oct 27, 2025
da399c4
Type differs after init, sigh
grynspan Oct 27, 2025
0eecc00
Expose si_pid and si_status (renaming in Swift doesn't seem to want t…
grynspan Oct 27, 2025
b665223
si_status and si_pid aren't macros on Darwin
grynspan Oct 27, 2025
3954121
Merge branch 'main' into jgrynspan/exit-tests-android
grynspan Oct 27, 2025
dec81ab
Merge branch 'main' into jgrynspan/exit-tests-android
grynspan Oct 30, 2025
7c6a47f
Disable core files and tombstones
grynspan Oct 30, 2025
d64048d
Fixups
grynspan Oct 30, 2025
c3f2163
Fix platform-specific missing bits
grynspan Oct 30, 2025
e297fd4
More fixups
grynspan Oct 30, 2025
8a1c30b
Work around posix_spawn nullability problems
grynspan Oct 30, 2025
947ad22
Optional -> ?
grynspan Oct 30, 2025
ecee9a9
Merge remote-tracking branch 'origin/jgrynspan/exit-tests-android' in…
grynspan Oct 30, 2025
c6fe393
Fix androidIfCompiler6_3 sigh
grynspan Oct 30, 2025
e9fc453
Add nullable
grynspan Oct 30, 2025
fc7023c
Add _Null_unspecified
grynspan Oct 30, 2025
47c5401
Merge branch 'main' into jgrynspan/exit-tests-android
grynspan Nov 6, 2025
d548176
Avoid non-portable sig_t in SIG_DFL stub
grynspan Nov 6, 2025
16e5804
Use close_range(2)
grynspan Nov 21, 2025
e83b9e1
More availability annotations
grynspan Nov 21, 2025
53c27ca
Merge branch 'main' into jgrynspan/exit-tests-android
grynspan Nov 21, 2025
9805a8a
More availability annotations
grynspan Nov 21, 2025
94d54dd
Use an availability flag instead of explicitly writing 'Android 28'
grynspan Nov 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -377,8 +377,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]))),
.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]))),
Expand Down Expand Up @@ -436,8 +436,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]))),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -180,20 +187,22 @@ 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: [
NSLocalizedDescriptionKey: "The 'zip' package is not installed.",
NSFilePathErrorKey: trueArchiverPath
])
}
#endif
#elseif SWT_TARGET_OS_APPLE || os(FreeBSD)
let archiverPath = "/usr/bin/tar"
#elseif os(Windows)
Expand All @@ -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]
Expand Down
15 changes: 12 additions & 3 deletions Sources/Testing/ExitTests/ExitStatus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {}
Expand All @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion Sources/Testing/ExitTests/ExitTest.CapturedValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 12 additions & 4 deletions Sources/Testing/ExitTests/ExitTest.Condition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 3 additions & 1 deletion Sources/Testing/ExitTests/ExitTest.Result.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions Sources/Testing/ExitTests/ExitTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:))
}
Expand Down
27 changes: 20 additions & 7 deletions Sources/Testing/ExitTests/SpawnProcess.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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],
Expand All @@ -71,18 +72,26 @@ func spawnExecutable(
standardError: borrowing FileHandle? = nil,
additionalFileHandles: [UnsafePointer<FileHandle>] = []
) 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> = T?
func asArgument<T>(_ p: UnsafeMutableBufferPointer<T?>) -> UnsafeMutablePointer<T?> { p.baseAddress! }
#elseif os(Linux)
typealias P<T> = T
func asArgument<T>(_ p: UnsafeMutableBufferPointer<T>) -> UnsafeMutablePointer<T> { p.baseAddress! }
#elseif os(Android)
typealias P<T> = T?
func asArgument<T>(_ p: UnsafeMutableBufferPointer<T?>) -> UnsafeMutablePointer<T> {
UnsafeMutableRawPointer(p.baseAddress!).bindMemory(to: T.self, capacity: 1)
}
#endif

#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD)
return try withUnsafeTemporaryAllocation(of: P<posix_spawn_file_actions_t>.self, capacity: 1) { fileActions in
let fileActions = fileActions.baseAddress!
let fileActionsInitialized = posix_spawn_file_actions_init(fileActions)
let fileActionsInitialized = posix_spawn_file_actions_init(fileActions.baseAddress!)
let fileActions = asArgument(fileActions)
guard 0 == fileActionsInitialized else {
throw CError(rawValue: fileActionsInitialized)
}
Expand All @@ -91,8 +100,8 @@ func spawnExecutable(
}

return try withUnsafeTemporaryAllocation(of: P<posix_spawnattr_t>.self, capacity: 1) { attrs in
let attrs = attrs.baseAddress!
let attrsInitialized = posix_spawnattr_init(attrs)
let attrsInitialized = posix_spawnattr_init(attrs.baseAddress!)
let attrs = asArgument(attrs)
guard 0 == attrsInitialized else {
throw CError(rawValue: attrsInitialized)
}
Expand Down Expand Up @@ -194,6 +203,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
Expand Down Expand Up @@ -463,6 +475,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] = [],
Expand Down
18 changes: 11 additions & 7 deletions Sources/Testing/ExitTests/WaitFor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -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))")
}
Expand Down Expand Up @@ -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<ExitStatus, any Error>], pthread_mutex_t>.create(
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down
8 changes: 6 additions & 2 deletions Sources/Testing/Expectations/Expectation+Macro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -876,7 +876,9 @@ public macro require<R>(
/// }
@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
Expand Down Expand Up @@ -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
Expand Down
Loading