Skip to content
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
1 change: 1 addition & 0 deletions Sources/Testing/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ add_library(Testing
Support/Graph.swift
Support/JSON.swift
Support/Locked.swift
Support/Locked+Platform.swift
Support/VersionNumber.swift
Support/Versions.swift
Discovery+Macro.swift
Expand Down
43 changes: 4 additions & 39 deletions Sources/Testing/ExitTests/WaitFor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,42 +80,7 @@ func wait(for pid: consuming pid_t) async throws -> ExitStatus {
}
#elseif SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD)
/// 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(
minimumCapacity: 1,
makingHeaderWith: { _ in [:] }
)

result.withUnsafeMutablePointers { _, lock in
_ = pthread_mutex_init(lock, nil)
}

return result
}()

/// Access the value in `_childProcessContinuations` while guarded by its lock.
///
/// - Parameters:
/// - body: A closure to invoke while the lock is held.
///
/// - Returns: Whatever is returned by `body`.
///
/// - Throws: Whatever is thrown by `body`.
private func _withLockedChildProcessContinuations<R>(
_ body: (
_ childProcessContinuations: inout [pid_t: CheckedContinuation<ExitStatus, any Error>],
_ lock: UnsafeMutablePointer<pthread_mutex_t>
) throws -> R
) rethrows -> R {
try _childProcessContinuations.withUnsafeMutablePointers { childProcessContinuations, lock in
_ = pthread_mutex_lock(lock)
defer {
_ = pthread_mutex_unlock(lock)
}

return try body(&childProcessContinuations.pointee, lock)
}
}
private let _childProcessContinuations = LockedWith<pthread_mutex_t, [pid_t: CheckedContinuation<ExitStatus, any Error>]>()

