Skip to content

Split SWT_NO_EXIT_TESTS into SWT_NO_EXIT_TESTS and SWT_NO_PROCESS_SPAWNING. #769

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 1 commit into from
Oct 17, 2024
Merged
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
2 changes: 2 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ extension Array where Element == PackageDescription.SwiftSetting {
.define("SWT_TARGET_OS_APPLE", .when(platforms: [.macOS, .iOS, .macCatalyst, .watchOS, .tvOS, .visionOS])),

.define("SWT_NO_EXIT_TESTS", .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android])),
.define("SWT_NO_PROCESS_SPAWNING", .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android])),
.define("SWT_NO_SNAPSHOT_TYPES", .when(platforms: [.linux, .windows, .wasi])),
.define("SWT_NO_DYNAMIC_LINKING", .when(platforms: [.wasi])),
.define("SWT_NO_PIPES", .when(platforms: [.wasi])),
Expand Down Expand Up @@ -164,6 +165,7 @@ extension Array where Element == PackageDescription.CXXSetting {

result += [
.define("SWT_NO_EXIT_TESTS", .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android])),
.define("SWT_NO_PROCESS_SPAWNING", .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android])),
.define("SWT_NO_SNAPSHOT_TYPES", .when(platforms: [.linux, .windows, .wasi])),
.define("SWT_NO_DYNAMIC_LINKING", .when(platforms: [.wasi])),
.define("SWT_NO_PIPES", .when(platforms: [.wasi])),
Expand Down
26 changes: 15 additions & 11 deletions Sources/Testing/ExitTests/ExitCondition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ private import _TestingInternals
/// ``require(exitsWith:_:sourceLocation:performing:)`` to configure which exit
/// statuses should be considered successful.
@_spi(Experimental)
#if SWT_NO_EXIT_TESTS
#if SWT_NO_PROCESS_SPAWNING
@available(*, unavailable, message: "Exit tests are not available on this platform.")
#elseif SWT_NO_EXIT_TESTS
@_spi(ForToolsIntegrationOnly)
#endif
public enum ExitCondition: Sendable {
/// The process terminated successfully with status `EXIT_SUCCESS`.
Expand Down Expand Up @@ -78,8 +80,10 @@ public enum ExitCondition: Sendable {

// MARK: - Equatable

#if SWT_NO_EXIT_TESTS
#if SWT_NO_PROCESS_SPAWNING
@available(*, unavailable, message: "Exit tests are not available on this platform.")
#elseif SWT_NO_EXIT_TESTS
@_spi(ForToolsIntegrationOnly)
#endif
extension ExitCondition {
/// Check whether or not two values of this type are equal.
Expand Down Expand Up @@ -108,9 +112,7 @@ extension ExitCondition {
///
/// For any values `a` and `b`, `a == b` implies that `a != b` is `false`.
public static func ==(lhs: Self, rhs: Self) -> Bool {
#if SWT_NO_EXIT_TESTS
fatalError("Unsupported")
#else
#if !SWT_NO_PROCESS_SPAWNING
return switch (lhs, rhs) {
case let (.failure, .exitCode(exitCode)), let (.exitCode(exitCode), .failure):
exitCode != EXIT_SUCCESS
Expand All @@ -122,6 +124,8 @@ extension ExitCondition {
default:
lhs === rhs
}
#else
fatalError("Unsupported")
#endif
}

Expand Down Expand Up @@ -152,10 +156,10 @@ extension ExitCondition {
///
/// For any values `a` and `b`, `a == b` implies that `a != b` is `false`.
public static func !=(lhs: Self, rhs: Self) -> Bool {
#if SWT_NO_EXIT_TESTS
fatalError("Unsupported")
#else
#if !SWT_NO_PROCESS_SPAWNING
!(lhs == rhs)
#else
fatalError("Unsupported")
#endif
}

Expand Down Expand Up @@ -226,10 +230,10 @@ extension ExitCondition {
///
/// For any values `a` and `b`, `a === b` implies that `a !== b` is `false`.
public static func !==(lhs: Self, rhs: Self) -> Bool {
#if SWT_NO_EXIT_TESTS
fatalError("Unsupported")
#else
#if !SWT_NO_PROCESS_SPAWNING
!(lhs === rhs)
#else
fatalError("Unsupported")
#endif
}
}
2 changes: 1 addition & 1 deletion Sources/Testing/ExitTests/SpawnProcess.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

internal import _TestingInternals

#if !SWT_NO_EXIT_TESTS
#if !SWT_NO_PROCESS_SPAWNING
/// A platform-specific value identifying a process running on the current
/// system.
#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD)
Expand Down
107 changes: 69 additions & 38 deletions Sources/Testing/ExitTests/WaitFor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

#if !SWT_NO_EXIT_TESTS

#if !SWT_NO_PROCESS_SPAWNING
internal import _TestingInternals

#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD)
Expand Down Expand Up @@ -41,8 +40,45 @@ private func _blockAndWait(for pid: consuming pid_t) throws -> ExitCondition {
}
}
}
#endif

#if SWT_TARGET_OS_APPLE && !SWT_NO_LIBDISPATCH
/// Asynchronously wait for a process to terminate using a dispatch source.
///
/// - Parameters:
/// - processID: The ID of the process to wait for.
///
/// - Returns: The exit condition of `processID`.
///
/// - Throws: If the exit status of the process with ID `processID` cannot be
/// determined (i.e. it does not represent an exit condition.)
///
/// This implementation of `wait(for:)` suspends the calling task until
/// libdispatch reports that `processID` has terminated, then synchronously
/// calls `_blockAndWait(for:)` (which should not block because `processID` will
/// have already terminated by that point.)
///
/// - Note: The open-source implementation of libdispatch available on Linux
/// and other platforms does not support `DispatchSourceProcess`. Those
/// platforms use an alternate implementation below.
func wait(for pid: consuming pid_t) async throws -> ExitCondition {
let pid = consume pid

let source = DispatchSource.makeProcessSource(identifier: pid, eventMask: .exit)
defer {
source.cancel()
}
await withCheckedContinuation { continuation in
source.setEventHandler {
continuation.resume()
}
source.resume()
}
withExtendedLifetime(source) {}

#if !(SWT_TARGET_OS_APPLE && !SWT_NO_LIBDISPATCH)
return try _blockAndWait(for: pid)
}
#elseif SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD)
/// A mapping of awaited child PIDs to their corresponding Swift continuations.
private let _childProcessContinuations = Locked<[pid_t: CheckedContinuation<ExitCondition, any Error>]>()

Expand All @@ -54,8 +90,9 @@ private nonisolated(unsafe) let _waitThreadNoChildrenCondition = {
return result
}()

/// The implementation of `_createWaitThread()`, run only once.
private let _createWaitThreadImpl: Void = {
/// Create a waiter thread that is responsible for waiting for child processes
/// to exit.
private let _createWaitThread: Void = {
// The body of the thread's run loop.
func waitForAnyChild() {
// Listen for child process exit events. WNOWAIT means we don't perturb the
Expand Down Expand Up @@ -128,42 +165,29 @@ private let _createWaitThreadImpl: Void = {
)
}()

/// Create a waiter thread that is responsible for waiting for child processes
/// to exit.
private func _createWaitThread() {
_createWaitThreadImpl
}
#endif

/// Wait for a given PID to exit and report its status.
/// Asynchronously wait for a process to terminate using a background thread
/// that calls `waitid()` in a loop.
///
/// - Parameters:
/// - pid: The PID to wait for.
/// - processID: The ID of the process to wait for.
///
/// - Returns: The exit condition of `processID`.
///
/// - Returns: The exit condition of `pid`.
/// - Throws: If the exit status of the process with ID `processID` cannot be
/// determined (i.e. it does not represent an exit condition.)
///
/// This implementation of `wait(for:)` suspends the calling task until
/// `waitid()`, called on a shared background thread, reports that `processID`
/// has terminated, then calls `_blockAndWait(for:)` (which should not block
/// because `processID` will have already terminated by that point.)
///
/// - Throws: Any error encountered calling `waitpid()` except for `EINTR`,
/// which is ignored.
/// On Apple platforms, the libdispatch-based implementation above is more
/// efficient because it does not need to permanently reserve a thread.
func wait(for pid: consuming pid_t) async throws -> ExitCondition {
let pid = consume pid

#if SWT_TARGET_OS_APPLE && !SWT_NO_LIBDISPATCH
let source = DispatchSource.makeProcessSource(identifier: pid, eventMask: .exit)
defer {
source.cancel()
}
await withCheckedContinuation { continuation in
source.setEventHandler {
continuation.resume()
}
source.resume()
}
withExtendedLifetime(source) {}

return try _blockAndWait(for: pid)
#else
// Ensure the waiter thread is running.
_createWaitThread()
_createWaitThread

return try await withCheckedThrowingContinuation { continuation in
_childProcessContinuations.withLock { childProcessContinuations in
Expand All @@ -179,19 +203,23 @@ func wait(for pid: consuming pid_t) async throws -> ExitCondition {
_ = pthread_cond_signal(_waitThreadNoChildrenCondition)
}
}
#endif
}
#elseif os(Windows)
/// Wait for a given process handle to exit and report its status.
/// Asynchronously wait for a process to terminate using the Windows thread
/// pool.
///
/// - Parameters:
/// - processHandle: The handle to wait for. This function takes ownership of
/// this handle and closes it when done.
/// - processHandle: A Windows handle representing the process to wait for.
/// This handle is closed before the function returns.
///
/// - Returns: The exit condition of `processHandle`.
///
/// - Throws: Any error encountered calling `WaitForSingleObject()` or
/// - Throws: Any error encountered calling `RegisterWaitForSingleObject()` or
/// `GetExitCodeProcess()`.
///
/// This implementation of `wait(for:)` calls `RegisterWaitForSingleObject()` to
/// wait for `processHandle`, suspends the calling task until the waiter's
/// callback is called, then calls `GetExitCodeProcess()`.
func wait(for processHandle: consuming HANDLE) async throws -> ExitCondition {
let processHandle = consume processHandle
defer {
Expand Down Expand Up @@ -235,5 +263,8 @@ func wait(for processHandle: consuming HANDLE) async throws -> ExitCondition {
// FIXME: handle SEH/VEH uncaught exceptions.
return .exitCode(CInt(bitPattern: .init(status)))
}
#else
#warning("Platform-specific implementation missing: cannot wait for child processes to exit")
func wait(for processID: consuming Never) async throws -> ExitCondition {}
#endif
#endif
4 changes: 4 additions & 0 deletions cmake/modules/shared/CompilerSettings.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ set(SWT_NO_EXIT_TESTS_LIST "iOS" "watchOS" "tvOS" "visionOS" "WASI" "Android")
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")
if(CMAKE_SYSTEM_NAME IN_LIST SWT_NO_PROCESS_SPAWNING_LIST)
add_compile_definitions("SWT_NO_PROCESS_SPAWNING")
endif()
if(NOT APPLE)
add_compile_definitions("SWT_NO_SNAPSHOT_TYPES")
endif()
Expand Down