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
39 changes: 35 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,60 @@ concurrency:

jobs:
library:
runs-on: macos-13
name: macOS
strategy:
matrix:
xcode: ['14.3.1']
config: ['debug', 'release']
runs-on: macos-13
steps:
- uses: actions/checkout@v3
- name: Select Xcode ${{ matrix.xcode }}
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
- name: Run ${{ matrix.config }} tests
run: CONFIG=${{ matrix.config }} make test

ubuntu-tests:
linux:
name: Linux
strategy:
matrix:
os: [ubuntu-20.04]
config: ['debug', 'release']

runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v3
- name: Build
run: swift build
- name: Run tests
run: swift test -c ${{ matrix.config }}

wasm:
name: Wasm
runs-on: ubuntu-latest
strategy:
matrix:
include:
- { toolchain: wasm-5.7.1-RELEASE }
steps:
- uses: actions/checkout@v3
- run: echo "${{ matrix.toolchain }}" > .swift-version
- uses: swiftwasm/[email protected]
with:
shell-action: carton test --environment node

windows:
name: Windows
strategy:
matrix:
os: [windows-latest]
config: ['debug', 'release']
runs-on: ${{ matrix.os }}
steps:
- uses: compnerd/gha-setup-swift@main
with:
branch: swift-5.8-release
tag: 5.8-RELEASE

