diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 460bd9a..4356a9c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,11 +15,12 @@ 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 }} @@ -27,17 +28,47 @@ jobs: - 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/swiftwasm-action@v5.7 + 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 }} diff --git a/Package.swift b/Package.swift index 9e86ee4..cbc09a4 100644 --- a/Package.swift +++ b/Package.swift @@ -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" @@ -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( diff --git a/Sources/ConcurrencyExtras/MainSerialExecutor.swift b/Sources/ConcurrencyExtras/MainSerialExecutor.swift index 2a4a918..8fa0c45 100644 --- a/Sources/ConcurrencyExtras/MainSerialExecutor.swift +++ b/Sources/ConcurrencyExtras/MainSerialExecutor.swift @@ -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 = - 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 = + dlsym(dlopen(nil, 0), "swift_task_enqueueGlobal_hook").assumingMemoryBound(to: Hook?.self) +#endif diff --git a/Tests/ConcurrencyExtrasTests/MainSerialExecutorTests.swift b/Tests/ConcurrencyExtrasTests/MainSerialExecutorTests.swift index 5effc4d..2c95eb6 100644 --- a/Tests/ConcurrencyExtrasTests/MainSerialExecutorTests.swift +++ b/Tests/ConcurrencyExtrasTests/MainSerialExecutorTests.swift @@ -1,140 +1,142 @@ -import ConcurrencyExtras -import XCTest +#if !os(WASI) && !os(Windows) + import ConcurrencyExtras + import XCTest -final class MainSerialExecutorTests: XCTestCase { - func testSerializedExecution() async { - let xs = LockIsolated<[Int]>([]) - await withMainSerialExecutor { - await withTaskGroup(of: Void.self) { group in - for x in 1...1000 { - group.addTask { - xs.withValue { $0.append(x) } + final class MainSerialExecutorTests: XCTestCase { + func testSerializedExecution() async { + let xs = LockIsolated<[Int]>([]) + await withMainSerialExecutor { + await withTaskGroup(of: Void.self) { group in + for x in 1...1000 { + group.addTask { + xs.withValue { $0.append(x) } + } } } } + XCTAssertEqual(Array(1...1000), xs.value) } - XCTAssertEqual(Array(1...1000), xs.value) - } - func testSerializedExecution_WithActor() async { - let xs = ActorIsolated<[Int]>([]) - await withMainSerialExecutor { - await withTaskGroup(of: Void.self) { group in - for x in 1...1000 { - group.addTask { - await xs.withValue { $0.append(x) } + func testSerializedExecution_WithActor() async { + let xs = ActorIsolated<[Int]>([]) + await withMainSerialExecutor { + await withTaskGroup(of: Void.self) { group in + for x in 1...1000 { + group.addTask { + await xs.withValue { $0.append(x) } + } } } } + let value = await xs.value + XCTAssertEqual(Array(1...1000), value) } - let value = await xs.value - XCTAssertEqual(Array(1...1000), value) - } - func testSerializedExecution_YieldEveryOtherValue() async { - let xs = LockIsolated<[Int]>([]) - await withMainSerialExecutor { - await withTaskGroup(of: Void.self) { group in - for x in 1...1000 { - group.addTask { - if x.isMultiple(of: 2) { await Task.yield() } - xs.withValue { $0.append(x) } + func testSerializedExecution_YieldEveryOtherValue() async { + let xs = LockIsolated<[Int]>([]) + await withMainSerialExecutor { + await withTaskGroup(of: Void.self) { group in + for x in 1...1000 { + group.addTask { + if x.isMultiple(of: 2) { await Task.yield() } + xs.withValue { $0.append(x) } + } } } } + XCTAssertEqual( + Array(0...499).map { $0 * 2 + 1 } + Array(1...500).map { $0 * 2 }, + xs.value + ) } - XCTAssertEqual( - Array(0...499).map { $0 * 2 + 1 } + Array(1...500).map { $0 * 2 }, - xs.value - ) - } - func testSerializedExecution_UnstructuredTasks() async { - await withMainSerialExecutor { - let xs = LockIsolated<[Int]>([]) - for x in 1...1000 { - Task { xs.withValue { $0.append(x) } } + func testSerializedExecution_UnstructuredTasks() async { + await withMainSerialExecutor { + let xs = LockIsolated<[Int]>([]) + for x in 1...1000 { + Task { xs.withValue { $0.append(x) } } + } + while xs.count < 1_000 { await Task.yield() } + XCTAssertEqual(Array(1...1000), xs.value) } - while xs.count < 1_000 { await Task.yield() } - XCTAssertEqual(Array(1...1000), xs.value) } - } - func testUncheckedUseMainSerialExecutor() async { - uncheckedUseMainSerialExecutor = true - defer { uncheckedUseMainSerialExecutor = false } + func testUncheckedUseMainSerialExecutor() async { + uncheckedUseMainSerialExecutor = true + defer { uncheckedUseMainSerialExecutor = false } - let xs = LockIsolated<[Int]>([]) - await withTaskGroup(of: Void.self) { group in - for x in 1...1000 { - group.addTask { - xs.withValue { $0.append(x) } + let xs = LockIsolated<[Int]>([]) + await withTaskGroup(of: Void.self) { group in + for x in 1...1000 { + group.addTask { + xs.withValue { $0.append(x) } + } } } + XCTAssertEqual(Array(1...1000), xs.value) } - XCTAssertEqual(Array(1...1000), xs.value) - } - func testOverlappingTaskOutsideOfScope() async throws { - guard #available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) else { return } + func testOverlappingTaskOutsideOfScope() async throws { + guard #available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) else { return } - let overlappingTask = Task { - try await Task.sleep(for: .milliseconds(500)) - Task { - XCTAssertEqual({ Thread.current.isMainThread }(), true) + let overlappingTask = Task { + try await Task.sleep(for: .milliseconds(500)) + Task { + XCTAssertEqual({ Thread.current.isMainThread }(), true) + } } - } - try await withMainSerialExecutor { - try await Task.sleep(for: .seconds(1)) - } + try await withMainSerialExecutor { + try await Task.sleep(for: .seconds(1)) + } - try await overlappingTask.value - } + try await overlappingTask.value + } - func testDetachedTask() async { - await withMainSerialExecutor { - await Task.detached { - XCTAssertEqual({ Thread.current.isMainThread }(), true) - }.value + func testDetachedTask() async { + await withMainSerialExecutor { + await Task.detached { + XCTAssertEqual({ Thread.current.isMainThread }(), true) + }.value + } } - } - func testUnstructuredTask() async { - await withMainSerialExecutor { - await Task { - XCTAssertTrue({ Thread.current.isMainThread }()) - }.value + func testUnstructuredTask() async { + await withMainSerialExecutor { + await Task { + XCTAssertTrue({ Thread.current.isMainThread }()) + }.value + } } - } -} + } -final class MainSerialExecutorInvocationTests: XCTestCase { - override func invokeTest() { - withMainSerialExecutor { - super.invokeTest() + final class MainSerialExecutorInvocationTests: XCTestCase { + override func invokeTest() { + withMainSerialExecutor { + super.invokeTest() + } } - } - func testSerializedExecution() async { - let xs = LockIsolated<[Int]>([]) - await withTaskGroup(of: Void.self) { group in - for x in 1...1000 { - group.addTask { - xs.withValue { $0.append(x) } + func testSerializedExecution() async { + let xs = LockIsolated<[Int]>([]) + await withTaskGroup(of: Void.self) { group in + for x in 1...1000 { + group.addTask { + xs.withValue { $0.append(x) } + } } } + XCTAssertEqual(Array(1...1000), xs.value) } - XCTAssertEqual(Array(1...1000), xs.value) - } - func testSerializedExecution_UnstructuredTasks() async { - let xs = LockIsolated<[Int]>([]) - for x in 1...1000 { - Task { xs.withValue { $0.append(x) } } + func testSerializedExecution_UnstructuredTasks() async { + let xs = LockIsolated<[Int]>([]) + for x in 1...1000 { + Task { xs.withValue { $0.append(x) } } + } + await Task.yield() + XCTAssertEqual(Array(1...1000), xs.value) } - await Task.yield() - XCTAssertEqual(Array(1...1000), xs.value) } -} +#endif