Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 17 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))),
.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 @@ -457,8 +470,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
23 changes: 20 additions & 3 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,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 @@ -662,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:)) {
Expand Down Expand Up @@ -699,7 +716,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
26 changes: 21 additions & 5 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,15 +72,16 @@ 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?
#elseif os(Linux)
#elseif os(Linux) || os(Android)
typealias P<T> = 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<posix_spawn_file_actions_t>.self, capacity: 1) { fileActions in
let fileActions = fileActions.baseAddress!
let fileActionsInitialized = posix_spawn_file_actions_init(fileActions)
Expand All @@ -92,7 +94,13 @@ func spawnExecutable(

return try withUnsafeTemporaryAllocation(of: P<posix_spawnattr_t>.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)
}
Expand Down Expand Up @@ -194,6 +202,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 @@ -225,7 +236,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)
}
Expand Down Expand Up @@ -463,6 +478,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
Loading
Loading