- uses: actions/checkout@v3
- name: Run tests
run: swift build -c ${{ matrix.config }}
10 changes: 7 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@ let package = Package(
targets: ["ConcurrencyExtras"]
)
],
dependencies: [
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0")
],
targets: [
.target(
name: "ConcurrencyExtras"
Expand All @@ -32,6 +29,13 @@ let package = Package(
]
)

#if !os(Windows)
// Add the documentation compiler plugin if possible
package.dependencies.append(
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0")
)
#endif

//for target in package.targets {
// target.swiftSettings = target.swiftSettings ?? []
// target.swiftSettings?.append(
Expand Down
142 changes: 72 additions & 70 deletions Sources/ConcurrencyExtras/MainSerialExecutor.swift
Original file line number Diff line number Diff line change
@@ -1,76 +1,78 @@
import Foundation
#if !os(WASI) && !os(Windows)
import Foundation

/// Perform an operation on the main serial executor.
///
/// Some asynchronous code is [notoriously
/// difficult](https://forums.swift.org/t/reliably-testing-code-that-adopts-swift-concurrency/57304)
/// to test in Swift due to how suspension points are processed by the runtime. This function runs
/// all tasks spawned in the given operation serially and deterministically. It makes asynchronous
/// tests faster and less flakey.
///
/// ```swift
/// await withMainSerialExecutor {
/// // Everything performed in this scope is performed serially...
/// }
/// ```
///
/// - Parameter operation: An operation to be performed on the main serial executor.
@MainActor
public func withMainSerialExecutor(
@_implicitSelfCapture operation: @MainActor @Sendable () async throws -> Void
) async rethrows {
let didUseMainSerialExecutor = uncheckedUseMainSerialExecutor
defer { uncheckedUseMainSerialExecutor = didUseMainSerialExecutor }
uncheckedUseMainSerialExecutor = true
try await operation()
}
/// Perform an operation on the main serial executor.
///
/// Some asynchronous code is [notoriously
/// difficult](https://forums.swift.org/t/reliably-testing-code-that-adopts-swift-concurrency/57304)
/// to test in Swift due to how suspension points are processed by the runtime. This function runs
/// all tasks spawned in the given operation serially and deterministically. It makes asynchronous
/// tests faster and less flakey.
///
/// ```swift
/// await withMainSerialExecutor {
/// // Everything performed in this scope is performed serially...
/// }
/// ```
///
/// - Parameter operation: An operation to be performed on the main serial executor.
@MainActor
public func withMainSerialExecutor(
@_implicitSelfCapture operation: @MainActor @Sendable () async throws -> Void
) async rethrows {
let didUseMainSerialExecutor = uncheckedUseMainSerialExecutor
defer { uncheckedUseMainSerialExecutor = didUseMainSerialExecutor }
uncheckedUseMainSerialExecutor = true
try await operation()
}

/// Perform an operation on the main serial executor.
///
/// A synchronous version of ``withMainSerialExecutor(operation:)-79jpc`` that can be used in
/// `XCTestCase.invokeTest` to ensure all async tests are performed serially:
///
/// ```swift
/// class BaseTestCase: XCTestCase {
/// override func invokeTest() {
/// withMainSerialExecutor {
/// super.invokeTest()
/// }
/// }
/// }
/// ```
///
/// - Parameter operation: An operation to be performed on the main serial executor.
public func withMainSerialExecutor(
@_implicitSelfCapture operation: () throws -> Void
) rethrows {
let didUseMainSerialExecutor = uncheckedUseMainSerialExecutor
defer { uncheckedUseMainSerialExecutor = didUseMainSerialExecutor }
uncheckedUseMainSerialExecutor = true
try operation()
}
/// Perform an operation on the main serial executor.
///
/// A synchronous version of ``withMainSerialExecutor(operation:)-79jpc`` that can be used in
/// `XCTestCase.invokeTest` to ensure all async tests are performed serially:
///
/// ```swift
/// class BaseTestCase: XCTestCase {
/// override func invokeTest() {
/// withMainSerialExecutor {
/// super.invokeTest()
/// }
/// }
/// }
/// ```
///
/// - Parameter operation: An operation to be performed on the main serial executor.
public func withMainSerialExecutor(
@_implicitSelfCapture operation: () throws -> Void
) rethrows {
let didUseMainSerialExecutor = uncheckedUseMainSerialExecutor
defer { uncheckedUseMainSerialExecutor = didUseMainSerialExecutor }
uncheckedUseMainSerialExecutor = true
try operation()
}

/// Overrides Swift's global executor with the main serial executor in an unchecked fashion.
///
/// > Warning: When set to `true`, all tasks will be enqueued on the main serial executor till set
/// > back to `false`. Consider using ``withMainSerialExecutor(operation:)-79jpc``, instead, which
/// > scopes this work to the duration of a given operation.
public var uncheckedUseMainSerialExecutor: Bool {
get { swift_task_enqueueGlobal_hook != nil }
set {
swift_task_enqueueGlobal_hook =
newValue
? { job, _ in MainActor.shared.enqueue(job) }
: nil
/// Overrides Swift's global executor with the main serial executor in an unchecked fashion.
///
/// > Warning: When set to `true`, all tasks will be enqueued on the main serial executor till set
/// > back to `false`. Consider using ``withMainSerialExecutor(operation:)-79jpc``, instead, which
/// > scopes this work to the duration of a given operation.
public var uncheckedUseMainSerialExecutor: Bool {
get { swift_task_enqueueGlobal_hook != nil }
set {
swift_task_enqueueGlobal_hook =
newValue
? { job, _ in MainActor.shared.enqueue(job) }
: nil
}
}
}

private typealias Original = @convention(thin) (UnownedJob) -> Void
private typealias Hook = @convention(thin) (UnownedJob, Original) -> Void
private typealias Original = @convention(thin) (UnownedJob) -> Void
private typealias Hook = @convention(thin) (UnownedJob, Original) -> Void

private var swift_task_enqueueGlobal_hook: Hook? {
get { _swift_task_enqueueGlobal_hook.pointee }
set { _swift_task_enqueueGlobal_hook.pointee = newValue }
}
private let _swift_task_enqueueGlobal_hook: UnsafeMutablePointer<Hook?> =
dlsym(dlopen(nil, 0), "swift_task_enqueueGlobal_hook").assumingMemoryBound(to: Hook?.self)
private var swift_task_enqueueGlobal_hook: Hook? {
get { _swift_task_enqueueGlobal_hook.pointee }
set { _swift_task_enqueueGlobal_hook.pointee = newValue }
}
private let _swift_task_enqueueGlobal_hook: UnsafeMutablePointer<Hook?> =
dlsym(dlopen(nil, 0), "swift_task_enqueueGlobal_hook").assumingMemoryBound(to: Hook?.self)
#endif
Loading