/// A condition variable used to suspend the waiter thread created by
/// `_createWaitThread()` when there are no child processes to await.
Expand Down Expand Up @@ -147,7 +112,7 @@ private let _createWaitThread: Void = {
var siginfo = siginfo_t()
if 0 == waitid(P_ALL, 0, &siginfo, WEXITED | WNOWAIT) {
if case let pid = siginfo.si_pid, pid != 0 {
let continuation = _withLockedChildProcessContinuations { childProcessContinuations, _ in
let continuation = _childProcessContinuations.withLock { childProcessContinuations in
childProcessContinuations.removeValue(forKey: pid)
}

Expand All @@ -168,7 +133,7 @@ private let _createWaitThread: Void = {
// newly-scheduled waiter process. (If this condition is spuriously
// woken, we'll just loop again, which is fine.) Note that we read errno
// outside the lock in case acquiring the lock perturbs it.
_withLockedChildProcessContinuations { childProcessContinuations, lock in
_childProcessContinuations.withUnsafeUnderlyingLock { lock, childProcessContinuations in
if childProcessContinuations.isEmpty {
_ = pthread_cond_wait(_waitThreadNoChildrenCondition, lock)
}
Expand Down Expand Up @@ -240,7 +205,7 @@ func wait(for pid: consuming pid_t) async throws -> ExitStatus {
_createWaitThread

return try await withCheckedThrowingContinuation { continuation in
_withLockedChildProcessContinuations { childProcessContinuations, _ in
_childProcessContinuations.withLock { childProcessContinuations in
// We don't need to worry about a race condition here because waitid()
// does not clear the wait/zombie state of the child process. If it sees
// the child process has terminated and manages to acquire the lock before
Expand Down
97 changes: 97 additions & 0 deletions Sources/Testing/Support/Locked+Platform.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2023–2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

internal import _TestingInternals

extension Never: Lockable {
static func initializeLock(at lock: UnsafeMutablePointer<Self>) {}
static func deinitializeLock(at lock: UnsafeMutablePointer<Self>) {}
static func unsafelyAcquireLock(at lock: UnsafeMutablePointer<Self>) {}
static func unsafelyRelinquishLock(at lock: UnsafeMutablePointer<Self>) {}
}

#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK
extension os_unfair_lock_s: Lockable {
static func initializeLock(at lock: UnsafeMutablePointer<Self>) {
lock.initialize(to: .init())
}

static func deinitializeLock(at lock: UnsafeMutablePointer<Self>) {
// No deinitialization needed.
}

static func unsafelyAcquireLock(at lock: UnsafeMutablePointer<Self>) {
os_unfair_lock_lock(lock)
}

static func unsafelyRelinquishLock(at lock: UnsafeMutablePointer<Self>) {
os_unfair_lock_unlock(lock)
}
}
#endif

#if os(FreeBSD) || os(OpenBSD)
typealias pthread_mutex_t = _TestingInternals.pthread_mutex_t?
typealias pthread_cond_t = _TestingInternals.pthread_cond_t?
#endif

#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || (os(WASI) && _runtime(_multithreaded))
extension pthread_mutex_t: Lockable {
static func initializeLock(at lock: UnsafeMutablePointer<Self>) {
_ = pthread_mutex_init(lock, nil)
}

static func deinitializeLock(at lock: UnsafeMutablePointer<Self>) {
_ = pthread_mutex_destroy(lock)
}

static func unsafelyAcquireLock(at lock: UnsafeMutablePointer<Self>) {
_ = pthread_mutex_lock(lock)
}

static func unsafelyRelinquishLock(at lock: UnsafeMutablePointer<Self>) {
_ = pthread_mutex_unlock(lock)
}
}
#endif

#if os(Windows)
extension SRWLOCK: Lockable {
static func initializeLock(at lock: UnsafeMutablePointer<Self>) {
InitializeSRWLock(lock)
}

static func deinitializeLock(at lock: UnsafeMutablePointer<Self>) {
// No deinitialization needed.
}

static func unsafelyAcquireLock(at lock: UnsafeMutablePointer<Self>) {
AcquireSRWLockExclusive(lock)
}

static func unsafelyRelinquishLock(at lock: UnsafeMutablePointer<Self>) {
ReleaseSRWLockExclusive(lock)
}
}
#endif

#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK
typealias DefaultLock = os_unfair_lock
#elseif SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || (os(WASI) && _runtime(_multithreaded))
typealias DefaultLock = pthread_mutex_t
#elseif os(Windows)
typealias DefaultLock = SRWLOCK
#elseif os(WASI)
// No locks on WASI without multithreaded runtime.
typealias DefaultLock = Never
#else
#warning("Platform-specific implementation missing: locking unavailable")
typealias DefaultLock = Never
#endif
133 changes: 83 additions & 50 deletions Sources/Testing/Support/Locked.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,37 @@
//

internal import _TestingInternals
private import Synchronization

/// A protocol defining a type, generally platform-specific, that satisfies the
/// requirements of a lock or mutex.
protocol Lockable {
/// Initialize the lock at the given address.
///
/// - Parameters:
/// - lock: A pointer to uninitialized memory that should be initialized as
/// an instance of this type.
static func initializeLock(at lock: UnsafeMutablePointer<Self>)

/// Deinitialize the lock at the given address.
///
/// - Parameters:
/// - lock: A pointer to initialized memory that should be deinitialized.
static func deinitializeLock(at lock: UnsafeMutablePointer<Self>)

/// Acquire the lock at the given address.
///
/// - Parameters:
/// - lock: The address of the lock to acquire.
static func unsafelyAcquireLock(at lock: UnsafeMutablePointer<Self>)

/// Relinquish the lock at the given address.
///
/// - Parameters:
/// - lock: The address of the lock to relinquish.
static func unsafelyRelinquishLock(at lock: UnsafeMutablePointer<Self>)
}

// MARK: -

/// A type that wraps a value requiring access from a synchronous caller during
/// concurrent execution.
Expand All @@ -22,48 +52,30 @@ private import Synchronization
/// concurrency tools.
///
/// This type is not part of the public interface of the testing library.
struct Locked<T> {
/// A type providing storage for the underlying lock and wrapped value.
#if SWT_TARGET_OS_APPLE && canImport(os)
private typealias _Storage = ManagedBuffer<T, os_unfair_lock_s>
#else
private final class _Storage {
let mutex: Mutex<T>

init(_ rawValue: consuming sending T) {
mutex = Mutex(rawValue)
struct LockedWith<L, T>: RawRepresentable where L: Lockable {
/// A type providing heap-allocated storage for an instance of ``Locked``.
private final class _Storage: ManagedBuffer<T, L> {
deinit {
withUnsafeMutablePointerToElements { lock in
L.deinitializeLock(at: lock)
}
}
}
#endif

/// Storage for the underlying lock and wrapped value.
private nonisolated(unsafe) var _storage: _Storage
}

extension Locked: Sendable where T: Sendable {}
private nonisolated(unsafe) var _storage: ManagedBuffer<T, L>

extension Locked: RawRepresentable {
init(rawValue: T) {
#if SWT_TARGET_OS_APPLE && canImport(os)
_storage = .create(minimumCapacity: 1, makingHeaderWith: { _ in rawValue })
_storage = _Storage.create(minimumCapacity: 1, makingHeaderWith: { _ in rawValue })
_storage.withUnsafeMutablePointerToElements { lock in
lock.initialize(to: .init())
L.initializeLock(at: lock)
}
#else
nonisolated(unsafe) let rawValue = rawValue
_storage = _Storage(rawValue)
#endif
}

var rawValue: T {
withLock { rawValue in
nonisolated(unsafe) let rawValue = rawValue
return rawValue
}
withLock { $0 }
}
}

extension Locked {
/// Acquire the lock and invoke a function while it is held.
///
/// - Parameters:
Expand All @@ -76,27 +88,55 @@ extension Locked {
/// This function can be used to synchronize access to shared data from a
/// synchronous caller. Wherever possible, use actor isolation or other Swift
/// concurrency tools.
func withLock<R>(_ body: (inout T) throws -> sending R) rethrows -> sending R where R: ~Copyable {
#if SWT_TARGET_OS_APPLE && canImport(os)
nonisolated(unsafe) let result = try _storage.withUnsafeMutablePointers { rawValue, lock in
os_unfair_lock_lock(lock)
nonmutating func withLock<R>(_ body: (inout T) throws -> R) rethrows -> R where R: ~Copyable {
try _storage.withUnsafeMutablePointers { rawValue, lock in
L.unsafelyAcquireLock(at: lock)
defer {
os_unfair_lock_unlock(lock)
L.unsafelyRelinquishLock(at: lock)
}
return try body(&rawValue.pointee)
}
return result
#else
try _storage.mutex.withLock { rawValue in
try body(&rawValue)
}

/// Acquire the lock and invoke a function while it is held, yielding both the
/// protected value and a reference to the underlying lock guarding it.
///
/// - Parameters:
/// - body: A closure to invoke while the lock is held.
///
/// - Returns: Whatever is returned by `body`.
///
/// - Throws: Whatever is thrown by `body`.
///
/// This function is equivalent to ``withLock(_:)`` except that the closure
/// passed to it also takes a reference to the underlying lock guarding this
/// instance's wrapped value. This function can be used when platform-specific
/// functionality such as a `pthread_cond_t` is needed. Because the caller has
/// direct access to the lock and is able to unlock and re-lock it, it is
/// unsafe to modify the protected value.
///
/// - Warning: Callers that unlock the lock _must_ lock it again before the
/// closure returns. If the lock is not acquired when `body` returns, the
/// effect is undefined.
nonmutating func withUnsafeUnderlyingLock<R>(_ body: (UnsafeMutablePointer<L>, T) throws -> R) rethrows -> R where R: ~Copyable {
try withLock { value in
try _storage.withUnsafeMutablePointerToElements { lock in
try body(lock, value)
}
}
#endif
}
}

extension LockedWith: Sendable where T: Sendable {}

/// A type that wraps a value requiring access from a synchronous caller during
/// concurrent execution and which uses the default platform-specific lock type
/// for the current platform.
typealias Locked<T> = LockedWith<DefaultLock, T>

// MARK: - Additions

extension Locked where T: AdditiveArithmetic & Sendable {
extension LockedWith where T: AdditiveArithmetic {
/// Add something to the current wrapped value of this instance.
///
/// - Parameters:
Expand All @@ -112,7 +152,7 @@ extension Locked where T: AdditiveArithmetic & Sendable {
}
}

extension Locked where T: Numeric & Sendable {
extension LockedWith where T: Numeric {
/// Increment the current wrapped value of this instance.
///
/// - Returns: The sum of ``rawValue`` and `1`.
Expand All @@ -132,7 +172,7 @@ extension Locked where T: Numeric & Sendable {
}
}

extension Locked {
extension LockedWith {
/// Initialize an instance of this type with a raw value of `nil`.
init<V>() where T == V? {
self.init(rawValue: nil)
Expand All @@ -148,10 +188,3 @@ extension Locked {
self.init(rawValue: [])
}
}

// MARK: - POSIX conveniences

#if os(FreeBSD) || os(OpenBSD)
typealias pthread_mutex_t = _TestingInternals.pthread_mutex_t?
typealias pthread_cond_t = _TestingInternals.pthread_cond_t?
#endif
Loading