Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion Documentation/EnvironmentVariables.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |

Expand Down
22 changes: 18 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Platform> {
#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.
Expand Down Expand Up @@ -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))),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compiles fine natively on Android, but no extra tests were run, so I examined this logic and it is wrong: you want these two defines set to disable these tests when not running Swift 6.3 on Android, not when it is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm good at Swift! 🫠

.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]))),
Expand Down Expand Up @@ -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"),
]
Expand Down Expand Up @@ -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]))),
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(_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 {
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
7 changes: 5 additions & 2 deletions Sources/Testing/ABI/EntryPoints/EntryPoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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).
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(_posixSpawnAPI, *)
#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(_posixSpawnAPI, *)
#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(_posixSpawnAPI, *)
#else
@_unavailableInEmbedded
@available(*, unavailable, message: "Exit tests are not available on this platform.")
#endif
extension ExitStatus: CustomStringConvertible {
Expand Down
6 changes: 5 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(_posixSpawnAPI, *)
#else
@_unavailableInEmbedded
@available(*, unavailable, message: "Exit tests are not available on this platform.")
#endif
Expand Down Expand Up @@ -131,6 +133,7 @@ extension ExitTest {
#if !SWT_NO_EXIT_TESTS
// MARK: - Collection conveniences

@available(_posixSpawnAPI, *)
extension Array where Element == ExitTest.CapturedValue {
init<each T>(_ wrappedValues: repeat each T) where repeat each T: Codable & Sendable {
self.init()
Expand All @@ -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.
///
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(_posixSpawnAPI, *)
#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(_posixSpawnAPI, *)
#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(_posixSpawnAPI, *)
#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(_posixSpawnAPI, *)
#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(_posixSpawnAPI, *)
#else
@_unavailableInEmbedded
@available(*, unavailable, message: "Exit tests are not available on this platform.")
#endif
Expand Down
47 changes: 43 additions & 4 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(_posixSpawnAPI, *)
#else
@_unavailableInEmbedded
@available(*, unavailable, message: "Exit tests are not available on this platform.")
#endif
Expand Down Expand Up @@ -149,6 +151,7 @@ public struct ExitTest: Sendable, ~Copyable {
#if !SWT_NO_EXIT_TESTS
// MARK: - Current

@available(_posixSpawnAPI, *)
extension ExitTest {
/// Storage for ``current``.
///
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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 {
Expand Down Expand Up @@ -405,6 +441,7 @@ extension ExitTest {
}

@_spi(ForToolsIntegrationOnly)
@available(_posixSpawnAPI, *)
extension ExitTest {
/// Find the exit test function at the given source location.
///
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:)) {
Expand Down Expand Up @@ -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:))
}
Expand Down
Loading