diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1d4c6169..93465b67 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -68,6 +68,11 @@ jobs: cocoapods-test: name: CocoaPods uses: SwiftyLab/ci/.github/workflows/cocoapods.yml@main + strategy: + matrix: + platforms: ['macos tvos', 'ios'] + with: + platforms: ${{ matrix.platforms }} xcode-test: name: Xcode diff --git a/AsyncObjects.xcodeproj/project.pbxproj b/AsyncObjects.xcodeproj/project.pbxproj index 39b7d6dc..cb0672e8 100644 --- a/AsyncObjects.xcodeproj/project.pbxproj +++ b/AsyncObjects.xcodeproj/project.pbxproj @@ -21,7 +21,7 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ - 286654AE55C95B05A5086634 /* AsyncObjects.docc in Sources */ = {isa = PBXBuildFile; fileRef = 64070662BE047028E995157C /* AsyncObjects.docc */; }; + 4628E75B869ACA19AA7E5BDE /* AsyncObjects.docc in Sources */ = {isa = PBXBuildFile; fileRef = B2A9446673C71002FDE80C4B /* AsyncObjects.docc */; }; OBJ_122 /* AsyncCountdownEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_11 /* AsyncCountdownEvent.swift */; }; OBJ_123 /* AsyncEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_12 /* AsyncEvent.swift */; }; OBJ_124 /* AsyncSemaphore.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_13 /* AsyncSemaphore.swift */; }; @@ -58,7 +58,7 @@ OBJ_173 /* TaskQueueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_49 /* TaskQueueTests.swift */; }; OBJ_174 /* ThrowingFutureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_50 /* ThrowingFutureTests.swift */; }; OBJ_175 /* TrackedContinuationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_51 /* TrackedContinuationTests.swift */; }; - OBJ_176 /* XCTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_52 /* XCTestCase.swift */; }; + OBJ_176 /* XCAsyncTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_52 /* XCAsyncTestCase.swift */; }; OBJ_178 /* AsyncObjects.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = asyncobjects::AsyncObjects::Product /* AsyncObjects.framework */; }; OBJ_179 /* OrderedCollections.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = swift-collections::OrderedCollections::Product /* OrderedCollections.framework */; }; OBJ_186 /* _HashTable+Bucket.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_59 /* _HashTable+Bucket.swift */; }; @@ -115,7 +115,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 64070662BE047028E995157C /* AsyncObjects.docc */ = {isa = PBXFileReference; includeInIndex = 1; path = AsyncObjects.docc; sourceTree = ""; }; + B2A9446673C71002FDE80C4B /* AsyncObjects.docc */ = {isa = PBXFileReference; includeInIndex = 1; path = AsyncObjects.docc; sourceTree = ""; }; OBJ_100 /* OrderedSet+Partial SetAlgebra+Operations.swift */ = {isa = PBXFileReference; path = "OrderedSet+Partial SetAlgebra+Operations.swift"; sourceTree = ""; }; OBJ_101 /* OrderedSet+Partial SetAlgebra+Predicates.swift */ = {isa = PBXFileReference; path = "OrderedSet+Partial SetAlgebra+Predicates.swift"; sourceTree = ""; }; OBJ_102 /* OrderedSet+RandomAccessCollection.swift */ = {isa = PBXFileReference; path = "OrderedSet+RandomAccessCollection.swift"; sourceTree = ""; }; @@ -162,7 +162,7 @@ OBJ_49 /* TaskQueueTests.swift */ = {isa = PBXFileReference; path = TaskQueueTests.swift; sourceTree = ""; }; OBJ_50 /* ThrowingFutureTests.swift */ = {isa = PBXFileReference; path = ThrowingFutureTests.swift; sourceTree = ""; }; OBJ_51 /* TrackedContinuationTests.swift */ = {isa = PBXFileReference; path = TrackedContinuationTests.swift; sourceTree = ""; }; - OBJ_52 /* XCTestCase.swift */ = {isa = PBXFileReference; path = XCTestCase.swift; sourceTree = ""; }; + OBJ_52 /* XCAsyncTestCase.swift */ = {isa = PBXFileReference; path = XCAsyncTestCase.swift; sourceTree = ""; }; OBJ_59 /* _HashTable+Bucket.swift */ = {isa = PBXFileReference; path = "_HashTable+Bucket.swift"; sourceTree = ""; }; OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; OBJ_60 /* _HashTable+BucketIterator.swift */ = {isa = PBXFileReference; path = "_HashTable+BucketIterator.swift"; sourceTree = ""; }; @@ -247,7 +247,7 @@ OBJ_35 /* TaskOperation.swift */, OBJ_36 /* TaskQueue.swift */, OBJ_37 /* TaskTracker.swift */, - 64070662BE047028E995157C /* AsyncObjects.docc */, + B2A9446673C71002FDE80C4B /* AsyncObjects.docc */, ); name = AsyncObjects; path = Sources/AsyncObjects; @@ -352,7 +352,7 @@ OBJ_49 /* TaskQueueTests.swift */, OBJ_50 /* ThrowingFutureTests.swift */, OBJ_51 /* TrackedContinuationTests.swift */, - OBJ_52 /* XCTestCase.swift */, + OBJ_52 /* XCAsyncTestCase.swift */, ); name = AsyncObjectsTests; path = Tests/AsyncObjectsTests; @@ -648,7 +648,7 @@ OBJ_141 /* TaskOperation.swift in Sources */, OBJ_142 /* TaskQueue.swift in Sources */, OBJ_143 /* TaskTracker.swift in Sources */, - 286654AE55C95B05A5086634 /* AsyncObjects.docc in Sources */, + 4628E75B869ACA19AA7E5BDE /* AsyncObjects.docc in Sources */, ); }; OBJ_152 /* Sources */ = { @@ -672,7 +672,7 @@ OBJ_173 /* TaskQueueTests.swift in Sources */, OBJ_174 /* ThrowingFutureTests.swift in Sources */, OBJ_175 /* TrackedContinuationTests.swift in Sources */, - OBJ_176 /* XCTestCase.swift in Sources */, + OBJ_176 /* XCAsyncTestCase.swift in Sources */, ); }; OBJ_185 /* Sources */ = { @@ -824,7 +824,7 @@ isa = XCBuildConfiguration; buildSettings = { LD = /usr/bin/true; - OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/ManifestAPI -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.0.sdk -package-description-version 5.6.0"; + OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/ManifestAPI -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk -package-description-version 5.6.0"; SWIFT_VERSION = 5.0; }; name = Debug; @@ -833,7 +833,7 @@ isa = XCBuildConfiguration; buildSettings = { LD = /usr/bin/true; - OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/ManifestAPI -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.0.sdk -package-description-version 5.6.0"; + OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/ManifestAPI -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk -package-description-version 5.6.0"; SWIFT_VERSION = 5.0; }; name = Release; @@ -972,7 +972,7 @@ isa = XCBuildConfiguration; buildSettings = { LD = /usr/bin/true; - OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/ManifestAPI -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.0.sdk -package-description-version 5.3.0"; + OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/ManifestAPI -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk -package-description-version 5.3.0"; SWIFT_VERSION = 5.0; }; name = Debug; @@ -981,7 +981,7 @@ isa = XCBuildConfiguration; buildSettings = { LD = /usr/bin/true; - OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/ManifestAPI -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.0.sdk -package-description-version 5.3.0"; + OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/ManifestAPI -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk -package-description-version 5.3.0"; SWIFT_VERSION = 5.0; }; name = Release; diff --git a/Sources/AsyncObjects/AsyncCountdownEvent.swift b/Sources/AsyncObjects/AsyncCountdownEvent.swift index df2f1fcf..b864d369 100644 --- a/Sources/AsyncObjects/AsyncCountdownEvent.swift +++ b/Sources/AsyncObjects/AsyncCountdownEvent.swift @@ -71,7 +71,7 @@ public actor AsyncCountdownEvent: AsyncObject, ContinuableCollectionActor, /// Indicates whether countdown event current count is within ``limit``. /// /// Queued tasks are resumed from suspension when event is set and until current count exceeds limit. - public var isSet: Bool { currentCount >= 0 && currentCount <= limit } + public var isSet: Bool { currentCount <= limit } // MARK: Internal @@ -178,11 +178,11 @@ public actor AsyncCountdownEvent: AsyncObject, ContinuableCollectionActor, @inlinable internal func decrementCount( by number: UInt = 1, - file: String, function: String, line: UInt + file: String = #fileID, + function: String = #function, + line: UInt = #line ) { - defer { - resumeContinuations(file: file, function: function, line: line) - } + defer { resume(file: file, function: function, line: line) } guard currentCount > 0 else { log("Least count", file: file, function: function, line: line) @@ -203,9 +203,7 @@ public actor AsyncCountdownEvent: AsyncObject, ContinuableCollectionActor, /// - line: The line resume originates from (there's usually no need to pass it /// explicitly as it defaults to `#line`). @inlinable - internal func resumeContinuations( - file: String, function: String, line: UInt - ) { + internal func resume(file: String, function: String, line: UInt) { while !continuations.isEmpty && isSet { let (key, continuation) = continuations.removeFirst() resumeContinuation(continuation) @@ -247,10 +245,7 @@ public actor AsyncCountdownEvent: AsyncObject, ContinuableCollectionActor, to count: UInt?, file: String, function: String, line: UInt ) { - defer { - resumeContinuations(file: file, function: function, line: line) - } - + defer { resume(file: file, function: function, line: line) } let count = count ?? initialCount initialCount = count self.currentCount = count @@ -352,12 +347,7 @@ public actor AsyncCountdownEvent: AsyncObject, ContinuableCollectionActor, function: String = #function, line: UInt = #line ) { - Task { - await decrementCount( - by: 1, - file: file, function: function, line: line - ) - } + self.signal(repeat: 1, file: file, function: function, line: line) } /// Registers multiple signals (decrements by provided count) with the countdown event. diff --git a/Sources/AsyncObjects/AsyncSemaphore.swift b/Sources/AsyncObjects/AsyncSemaphore.swift index 1dc01fbf..020a5167 100644 --- a/Sources/AsyncObjects/AsyncSemaphore.swift +++ b/Sources/AsyncObjects/AsyncSemaphore.swift @@ -147,7 +147,11 @@ public actor AsyncSemaphore: AsyncObject, ContinuableCollectionActor, /// - line: The line signal originates from (there's usually no need to pass it /// explicitly as it defaults to `#line`). @inlinable - internal func signalSemaphore(file: String, function: String, line: UInt) { + internal func signalSemaphore( + file: String = #fileID, + function: String = #function, + line: UInt = #line + ) { incrementCount() guard !continuations.isEmpty else { return } log("Signalling", file: file, function: function, line: line) diff --git a/Sources/AsyncObjects/Base/AsyncObject+Clock.swift b/Sources/AsyncObjects/Base/AsyncObject+Clock.swift index 9892c51f..b5d4256a 100644 --- a/Sources/AsyncObjects/Base/AsyncObject+Clock.swift +++ b/Sources/AsyncObjects/Base/AsyncObject+Clock.swift @@ -230,27 +230,28 @@ public func waitForAny( /// pass it explicitly as it defaults to `#function`). /// - line: The line task passed from (there's usually no need to pass it /// explicitly as it defaults to `#line`). -/// - task: The task to execute and wait for completion. +/// - task: The action to execute and wait for completion result. /// +/// - Returns: The result of the action provided. /// - Throws: `CancellationError` if cancelled /// or `TimeoutError` if timed out. @available(swift 5.7) @available(macOS 13, iOS 16, macCatalyst 16, tvOS 16, watchOS 9, *) @Sendable -public func waitForTaskCompletion( +public func waitForTaskCompletion( until deadline: C.Instant, tolerance: C.Instant.Duration? = nil, clock: C, file: String = #fileID, function: String = #function, line: UInt = #line, - _ task: @escaping @Sendable () async throws -> Void -) async throws { - try await withThrowingTaskGroup(of: Void.self) { group in + _ task: @escaping @Sendable () async throws -> T +) async throws -> T { + try await withThrowingTaskGroup(of: T.self) { group in await GlobalContinuation.with { continuation in group.addTask { continuation.resume() - try await task() + return try await task() } } group.addTask { @@ -263,7 +264,10 @@ public func waitForTaskCompletion( throw TimeoutError(until: deadline, tolerance: tolerance) } defer { group.cancelAll() } - try await group.next() + guard + let result = try await group.next() + else { throw CancellationError() } + return result } } diff --git a/Sources/AsyncObjects/Base/AsyncObject+Duration.swift b/Sources/AsyncObjects/Base/AsyncObject+Duration.swift index 0a356a3b..dde992c4 100644 --- a/Sources/AsyncObjects/Base/AsyncObject+Duration.swift +++ b/Sources/AsyncObjects/Base/AsyncObject+Duration.swift @@ -187,23 +187,24 @@ public func waitForAny( /// pass it explicitly as it defaults to `#function`). /// - line: The line task passed from (there's usually no need to pass it /// explicitly as it defaults to `#line`). -/// - task: The task to execute and wait for completion. +/// - task: The action to execute and wait for completion result. /// +/// - Returns: The result of the action provided. /// - Throws: `CancellationError` if cancelled /// or `DurationTimeoutError` if timed out. @Sendable -public func waitForTaskCompletion( +public func waitForTaskCompletion( withTimeoutInNanoseconds timeout: UInt64, file: String = #fileID, function: String = #function, line: UInt = #line, - _ task: @escaping @Sendable () async throws -> Void -) async throws { - try await withThrowingTaskGroup(of: Void.self) { group in + _ task: @escaping @Sendable () async throws -> T +) async throws -> T { + return try await withThrowingTaskGroup(of: T.self) { group in await GlobalContinuation.with { continuation in group.addTask { continuation.resume() - try await task() + return try await task() } } group.addTask { @@ -212,7 +213,10 @@ public func waitForTaskCompletion( throw DurationTimeoutError(for: timeout, tolerance: 1_000) } defer { group.cancelAll() } - try await group.next() + guard + let result = try await group.next() + else { throw CancellationError() } + return result } } diff --git a/Sources/AsyncObjects/CancellationSource.swift b/Sources/AsyncObjects/CancellationSource.swift index baac2059..52240e33 100644 --- a/Sources/AsyncObjects/CancellationSource.swift +++ b/Sources/AsyncObjects/CancellationSource.swift @@ -30,7 +30,7 @@ /// /// - Warning: Cancellation sources propagate cancellation event to other linked cancellation sources. /// In case of circular dependency between cancellation sources, app will go into infinite recursion. -public actor CancellationSource { +public actor CancellationSource: LoggableActor { /// All the registered tasks for cooperative cancellation. @usableFromInline internal private(set) var registeredTasks: [AnyHashable: () -> Void] = [:] @@ -47,45 +47,92 @@ public actor CancellationSource { /// Add task to registered cooperative cancellation tasks list. /// - /// - Parameter task: The task to register. + /// - Parameters: + /// - task: The task to register. + /// - file: The file registration request originates from. + /// - function: The function registration request originates from. + /// - line: The line registration request originates from. @inlinable - internal func add(task: Task) { - guard !task.isCancelled else { return } + internal func add( + task: Task, + file: String, function: String, line: UInt + ) { + guard !task.isCancelled else { + log("Already cancelled", file: file, function: function, line: line) + return + } + registeredTasks[task] = { task.cancel() } + log("Registered", file: file, function: function, line: line) } /// Remove task from registered cooperative cancellation tasks list. /// - /// - Parameter task: The task to remove. + /// - Parameters: + /// - task: The task to remove. + /// - file: The file remove request originates from. + /// - function: The function remove request originates from. + /// - line: The line remove request originates from. @inlinable - internal func remove(task: Task) { + internal func remove( + task: Task, + file: String, function: String, line: UInt + ) { registeredTasks.removeValue(forKey: task) + log("Removed", file: file, function: function, line: line) } /// Add cancellation source to linked cancellation sources list to propagate cancellation event. /// - /// - Parameter task: The source to link. + /// - Parameters: + /// - source: The source to link. + /// - file: The file link request originates from. + /// - function: The function link request originates from. + /// - line: The line link request originates from. @inlinable - internal func addSource(_ source: CancellationSource) { + internal func addSource( + _ source: CancellationSource, + file: String, function: String, line: UInt + ) { linkedSources.append(source) + log("Added", file: file, function: function, line: line) } /// Propagate cancellation to linked cancellation sources. + /// + /// - Parameters: + /// - file: The file cancel request originates from. + /// - function: The function cancel request originates from. + /// - line: The line cancel request originates from. @inlinable - internal nonisolated func propagateCancellation() async { + internal func propagateCancellation( + file: String, function: String, line: UInt + ) async { await withTaskGroup(of: Void.self) { group in - let linkedSources = await linkedSources - linkedSources.forEach { s in group.addTask { s.cancel() } } + linkedSources.forEach { s in + group.addTask { + await s.cancelAll( + file: file, function: function, line: line + ) + } + } await group.waitForAll() } } /// Trigger cancellation event, initiate cooperative cancellation of registered tasks /// and propagate cancellation to linked cancellation sources. - internal func cancelAll() async { + /// + /// - Parameters: + /// - file: The file cancel request originates from. + /// - function: The function cancel request originates from. + /// - line: The line cancel request originates from. + @usableFromInline + internal func cancelAll(file: String, function: String, line: UInt) async { registeredTasks.forEach { $1() } registeredTasks = [:] - await propagateCancellation() + await propagateCancellation(file: file, function: function, line: line) + log("Cancelled", file: file, function: function, line: line) } // MARK: Public @@ -101,15 +148,32 @@ public actor CancellationSource { /// Initiating cancellation in any of the provided cancellation sources /// will ensure newly created cancellation source receive cancellation event. /// - /// - Parameter sources: The cancellation sources the newly created object will be linked to. + /// - Parameters: + /// - sources: The cancellation sources the newly created object will be linked to. + /// - file: The file link request originates from (there's usually no need to pass it + /// explicitly as it defaults to `#fileID`). + /// - function: The function link request originates from (there's usually no need to + /// pass it explicitly as it defaults to `#function`). + /// - line: The line link request originates from (there's usually no need to pass it + /// explicitly as it defaults to `#line`). /// /// - Returns: The newly created cancellation source. - public init(linkedWith sources: [CancellationSource]) { + public init( + linkedWith sources: [CancellationSource], + file: String = #fileID, + function: String = #function, + line: UInt = #line + ) { self.init() Task { await withTaskGroup(of: Void.self) { group in sources.forEach { source in - group.addTask { await source.addSource(self) } + group.addTask { + await source.addSource( + self, + file: file, function: function, line: line + ) + } } await group.waitForAll() } @@ -121,11 +185,26 @@ public actor CancellationSource { /// Initiating cancellation in any of the provided cancellation sources /// will ensure newly created cancellation source receive cancellation event. /// - /// - Parameter sources: The cancellation sources the newly created object will be linked to. + /// - Parameters: + /// - sources: The cancellation sources the newly created object will be linked to. + /// - file: The file link request originates from (there's usually no need to pass it + /// explicitly as it defaults to `#fileID`). + /// - function: The function link request originates from (there's usually no need to + /// pass it explicitly as it defaults to `#function`). + /// - line: The line link request originates from (there's usually no need to pass it + /// explicitly as it defaults to `#line`). /// /// - Returns: The newly created cancellation source. - public init(linkedWith sources: CancellationSource...) { - self.init(linkedWith: sources) + public init( + linkedWith sources: CancellationSource..., + file: String = #fileID, + function: String = #function, + line: UInt = #line + ) { + self.init( + linkedWith: sources, + file: file, function: function, line: line + ) } /// Creates a new cancellation source object @@ -151,9 +230,7 @@ public actor CancellationSource { Task { [weak self] in try await self?.cancel( afterNanoseconds: nanoseconds, - file: file, - function: function, - line: line + file: file, function: function, line: line ) } } @@ -195,15 +272,32 @@ public actor CancellationSource { /// Initiating cancellation in any of the provided cancellation sources /// will ensure newly created cancellation source receive cancellation event. /// - /// - Parameter sources: The cancellation sources the newly created object will be linked to. + /// - Parameters: + /// - sources: The cancellation sources the newly created object will be linked to. + /// - file: The file link request originates from (there's usually no need to pass it + /// explicitly as it defaults to `#fileID`). + /// - function: The function link request originates from (there's usually no need to + /// pass it explicitly as it defaults to `#function`). + /// - line: The line link request originates from (there's usually no need to pass it + /// explicitly as it defaults to `#line`). /// /// - Returns: The newly created cancellation source. - public convenience init(linkedWith sources: [CancellationSource]) { + public convenience init( + linkedWith sources: [CancellationSource], + file: String = #fileID, + function: String = #function, + line: UInt = #line + ) { self.init() Task { await withTaskGroup(of: Void.self) { group in sources.forEach { source in - group.addTask { await source.addSource(self) } + group.addTask { + await source.addSource( + self, + file: file, function: function, line: line + ) + } } await group.waitForAll() } @@ -215,11 +309,26 @@ public actor CancellationSource { /// Initiating cancellation in any of the provided cancellation sources /// will ensure newly created cancellation source receive cancellation event. /// - /// - Parameter sources: The cancellation sources the newly created object will be linked to. + /// - Parameters: + /// - sources: The cancellation sources the newly created object will be linked to. + /// - file: The file link request originates from (there's usually no need to pass it + /// explicitly as it defaults to `#fileID`). + /// - function: The function link request originates from (there's usually no need to + /// pass it explicitly as it defaults to `#function`). + /// - line: The line link request originates from (there's usually no need to pass it + /// explicitly as it defaults to `#line`). /// /// - Returns: The newly created cancellation source. - public convenience init(linkedWith sources: CancellationSource...) { - self.init(linkedWith: sources) + public convenience init( + linkedWith sources: CancellationSource..., + file: String = #fileID, + function: String = #function, + line: UInt = #line + ) { + self.init( + linkedWith: sources, + file: file, function: function, line: line + ) } /// Creates a new cancellation source object @@ -245,9 +354,7 @@ public actor CancellationSource { Task { [weak self] in try await self?.cancel( afterNanoseconds: nanoseconds, - file: file, - function: function, - line: line + file: file, function: function, line: line ) } } @@ -273,9 +380,16 @@ public actor CancellationSource { line: UInt = #line ) { Task { [weak self] in - await self?.add(task: task) + await self?.add( + task: task, + file: file, function: function, line: line + ) + let _ = await task.result - await self?.remove(task: task) + await self?.remove( + task: task, + file: file, function: function, line: line + ) } } @@ -295,7 +409,7 @@ public actor CancellationSource { function: String = #function, line: UInt = #line ) { - Task { await cancelAll() } + Task { await cancelAll(file: file, function: function, line: line) } } /// Trigger cancellation event after provided delay. @@ -321,7 +435,7 @@ public actor CancellationSource { line: UInt = #line ) async throws { try await Task.sleep(nanoseconds: nanoseconds) - await cancelAll() + await cancelAll(file: file, function: function, line: line) } #if swift(>=5.7) @@ -352,7 +466,7 @@ public actor CancellationSource { line: UInt = #line ) async throws { try await Task.sleep(until: deadline, clock: clock) - await cancelAll() + await cancelAll(file: file, function: function, line: line) } #endif } @@ -500,3 +614,19 @@ public extension Task { return task } } + +#if canImport(Logging) +import Logging + +extension CancellationSource { + /// Type specific metadata to attach to all log messages. + @usableFromInline + var metadata: Logger.Metadata { + return [ + "obj": "\(self)(\(Unmanaged.passUnretained(self).toOpaque()))", + "linked_sources": "\(linkedSources.count)", + "registered_tasks": "\(registeredTasks.count)", + ] + } +} +#endif diff --git a/Sources/AsyncObjects/Continuation/ContinuableCollection.swift b/Sources/AsyncObjects/Continuation/ContinuableCollection.swift index 41672fb8..9e7f5329 100644 --- a/Sources/AsyncObjects/Continuation/ContinuableCollection.swift +++ b/Sources/AsyncObjects/Continuation/ContinuableCollection.swift @@ -85,7 +85,7 @@ where /// - Returns: The value continuation is resumed with. /// - Throws: If `resume(throwing:)` is called on the continuation, this function throws that error. @inlinable - nonisolated func withPromisedContinuation( + func withPromisedContinuation( withKey key: Key, file: String, function: String, line: UInt ) async rethrows -> Continuation.Success { diff --git a/Sources/AsyncObjects/Continuation/ContinuableCollectionActor.swift b/Sources/AsyncObjects/Continuation/ContinuableCollectionActor.swift index 2f91aae0..2ea7ddd8 100644 --- a/Sources/AsyncObjects/Continuation/ContinuableCollectionActor.swift +++ b/Sources/AsyncObjects/Continuation/ContinuableCollectionActor.swift @@ -92,7 +92,7 @@ where /// - Returns: The value continuation is resumed with. /// - Throws: If `resume(throwing:)` is called on the continuation, this function throws that error. @inlinable - nonisolated func withPromisedContinuation( + func withPromisedContinuation( withKey key: Key, file: String, function: String, line: UInt ) async rethrows -> Continuation.Success { diff --git a/Sources/AsyncObjects/Continuation/TrackableContinuable.swift b/Sources/AsyncObjects/Continuation/TrackableContinuable.swift index 8bc77552..22ba2dd8 100644 --- a/Sources/AsyncObjects/Continuation/TrackableContinuable.swift +++ b/Sources/AsyncObjects/Continuation/TrackableContinuable.swift @@ -77,7 +77,7 @@ where Self: Sendable, Value: Sendable & ThrowingContinuable { function: String = #function, line: UInt = #line, handler: @Sendable (Self) -> Void, - operation: (Self, @escaping @Sendable () -> Void) -> Void + operation: @Sendable (Self, @escaping @Sendable () -> Void) -> Void ) async rethrows -> Success { let cancellable = Self( with: nil, id: id, @@ -168,7 +168,7 @@ where Self: Sendable, Value: Sendable & NonThrowingContinuable { function: String = #function, line: UInt = #line, handler: @Sendable (Self) -> Void, - operation: (Self, @escaping @Sendable () -> Void) -> Void + operation: @Sendable (Self, @escaping @Sendable () -> Void) -> Void ) async -> Success { let cancellable = Self( with: nil, id: id, diff --git a/Sources/AsyncObjects/Future.swift b/Sources/AsyncObjects/Future.swift index 3317147c..225df1c3 100644 --- a/Sources/AsyncObjects/Future.swift +++ b/Sources/AsyncObjects/Future.swift @@ -285,7 +285,7 @@ public actor Future: LoggableActor { continuations.forEach { key, value in value.resume(with: result) log( - "Fulfilled", id: key, + "Fulfilling", id: key, file: file, function: function, line: line ) } @@ -312,7 +312,7 @@ extension Future where Failure == Never { /// /// - Returns: The value continuation is resumed with. @inlinable - internal nonisolated func withPromisedContinuation( + internal func withPromisedContinuation( withKey key: UUID, file: String, function: String, line: UInt ) async -> Output { @@ -688,7 +688,7 @@ extension Future where Failure == Error { /// - Returns: The value continuation is resumed with. /// - Throws: If `resume(throwing:)` is called on the continuation, this function throws that error. @inlinable - internal nonisolated func withPromisedContinuation( + internal func withPromisedContinuation( withKey key: UUID, file: String, function: String, line: UInt ) async throws -> Output { diff --git a/Sources/AsyncObjects/TaskQueue.swift b/Sources/AsyncObjects/TaskQueue.swift index 84e6dc07..ea773176 100644 --- a/Sources/AsyncObjects/TaskQueue.swift +++ b/Sources/AsyncObjects/TaskQueue.swift @@ -249,7 +249,7 @@ public actor TaskQueue: AsyncObject, LoggableActor { _ continuation: QueuedContinuation, atKey key: UUID, file: String, function: String, line: UInt, - preinit: @escaping @Sendable () -> Void + preinit: @Sendable () -> Void ) { preinit() log( @@ -407,7 +407,7 @@ public actor TaskQueue: AsyncObject, LoggableActor { /// /// - Throws: If `resume(throwing:)` is called on the continuation, this function throws that error. @inlinable - internal nonisolated func withPromisedContinuation( + internal func withPromisedContinuation( flags: Flags = [], withKey key: UUID, file: String, function: String, line: UInt @@ -585,12 +585,6 @@ public actor TaskQueue: AsyncObject, LoggableActor { "Waiting", flags: flags, id: key, file: file, function: function, line: line ) - defer { - log( - "Executed", flags: flags, id: key, - file: file, function: function, line: line - ) - } do { try await withPromisedContinuation( @@ -601,6 +595,12 @@ public actor TaskQueue: AsyncObject, LoggableActor { try cancellation(error) } + defer { + log( + "Executed", flags: flags, id: key, + file: file, function: function, line: line + ) + } return try await runTask(withKey: key, operation) } diff --git a/Tests/AsyncObjectsTests/AsyncCountdownEventTests.swift b/Tests/AsyncObjectsTests/AsyncCountdownEventTests.swift index 003b080f..8edff022 100644 --- a/Tests/AsyncObjectsTests/AsyncCountdownEventTests.swift +++ b/Tests/AsyncObjectsTests/AsyncCountdownEventTests.swift @@ -4,94 +4,81 @@ import XCTest @MainActor class AsyncCountdownEventTests: XCTestCase { - func testWaitWithoutIncrement() async throws { + func testWithoutIncrement() async throws { let event = AsyncCountdownEvent() - try await Self.checkExecInterval(durationInSeconds: 0) { - try await event.wait() - } + try await event.wait(forSeconds: 3) } - func testWaitWithIncrement() async throws { + func testWithIncrement() async throws { let event = AsyncCountdownEvent() event.increment(by: 10) - try await Self.sleep(seconds: 0.001) - Self.signalCountdownEvent(event, times: 10) - try await Self.checkExecInterval(durationInSeconds: 5) { - try await event.wait() - } + try await waitUntil(event, timeout: 5) { $0.currentCount == 10 } + await event.signal(concurrent: 10) + try await event.wait(forSeconds: 5) } - func testWaitWithLimitAndIncrement() async throws { + func testWithOverIncrement() async throws { + let event = AsyncCountdownEvent() + event.increment(by: 10) + try await waitUntil(event, timeout: 5) { $0.currentCount == 10 } + await event.signal(concurrent: 15) + try await event.wait(forSeconds: 5) + } + + func testWithLimitAndIncrement() async throws { let event = AsyncCountdownEvent(until: 3) event.increment(by: 10) - try await Self.sleep(seconds: 0.001) - Self.signalCountdownEvent(event, times: 10) - try await Self.checkExecInterval(durationInRange: 3.5..<4.3) { - try await event.wait() - } + try await waitUntil(event, timeout: 5) { $0.currentCount == 10 } + await event.signal(concurrent: 7) + try await event.wait(forSeconds: 5) } - func testWaitWithLimitInitialCountAndIncrement() async throws { + func testWithLimitInitialCountAndIncrement() async throws { let event = AsyncCountdownEvent(until: 3, initial: 2) event.increment(by: 10) - try await Self.sleep(seconds: 0.001) - Self.signalCountdownEvent(event, times: 10) - try await Self.checkExecInterval(durationInRange: 4.5..<5.3) { - try await event.wait() - } + try await waitUntil(event, timeout: 5) { $0.currentCount == 12 } + await event.signal(concurrent: 9) + try await event.wait(forSeconds: 5) } - func testWaitWithIncrementAndReset() async throws { + func testWithIncrementAndReset() async throws { let event = AsyncCountdownEvent() event.increment(by: 10) - try await Self.sleep(seconds: 0.001) - Task.detached { - try await Self.sleep(seconds: 3) - event.reset() - } - try await Self.checkExecInterval(durationInSeconds: 3) { - try await event.wait() - } + try await waitUntil(event, timeout: 5) { $0.currentCount == 10 } + event.reset() + try await event.wait(forSeconds: 5) } - func testWaitWithIncrementAndResetToCount() async throws { + func testWithIncrementAndResetToCount() async throws { let event = AsyncCountdownEvent() event.increment(by: 10) - try await Self.sleep(seconds: 0.001) - Task.detached { - try await Self.sleep(seconds: 3) - event.reset(to: 2) - Self.signalCountdownEvent(event, times: 10) - } - try await Self.checkExecInterval(durationInSeconds: 4) { - try await event.wait() - } + try await waitUntil(event, timeout: 5) { $0.currentCount == 10 } + event.reset(to: 2) + try await waitUntil(event, timeout: 5) { $0.currentCount == 2 } + await event.signal(concurrent: 2) + try await event.wait(forSeconds: 5) } - func testWaitWithConcurrentIncrementAndResetToCount() async throws { + func testWithConcurrentIncrementAndResetToCount() async throws { let event = AsyncCountdownEvent() event.increment(by: 10) - try await Self.sleep(seconds: 0.001) + try await waitUntil(event, timeout: 5) { $0.currentCount == 10 } Task.detached { - try await Self.sleep(seconds: 2) + try await waitUntil(event, timeout: 5) { $0.currentCount == 6 } event.reset(to: 2) } - Self.signalCountdownEvent(event, times: 10) - try await Self.checkExecInterval(durationInRange: 2.5...3.2) { - try await event.wait() - } + await event.signal(concurrent: 4) + try await waitUntil(event, timeout: 10) { $0.currentCount == 2 } + await event.signal(concurrent: 2) + try await event.wait(forSeconds: 5) } func testDeinit() async throws { let event = AsyncCountdownEvent(until: 0, initial: 1) - Task.detached { - try await Self.sleep(seconds: 1) - event.signal() - } - try await event.wait() + event.signal() + try await event.wait(forSeconds: 5) self.addTeardownBlock { [weak event] in - try await Self.sleep(seconds: 1) - XCTAssertNil(event) + event.assertReleased() } } @@ -100,12 +87,10 @@ class AsyncCountdownEventTests: XCTestCase { for _ in 0..<10 { group.addTask { let event = AsyncCountdownEvent(initial: 1) - try await Self.checkExecInterval(durationInSeconds: 0) { - try await withThrowingTaskGroup(of: Void.self) { g in - g.addTask { try await event.wait() } - g.addTask { event.signal() } - try await g.waitForAll() - } + try await withThrowingTaskGroup(of: Void.self) { g in + g.addTask { try await event.wait(forSeconds: 5) } + g.addTask { event.signal() } + try await g.waitForAll() } } try await group.waitForAll() @@ -117,84 +102,60 @@ class AsyncCountdownEventTests: XCTestCase { @MainActor class AsyncCountdownEventTimeoutTests: XCTestCase { - func testWaitTimeoutWithIncrement() async throws { + func testTimeoutWithIncrement() async throws { let event = AsyncCountdownEvent() event.increment(by: 10) - try await Self.sleep(seconds: 0.001) - Self.signalCountdownEvent(event, times: 10) - await Self.checkExecInterval(durationInSeconds: 3) { - do { - try await event.wait(forSeconds: 3) - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue(type(of: error) == DurationTimeoutError.self) - } + try await waitUntil(event, timeout: 5) { $0.currentCount == 10 } + await event.signal(concurrent: 9) + do { + try await event.wait(forSeconds: 5) + XCTFail("Unexpected task progression") + } catch is DurationTimeoutError { + try await waitUntil(event, timeout: 3) { $0.currentCount == 1 } } } - func testWaitTimeoutWithLimitAndIncrement() async throws { + func testTimeoutWithLimitAndIncrement() async throws { let event = AsyncCountdownEvent(until: 3) event.increment(by: 10) - try await Self.sleep(seconds: 0.001) - Self.signalCountdownEvent(event, times: 10) - await Self.checkExecInterval(durationInSeconds: 2) { - do { - try await event.wait(forSeconds: 2) - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue(type(of: error) == DurationTimeoutError.self) - } + try await waitUntil(event, timeout: 5) { $0.currentCount == 10 } + await event.signal(concurrent: 6) + do { + try await event.wait(forSeconds: 3) + XCTFail("Unexpected task progression") + } catch is DurationTimeoutError { + try await waitUntil(event, timeout: 3) { $0.currentCount == 4 } } } - func testWaitTimeoutWithLimitInitialCountAndIncrement() async throws { + func testTimeoutWithLimitInitialCountAndIncrement() async throws { let event = AsyncCountdownEvent(until: 3, initial: 3) event.increment(by: 10) - try await Self.sleep(seconds: 0.001) - Self.signalCountdownEvent(event, times: 10) - await Self.checkExecInterval(durationInSeconds: 3) { - do { - try await event.wait(forSeconds: 3) - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue(type(of: error) == DurationTimeoutError.self) - } + try await waitUntil(event, timeout: 5) { $0.currentCount == 13 } + await event.signal(concurrent: 9) + do { + try await event.wait(forSeconds: 3) + XCTFail("Unexpected task progression") + } catch is DurationTimeoutError { + try await waitUntil(event, timeout: 3) { $0.currentCount == 4 } } } - func testWaitTimeoutWithIncrementAndReset() async throws { + func testTimeoutWithIncrementAndResetToCount() async throws { let event = AsyncCountdownEvent() event.increment(by: 10) - try await Self.sleep(seconds: 0.001) + try await waitUntil(event, timeout: 5) { $0.currentCount == 10 } + Task.detached { await event.signal(concurrent: 8) } Task.detached { - try await Self.sleep(seconds: 3) - event.reset() - } - await Self.checkExecInterval(durationInSeconds: 2) { - do { - try await event.wait(forSeconds: 2) - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue(type(of: error) == DurationTimeoutError.self) - } - } - } - - func testWaitTimeoutWithIncrementAndResetToCount() async throws { - let event = AsyncCountdownEvent() - event.increment(by: 10) - try await Self.sleep(seconds: 0.001) - Task.detached { - try await Self.sleep(seconds: 3) + try await waitUntil(event, timeout: 5) { $0.currentCount <= 6 } event.reset(to: 6) - Self.signalCountdownEvent(event, times: 10) } - await Self.checkExecInterval(durationInSeconds: 3) { - do { - try await event.wait(forSeconds: 3) - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue(type(of: error) == DurationTimeoutError.self) + do { + try await event.wait(forSeconds: 3) + XCTFail("Unexpected task progression") + } catch is DurationTimeoutError { + try await waitUntil(event, timeout: 3) { + (2...6).contains($0.currentCount) } } } @@ -204,7 +165,7 @@ class AsyncCountdownEventTimeoutTests: XCTestCase { @MainActor class AsyncCountdownEventClockTimeoutTests: XCTestCase { - func testWaitTimeoutWithIncrement() async throws { + func testTimeoutWithIncrement() async throws { guard #available(macOS 13, iOS 16, macCatalyst 16, tvOS 16, watchOS 9, *) else { @@ -213,21 +174,17 @@ class AsyncCountdownEventClockTimeoutTests: XCTestCase { let clock: ContinuousClock = .continuous let event = AsyncCountdownEvent() event.increment(by: 10) - try await Self.sleep(seconds: 0.001, clock: clock) - Self.signalCountdownEvent(event, times: 10) - await Self.checkExecInterval(duration: .seconds(3), clock: clock) { - do { - try await event.wait(forSeconds: 3, clock: clock) - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue( - type(of: error) == TimeoutError.self - ) - } + try await waitUntil(event, timeout: 5) { $0.currentCount == 10 } + await event.signal(concurrent: 9) + do { + try await event.wait(forSeconds: 3, clock: clock) + XCTFail("Unexpected task progression") + } catch is TimeoutError { + try await waitUntil(event, timeout: 3) { $0.currentCount == 1 } } } - func testWaitTimeoutWithLimitAndIncrement() async throws { + func testTimeoutWithLimitAndIncrement() async throws { guard #available(macOS 13, iOS 16, macCatalyst 16, tvOS 16, watchOS 9, *) else { @@ -236,21 +193,17 @@ class AsyncCountdownEventClockTimeoutTests: XCTestCase { let clock: ContinuousClock = .continuous let event = AsyncCountdownEvent(until: 3) event.increment(by: 10) - try await Self.sleep(seconds: 0.001, clock: clock) - Self.signalCountdownEvent(event, times: 10) - await Self.checkExecInterval(duration: .seconds(2), clock: clock) { - do { - try await event.wait(forSeconds: 2, clock: clock) - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue( - type(of: error) == TimeoutError.self - ) - } + try await waitUntil(event, timeout: 5) { $0.currentCount == 10 } + await event.signal(concurrent: 6) + do { + try await event.wait(forSeconds: 3, clock: clock) + XCTFail("Unexpected task progression") + } catch is TimeoutError { + try await waitUntil(event, timeout: 3) { $0.currentCount == 4 } } } - func testWaitTimeoutWithLimitInitialCountAndIncrement() async throws { + func testTimeoutWithLimitInitialCountAndIncrement() async throws { guard #available(macOS 13, iOS 16, macCatalyst 16, tvOS 16, watchOS 9, *) else { @@ -259,47 +212,17 @@ class AsyncCountdownEventClockTimeoutTests: XCTestCase { let clock: ContinuousClock = .continuous let event = AsyncCountdownEvent(until: 3, initial: 3) event.increment(by: 10) - try await Self.sleep(seconds: 0.001, clock: clock) - Self.signalCountdownEvent(event, times: 10) - await Self.checkExecInterval(duration: .seconds(3), clock: clock) { - do { - try await event.wait(forSeconds: 3, clock: clock) - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue( - type(of: error) == TimeoutError.self - ) - } - } - } - - func testWaitTimeoutWithIncrementAndReset() async throws { - guard - #available(macOS 13, iOS 16, macCatalyst 16, tvOS 16, watchOS 9, *) - else { - throw XCTSkip("Clock API not available") - } - let clock: ContinuousClock = .continuous - let event = AsyncCountdownEvent() - event.increment(by: 10) - try await Self.sleep(seconds: 0.001, clock: clock) - Task.detached { - try await Self.sleep(seconds: 3, clock: clock) - event.reset() - } - await Self.checkExecInterval(duration: .seconds(2), clock: clock) { - do { - try await event.wait(forSeconds: 2, clock: clock) - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue( - type(of: error) == TimeoutError.self - ) - } + try await waitUntil(event, timeout: 5) { $0.currentCount == 13 } + await event.signal(concurrent: 9) + do { + try await event.wait(forSeconds: 3, clock: clock) + XCTFail("Unexpected task progression") + } catch is TimeoutError { + try await waitUntil(event, timeout: 3) { $0.currentCount == 4 } } } - func testWaitTimeoutWithIncrementAndResetToCount() async throws { + func testTimeoutWithIncrementAndResetToCount() async throws { guard #available(macOS 13, iOS 16, macCatalyst 16, tvOS 16, watchOS 9, *) else { @@ -308,20 +231,18 @@ class AsyncCountdownEventClockTimeoutTests: XCTestCase { let clock: ContinuousClock = .continuous let event = AsyncCountdownEvent() event.increment(by: 10) - try await Self.sleep(seconds: 0.001, clock: clock) + try await waitUntil(event, timeout: 5) { $0.currentCount == 10 } + Task.detached { await event.signal(concurrent: 8) } Task.detached { - try await Self.sleep(seconds: 3, clock: clock) + try await waitUntil(event, timeout: 5) { $0.currentCount <= 6 } event.reset(to: 6) - Self.signalCountdownEvent(event, times: 10) } - await Self.checkExecInterval(duration: .seconds(3), clock: clock) { - do { - try await event.wait(forSeconds: 3, clock: clock) - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue( - type(of: error) == TimeoutError.self - ) + do { + try await event.wait(forSeconds: 3, clock: clock) + XCTFail("Unexpected task progression") + } catch is TimeoutError { + try await waitUntil(event, timeout: 3) { + (2...6).contains($0.currentCount) } } } @@ -331,50 +252,42 @@ class AsyncCountdownEventClockTimeoutTests: XCTestCase { @MainActor class AsyncCountdownEventCancellationTests: XCTestCase { - func testWaitCancellation() async throws { + func testCancellation() async throws { let event = AsyncCountdownEvent(initial: 1) - let task = Task.detached { - try await Self.checkExecInterval(durationInSeconds: 0) { - try await event.wait() - } - } + let task = Task.detached { try await event.wait() } task.cancel() - try? await task.value + do { + try await task.value + XCTFail("Unexpected task progression") + } catch {} } func testAlreadyCancelledTask() async throws { let event = AsyncCountdownEvent(initial: 1) let task = Task.detached { - try await Self.checkExecInterval(durationInSeconds: 0) { - do { - try await Self.sleep(seconds: 5) - XCTFail("Unexpected task progression") - } catch {} - XCTAssertTrue(Task.isCancelled) + do { try await event.wait() - } + XCTFail("Unexpected task progression") + } catch {} + XCTAssertTrue(Task.isCancelled) + try await event.wait() } task.cancel() - try? await task.value + do { + try await task.value + XCTFail("Unexpected task progression") + } catch {} } } -fileprivate extension XCTestCase { +fileprivate extension AsyncCountdownEvent { - static func signalCountdownEvent( - _ event: AsyncCountdownEvent, - times count: UInt - ) { - Task.detached { - try await withThrowingTaskGroup(of: Void.self) { group in - for i in 0.. {} } - func testReleasedWait() async throws { + func testResetSignal() async throws { guard #available(macOS 13, iOS 16, macCatalyst 16, tvOS 16, watchOS 9, *) else { @@ -116,29 +98,12 @@ class AsyncEventClockTimeoutTests: XCTestCase { } let clock: ContinuousClock = .continuous let event = AsyncEvent() - try await Self.checkExecInterval(duration: .seconds(0), clock: clock) { - try await event.wait(forSeconds: 2, clock: clock) - } - } - - func testWaitTimeout() async throws { - guard - #available(macOS 13, iOS 16, macCatalyst 16, tvOS 16, watchOS 9, *) - else { - throw XCTSkip("Clock API not available") - } - let clock: ContinuousClock = .continuous - let event = AsyncEvent(signaledInitially: false) - await Self.checkExecInterval(duration: .seconds(1), clock: clock) { - do { - try await event.wait(forSeconds: 1, clock: clock) - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue( - type(of: error) == TimeoutError.self - ) - } - } + event.reset() + try await waitUntil(event, timeout: 3) { !$0.signalled } + do { + try await event.wait(forSeconds: 3, clock: clock) + XCTFail("Unexpected task progression") + } catch is TimeoutError {} } } #endif @@ -146,56 +111,30 @@ class AsyncEventClockTimeoutTests: XCTestCase { @MainActor class AsyncEventCancellationTests: XCTestCase { - func testWaitCancellation() async throws { + func testCancellation() async throws { let event = AsyncEvent(signaledInitially: false) - let task = Task.detached { - await Self.checkExecInterval(durationInSeconds: 0) { - do { - try await event.wait() - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue(type(of: error) == CancellationError.self) - } - } - } + let task = Task.detached { try await event.wait() } task.cancel() - await task.value + do { + try await task.value + XCTFail("Unexpected task progression") + } catch {} } func testAlreadyCancelledTask() async throws { let event = AsyncEvent(signaledInitially: false) let task = Task.detached { - await Self.checkExecInterval(durationInSeconds: 0) { - do { - try await Self.sleep(seconds: 5) - XCTFail("Unexpected task progression") - } catch {} - XCTAssertTrue(Task.isCancelled) - try? await event.wait() - } + do { + try await event.wait() + XCTFail("Unexpected task progression") + } catch {} + XCTAssertTrue(Task.isCancelled) + try await event.wait() } task.cancel() - await task.value - } -} - -fileprivate extension XCTestCase { - - static func checkWait( - for event: AsyncEvent, - signalIn interval: UInt64 = 1, - durationInSeconds seconds: Int = 1, - file: StaticString = #filePath, - function: StaticString = #function, - line: UInt = #line - ) async throws { - Task.detached { - try await Self.sleep(seconds: interval) - event.signal() - } - try await Self.checkExecInterval( - durationInSeconds: seconds, - file: file, function: function, line: line - ) { try await event.wait() } + do { + try await task.value + XCTFail("Unexpected task progression") + } catch {} } } diff --git a/Tests/AsyncObjectsTests/AsyncObjectTests.swift b/Tests/AsyncObjectsTests/AsyncObjectTests.swift index 9bd25b18..14992e4d 100644 --- a/Tests/AsyncObjectsTests/AsyncObjectTests.swift +++ b/Tests/AsyncObjectsTests/AsyncObjectTests.swift @@ -6,13 +6,9 @@ class AsyncObjectTests: XCTestCase { func testMultipleObjectWaitAll() async throws { let event = AsyncEvent(signaledInitially: false) - let mutex = AsyncSemaphore() - Task.detached { - try await Self.sleep(seconds: 1) - event.signal() - mutex.signal() - } - try await Self.checkExecInterval(durationInSeconds: 1) { + let mutex = AsyncSemaphore(value: 1) + Task.detached { event.signal(); mutex.signal() } + try await waitForTaskCompletion(withTimeoutInNanoseconds: UInt64(3E9)) { try await waitForAll(event, mutex) } } @@ -20,13 +16,8 @@ class AsyncObjectTests: XCTestCase { func testMultipleObjectWaitAny() async throws { let event = AsyncEvent(signaledInitially: false) let mutex = AsyncSemaphore() - Task.detached { - try await Self.sleep(seconds: 1) - event.signal() - try await Self.sleep(seconds: 1) - mutex.signal() - } - try await Self.checkExecInterval(durationInSeconds: 1) { + Task.detached { event.signal() } + try await waitForTaskCompletion(withTimeoutInNanoseconds: UInt64(3E9)) { try await waitForAny(event, mutex) } } @@ -34,19 +25,10 @@ class AsyncObjectTests: XCTestCase { func testMultipleObjectWaitMultiple() async throws { let event = AsyncEvent(signaledInitially: false) let mutex = AsyncSemaphore() - let op = TaskOperation { - try await Self.sleep(seconds: 3) - } - Task.detached { - try await Self.sleep(seconds: 1) - event.signal() - } - Task.detached { - try await Self.sleep(seconds: 2) - mutex.signal() - } + let op = TaskOperation { /* Do nothing */ } + Task.detached { event.signal() } op.signal() - try await Self.checkExecInterval(durationInSeconds: 2) { + try await waitForTaskCompletion(withTimeoutInNanoseconds: UInt64(3E9)) { try await waitForAny(event, mutex, op, count: 2) } } @@ -58,96 +40,39 @@ class AsyncObjectTimeoutTests: XCTestCase { func testMultipleObjectWaitAll() async throws { let event = AsyncEvent(signaledInitially: false) let mutex = AsyncSemaphore() - Task.detached { - try await Self.sleep(seconds: 1) - event.signal() - mutex.signal() - } - try await Self.checkExecInterval(durationInSeconds: 1) { - try await waitForAll( - event, mutex, - forNanoseconds: UInt64(2E9) - ) - } + do { + try await waitForAll(event, mutex, forNanoseconds: UInt64(3E9)) + XCTFail("Unexpected task progression") + } catch is DurationTimeoutError {} } func testMultipleObjectWaitAny() async throws { let event = AsyncEvent(signaledInitially: false) let mutex = AsyncSemaphore() - Task.detached { - try await Self.sleep(seconds: 1) - event.signal() - try await Self.sleep(seconds: 1) - mutex.signal() - } - try await Self.checkExecInterval(durationInSeconds: 1) { + do { try await waitForAny( event, mutex, - forNanoseconds: UInt64(2E9) + count: 2, + forNanoseconds: UInt64(3E9) ) - } - } - - func testMultipleObjectWaitAllTimeout() async throws { - let event = AsyncEvent(signaledInitially: false) - let mutex = AsyncSemaphore() - await Self.checkExecInterval(durationInSeconds: 1) { - do { - try await waitForAll( - event, mutex, - forNanoseconds: UInt64(1E9) - ) - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue(type(of: error) == DurationTimeoutError.self) - } - } - } - - func testMultipleObjectWaitAnyTimeout() async throws { - let event = AsyncEvent(signaledInitially: false) - let mutex = AsyncSemaphore() - await Self.checkExecInterval(durationInSeconds: 1) { - do { - try await waitForAny( - event, mutex, - count: 2, - forNanoseconds: UInt64(1E9) - ) - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue(type(of: error) == DurationTimeoutError.self) - } - } + XCTFail("Unexpected task progression") + } catch is DurationTimeoutError {} } - func testMultipleObjectWaitMultipleTimeout() async throws { + func testMultipleObjectWaitMultiple() async throws { let event = AsyncEvent(signaledInitially: false) let mutex = AsyncSemaphore() - let op = TaskOperation { - try await Self.sleep(seconds: 4) - } - Task.detached { - try await Self.sleep(seconds: 1) - event.signal() - } - Task.detached { - try await Self.sleep(seconds: 3) - mutex.signal() - } + let op = TaskOperation { try await mutex.wait() } op.signal() - await Self.checkExecInterval(durationInSeconds: 2) { - do { - try await waitForAny( - event, mutex, op, - count: 2, - forNanoseconds: UInt64(2E9) - ) - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue(type(of: error) == DurationTimeoutError.self) - } - } + Task.detached { event.signal() } + do { + try await waitForAny( + event, mutex, op, + count: 2, + forNanoseconds: UInt64(3E9) + ) + XCTFail("Unexpected task progression") + } catch is DurationTimeoutError {} } } @@ -164,18 +89,14 @@ class AsyncObjectClockTimeoutTests: XCTestCase { let clock: ContinuousClock = .continuous let event = AsyncEvent(signaledInitially: false) let mutex = AsyncSemaphore() - Task.detached { - try await Self.sleep(seconds: 1) - event.signal() - mutex.signal() - } - try await Self.checkExecInterval(duration: .seconds(1), clock: clock) { + do { try await waitForAll( event, mutex, - until: .now + .seconds(2), + until: .now + .seconds(3), clock: clock ) - } + XCTFail("Unexpected task progression") + } catch is TimeoutError {} } func testMultipleObjectWaitAny() async throws { @@ -187,73 +108,17 @@ class AsyncObjectClockTimeoutTests: XCTestCase { let clock: ContinuousClock = .continuous let event = AsyncEvent(signaledInitially: false) let mutex = AsyncSemaphore() - Task.detached { - try await Self.sleep(seconds: 1) - event.signal() - try await Self.sleep(seconds: 1) - mutex.signal() - } - try await Self.checkExecInterval(duration: .seconds(1), clock: clock) { + do { try await waitForAny( event, mutex, - until: .now + .seconds(2), + until: .now + .seconds(3), clock: clock ) - } - } - - func testMultipleObjectWaitAllTimeout() async throws { - guard - #available(macOS 13, iOS 16, macCatalyst 16, tvOS 16, watchOS 9, *) - else { - throw XCTSkip("Clock API not available") - } - let clock: ContinuousClock = .continuous - let event = AsyncEvent(signaledInitially: false) - let mutex = AsyncSemaphore() - await Self.checkExecInterval(duration: .seconds(1), clock: clock) { - do { - try await waitForAll( - event, mutex, - until: .now + .seconds(1), - clock: clock - ) - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue( - type(of: error) == TimeoutError.self - ) - } - } - } - - func testMultipleObjectWaitAnyTimeout() async throws { - guard - #available(macOS 13, iOS 16, macCatalyst 16, tvOS 16, watchOS 9, *) - else { - throw XCTSkip("Clock API not available") - } - let clock: ContinuousClock = .continuous - let event = AsyncEvent(signaledInitially: false) - let mutex = AsyncSemaphore() - await Self.checkExecInterval(duration: .seconds(1), clock: clock) { - do { - try await waitForAny( - event, mutex, - count: 2, - until: .now + .seconds(1), - clock: clock - ) - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue( - type(of: error) == TimeoutError.self - ) - } - } + XCTFail("Unexpected task progression") + } catch is TimeoutError {} } - func testMultipleObjectWaitMultipleTimeout() async throws { + func testMultipleObjectWaitMultiple() async throws { guard #available(macOS 13, iOS 16, macCatalyst 16, tvOS 16, watchOS 9, *) else { @@ -262,33 +127,18 @@ class AsyncObjectClockTimeoutTests: XCTestCase { let clock: ContinuousClock = .continuous let event = AsyncEvent(signaledInitially: false) let mutex = AsyncSemaphore() - let op = TaskOperation { - try await Self.sleep(seconds: 4) - } - Task.detached { - try await Self.sleep(seconds: 1) - event.signal() - } - Task.detached { - try await Self.sleep(seconds: 3) - mutex.signal() - } + let op = TaskOperation { try await mutex.wait() } op.signal() - await Self.checkExecInterval(duration: .seconds(2), clock: clock) { - do { - try await waitForAny( - event, mutex, op, - count: 2, - until: .now + .seconds(2), - clock: clock - ) - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue( - type(of: error) == TimeoutError.self - ) - } - } + Task.detached { event.signal() } + do { + try await waitForAny( + event, mutex, op, + count: 2, + until: .now + .seconds(3), + clock: clock + ) + XCTFail("Unexpected task progression") + } catch is TimeoutError {} } } #endif diff --git a/Tests/AsyncObjectsTests/AsyncSemaphoreTests.swift b/Tests/AsyncObjectsTests/AsyncSemaphoreTests.swift index 1df716d6..a3523219 100644 --- a/Tests/AsyncObjectsTests/AsyncSemaphoreTests.swift +++ b/Tests/AsyncObjectsTests/AsyncSemaphoreTests.swift @@ -4,34 +4,38 @@ import XCTest @MainActor class AsyncSemaphoreTests: XCTestCase { - func testWaitWithTasksLessThanCount() async throws { + func testWithTasksLessThanCount() async throws { let semaphore = AsyncSemaphore(value: 3) - try await Self.checkSemaphoreWait(for: semaphore, taskCount: 2) + await semaphore.spinTasks(count: 2, duration: 10) + try await semaphore.wait(forSeconds: 3) } - func testWaitWithTasksEqualToCount() async throws { + func testWithTasksEqualToCount() async throws { let semaphore = AsyncSemaphore(value: 3) - try await Self.checkSemaphoreWait(for: semaphore, taskCount: 3) + await semaphore.spinTasks(count: 3, duration: 10) + do { + try await semaphore.wait(forSeconds: 3) + XCTFail("Unexpected task progression") + } catch is DurationTimeoutError {} } - func testWaitWithTasksGreaterThanCount() async throws { + func testWithTasksGreaterThanCount() async throws { let semaphore = AsyncSemaphore(value: 3) - try await Self.checkSemaphoreWait( - for: semaphore, - taskCount: 5, - durationInSeconds: 2 - ) + await semaphore.spinTasks(count: 5, duration: 10) + do { + try await semaphore.wait(forSeconds: 3) + XCTFail("Unexpected task progression") + } catch is DurationTimeoutError {} } func testSignaledWaitWithTasksGreaterThanCount() async throws { let semaphore = AsyncSemaphore(value: 3) - semaphore.signal() - try await Self.sleep(seconds: 0.001) - try await Self.checkSemaphoreWait( - for: semaphore, - taskCount: 4, - durationInSeconds: 2 - ) + await semaphore.signalSemaphore() + await semaphore.spinTasks(count: 4, duration: 10) + do { + try await semaphore.wait(forSeconds: 3) + XCTFail("Unexpected task progression") + } catch is DurationTimeoutError {} } func testConcurrentMutation() async throws { @@ -50,150 +54,70 @@ class AsyncSemaphoreTests: XCTestCase { XCTAssertEqual(data.items.count, 10) } - func testDeinit() async throws { - let semaphore = AsyncSemaphore() - Task.detached { - try await Self.sleep(seconds: 1) - semaphore.signal() - } - try await semaphore.wait() - self.addTeardownBlock { [weak semaphore] in - try await Self.sleep(seconds: 1) - XCTAssertNil(semaphore) - } - } - func testConcurrentAccess() async throws { try await withThrowingTaskGroup(of: Void.self) { group in for _ in 0..<10 { group.addTask { let semaphore = AsyncSemaphore(value: 1) - try await Self.checkExecInterval(durationInSeconds: 0) { - try await withThrowingTaskGroup(of: Void.self) { - group in - group.addTask { try await semaphore.wait() } - group.addTask { semaphore.signal() } - try await group.waitForAll() - } + try await withThrowingTaskGroup(of: Void.self) { g in + g.addTask { try await semaphore.wait(forSeconds: 3) } + g.addTask { semaphore.signal() } + try await g.waitForAll() } } try await group.waitForAll() } } } + + func testDeinit() async throws { + let semaphore = AsyncSemaphore(value: 1) + try await semaphore.wait(forSeconds: 3) + self.addTeardownBlock { [weak semaphore] in + semaphore.assertReleased() + } + } } @MainActor class AsyncSemaphoreTimeoutTests: XCTestCase { - static func checkSemaphoreWaitWithTimeOut( - value: UInt = 3, - taskCount count: Int = 1, - withDelay delay: UInt64 = 2, - timeout: UInt64 = 1, - durationInSeconds seconds: Int = 0, - file: StaticString = #filePath, - function: StaticString = #function, - line: UInt = #line - ) async throws { - let semaphore = AsyncSemaphore(value: value) - try await Self.checkExecInterval( - durationInSeconds: seconds, - file: file, function: function, line: line - ) { - try await withThrowingTaskGroup(of: Bool.self) { group in - for _ in 0..( - value: UInt = 3, - taskCount count: Int = 1, - withDelay delay: UInt64 = 2, - timeout: UInt64 = 1, - durationInSeconds seconds: Int = 0, - clock: C, - file: StaticString = #filePath, - function: StaticString = #function, - line: UInt = #line - ) async throws where C.Duration == Duration { - let semaphore = AsyncSemaphore(value: value) - try await Self.checkExecInterval( - durationInSeconds: seconds, - file: file, function: function, line: line - ) { - try await withThrowingTaskGroup(of: Bool.self) { group in - for _ in 0...self - ) - } - } - } - - func testMutexWait() async throws { - guard - #available(macOS 13, iOS 16, macCatalyst 16, tvOS 16, watchOS 9, *) - else { - throw XCTSkip("Clock API not available") - } - let clock: ContinuousClock = .continuous - let mutex = AsyncSemaphore() - Task.detached { - try await Self.sleep(seconds: 1, clock: clock) - mutex.signal() - } - try await Self.checkExecInterval(duration: .seconds(1), clock: clock) { - try await mutex.wait(forSeconds: 2, clock: clock) - } + do { + try await mutex.wait(forSeconds: 3, clock: clock) + XCTFail("Unexpected task progression") + } catch is TimeoutError {} } func testWaitTimeoutWithTasksLessThanCount() async throws { @@ -296,10 +147,9 @@ class AsyncSemaphoreClockTimeoutTests: XCTestCase { throw XCTSkip("Clock API not available") } let clock: ContinuousClock = .continuous - try await Self.checkSemaphoreWaitWithTimeOut( - taskCount: 3, - timeout: 3, - durationInSeconds: 2, + let semaphore = AsyncSemaphore(value: 3) + try await semaphore.spinTasks( + count: 3, duration: 2, timeout: 3, clock: clock ) } @@ -311,10 +161,9 @@ class AsyncSemaphoreClockTimeoutTests: XCTestCase { throw XCTSkip("Clock API not available") } let clock: ContinuousClock = .continuous - try await Self.checkSemaphoreWaitWithTimeOut( - taskCount: 5, - timeout: 1, - durationInSeconds: 2, + let semaphore = AsyncSemaphore(value: 3) + try await semaphore.spinTasks( + count: 5, duration: 5, timeout: 3, clock: clock ) } @@ -327,32 +176,23 @@ class AsyncSemaphoreClockTimeoutTests: XCTestCase { } let clock: ContinuousClock = .continuous let semaphore = AsyncSemaphore(value: 3) - try await Self.checkExecInterval(duration: .seconds(4), clock: clock) { - try await withThrowingTaskGroup(of: Void.self) { group in - for index in 0..<8 { - group.addTask { - if index <= 3 || index.isMultiple(of: 2) { - try await semaphore.wait() - try await Self.sleep(seconds: 2, clock: clock) - semaphore.signal() - } else { - do { - try await semaphore.wait( - forSeconds: 1, - clock: clock - ) - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue( - type(of: error) - == TimeoutError.self - ) - } - } + try await withThrowingTaskGroup(of: Void.self) { group in + for index in 0..<8 { + group.addTask { + if index <= 3 || index.isMultiple(of: 2) { + try await semaphore.wait() + try await Task.sleep(seconds: 5, clock: clock) + semaphore.signal() + } else { + do { + try await semaphore.wait( + forSeconds: 3, clock: clock) + XCTFail("Unexpected task progression") + } catch is TimeoutError {} } } - try await group.waitForAll() } + try await group.waitForAll() } } } @@ -363,62 +203,123 @@ class AsyncSemaphoreCancellationTests: XCTestCase { func testWaitCancellation() async throws { let semaphore = AsyncSemaphore() - let task = Task.detached { - await Self.checkExecInterval(durationInSeconds: 0) { - try? await semaphore.wait() - } - } + let task = Task.detached { try await semaphore.wait() } task.cancel() - await task.value + do { + try await task.value + XCTFail("Unexpected task progression") + } catch {} } func testAlreadyCancelledTask() async throws { let semaphore = AsyncSemaphore() let task = Task.detached { - await Self.checkExecInterval(durationInSeconds: 0) { - do { - try await Self.sleep(seconds: 5) - XCTFail("Unexpected task progression") - } catch {} - XCTAssertTrue(Task.isCancelled) - try? await semaphore.wait() - } + do { + try await Task.sleep(seconds: 5) + XCTFail("Unexpected task progression") + } catch {} + XCTAssertTrue(Task.isCancelled) + try await semaphore.wait() } task.cancel() - await task.value + do { + try await task.value + XCTFail("Unexpected task progression") + } catch {} } } -fileprivate extension XCTestCase { +final class ArrayDataStore: @unchecked Sendable { + var items: [Int] = [] + func add(_ item: Int) { items.append(item) } +} - static func checkSemaphoreWait( - for semaphore: AsyncSemaphore, - taskCount count: Int = 1, - withDelay delay: UInt64 = 1, - durationInSeconds seconds: Int = 1, +fileprivate extension AsyncSemaphore { + + func spinTasks(count: UInt, duration: UInt64) async { + let stream = AsyncStream { continuation in + for _ in 0..=5.7) + @available(macOS 13, iOS 16, macCatalyst 16, tvOS 16, watchOS 9, *) + func spinTasks( + count: UInt, duration: UInt64, timeout: UInt64, clock: C, + file: StaticString = #filePath, + function: StaticString = #function, + line: UInt = #line + ) async throws where C.Duration == Duration { + try await withThrowingTaskGroup(of: Bool.self) { group in + for _ in 0.. Task { - return Task(cancellationSource: source) { - do { - try await Self.sleep(seconds: 1) - } catch { - XCTAssertTrue(Task.isCancelled) - } - } - } - - func testTaskCancellationWithSourcePassedOnSyncInitialization() async throws - { - let source = CancellationSource() - let task = createTaskWithCancellationSource(source) - Task { - try await Self.sleep(seconds: 2) - source.cancel() - } - await task.value - } - - func createDetachedTaskWithCancellationSource( - _ source: CancellationSource - ) -> Task { - return Task.detached(cancellationSource: source) { - do { - try await Self.sleep(seconds: 2) - XCTFail("Unexpected task progression") - } catch {} - } - } - - func testDetachedTaskCancellationWithSourcePassedOnSyncInitialization() - async throws - { - let source = CancellationSource() - let task = createDetachedTaskWithCancellationSource(source) - Task { - try await Self.sleep(seconds: 1) - source.cancel() - } - await task.value - } - - func createThrowingTaskWithCancellationSource( - _ source: CancellationSource - ) -> Task { - return Task(cancellationSource: source) { - try await Self.sleep(seconds: 2) - XCTFail("Unexpected task progression") - } - } - - func testThrowingTaskCancellationWithSourcePassedOnSyncInitialization() - async throws - { - let source = CancellationSource() - let task = createThrowingTaskWithCancellationSource(source) - Task { - try await Self.sleep(seconds: 1) - source.cancel() - } - let value: Void? = try? await task.value - XCTAssertNil(value) - } - - func createThrowingDetachedTaskWithCancellationSource( - _ source: CancellationSource - ) throws -> Task { - return Task.detached(cancellationSource: source) { - try await Self.sleep(seconds: 2) - XCTFail("Unexpected task progression") - } - } - - func - testThrowingDetachedTaskCancellationWithSourcePassedOnSyncInitialization() - async throws - { - let source = CancellationSource() - let task = try createThrowingDetachedTaskWithCancellationSource(source) - Task { - try await Self.sleep(seconds: 1) - source.cancel() - } - let value: Void? = try? await task.value - XCTAssertNil(value) - } - func testDeinit() async throws { let source = CancellationSource() - let task = try createThrowingDetachedTaskWithCancellationSource(source) - Task.detached { - try await Self.sleep(seconds: 1) - source.cancel() + let task = Task.detached(cancellationSource: source) { + try await Task.sleep(seconds: 10) + XCTFail("Unexpected task progression") } + try await waitUntil(source, timeout: 3) { !$0.registeredTasks.isEmpty } + source.cancel() + try await waitUntil(source, timeout: 3) { $0.registeredTasks.isEmpty } try? await task.value self.addTeardownBlock { [weak source] in - try await Self.sleep(seconds: 1) - XCTAssertNil(source) + source.assertReleased() } } } diff --git a/Tests/AsyncObjectsTests/LockerTests.swift b/Tests/AsyncObjectsTests/LockerTests.swift index 8060d02a..77219fdb 100644 --- a/Tests/AsyncObjectsTests/LockerTests.swift +++ b/Tests/AsyncObjectsTests/LockerTests.swift @@ -27,66 +27,57 @@ class LockerTests: XCTestCase { XCTAssertEqual(iterations, 5) } - static func threadLocalValue(forKey key: NSCopying) -> T? { - let threadDictionary = Thread.current.threadDictionary - return threadDictionary[key] as? T - } - func testLockReleasedAfterError() throws { let lock = Locker() - XCTAssertFalse(Self.threadLocalValue(forKey: lock) ?? false) + XCTAssertFalse(lock.isNested) XCTAssertThrowsError( try lock.perform { - defer { - XCTAssertTrue(Self.threadLocalValue(forKey: lock) ?? false) - } - XCTAssertTrue(Self.threadLocalValue(forKey: lock) ?? false) + defer { XCTAssertTrue(lock.isNested) } + XCTAssertTrue(lock.isNested) throw CancellationError() } ) - XCTAssertFalse(Self.threadLocalValue(forKey: lock) ?? false) + XCTAssertFalse(lock.isNested) } func testNestedLocking() { let lock = Locker() - XCTAssertFalse(Self.threadLocalValue(forKey: lock) ?? false) + XCTAssertFalse(lock.isNested) let _: UInt64 = lock.perform { - defer { - XCTAssertTrue(Self.threadLocalValue(forKey: lock) ?? false) - } - XCTAssertTrue(Self.threadLocalValue(forKey: lock) ?? false) + defer { XCTAssertTrue(lock.isNested) } + XCTAssertTrue(lock.isNested) var generator = SystemRandomNumberGenerator() return lock.perform { - defer { - XCTAssertTrue(Self.threadLocalValue(forKey: lock) ?? false) - } - XCTAssertTrue(Self.threadLocalValue(forKey: lock) ?? false) + defer { XCTAssertTrue(lock.isNested) } + XCTAssertTrue(lock.isNested) return generator.next() } } - XCTAssertFalse(Self.threadLocalValue(forKey: lock) ?? false) + XCTAssertFalse(lock.isNested) } func testLockReleasedAfterNestedError() throws { let lock = Locker() - XCTAssertFalse(Self.threadLocalValue(forKey: lock) ?? false) + XCTAssertFalse(lock.isNested) XCTAssertThrowsError( try lock.perform { - defer { - XCTAssertTrue(Self.threadLocalValue(forKey: lock) ?? false) - } - XCTAssertTrue(Self.threadLocalValue(forKey: lock) ?? false) + defer { XCTAssertTrue(lock.isNested) } + XCTAssertTrue(lock.isNested) return try lock.perform { - defer { - XCTAssertTrue( - Self.threadLocalValue(forKey: lock) ?? false - ) - } - XCTAssertTrue(Self.threadLocalValue(forKey: lock) ?? false) + defer { XCTAssertTrue(lock.isNested) } + XCTAssertTrue(lock.isNested) throw CancellationError() } } as UInt64 ) - XCTAssertFalse(Self.threadLocalValue(forKey: lock) ?? false) + XCTAssertFalse(lock.isNested) + } +} + +fileprivate extension Locker { + + var isNested: Bool { + let threadDictionary = Thread.current.threadDictionary + return threadDictionary[self] as? Bool ?? false } } diff --git a/Tests/AsyncObjectsTests/NonThrowingFutureTests.swift b/Tests/AsyncObjectsTests/NonThrowingFutureTests.swift index d2a33063..fd8b9eae 100644 --- a/Tests/AsyncObjectsTests/NonThrowingFutureTests.swift +++ b/Tests/AsyncObjectsTests/NonThrowingFutureTests.swift @@ -5,81 +5,77 @@ import Dispatch @MainActor class NonThrowingFutureTests: XCTestCase { - func testFutureFulfilledInitialization() async throws { + func testFulfilledInitialization() async throws { let future = Future(with: .success(5)) - let value = await future.get() + let value = try await future.wait(forSeconds: 3) XCTAssertEqual(value, 5) } - func testFutureFulfillAfterInitialization() async throws { + func testFulfillAfterInitialization() async throws { let future = Future() - await withTaskGroup(of: Void.self) { group in + try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { - let value = await future.get() + let value = try await future.wait(forSeconds: 3) XCTAssertEqual(value, 5) } group.addTask { - try! await Self.sleep(seconds: 1) await future.fulfill(producing: 5) } - await group.waitForAll() + try await group.waitForAll() } } - func testFutureFulfilledWithAttemptClosure() async throws { + func testFulfilledWithAttemptClosure() async throws { let future = Future { promise in DispatchQueue.global(qos: .background) - .asyncAfter(deadline: .now() + 2) { + .asyncAfter(deadline: .now() + 0.5) { promise(.success(5)) } } - let value = await future.get() + let value = try await future.wait(forSeconds: 5) XCTAssertEqual(value, 5) } func testMultipleTimesFutureFulfilled() async throws { let future = Future(with: .success(5)) await future.fulfill(producing: 10) - let value = await future.get() + let value = try await future.wait(forSeconds: 3) XCTAssertEqual(value, 5) } - func testFutureAsyncInitializerDuration() async throws { - await Self.checkExecInterval(durationInSeconds: 0) { - let _ = Future { promise in - try! await Self.sleep(seconds: 1) - promise(.success(5)) - } + func testAsyncInitializerDuration() async throws { + let future = Future { promise in + try! await Task.sleep(seconds: 2) + promise(.success(5)) } + let value = try await future.wait(forSeconds: 5) + XCTAssertEqual(value, 5) } func testDeinit() async throws { let future = Future() - Task.detached { - try await Self.sleep(seconds: 1) - await future.fulfill(producing: 5) - } - let _ = await future.get() + let task = Task.detached { await future.fulfill(producing: 5) } + let _ = try await future.wait(forSeconds: 3) + await task.value self.addTeardownBlock { [weak future] in - try await Self.sleep(seconds: 1) - XCTAssertNil(future) + future.assertReleased() } } func testConcurrentAccess() async throws { - await withTaskGroup(of: Void.self) { group in + try await withThrowingTaskGroup(of: Void.self) { group in for i in 0..<10 { group.addTask { let future = Future() - await Self.checkExecInterval(durationInSeconds: 0) { - await withTaskGroup(of: Void.self) { group in - group.addTask { let _ = await future.get() } - group.addTask { await future.fulfill(producing: i) } - await group.waitForAll() + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + let _ = try await future.wait(forSeconds: 3) } + group.addTask { await future.fulfill(producing: i) } + try await group.waitForAll() } } - await group.waitForAll() + try await group.waitForAll() } } } @@ -92,29 +88,16 @@ class NonThrowingFutureCombiningTests: XCTestCase { let future1 = Future() let future2 = Future() let future3 = Future() - let allFuture = Future.all(future1, future3, future2) - try await Self.checkExecInterval(durationInSeconds: 3) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - let value = await allFuture.get() - XCTAssertEqual(value, [1, 3, 2]) - } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - group.addTask { - try await Self.sleep(seconds: 1) - await future1.fulfill(producing: 1) - } - group.addTask { - try await Self.sleep(seconds: 2) - await future2.fulfill(producing: 2) - } - group.addTask { - try await Self.sleep(seconds: 3) - await future3.fulfill(producing: 3) - } - try await group.waitForAll() + let future = Future.all(future1, future3, future2) + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + let value = try await future.wait(forSeconds: 3) + XCTAssertEqual(value, [1, 3, 2]) } + group.addTask { await future1.fulfill(producing: 1) } + group.addTask { await future2.fulfill(producing: 2) } + group.addTask { await future3.fulfill(producing: 3) } + try await group.waitForAll() } } @@ -122,36 +105,23 @@ class NonThrowingFutureCombiningTests: XCTestCase { let future1 = Future() let future2 = Future() let future3 = Future() - let allFuture = Future.allSettled(future1, future2, future3) - try await Self.checkExecInterval(durationInSeconds: 3) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - let values = await allFuture.get() - for (index, item) in values.enumerated() { - switch item { - case .success(let value): - XCTAssertEqual(value, index + 1) - default: - XCTFail("Unexpected future fulfillment") - } + let future = Future.allSettled(future1, future2, future3) + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + let values = try await future.wait(forSeconds: 3) + for (index, item) in values.enumerated() { + switch item { + case .success(let value): + XCTAssertEqual(value, index + 1) + default: + XCTFail("Unexpected future fulfillment") } } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - group.addTask { - try await Self.sleep(seconds: 1) - await future1.fulfill(producing: 1) - } - group.addTask { - try await Self.sleep(seconds: 2) - await future2.fulfill(producing: 2) - } - group.addTask { - try await Self.sleep(seconds: 3) - await future3.fulfill(producing: 3) - } - try await group.waitForAll() } + await future1.fulfill(producing: 1) + await future2.fulfill(producing: 2) + await future3.fulfill(producing: 3) + try await group.waitForAll() } } @@ -159,31 +129,17 @@ class NonThrowingFutureCombiningTests: XCTestCase { let future1 = Future() let future2 = Future() let future3 = Future() - let allFuture = Future.race(future1, future2, future3) - try await Self.checkExecInterval(durationInSeconds: 3) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - await Self.checkExecInterval(durationInSeconds: 1) { - let value = await allFuture.get() - XCTAssertEqual(value, 1) - } - } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - group.addTask { - try await Self.sleep(seconds: 1) - await future1.fulfill(producing: 1) - } - group.addTask { - try await Self.sleep(seconds: 2) - await future2.fulfill(producing: 2) - } - group.addTask { - try await Self.sleep(seconds: 3) - await future3.fulfill(producing: 3) - } - try await group.waitForAll() + let future = Future.race(future1, future2, future3) + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + let value = try await future.wait(forSeconds: 3) + XCTAssertEqual(value, 1) } + await future1.fulfill(producing: 1) + try await waitUntil(future, timeout: 5) { $0.result != nil } + await future2.fulfill(producing: 2) + await future3.fulfill(producing: 3) + try await group.waitForAll() } } @@ -191,43 +147,41 @@ class NonThrowingFutureCombiningTests: XCTestCase { let future1 = Future() let future2 = Future() let future3 = Future() - let allFuture = Future.any(future1, future2, future3) - try await Self.checkExecInterval(durationInSeconds: 3) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - await Self.checkExecInterval(durationInSeconds: 1) { - let value = await allFuture.get() - XCTAssertEqual(value, 1) - } - } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - group.addTask { - try await Self.sleep(seconds: 1) - await future1.fulfill(producing: 1) - } - group.addTask { - try await Self.sleep(seconds: 2) - await future2.fulfill(producing: 2) - } - group.addTask { - try await Self.sleep(seconds: 3) - await future3.fulfill(producing: 3) - } - try await group.waitForAll() + let future = Future.any(future1, future2, future3) + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + let value = try await future.wait(forSeconds: 3) + XCTAssertEqual(value, 1) } + await future1.fulfill(producing: 1) + try await waitUntil(future, timeout: 5) { $0.result != nil } + await future2.fulfill(producing: 2) + await future3.fulfill(producing: 3) + try await group.waitForAll() } } - func testConstructingAllFutureFromEmpty() async { + func testConstructingAllFutureFromEmpty() async throws { let future = Future.all() - let value = await future.get() + let value = try await future.wait(forSeconds: 3) XCTAssertTrue(value.isEmpty) } - func testConstructingAllSettledFutureFromEmpty() async { + func testConstructingAllSettledFutureFromEmpty() async throws { let future = Future.allSettled() - let value = await future.get() + let value = try await future.wait(forSeconds: 3) XCTAssertTrue(value.isEmpty) } } + +extension Future where Failure == Never { + @Sendable + @inlinable + func wait(forSeconds seconds: UInt64) async throws -> Output { + return try await waitForTaskCompletion( + withTimeoutInNanoseconds: seconds * 1_000_000_000 + ) { + return await self.get() + } + } +} diff --git a/Tests/AsyncObjectsTests/StandardLibraryTests.swift b/Tests/AsyncObjectsTests/StandardLibraryTests.swift index 159cbfea..784716c4 100644 --- a/Tests/AsyncObjectsTests/StandardLibraryTests.swift +++ b/Tests/AsyncObjectsTests/StandardLibraryTests.swift @@ -6,7 +6,7 @@ class StandardLibraryTests: XCTestCase { func testTaskValueFetchingCancelation() async throws { let task = Task { () -> Int in - try await Self.sleep(seconds: 1) + try await Task.sleep(seconds: 1) return 5 } @@ -32,7 +32,7 @@ class StandardLibraryTests: XCTestCase { let time = DispatchTime.now() async let val: Void = Task { do { - try await Self.sleep(seconds: 1) + try await Task.sleep(seconds: 1) print("\(#function): Async task completed") } catch { XCTFail("Unrecognized task cancellation") @@ -156,7 +156,7 @@ class StandardLibraryTests: XCTestCase { func testCancellationHandlerFromAlreadyCancelledTask() async throws { let task = Task { do { - try await Self.sleep(seconds: 5) + try await Task.sleep(seconds: 1) } catch { await withTaskCancellationHandler { XCTAssertTrue(Task.isCancelled) @@ -169,4 +169,20 @@ class StandardLibraryTests: XCTestCase { task.cancel() await task.value } + + func testTaskGroupTaskStart() async throws { + await withTaskGroup(of: Void.self) { group in + let store = ArrayDataStore() + await group.addTaskAndStart { XCTAssertTrue(store.items.isEmpty) } + store.add(1) + } + } + + func testThrowingTaskGroupTaskStart() async throws { + await withThrowingTaskGroup(of: Void.self) { group in + let store = ArrayDataStore() + await group.addTaskAndStart { XCTAssertTrue(store.items.isEmpty) } + store.add(1) + } + } } diff --git a/Tests/AsyncObjectsTests/TaskOperationTests.swift b/Tests/AsyncObjectsTests/TaskOperationTests.swift index 4d47d61e..936b42a1 100644 --- a/Tests/AsyncObjectsTests/TaskOperationTests.swift +++ b/Tests/AsyncObjectsTests/TaskOperationTests.swift @@ -5,9 +5,9 @@ import Dispatch @MainActor class TaskOperationTests: XCTestCase { - func testTaskOperation() async throws { + func testExecution() async throws { let operation = TaskOperation { - (try? await Self.sleep(seconds: 3)) != nil + (try? await Task.sleep(seconds: 1)) != nil } XCTAssertTrue(operation.isAsynchronous) XCTAssertFalse(operation.isExecuting) @@ -19,17 +19,20 @@ class TaskOperationTests: XCTestCase { #else operation.start() #endif - expectation( - for: NSPredicate { _, _ in operation.isExecuting }, - evaluatedWith: nil, - handler: nil - ) - waitForExpectations(timeout: 2) - await GlobalContinuation.with { continuation in - DispatchQueue.global(qos: .default).async { - operation.waitUntilFinished() - continuation.resume() + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await waitUntil( + operation, + timeout: 3, + satisfies: \.isExecuting + ) + } + group.addTask { + await GlobalContinuation.with { continuation in + operation.completionBlock = { continuation.resume() } + } } + try await group.waitForAll() } XCTAssertTrue(operation.isFinished) XCTAssertFalse(operation.isExecuting) @@ -40,9 +43,9 @@ class TaskOperationTests: XCTestCase { } } - func testThrowingTaskOperation() async throws { + func testThrowingExecution() async throws { let operation = TaskOperation { - try await Self.sleep(seconds: 3) + try await Task.sleep(seconds: 1) } XCTAssertFalse(operation.isExecuting) XCTAssertFalse(operation.isFinished) @@ -53,12 +56,7 @@ class TaskOperationTests: XCTestCase { #else operation.start() #endif - expectation( - for: NSPredicate { _, _ in operation.isExecuting }, - evaluatedWith: nil, - handler: nil - ) - waitForExpectations(timeout: 2) + try await waitUntil(operation, timeout: 3, satisfies: \.isExecuting) await GlobalContinuation.with { continuation in DispatchQueue.global(qos: .default).async { operation.waitUntilFinished() @@ -70,23 +68,26 @@ class TaskOperationTests: XCTestCase { XCTAssertFalse(operation.isCancelled) } - func testTaskOperationAsyncWait() async throws { - let operation = TaskOperation { - (try? await Self.sleep(seconds: 3)) != nil - } + func testAsyncWait() async throws { + let operation = TaskOperation { /* Do nothing */ } + operation.signal() + try await operation.wait(forSeconds: 3) + } + + func testFinisheAsyncdWait() async throws { + let operation = TaskOperation { /* Do nothing */ } operation.signal() - try await Self.checkExecInterval( - durationInRange: ...3 - ) { try await operation.wait() } + try await operation.wait(forSeconds: 3) } func testDeinit() async throws { - let operation = TaskOperation { try await Self.sleep(seconds: 1) } + let operation = TaskOperation { + try await Task.sleep(seconds: 1) + } operation.signal() - try await operation.wait() + try await operation.wait(forSeconds: 5) self.addTeardownBlock { [weak operation] in - try await Self.sleep(seconds: 1) - XCTAssertNil(operation) + operation.assertReleased() } } @@ -95,12 +96,10 @@ class TaskOperationTests: XCTestCase { for _ in 0..<10 { group.addTask { let operation = TaskOperation {} - try await Self.checkExecInterval(durationInSeconds: 0) { - try await withThrowingTaskGroup(of: Void.self) { g in - g.addTask { try await operation.wait() } - g.addTask { operation.signal() } - try await g.waitForAll() - } + try await withThrowingTaskGroup(of: Void.self) { g in + g.addTask { try await operation.wait(forSeconds: 3) } + g.addTask { operation.signal() } + try await g.waitForAll() } } try await group.waitForAll() @@ -112,35 +111,15 @@ class TaskOperationTests: XCTestCase { @MainActor class TaskOperationTimeoutTests: XCTestCase { - func testWait() async throws { - let operation = TaskOperation { - (try? await Self.sleep(seconds: 1)) != nil - } - operation.signal() - try await Self.checkExecInterval(durationInSeconds: 1) { - try await operation.wait(forSeconds: 2) - } - } - - func testFinishedWait() async throws { - let operation = TaskOperation { /* Do nothing */ } - operation.signal() - try await operation.wait(forSeconds: 1) - } - func testWaitTimeout() async throws { let operation = TaskOperation { - (try? await Self.sleep(seconds: 3)) != nil + try await Task.sleep(seconds: 10) } operation.signal() - await Self.checkExecInterval(durationInSeconds: 1) { - do { - try await operation.wait(forSeconds: 1) - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue(type(of: error) == DurationTimeoutError.self) - } - } + do { + try await operation.wait(forSeconds: 3) + XCTFail("Unexpected task progression") + } catch is DurationTimeoutError {} } } @@ -148,36 +127,6 @@ class TaskOperationTimeoutTests: XCTestCase { @MainActor class TaskOperationClockTimeoutTests: XCTestCase { - func testWait() async throws { - guard - #available(macOS 13, iOS 16, macCatalyst 16, tvOS 16, watchOS 9, *) - else { - throw XCTSkip("Clock API not available") - } - let clock: ContinuousClock = .continuous - let operation = TaskOperation { - (try? await Self.sleep(seconds: 1, clock: clock)) != nil - } - operation.signal() - try await Self.checkExecInterval(duration: .seconds(1), clock: clock) { - try await operation.wait(forSeconds: 2, clock: clock) - } - } - - func testFinishedWait() async throws { - guard - #available(macOS 13, iOS 16, macCatalyst 16, tvOS 16, watchOS 9, *) - else { - throw XCTSkip("Clock API not available") - } - let clock: ContinuousClock = .continuous - let operation = TaskOperation { /* Do nothing */ } - operation.signal() - try await Self.checkExecInterval(duration: .seconds(0), clock: clock) { - try await operation.wait(forSeconds: 2, clock: clock) - } - } - func testWaitTimeout() async throws { guard #available(macOS 13, iOS 16, macCatalyst 16, tvOS 16, watchOS 9, *) @@ -186,19 +135,13 @@ class TaskOperationClockTimeoutTests: XCTestCase { } let clock: ContinuousClock = .continuous let operation = TaskOperation { - (try? await Self.sleep(seconds: 3, clock: clock)) != nil + try await Task.sleep(until: .now + .seconds(10), clock: clock) } operation.signal() - await Self.checkExecInterval(duration: .seconds(1), clock: clock) { - do { - try await operation.wait(forSeconds: 1, clock: clock) - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue( - type(of: error) == TimeoutError.self - ) - } - } + do { + try await operation.wait(forSeconds: 3, clock: clock) + XCTFail("Unexpected task progression") + } catch is TimeoutError {} } } #endif @@ -208,7 +151,7 @@ class TaskOperationCancellationTests: XCTestCase { func testCancellation() async throws { let operation = TaskOperation { - (try? await Self.sleep(seconds: 3)) != nil + (try? await Task.sleep(seconds: 10)) != nil } XCTAssertFalse(operation.isExecuting) XCTAssertFalse(operation.isFinished) @@ -219,25 +162,9 @@ class TaskOperationCancellationTests: XCTestCase { #else operation.start() #endif - expectation( - for: NSPredicate { _, _ in operation.isExecuting }, - evaluatedWith: nil, - handler: nil - ) - waitForExpectations(timeout: 2) + try await waitUntil(operation, timeout: 3, satisfies: \.isExecuting) operation.cancel() - await GlobalContinuation.with { continuation in - DispatchQueue.global(qos: .default).async { - operation.waitUntilFinished() - continuation.resume() - } - } - expectation( - for: NSPredicate { _, _ in operation.isCancelled }, - evaluatedWith: nil, - handler: nil - ) - waitForExpectations(timeout: 2) + try await waitUntil(operation, timeout: 3, satisfies: \.isCancelled) XCTAssertTrue(operation.isFinished) XCTAssertFalse(operation.isExecuting) switch await operation.result { @@ -248,7 +175,7 @@ class TaskOperationCancellationTests: XCTestCase { func testThrowingCancellation() async throws { let operation = TaskOperation { - try await Self.sleep(seconds: 3) + try await Task.sleep(seconds: 1) } XCTAssertFalse(operation.isExecuting) XCTAssertFalse(operation.isFinished) @@ -259,79 +186,56 @@ class TaskOperationCancellationTests: XCTestCase { #else operation.start() #endif - expectation( - for: NSPredicate { _, _ in operation.isExecuting }, - evaluatedWith: nil, - handler: nil - ) - waitForExpectations(timeout: 2) + try await waitUntil(operation, timeout: 3, satisfies: \.isExecuting) operation.cancel() - await GlobalContinuation.with { continuation in - DispatchQueue.global(qos: .default).async { - operation.waitUntilFinished() - continuation.resume() - } - } - expectation( - for: NSPredicate { _, _ in operation.isCancelled }, - evaluatedWith: nil, - handler: nil - ) - waitForExpectations(timeout: 2) + try await waitUntil(operation, timeout: 3, satisfies: \.isCancelled) XCTAssertTrue(operation.isFinished) XCTAssertFalse(operation.isExecuting) } func testWaitCancellation() async throws { - let operation = TaskOperation { try await Self.sleep(seconds: 10) } + let operation = TaskOperation { + try await Task.sleep(seconds: 1) + } let task = Task.detached { - try await Self.checkExecInterval(durationInSeconds: 0) { - try await operation.wait() - } + try await operation.wait(forSeconds: 3) } task.cancel() do { try await task.value XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue(type(of: error) == CancellationError.self) - } + } catch is CancellationError {} } func testAlreadyCancelledTask() async throws { - let operation = TaskOperation { try await Self.sleep(seconds: 10) } + let operation = TaskOperation { try await Task.sleep(seconds: 10) } let task = Task.detached { - try await Self.checkExecInterval(durationInSeconds: 0) { - do { - try await Self.sleep(seconds: 5) - XCTFail("Unexpected task progression") - } catch {} - XCTAssertTrue(Task.isCancelled) - try await operation.wait() - } + do { + try await Task.sleep(seconds: 1) + XCTFail("Unexpected task progression") + } catch {} + XCTAssertTrue(Task.isCancelled) + try await operation.wait() } task.cancel() do { try await task.value XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue(type(of: error) == CancellationError.self) - } + } catch is CancellationError {} } func testDeinit() async throws { let operation = TaskOperation { do { - try await Self.sleep(seconds: 2) + try await Task.sleep(seconds: 1) XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue(type(of: error) == CancellationError.self) - } + } catch is CancellationError {} } operation.signal() + operation.cancel() + try await waitUntil(operation, timeout: 3, satisfies: \.isCancelled) self.addTeardownBlock { [weak operation] in - try await Self.sleep(seconds: 1) - XCTAssertNil(operation) + operation.assertReleased() } } } @@ -339,67 +243,54 @@ class TaskOperationCancellationTests: XCTestCase { @MainActor class TaskOperationTaskManagementTests: XCTestCase { - func createOperationWithChildTasks( - track: Bool = false - ) -> TaskOperation { - return TaskOperation(flags: track ? .trackUnstructuredTasks : []) { - Task { - try await Self.sleep(seconds: 1) - } - Task { - try await Self.sleep(seconds: 2) - } - Task { - try await Self.sleep(seconds: 3) - } - Task.detached { - try await Self.sleep(seconds: 5) - } - } - } - func testOperationWithoutTrackingChildTasks() async throws { - let operation = createOperationWithChildTasks(track: false) + let operation = TaskOperation(track: false) operation.signal() - try await Self.checkExecInterval(durationInSeconds: 0) { - try await operation.wait() - } + try await operation.wait(forSeconds: 3) } func testOperationWithTrackingChildTasks() async throws { - let operation = createOperationWithChildTasks(track: true) + let operation = TaskOperation(track: true) operation.signal() - try await Self.checkExecInterval(durationInSeconds: 3) { - try await operation.wait() - } + try await operation.wait(forSeconds: 8) } func testNotStartedError() async throws { - let operation = TaskOperation { try await Self.sleep(seconds: 1) } + let operation = TaskOperation { + try await Task.sleep(seconds: 1) + } let result = await operation.result switch result { - case .success: XCTFail("Unexpected operation result") - case .failure(let error): - XCTAssertTrue(type(of: error) == EarlyInvokeError.self) - print( - "[\(#function)] [\(type(of: error))] \(error.localizedDescription)" - ) + case .failure(let error as EarlyInvokeError): XCTAssertFalse(error.localizedDescription.isEmpty) + default: XCTFail("Unexpected operation result") } } func testNotStartedCancellationError() async throws { - let operation = TaskOperation { try await Self.sleep(seconds: 1) } + let operation = TaskOperation { + try await Task.sleep(seconds: 1) + } operation.cancel() let result = await operation.result switch result { - case .success: XCTFail("Unexpected operation result") - case .failure(let error): - XCTAssertTrue(type(of: error) == CancellationError.self) - print( - "[\(#function)] [\(type(of: error))] \(error.localizedDescription)" - ) + case .failure(let error as CancellationError): XCTAssertFalse(error.localizedDescription.isEmpty) + default: XCTFail("Unexpected operation result") + } + } +} + +fileprivate extension TaskOperation { + + convenience init(track: Bool) where R == Void { + self.init(flags: track ? .trackUnstructuredTasks : []) { + for i in 0..<5 { + Task { + let duration = UInt64(Double(i + 1) * 1E9) + try await Task.sleep(nanoseconds: duration) + } + } } } } diff --git a/Tests/AsyncObjectsTests/TaskQueueTests.swift b/Tests/AsyncObjectsTests/TaskQueueTests.swift index 8d211b05..349ee563 100644 --- a/Tests/AsyncObjectsTests/TaskQueueTests.swift +++ b/Tests/AsyncObjectsTests/TaskQueueTests.swift @@ -1,6 +1,12 @@ import XCTest +import OrderedCollections @testable import AsyncObjects +typealias QE = OrderedDictionary.Element +typealias TaskOption = ( + queue: TaskPriority?, task: TaskPriority?, flags: TaskQueue.Flags +) + @MainActor class TaskQueueTests: XCTestCase { @@ -13,15 +19,16 @@ class TaskQueueTests: XCTestCase { func testSignalingBlockedDoesNothing() async throws { let queue = TaskQueue() - Task.detached { + let task = Task.detached { try await queue.exec(flags: .block) { - try await Self.sleep(seconds: 3) + try await Task.sleep(seconds: 10) } } - try await Self.sleep(seconds: 1) + try await waitUntil(queue, timeout: 3) { $0.blocked } queue.signal() let blocked = await queue.blocked XCTAssertTrue(blocked) + task.cancel() } func testWait() async throws { @@ -39,9 +46,7 @@ class TaskQueueTests: XCTestCase { ] try await withThrowingTaskGroup(of: Void.self) { group in options.forEach { option in - group.addTask { - try await Self.checkWaitOnQueue(option: option) - } + group.addTask { try await TaskQueue().checkWait(for: option) } } try await group.waitForAll() } @@ -49,29 +54,23 @@ class TaskQueueTests: XCTestCase { func testTaskExecutionWithJustAddingTasks() async throws { let queue = TaskQueue() - queue.addTask(flags: .barrier) { - try await Self.sleep(seconds: 2) - } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.001) - try await Self.checkExecInterval(durationInSeconds: 2) { - queue.addTask { try! await Self.sleep(seconds: 2) } - try await queue.wait() + let stream = AsyncStream { c in + Task.detached { + await queue.addTaskAndStart(flags: .barrier) { c.yield(1) } + await queue.addTaskAndStart { c.yield(2) } + c.finish() + } } + await stream.assertElements() + try await queue.wait(forSeconds: 3) } func testDeinit() async throws { let queue = TaskQueue() - try await queue.exec(flags: .barrier) { - try await Self.sleep(seconds: 1) - } - try await queue.exec { - try await Self.sleep(seconds: 1) - } - try await Self.sleep(seconds: 0.001) + await queue.exec(flags: .barrier) { /* Do nothing */ } + await queue.exec { /* Do nothing */ } self.addTeardownBlock { [weak queue] in - try await Self.sleep(seconds: 1) - XCTAssertNil(queue) + queue.assertReleased() } } } @@ -79,52 +78,6 @@ class TaskQueueTests: XCTestCase { @MainActor class TaskQueueTimeoutTests: XCTestCase { - private static func checkWaitTimeoutOnQueue( - option: TaskOption, - file: StaticString = #filePath, - function: StaticString = #function, - line: UInt = #line - ) async throws { - let queue = TaskQueue(priority: option.queue) - try await Self.checkExecInterval( - name: "For queue priority: \(option.queue.str), " - + "task priority: \(option.task.str) " - + "and flags: \(option.flags.rawValue)", - durationInSeconds: 1, - file: file, function: function, line: line - ) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - try await queue.exec( - priority: option.task, - flags: [option.flags, .block] - ) { - try await Self.sleep(seconds: 2) - } - } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - group.addTask { - do { - try await queue.wait(forSeconds: 1) - XCTFail( - "Unexpected task progression", - file: file, line: line - ) - } catch { - XCTAssertTrue( - type(of: error) == DurationTimeoutError.self, - file: file, line: line - ) - } - } - for try await _ in group.prefix(1) { - group.cancelAll() - } - } - } - } - func testWaitTimeout() async throws { let options: [TaskOption] = [ (queue: nil, task: nil, flags: []), @@ -141,7 +94,7 @@ class TaskQueueTimeoutTests: XCTestCase { try await withThrowingTaskGroup(of: Void.self) { group in options.forEach { option in group.addTask { - try await Self.checkWaitTimeoutOnQueue(option: option) + try await TaskQueue().checkWaitTimeout(for: option) } } try await group.waitForAll() @@ -149,55 +102,6 @@ class TaskQueueTimeoutTests: XCTestCase { } #if swift(>=5.7) - @available(macOS 13, iOS 16, macCatalyst 16, tvOS 16, watchOS 9, *) - private static func checkWaitTimeoutOnQueue( - option: TaskOption, - clock: C, - file: StaticString = #filePath, - function: StaticString = #function, - line: UInt = #line - ) async throws where C.Duration == Duration { - let queue = TaskQueue(priority: option.queue) - try await Self.checkExecInterval( - name: "For queue priority: \(option.queue.str), " - + "task priority: \(option.task.str) " - + "and flags: \(option.flags.rawValue)", - durationInSeconds: 1, - file: file, function: function, line: line - ) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - try await queue.exec( - priority: option.task, - flags: [option.flags, .block] - ) { - try await Self.sleep(seconds: 2, clock: clock) - } - } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01, clock: clock) - group.addTask { - do { - try await queue.wait(forSeconds: 1, clock: clock) - XCTFail( - "Unexpected task progression", - file: file, line: line - ) - } catch { - XCTAssertTrue( - type(of: error) - == TimeoutError.self, - file: file, line: line - ) - } - } - for try await _ in group.prefix(1) { - group.cancelAll() - } - } - } - } - func testWaitClockTimeout() async throws { guard #available(macOS 13, iOS 16, macCatalyst 16, tvOS 16, watchOS 9, *) @@ -220,8 +124,8 @@ class TaskQueueTimeoutTests: XCTestCase { try await withThrowingTaskGroup(of: Void.self) { group in options.forEach { option in group.addTask { - try await Self.checkWaitTimeoutOnQueue( - option: option, + try await TaskQueue().checkWaitTimeout( + for: option, clock: clock ) } @@ -237,210 +141,150 @@ class TaskQueueBlockOperationTests: XCTestCase { func testExecutionOfTwoOperations() async throws { let queue = TaskQueue() - try await Self.checkExecInterval(durationInSeconds: 3) { - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - try await queue.exec(flags: .block) { - try await Self.sleep(seconds: 1) - } - } - group.addTask { - try await queue.exec(flags: .block) { - try await Self.sleep(seconds: 2) - } - } - try await group.waitForAll() + let stream = AsyncStream { c in + Task.detached { + await queue.addTaskAndStart(flags: .block) { c.yield(1) } + await queue.addTaskAndStart(flags: .block) { c.yield(2) } + c.finish() } } + await stream.assertElements() + try await queue.wait(forSeconds: 3) } func testExecutionOfTaskBeforeOperation() async throws { let queue = TaskQueue() - try await Self.checkExecInterval(durationInSeconds: 1) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - try await queue.exec { - try await Self.sleep(seconds: 1) - } + let stream = AsyncStream { c in + Task.detached { + await queue.addTaskAndStart { + c.yield(1) + try await Task.sleep(seconds: 10) } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - group.addTask { - try await queue.exec(flags: .block) { - try await Self.sleep(seconds: 1) - } - } - try await group.waitForAll() + await queue.addTaskAndStart(flags: .block) { c.yield(2) } + c.finish() } } + try await queue.wait(forSeconds: 3) + await stream.assertElements() } func testExecutionOfTaskAfterOperation() async throws { let queue = TaskQueue() - try await Self.checkExecInterval(durationInSeconds: 3) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - try await queue.exec(flags: .block) { - try await Self.sleep(seconds: 2) - } - } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - group.addTask { - try await queue.exec { - try await Self.sleep(seconds: 1) - } + let stream = AsyncStream { c in + Task.detached { + await queue.addTaskAndStart(flags: .block) { + c.yield(1) + try await Task.sleep(seconds: 1) + c.yield(2) } - try await group.waitForAll() + await queue.addTaskAndStart { c.yield(3) } + c.finish() } } + await stream.assertElements() + try await queue.wait(forSeconds: 3) } func testCancellation() async throws { let queue = TaskQueue() - queue.addTask(flags: .block) { - try await Self.sleep(seconds: 10) + await queue.addTaskAndStart(flags: .block) { + try await Task.sleep(seconds: 10) } let task = Task.detached { - try await Self.checkExecInterval(durationInSeconds: 0) { - await queue.exec(flags: .block) {} - try await queue.wait() - } + await queue.exec(flags: .block) {} + try await queue.wait(forSeconds: 3) + XCTFail("Unexpected task progression") } - task.cancel() do { + task.cancel() try await task.value XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue(type(of: error) == CancellationError.self) - } + } catch is CancellationError {} } func testAlreadyCancelledTask() async throws { let queue = TaskQueue() - queue.addTask(flags: .block) { - try await Self.sleep(seconds: 10) + await queue.addTaskAndStart(flags: .block) { + try await Task.sleep(seconds: 10) } let task = Task.detached { - try await Self.checkExecInterval(durationInSeconds: 0) { - do { - try await Self.sleep(seconds: 5) - XCTFail("Unexpected task progression") - } catch {} - XCTAssertTrue(Task.isCancelled) - await queue.exec(flags: .block) {} - try await queue.wait() - } + do { + try await Task.sleep(seconds: 10) + XCTFail("Unexpected task progression") + } catch {} + XCTAssertTrue(Task.isCancelled) + await queue.exec(flags: .block) {} + try await queue.wait(forSeconds: 3) + XCTFail("Unexpected task progression") } - task.cancel() do { + task.cancel() try await task.value XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue(type(of: error) == CancellationError.self) - } + } catch is CancellationError {} } func testCancellationWithoutBlocking() async throws { let queue = TaskQueue() - try await Self.checkExecInterval(durationInSeconds: 3) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - try await Self.sleep(seconds: 1) - // Throws error for waiting method - throw CancellationError() - } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - group.addTask { - try await queue.exec(flags: .block) { - try await Self.sleep(seconds: 2) - } - } - do { - try await group.waitForAll() - } catch { - // Cancels block task - group.cancelAll() - } - try await queue.exec { - try await Self.sleep(seconds: 2) + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { throw CancellationError() } + group.addTask { + try await queue.exec(flags: .block) { + try await Task.sleep(seconds: 10) } } + try? await group.waitForAll() + // Cancels block task + group.cancelAll() } + try await queue.wait(forSeconds: 3) } func testMultipleCancellationWithoutBlocking() async throws { let queue = TaskQueue() - try await Self.checkExecInterval(durationInSeconds: 3) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - try await Self.sleep(seconds: 1) - // Throws error for waiting method - throw CancellationError() - } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - group.addTask { - try await queue.exec(flags: .block) { - try await Self.sleep(seconds: 2) - } + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { throw CancellationError() } + group.addTask { + try await queue.exec(flags: .block) { + try await Task.sleep(seconds: 10) } - group.addTask { - try await queue.exec(flags: .block) { - try await Self.sleep(seconds: 3) - } - } - do { - try await group.waitForAll() - } catch { - // Cancels block tasks - group.cancelAll() - } - try await queue.exec { - try await Self.sleep(seconds: 2) + } + group.addTask { + try await queue.exec(flags: .block) { + try await Task.sleep(seconds: 10) } } + try? await group.waitForAll() + // Cancels block task + group.cancelAll() } + try await queue.wait(forSeconds: 3) } func testMixedeCancellationWithoutBlocking() async throws { let queue = TaskQueue() - try await Self.checkExecInterval(durationInSeconds: 3) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - try await Self.sleep(seconds: 1) - // Throws error for waiting method - throw CancellationError() - } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - group.addTask { - try await queue.exec(flags: .block) { - try await Self.sleep(seconds: 2) - } - } - group.addTask { - try await queue.exec(flags: .block) { - try await Self.sleep(seconds: 3) - } - } - group.addTask { - try await queue.exec { - try await Self.sleep(seconds: 4) - } + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { throw CancellationError() } + group.addTask { + try await queue.exec(flags: .block) { + try await Task.sleep(seconds: 10) } - do { - try await group.waitForAll() - } catch { - // Cancels block tasks - group.cancelAll() + } + group.addTask { + try await queue.exec(flags: .block) { + try await Task.sleep(seconds: 10) } + } + group.addTask { try await queue.exec { - try await Self.sleep(seconds: 2) + try await Task.sleep(seconds: 3) } } + try? await group.waitForAll() + // Cancels block task + group.cancelAll() } + try await queue.wait(forSeconds: 3) } } @@ -449,210 +293,151 @@ class TaskQueueBarrierOperationTests: XCTestCase { func testExecutionOfTwoOperations() async throws { let queue = TaskQueue() - try await Self.checkExecInterval(durationInSeconds: 3) { - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - try await queue.exec(flags: .barrier) { - try await Self.sleep(seconds: 2) - } - } - group.addTask { - try await queue.exec(flags: .barrier) { - try await Self.sleep(seconds: 1) - } - } - try await group.waitForAll() + let stream = AsyncStream { c in + Task.detached { + await queue.addTaskAndStart(flags: .barrier) { c.yield(1) } + await queue.addTaskAndStart(flags: .barrier) { c.yield(2) } + c.finish() } } + await stream.assertElements() + try await queue.wait(forSeconds: 3) } func testExecutionOfTaskBeforeOperation() async throws { let queue = TaskQueue() - try await Self.checkExecInterval(durationInSeconds: 3) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - try await queue.exec { - try await Self.sleep(seconds: 2) - } + let stream = AsyncStream { c in + Task.detached { + await queue.addTaskAndStart { + c.yield(1) + try await Task.sleep(seconds: 1) + c.yield(2) } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - group.addTask { - try await queue.exec(flags: .barrier) { - try await Self.sleep(seconds: 1) - } - } - try await group.waitForAll() + await queue.addTaskAndStart(flags: .barrier) { c.yield(3) } + c.finish() } } + try await queue.wait(forSeconds: 3) + await stream.assertElements() } func testExecutionOfTaskAfterOperation() async throws { let queue = TaskQueue() - try await Self.checkExecInterval(durationInSeconds: 3) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - try await queue.exec(flags: .barrier) { - try await Self.sleep(seconds: 2) - } + let stream = AsyncStream { c in + Task.detached { + await queue.addTaskAndStart(flags: .barrier) { + c.yield(1) + try await Task.sleep(seconds: 1) + c.yield(2) } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - group.addTask { - try await queue.exec { - try await Self.sleep(seconds: 1) - } - } - try await group.waitForAll() + await queue.addTaskAndStart { c.yield(3) } + c.finish() } } + await stream.assertElements() + try await queue.wait(forSeconds: 3) } func testCancellation() async throws { let queue = TaskQueue() - queue.addTask(flags: .barrier) { - try await Self.sleep(seconds: 10) + await queue.addTaskAndStart(flags: .barrier) { + try await Task.sleep(seconds: 10) } let task = Task.detached { - try await Self.checkExecInterval(durationInSeconds: 0) { - await queue.exec(flags: .barrier) {} - try await queue.wait() - } + await queue.exec(flags: .block) {} + try await queue.wait(forSeconds: 3) + XCTFail("Unexpected task progression") } - task.cancel() do { + task.cancel() try await task.value XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue(type(of: error) == CancellationError.self) - } + } catch is CancellationError {} } func testAlreadyCancelledTask() async throws { let queue = TaskQueue() - queue.addTask(flags: .barrier) { - try await Self.sleep(seconds: 10) + await queue.addTaskAndStart(flags: .barrier) { + try await Task.sleep(seconds: 10) } let task = Task.detached { - try await Self.checkExecInterval(durationInSeconds: 0) { - do { - try await Self.sleep(seconds: 5) - XCTFail("Unexpected task progression") - } catch {} - XCTAssertTrue(Task.isCancelled) - await queue.exec(flags: .barrier) {} - try await queue.wait() - } + do { + try await Task.sleep(seconds: 10) + XCTFail("Unexpected task progression") + } catch {} + XCTAssertTrue(Task.isCancelled) + await queue.exec(flags: .barrier) {} + try await queue.wait(forSeconds: 3) + XCTFail("Unexpected task progression") } - task.cancel() do { + task.cancel() try await task.value XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue(type(of: error) == CancellationError.self) - } + } catch is CancellationError {} } func testCancellationWithoutBlocking() async throws { let queue = TaskQueue() - try await Self.checkExecInterval(durationInSeconds: 3) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - try await Self.sleep(seconds: 1) - // Throws error for waiting method - throw CancellationError() - } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - group.addTask { - try await queue.exec(flags: .barrier) { - try await Self.sleep(seconds: 2) - } - } - do { - try await group.waitForAll() - } catch { - // Cancels block task - group.cancelAll() - } - try await queue.exec { - try await Self.sleep(seconds: 2) + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { throw CancellationError() } + group.addTask { + try await queue.exec(flags: .barrier) { + try await Task.sleep(seconds: 10) } } + try? await group.waitForAll() + // Cancels block task + group.cancelAll() } + try await queue.wait(forSeconds: 3) } func testMultipleCancellationWithoutBlocking() async throws { let queue = TaskQueue() - try await Self.checkExecInterval(durationInSeconds: 3) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - try await Self.sleep(seconds: 1) - // Throws error for waiting method - throw CancellationError() - } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - group.addTask { - try await queue.exec(flags: .barrier) { - try await Self.sleep(seconds: 3) - } - } - group.addTask { - try await queue.exec(flags: .barrier) { - try await Self.sleep(seconds: 2) - } - } - do { - try await group.waitForAll() - } catch { - // Cancels block tasks - group.cancelAll() + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { throw CancellationError() } + group.addTask { + try await queue.exec(flags: .barrier) { + try await Task.sleep(seconds: 10) } - try await queue.exec { - try await Self.sleep(seconds: 2) + } + group.addTask { + try await queue.exec(flags: .barrier) { + try await Task.sleep(seconds: 10) } } + try? await group.waitForAll() + // Cancels block task + group.cancelAll() } + try await queue.wait(forSeconds: 3) } func testMixedCancellationWithoutBlocking() async throws { let queue = TaskQueue() - try await Self.checkExecInterval(durationInSeconds: 3) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - try await Self.sleep(seconds: 1) - // Throws error for waiting method - throw CancellationError() - } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - group.addTask { - try await queue.exec(flags: .barrier) { - try await Self.sleep(seconds: 3) - } + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { throw CancellationError() } + group.addTask { + try await queue.exec(flags: .barrier) { + try await Task.sleep(seconds: 10) } - group.addTask { - try await queue.exec(flags: .barrier) { - try await Self.sleep(seconds: 2) - } - } - group.addTask { - try await queue.exec { - try await Self.sleep(seconds: 4) - } - } - do { - try await group.waitForAll() - } catch { - // Cancels block tasks - group.cancelAll() + } + group.addTask { + try await queue.exec(flags: .barrier) { + try await Task.sleep(seconds: 10) } + } + group.addTask { try await queue.exec { - try await Self.sleep(seconds: 2) + try await Task.sleep(seconds: 3) } } + try? await group.waitForAll() + // Cancels block task + group.cancelAll() } + try await queue.wait(forSeconds: 3) } } @@ -661,75 +446,57 @@ class TaskQueueMixedOperationTests: XCTestCase { func testExecutionOfBlockTaskBeforeBarrierOperation() async throws { let queue = TaskQueue() - try await Self.checkExecInterval(durationInSeconds: 3) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - try await queue.exec(flags: .block) { - try await Self.sleep(seconds: 2) - } + let stream = AsyncStream { c in + Task.detached { + await queue.addTaskAndStart(flags: .block) { + c.yield(1) + try await Task.sleep(seconds: 1) + c.yield(2) } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - group.addTask { - try await queue.exec(flags: .barrier) { - try await Self.sleep(seconds: 1) - } - } - try await group.waitForAll() + await queue.addTaskAndStart(flags: .barrier) { c.yield(3) } + c.finish() } } + await stream.assertElements() + try await queue.wait(forSeconds: 3) } func testExecutionOfBlockTaskAfterBarrierOperation() async throws { let queue = TaskQueue() - try await Self.checkExecInterval(durationInSeconds: 3) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - try await queue.exec(flags: .barrier) { - try await Self.sleep(seconds: 2) - } + let stream = AsyncStream { c in + Task.detached { + await queue.addTaskAndStart(flags: .barrier) { + c.yield(1) + try await Task.sleep(seconds: 1) + c.yield(2) } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - group.addTask { - try await queue.exec(flags: .block) { - try await Self.sleep(seconds: 1) - } - } - try await group.waitForAll() + await queue.addTaskAndStart(flags: .block) { c.yield(3) } + c.finish() } } + await stream.assertElements() + try await queue.wait(forSeconds: 3) } func testLongRunningConcurrentTaskWithShortBlockTaskBeforeBarrierOperation() async throws { let queue = TaskQueue() - // Concurrent + Barrier - try await Self.checkExecInterval(durationInSeconds: 5) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - try await queue.exec { - try await Self.sleep(seconds: 2) - } + // Concurrent + Block + Barrier + let stream = AsyncStream { c in + Task.detached { + await queue.addTaskAndStart { + c.yield(1) + try await Task.sleep(seconds: 3) + c.yield(2) } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - await group.addTaskAndStart { - try await queue.exec(flags: .block) { - try await Self.sleep(seconds: 1) - } - } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - await group.addTaskAndStart { - try await queue.exec(flags: .barrier) { - try await Self.sleep(seconds: 3) - } - } - try await group.waitForAll() + await queue.addTaskAndStart(flags: .block) { c.yield(2) } + await queue.addTaskAndStart(flags: .barrier) { c.yield(3) } + c.finish() } } + await stream.assertElements() + try await queue.wait(forSeconds: 3) } func testLongRunningConcurrentTaskWithShortBlockTaskAfterBarrierOperation() @@ -737,268 +504,259 @@ class TaskQueueMixedOperationTests: XCTestCase { { let queue = TaskQueue() // Concurrent + Barrier + Block - await Self.checkExecInterval(durationInSeconds: 6) { - await withTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - await queue.exec { - try! await Self.sleep(seconds: 3) - } + let stream = AsyncStream { c in + Task.detached { + await queue.addTaskAndStart { + c.yield(1) + try await Task.sleep(seconds: 1) + c.yield(2) } - // Make sure previous tasks started - try! await Self.sleep(seconds: 0.01) - await group.addTaskAndStart { - await queue.exec(flags: .barrier) { - try! await Self.sleep(seconds: 2) - } - } - // Make sure previous tasks started - try! await Self.sleep(seconds: 0.01) - await group.addTaskAndStart { - await queue.exec(flags: .block) { - try! await Self.sleep(seconds: 1) - } + await queue.addTaskAndStart(flags: .barrier) { + c.yield(3) + try await Task.sleep(seconds: 1) + c.yield(4) } - await group.waitForAll() + await queue.addTaskAndStart(flags: .block) { c.yield(5) } + c.finish() } } + await stream.assertElements() + try await queue.wait(forSeconds: 3) } /// Scenario described in: /// https://forums.swift.org/t/concurrency-suspending-an-actor-async-func-until-the-actor-meets-certain-conditions/56580 func testBarrierTaskWithMultipleConcurrentTasks() async throws { let queue = TaskQueue() - await Self.checkExecInterval(durationInSeconds: 8) { - await withTaskGroup(of: Void.self) { group in - group.addTask { - await queue.exec { - try! await Self.sleep(seconds: 1) - } - } - group.addTask { - await queue.exec { - try! await Self.sleep(seconds: 2) - } - } - group.addTask { - await queue.exec { - try! await Self.sleep(seconds: 3) + let stream = AsyncStream { c in + Task.detached { + await withTaskGroup(of: Void.self) { group in + for _ in 0..<3 { + group.addTask { + await queue.addTaskAndStart { + c.yield(1) + try await Task.sleep(seconds: 1) + c.yield(1) + } + } } + await group.waitForAll() } - // Make sure previous tasks started - try! await Self.sleep(seconds: 0.01) - await group.addTaskAndStart { - await queue.exec(flags: .barrier) { - try! await Self.sleep(seconds: 2) - } + await queue.addTaskAndStart(flags: .barrier) { + c.yield(2) + try await Task.sleep(seconds: 1) + c.yield(3) } - // Make sure previous tasks started - try! await Self.sleep(seconds: 0.01) - group.addTask { - await queue.exec { - try! await Self.sleep(seconds: 1) - } + await queue.addTaskAndStart { c.yield(4) } + await queue.addTaskAndStart { c.yield(4) } + await queue.addTaskAndStart { c.yield(4) } + c.finish() + } + } + await stream.assertElements() + try await queue.wait(forSeconds: 3) + } + + func testCancellableAndNonCancellableTasks() async throws { + let queue = TaskQueue() + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await queue.exec { + try await Task.sleep(seconds: 10) } - group.addTask { - await queue.exec { - try! await Self.sleep(seconds: 2) - } + } + group.addTask { + try await queue.exec { + try await Task.sleep(seconds: 10) } - group.addTask { - await queue.exec { - try! await Self.sleep(seconds: 3) + } + group.addTask { + await queue.exec { + do { + try await Task.sleep(seconds: 10) + XCTFail("Unexpected task progression") + } catch is CancellationError { + /* Do nothing */ + } catch { + XCTFail("Unexpected error \(error)") } } } + group.cancelAll() } } func testCancellableAndNonCancellableTasksWithBarrier() async throws { let queue = TaskQueue() - try await Self.checkExecInterval(durationInSeconds: 3) { - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - try await queue.exec { - try await Self.sleep(seconds: 1) - } - } - group.addTask { - try await queue.exec { - try await Self.sleep(seconds: 2) - } - } - group.addTask { - try await queue.exec { - try await Self.sleep(seconds: 3) + try await withThrowingTaskGroup(of: Void.self) { group in + await withTaskGroup(of: Void.self) { g in + for _ in 0..<3 { + g.addTask { + await queue.addTaskAndStart { + try await Task.sleep(seconds: 1) + } } } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - await group.addTaskAndStart { - try await queue.exec(flags: .barrier) { - try await Self.sleep(seconds: 2) - } + await g.waitForAll() + } + group.addTask { + try await queue.exec(flags: .barrier) { + try await Task.sleep(seconds: 10) } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - group.addTask { - try await queue.exec { - try await Self.sleep(seconds: 2) - } + } + try await waitUntil(queue, timeout: 5) { + guard + let (_, (_, flags)) = $0.queue.reversed().first + else { return $0.blocked } + return flags.contains(.barrier) + } + group.addTask { + try await queue.exec { + try await Task.sleep(seconds: 1) + XCTFail("Unexpected task progression") } - group.addTask { - await queue.exec { - do { - try await Self.sleep(seconds: 3) - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue( - type(of: error) == CancellationError.self - ) - } + } + group.addTask { + await queue.exec { + do { + try await Task.sleep(seconds: 1) + XCTFail("Unexpected task progression") + } catch is CancellationError { + /* Do nothing */ + } catch { + XCTFail("Unexpected error \(error)") } } - group.addTask { - await queue.exec { - do { - try await Self.sleep(seconds: 4) - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue( - type(of: error) == CancellationError.self - ) - } + } + group.addTask { + await queue.exec { + do { + try await Task.sleep(seconds: 1) + XCTFail("Unexpected task progression") + } catch is CancellationError { + /* Do nothing */ + } catch { + XCTFail("Unexpected error \(error)") } } - - for _ in 0..<3 { try await group.next() } - group.cancelAll() } + try await waitUntil(queue, timeout: 5) { $0.blocked } + group.cancelAll() } + try await queue.wait(forSeconds: 3) } } -@MainActor -class TaskQueueCancellationTests: XCTestCase { - - func testWaitCancellation() async throws { - let queue = TaskQueue() - queue.addTask(flags: .barrier) { - try await Self.sleep(seconds: 10) +extension Optional where Wrapped == TaskPriority { + var str: String { + switch self { + case .none: + return "none" + case .some(let wrapped): + return "\(wrapped.rawValue)" } - let task = Task.detached { - try await Self.checkExecInterval(durationInSeconds: 0) { - try await queue.wait() + } +} + +fileprivate extension TaskQueue { + func addTaskAndStart( + priority: TaskPriority? = nil, + flags: Flags = [], + operation: @Sendable @escaping () async -> T + ) async { + await GlobalContinuation.with { continuation in + self.addTask(priority: priority, flags: flags) { () -> T in + continuation.resume() + return await operation() } } - task.cancel() - do { - try await task.value - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue(type(of: error) == CancellationError.self) - } } - func testAlreadyCancelledTask() async throws { - let queue = TaskQueue() - queue.addTask(flags: .barrier) { - try await Self.sleep(seconds: 10) - } - let task = Task.detached { - try await Self.checkExecInterval(durationInSeconds: 0) { - do { - try await Self.sleep(seconds: 5) - XCTFail("Unexpected task progression") - } catch {} - XCTAssertTrue(Task.isCancelled) - try await queue.wait() + func addTaskAndStart( + priority: TaskPriority? = nil, + flags: Flags = [], + operation: @Sendable @escaping () async throws -> T + ) async { + await GlobalContinuation.with { continuation in + self.addTask(priority: priority, flags: flags) { () -> T in + continuation.resume() + return try await operation() } } - task.cancel() - do { - try await task.value - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue(type(of: error) == CancellationError.self) - } } - func testCancellableAndNonCancellableTasks() async throws { - let queue = TaskQueue() - await Self.checkExecInterval(durationInSeconds: 0) { - await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - try await queue.exec { - try await Self.sleep(seconds: 2) - } - } - group.addTask { - try await queue.exec { - try await Self.sleep(seconds: 3) - } - } - group.addTask { - await queue.exec { - do { - try await Self.sleep(seconds: 4) - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue( - type(of: error) == CancellationError.self - ) - } - } - } - group.cancelAll() + @MainActor + func checkWait( + for option: TaskOption, + file: StaticString = #filePath, + function: StaticString = #function, + line: UInt = #line + ) async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + await addTaskAndStart(priority: option.task, flags: option.flags) { + try await Task.sleep(seconds: 1) } + group.addTask { try await self.wait(forSeconds: 3) } + try await group.waitForAll() } + try await self.wait(forSeconds: 3) } -} -fileprivate extension XCTestCase { - typealias TaskOption = ( - queue: TaskPriority?, task: TaskPriority?, flags: TaskQueue.Flags - ) - - static func checkWaitOnQueue( - option: TaskOption, + @MainActor + func checkWaitTimeout( + for option: TaskOption, file: StaticString = #filePath, function: StaticString = #function, line: UInt = #line ) async throws { - let queue = TaskQueue(priority: option.queue) - try await Self.checkExecInterval( - name: "For queue priority: \(option.queue.str), " - + "task priority: \(option.task.str) " - + "and flags: \(option.flags.rawValue)", - durationInSeconds: 1, - file: file, function: function, line: line + await addTaskAndStart( + priority: option.task, + flags: [option.flags, .block] ) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - try await queue.exec( - priority: option.task, - flags: option.flags - ) { - try await Self.sleep(seconds: 1) - } - } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - group.addTask { try await queue.wait() } - try await group.waitForAll() - } + try await Task.sleep(seconds: 10) } + do { + try await self.wait(forSeconds: 5) + XCTFail("Unexpected task progression", file: file, line: line) + } catch is DurationTimeoutError {} } + + #if swift(>=5.7) + @available(macOS 13, iOS 16, macCatalyst 16, tvOS 16, watchOS 9, *) + @MainActor + func checkWaitTimeout( + for option: TaskOption, + clock: C, + file: StaticString = #filePath, + function: StaticString = #function, + line: UInt = #line + ) async throws where C.Duration == Duration { + await addTaskAndStart( + priority: option.task, + flags: [option.flags, .block] + ) { + try await Task.sleep(seconds: 10) + } + do { + try await self.wait(forSeconds: 5) + XCTFail("Unexpected task progression", file: file, line: line) + } catch is DurationTimeoutError {} + } + #endif } -extension Optional where Wrapped == TaskPriority { - var str: String { - switch self { - case .none: - return "none" - case .some(let wrapped): - return "\(wrapped.rawValue)" +fileprivate extension AsyncSequence where Element: BinaryInteger { + func assertElements( + initial value: Element = .zero, + diff: Element = 1, + file: StaticString = #filePath, + function: StaticString = #function, + line: UInt = #line + ) async rethrows { + var value = value + for try await val in self where value != val { + XCTAssertEqual(val, value + diff, file: file, line: line) + value = val } } } diff --git a/Tests/AsyncObjectsTests/ThrowingFutureTests.swift b/Tests/AsyncObjectsTests/ThrowingFutureTests.swift index 0504665d..01c226ea 100644 --- a/Tests/AsyncObjectsTests/ThrowingFutureTests.swift +++ b/Tests/AsyncObjectsTests/ThrowingFutureTests.swift @@ -7,7 +7,7 @@ class ThrowingFutureTests: XCTestCase { func testFutureFulfilledInitialization() async throws { let future = Future(with: .success(5)) - let value = try await future.get() + let value = try await future.wait(forSeconds: 3) XCTAssertEqual(value, 5) } @@ -15,11 +15,10 @@ class ThrowingFutureTests: XCTestCase { let future = Future() try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { - let value = try await future.get() + let value = try await future.wait(forSeconds: 3) XCTAssertEqual(value, 5) } group.addTask { - try await Self.sleep(seconds: 1) await future.fulfill(producing: 5) } try await group.waitForAll() @@ -31,14 +30,11 @@ class ThrowingFutureTests: XCTestCase { try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { do { - let _ = try await future.get() + let _ = try await future.wait(forSeconds: 3) XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue(type(of: error) == CancellationError.self) - } + } catch is CancellationError {} } group.addTask { - try await Self.sleep(seconds: 1) await future.fulfill(throwing: CancellationError()) } try await group.waitForAll() @@ -49,80 +45,58 @@ class ThrowingFutureTests: XCTestCase { let future = Future() let waitTask = Task { do { - let _ = try await future.get() + let _ = try await future.wait(forSeconds: 3) XCTFail("Future fulfillments wait not cancelled") - } catch { - XCTAssertTrue(type(of: error) == CancellationError.self) - } - } - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - try await Self.sleep(seconds: 1) - waitTask.cancel() - } - group.addTask { - try await Self.sleep(seconds: 2) - await future.fulfill(producing: 5) - } - try await group.waitForAll() + } catch is CancellationError {} } + waitTask.cancel() + try await waitTask.value } func testMultipleTimesFutureFulfilled() async throws { let future = Future(with: .success(5)) await future.fulfill(producing: 10) - let value = try await future.get() + let value = try await future.wait(forSeconds: 3) XCTAssertEqual(value, 5) } func testDeinit() async throws { let future = Future() - Task.detached { - try await Self.sleep(seconds: 1) - await future.fulfill(producing: 5) - } - let _ = try await future.get() + let task = Task.detached { await future.fulfill(producing: 5) } + let _ = try await future.wait(forSeconds: 3) + await task.value self.addTeardownBlock { [weak future] in - try await Self.sleep(seconds: 1) - XCTAssertNil(future) + future.assertReleased() } } func testWaitCancellationWhenTaskCancelled() async throws { let future = Future() let task = Task.detached { - await Self.checkExecInterval(durationInSeconds: 0) { - do { - let _ = try await future.get() - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue(type(of: error) == CancellationError.self) - } - } + do { + let _ = try await future.wait(forSeconds: 3) + XCTFail("Unexpected task progression") + } catch is CancellationError {} } task.cancel() - await task.value + try await task.value } func testWaitCancellationForAlreadyCancelledTask() async throws { let future = Future() let task = Task.detached { - await Self.checkExecInterval(durationInSeconds: 0) { - do { - try await Self.sleep(seconds: 5) - XCTFail("Unexpected task progression") - } catch {} - XCTAssertTrue(Task.isCancelled) - do { - let _ = try await future.get() - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue(type(of: error) == CancellationError.self) - } - } + do { + try await Task.sleep(seconds: 10) + XCTFail("Unexpected task progression") + } catch {} + XCTAssertTrue(Task.isCancelled) + do { + let _ = try await future.wait(forSeconds: 3) + XCTFail("Unexpected task progression") + } catch is CancellationError {} } task.cancel() - await task.value + try await task.value } func testConcurrentAccess() async throws { @@ -130,13 +104,12 @@ class ThrowingFutureTests: XCTestCase { for i in 0..<10 { group.addTask { let future = Future() - try await Self.checkExecInterval(durationInSeconds: 0) { - try await withThrowingTaskGroup(of: Void.self) { - group in - group.addTask { let _ = try await future.get() } - group.addTask { await future.fulfill(producing: i) } - try await group.waitForAll() + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + let _ = try await future.wait(forSeconds: 3) } + group.addTask { await future.fulfill(producing: i) } + try await group.waitForAll() } } try await group.waitForAll() @@ -152,29 +125,16 @@ class ThrowingFutureCombiningAllTests: XCTestCase { let future1 = Future() let future2 = Future() let future3 = Future() - let allFuture = Future.all(future1, future2, future3) - try await Self.checkExecInterval(durationInSeconds: 3) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - let value = try await allFuture.get() - XCTAssertEqual(value, [1, 2, 3]) - } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - group.addTask { - try await Self.sleep(seconds: 1) - await future1.fulfill(producing: 1) - } - group.addTask { - try await Self.sleep(seconds: 2) - await future2.fulfill(producing: 2) - } - group.addTask { - try await Self.sleep(seconds: 3) - await future3.fulfill(producing: 3) - } - try await group.waitForAll() + let future = Future.all(future1, future2, future3) + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + let value = try await future.wait(forSeconds: 3) + XCTAssertEqual(value, [1, 2, 3]) } + group.addTask { await future1.fulfill(producing: 1) } + group.addTask { await future2.fulfill(producing: 2) } + group.addTask { await future3.fulfill(producing: 3) } + try await group.waitForAll() } } @@ -182,43 +142,26 @@ class ThrowingFutureCombiningAllTests: XCTestCase { let future1 = Future() let future2 = Future() let future3 = Future() - let allFuture = Future.all(future1, future2, future3) - try await Self.checkExecInterval(durationInSeconds: 3) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - await Self.checkExecInterval(durationInSeconds: 2) { - do { - let _ = try await allFuture.get() - XCTFail("Future fulfillment did not fail") - } catch { - XCTAssertTrue( - type(of: error) == CancellationError.self - ) - } - } - } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - group.addTask { - try await Self.sleep(seconds: 1) - await future1.fulfill(producing: 1) - } - group.addTask { - try await Self.sleep(seconds: 2) - await future2.fulfill(throwing: CancellationError()) - } - group.addTask { - try await Self.sleep(seconds: 3) - await future3.fulfill(producing: 3) - } - try await group.waitForAll() + let future = Future.all(future1, future2, future3) + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + do { + let _ = try await future.wait(forSeconds: 3) + XCTFail("Future fulfillment did not fail") + } catch is CancellationError {} + } + group.addTask { await future1.fulfill(producing: 1) } + group.addTask { + await future2.fulfill(throwing: CancellationError()) } + group.addTask { await future3.fulfill(producing: 3) } + try await group.waitForAll() } } func testEmptyConstructing() async throws { let future = Future.all() - let value = try await future.get() + let value = try await future.wait(forSeconds: 3) XCTAssertTrue(value.isEmpty) } } @@ -230,36 +173,23 @@ class ThrowingFutureCombiningAllSettledTests: XCTestCase { let future1 = Future() let future2 = Future() let future3 = Future() - let allFuture = Future.allSettled(future1, future2, future3) - try await Self.checkExecInterval(durationInSeconds: 3) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - let values = await allFuture.get() - for (index, item) in values.enumerated() { - switch item { - case .success(let value): - XCTAssertEqual(value, index + 1) - default: - XCTFail("Unexpected future fulfillment") - } + let future = Future.allSettled(future1, future2, future3) + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + let values = try await future.wait(forSeconds: 3) + for (index, item) in values.enumerated() { + switch item { + case .success(let value): + XCTAssertEqual(value, index + 1) + default: + XCTFail("Unexpected future fulfillment") } } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - group.addTask { - try await Self.sleep(seconds: 1) - await future1.fulfill(producing: 1) - } - group.addTask { - try await Self.sleep(seconds: 2) - await future2.fulfill(producing: 2) - } - group.addTask { - try await Self.sleep(seconds: 3) - await future3.fulfill(producing: 3) - } - try await group.waitForAll() } + await future1.fulfill(producing: 1) + await future2.fulfill(producing: 2) + await future3.fulfill(producing: 3) + try await group.waitForAll() } } @@ -267,47 +197,31 @@ class ThrowingFutureCombiningAllSettledTests: XCTestCase { let future1 = Future() let future2 = Future() let future3 = Future() - let allFuture = Future.allSettled(future1, future2, future3) - try await Self.checkExecInterval(durationInSeconds: 3) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - await Self.checkExecInterval(durationInSeconds: 3) { - let values = await allFuture.get() - for (index, item) in values.enumerated() { - switch item { - case .success(let value): - XCTAssertEqual(value, index + 1) - case .failure(let error): - XCTAssertTrue( - type(of: error) == CancellationError.self - ) - XCTAssertEqual(index + 1, 2) - } - } + let future = Future.allSettled(future1, future2, future3) + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + let values = try await future.wait(forSeconds: 3) + for (index, item) in values.enumerated() { + switch item { + case .success(let value): + XCTAssertEqual(value, index + 1) + case .failure(is CancellationError): + XCTAssertEqual(index + 1, 2) + default: + XCTFail("Unexpected future fulfillment") } } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - group.addTask { - try await Self.sleep(seconds: 1) - await future1.fulfill(producing: 1) - } - group.addTask { - try await Self.sleep(seconds: 2) - await future2.fulfill(throwing: CancellationError()) - } - group.addTask { - try await Self.sleep(seconds: 3) - await future3.fulfill(producing: 3) - } - try await group.waitForAll() } + await future1.fulfill(producing: 1) + await future2.fulfill(throwing: CancellationError()) + await future3.fulfill(producing: 3) + try await group.waitForAll() } } func testEmptyConstructing() async throws { let future = Future.allSettled() - let value = await future.get() + let value = try await future.wait(forSeconds: 3) XCTAssertTrue(value.isEmpty) } } @@ -319,31 +233,17 @@ class ThrowingFutureRacingTests: XCTestCase { let future1 = Future() let future2 = Future() let future3 = Future() - let allFuture = Future.race(future1, future2, future3) - try await Self.checkExecInterval(durationInSeconds: 3) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - try await Self.checkExecInterval(durationInSeconds: 1) { - let value = try await allFuture.get() - XCTAssertEqual(value, 1) - } - } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - group.addTask { - try await Self.sleep(seconds: 1) - await future1.fulfill(producing: 1) - } - group.addTask { - try await Self.sleep(seconds: 2) - await future2.fulfill(producing: 2) - } - group.addTask { - try await Self.sleep(seconds: 3) - await future3.fulfill(producing: 3) - } - try await group.waitForAll() + let future = Future.race(future1, future2, future3) + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + let value = try await future.wait(forSeconds: 3) + XCTAssertEqual(value, 1) } + await future1.fulfill(producing: 1) + try await waitUntil(future, timeout: 5) { $0.result != nil } + await future2.fulfill(producing: 2) + await future3.fulfill(producing: 3) + try await group.waitForAll() } } @@ -351,37 +251,19 @@ class ThrowingFutureRacingTests: XCTestCase { let future1 = Future() let future2 = Future() let future3 = Future() - let allFuture = Future.race(future1, future2, future3) - try await Self.checkExecInterval(durationInSeconds: 3) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - await Self.checkExecInterval(durationInSeconds: 1) { - do { - let _ = try await allFuture.get() - XCTFail("Future fulfillment did not fail") - } catch { - XCTAssertTrue( - type(of: error) == CancellationError.self - ) - } - } - } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - group.addTask { - try await Self.sleep(seconds: 1) - await future1.fulfill(throwing: CancellationError()) - } - group.addTask { - try await Self.sleep(seconds: 2) - await future2.fulfill(producing: 2) - } - group.addTask { - try await Self.sleep(seconds: 3) - await future3.fulfill(producing: 3) - } - try await group.waitForAll() + let future = Future.race(future1, future2, future3) + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + do { + let _ = try await future.wait(forSeconds: 3) + XCTFail("Future fulfillment did not fail") + } catch is CancellationError {} } + await future1.fulfill(throwing: CancellationError()) + try await waitUntil(future, timeout: 5) { $0.result != nil } + await future2.fulfill(producing: 2) + await future3.fulfill(producing: 3) + try await group.waitForAll() } } } @@ -393,31 +275,17 @@ class ThrowingFutureSelectAnyTests: XCTestCase { let future1 = Future() let future2 = Future() let future3 = Future() - let allFuture = Future.any(future1, future2, future3) - try await Self.checkExecInterval(durationInSeconds: 3) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - try await Self.checkExecInterval(durationInSeconds: 1) { - let value = try await allFuture.get() - XCTAssertEqual(value, 1) - } - } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - group.addTask { - try await Self.sleep(seconds: 1) - await future1.fulfill(producing: 1) - } - group.addTask { - try await Self.sleep(seconds: 2) - await future2.fulfill(producing: 2) - } - group.addTask { - try await Self.sleep(seconds: 3) - await future3.fulfill(producing: 3) - } - try await group.waitForAll() + let future = Future.any(future1, future2, future3) + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + let value = try await future.wait(forSeconds: 3) + XCTAssertEqual(value, 1) } + await future1.fulfill(producing: 1) + try await waitUntil(future, timeout: 5) { $0.result != nil } + await future2.fulfill(producing: 2) + await future3.fulfill(producing: 3) + try await group.waitForAll() } } @@ -425,31 +293,17 @@ class ThrowingFutureSelectAnyTests: XCTestCase { let future1 = Future() let future2 = Future() let future3 = Future() - let allFuture = Future.any(future1, future2, future3) - try await Self.checkExecInterval(durationInSeconds: 3) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - try await Self.checkExecInterval(durationInSeconds: 2) { - let value = try await allFuture.get() - XCTAssertEqual(value, 2) - } - } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - group.addTask { - try await Self.sleep(seconds: 1) - await future1.fulfill(throwing: CancellationError()) - } - group.addTask { - try await Self.sleep(seconds: 2) - await future2.fulfill(producing: 2) - } - group.addTask { - try await Self.sleep(seconds: 3) - await future3.fulfill(producing: 3) - } - try await group.waitForAll() + let future = Future.any(future1, future2, future3) + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + let value = try await future.wait(forSeconds: 3) + XCTAssertEqual(value, 2) } + await future1.fulfill(throwing: CancellationError()) + await future2.fulfill(producing: 2) + try await waitUntil(future, timeout: 5) { $0.result != nil } + await future3.fulfill(producing: 3) + try await group.waitForAll() } } @@ -457,37 +311,24 @@ class ThrowingFutureSelectAnyTests: XCTestCase { let future1 = Future() let future2 = Future() let future3 = Future() - let allFuture = Future.any(future1, future2, future3) - try await Self.checkExecInterval(durationInSeconds: 3) { - try await withThrowingTaskGroup(of: Void.self) { group in - await group.addTaskAndStart { - await Self.checkExecInterval(durationInSeconds: 3) { - do { - let _ = try await allFuture.get() - XCTFail("Future fulfillment did not fail") - } catch { - XCTAssertTrue( - type(of: error) == CancellationError.self - ) - } - } - } - // Make sure previous tasks started - try await Self.sleep(seconds: 0.01) - group.addTask { - try await Self.sleep(seconds: 1) - await future1.fulfill(throwing: CancellationError()) - } - group.addTask { - try await Self.sleep(seconds: 2) - await future2.fulfill(throwing: CancellationError()) - } - group.addTask { - try await Self.sleep(seconds: 3) - await future3.fulfill(throwing: CancellationError()) - } - try await group.waitForAll() + let future = Future.any(future1, future2, future3) + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + do { + let _ = try await future.wait(forSeconds: 3) + XCTFail("Future fulfillment did not fail") + } catch is CancellationError {} + } + group.addTask { + await future1.fulfill(throwing: CancellationError()) } + group.addTask { + await future2.fulfill(throwing: CancellationError()) + } + group.addTask { + await future3.fulfill(throwing: CancellationError()) + } + try await group.waitForAll() } } @@ -495,9 +336,20 @@ class ThrowingFutureSelectAnyTests: XCTestCase { let future = Future.any() let result = await future.result switch result { - case .failure(let error): - XCTAssertTrue(type(of: error) == CancellationError.self) + case .failure(is CancellationError): break default: XCTFail("Unexpected future fulfillment") } } } + +extension Future where Failure == Error { + @Sendable + @inlinable + func wait(forSeconds seconds: UInt64) async throws -> Output { + return try await waitForTaskCompletion( + withTimeoutInNanoseconds: seconds * 1_000_000_000 + ) { + return try await self.get() + } + } +} diff --git a/Tests/AsyncObjectsTests/TrackedContinuationTests.swift b/Tests/AsyncObjectsTests/TrackedContinuationTests.swift index 845a7159..b680d33f 100644 --- a/Tests/AsyncObjectsTests/TrackedContinuationTests.swift +++ b/Tests/AsyncObjectsTests/TrackedContinuationTests.swift @@ -15,29 +15,23 @@ class TrackedContinuationTests: XCTestCase { } func testDirectResumeWithSuccess() async throws { - await Self.checkExecInterval(durationInSeconds: 0) { - await TrackedContinuation>.with { - XCTAssertFalse($0.resumed) - $0.resume() - XCTAssertTrue($0.resumed) - } + await TrackedContinuation>.with { + XCTAssertFalse($0.resumed) + $0.resume() + XCTAssertTrue($0.resumed) } } func testDirectResumeWithError() async throws { typealias C = GlobalContinuation - await Self.checkExecInterval(durationInSeconds: 0) { - do { - try await TrackedContinuation.with { c in - XCTAssertFalse(c.resumed) - c.resume(throwing: CancellationError()) - XCTAssertTrue(c.resumed) - } - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue(type(of: error) == CancellationError.self) + do { + try await TrackedContinuation.with { c in + XCTAssertFalse(c.resumed) + c.resume(throwing: CancellationError()) + XCTAssertTrue(c.resumed) } - } + XCTFail("Unexpected task progression") + } catch is CancellationError {} } func testInitializedWithoutContinuationWithStatusWaiting() async throws { @@ -55,70 +49,60 @@ class TrackedContinuationTests: XCTestCase { func testCancellationHandlerWhenTaskCancelled() async throws { typealias C = GlobalContinuation let task = Task.detached { - await Self.checkExecInterval(durationInSeconds: 0) { - do { - try await TrackedContinuation - .withCancellation(id: .init()) { - $0.cancel() - } operation: { _, preinit in - preinit() - } - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue(type(of: error) == CancellationError.self) - } - } + do { + try await TrackedContinuation + .withCancellation(id: .init()) { + $0.cancel() + } operation: { _, preinit in + preinit() + } + XCTFail("Unexpected task progression") + } catch is CancellationError {} } task.cancel() - await task.value + try await task.value } func testCancellationHandlerForAlreadyCancelledTask() async throws { typealias C = GlobalContinuation let task = Task.detached { - await Self.checkExecInterval(durationInSeconds: 0) { - do { - try await Self.sleep(seconds: 5) - XCTFail("Unexpected task progression") - } catch {} - XCTAssertTrue(Task.isCancelled) - do { - try await TrackedContinuation - .withCancellation(id: .init()) { - $0.cancel() - } operation: { _, preinit in - preinit() - } - XCTFail("Unexpected task progression") - } catch { - XCTAssertTrue(type(of: error) == CancellationError.self) - } - } + do { + try await Task.sleep(seconds: 5) + XCTFail("Unexpected task progression") + } catch {} + XCTAssertTrue(Task.isCancelled) + do { + try await TrackedContinuation + .withCancellation(id: .init()) { + $0.cancel() + } operation: { _, preinit in + preinit() + } + XCTFail("Unexpected task progression") + } catch is CancellationError {} } task.cancel() - await task.value + try await task.value } func testNonCancellableContinuation() async throws { typealias C = GlobalContinuation let task = Task.detached { - await Self.checkExecInterval(durationInSeconds: 1) { - do { - try await Self.sleep(seconds: 5) - XCTFail("Unexpected task progression") - } catch {} - XCTAssertTrue(Task.isCancelled) - await TrackedContinuation - .withCancellation(id: .init()) { _ in - // Do nothing - } operation: { continuation, preinit in - preinit() - Task { - defer { continuation.resume() } - try await Self.sleep(seconds: 1) - } + do { + try await Task.sleep(seconds: 5) + XCTFail("Unexpected task progression") + } catch {} + XCTAssertTrue(Task.isCancelled) + await TrackedContinuation + .withCancellation(id: .init()) { _ in + // Do nothing + } operation: { continuation, preinit in + preinit() + Task { + defer { continuation.resume() } + try await Task.sleep(seconds: 1) } - } + } } task.cancel() await task.value diff --git a/Tests/AsyncObjectsTests/XCAsyncTestCase.swift b/Tests/AsyncObjectsTests/XCAsyncTestCase.swift new file mode 100644 index 00000000..a3c16471 --- /dev/null +++ b/Tests/AsyncObjectsTests/XCAsyncTestCase.swift @@ -0,0 +1,98 @@ +import XCTest +import Dispatch +@testable import AsyncObjects + +func waitUntil( + _ actor: T, + timeout: TimeInterval, + satisfies condition: @escaping (isolated T) async throws -> Bool +) async throws { + let maxWait = timeout * 1E9 + try await waitForTaskCompletion(withTimeoutInNanoseconds: UInt64(maxWait)) { + var interval = maxWait + var retryWait = 2.0 + while case let result = try await Task( + priority: .background, + operation: { try await condition(actor) } + ).value { + guard !result else { break } + try Task.checkCancellation() + try await Task.sleep(nanoseconds: UInt64(retryWait)) + interval -= retryWait + if interval < 0 { + throw DurationTimeoutError( + for: UInt64(maxWait), + tolerance: UInt64(interval * -1) + ) + } else if interval < retryWait / 2 { + retryWait = max(1E9, retryWait.squareRoot()) + } else { + retryWait *= retryWait + } + } + } +} + +extension Task where Success == Never, Failure == Never { + static func sleep(seconds: T) async throws { + let second: T = 1_000_000_000 + try await Task.sleep(nanoseconds: UInt64(exactly: seconds * second)!) + } +} + +extension AsyncObject { + @Sendable + @inlinable + func wait(forSeconds seconds: UInt64) async throws { + return try await self.wait(forNanoseconds: seconds * 1_000_000_000) + } +} + +#if swift(>=5.7) +@available(macOS 13, iOS 16, macCatalyst 16, tvOS 16, watchOS 9, *) +extension Task where Success == Never, Failure == Never { + + static func sleep( + seconds: T, + clock: C + ) async throws where C.Duration == Duration { + try await Task.sleep( + until: clock.now.advanced(by: .seconds(seconds)), + clock: clock + ) + } +} + +@available(macOS 13, iOS 16, macCatalyst 16, tvOS 16, watchOS 9, *) +extension AsyncObject { + @Sendable + @inlinable + func wait( + forSeconds seconds: T, + clock: C + ) async throws where C.Duration == Duration { + return try await self.wait( + until: clock.now.advanced(by: .seconds(seconds)), + tolerance: .microseconds(1), + clock: clock + ) + } +} +#endif + +extension Optional where Wrapped: AnyObject { + func assertReleased( + file: StaticString = #filePath, + function: StaticString = #function, + line: UInt = #line + ) { + switch self { + case .none: + break + case .some(let value): + let wr = _getUnownedRetainCount(value) + _getWeakRetainCount(value) + let rc = _getRetainCount(value) - wr + XCTAssertEqual(rc, 0, file: file, line: line) + } + } +} diff --git a/Tests/AsyncObjectsTests/XCTestCase.swift b/Tests/AsyncObjectsTests/XCTestCase.swift deleted file mode 100644 index f2013a61..00000000 --- a/Tests/AsyncObjectsTests/XCTestCase.swift +++ /dev/null @@ -1,178 +0,0 @@ -import XCTest -import Dispatch -@testable import AsyncObjects - -@MainActor -extension XCTestCase { - private static var activitySupported = ProcessInfo.processInfo.environment - .keys.contains("__XCODE_BUILT_PRODUCTS_DIR_PATHS") - - private static func runAssertions( - with name: String?, - _ assertions: () -> Void - ) { - #if canImport(Darwin) - if let name = name, activitySupported { - XCTContext.runActivity(named: name) { _ in - assertions() - } - } else { - assertions() - } - #else - assertions() - #endif - } - - static func checkExecInterval( - name: String? = nil, - durationInSeconds seconds: T = .zero, - file: StaticString = #filePath, - function: StaticString = #function, - line: UInt = #line, - for task: () async throws -> Void - ) async rethrows where T: Comparable { - let second: T = 1_000_000_000 - let time = DispatchTime.now().uptimeNanoseconds - try await task() - guard - let span = T(exactly: DispatchTime.now().uptimeNanoseconds - time), - case let duration = span / second - else { - XCTFail("Invalid number type: \(T.self)", file: file, line: line) - return - } - - let assertions = { - XCTAssertLessThanOrEqual( - duration, seconds + 3, - file: file, line: line - ) - XCTAssertGreaterThanOrEqual( - duration, seconds - 3, - file: file, line: line - ) - } - runAssertions(with: name, assertions) - } - - static func checkExecInterval( - name: String? = nil, - durationInRange range: R, - file: StaticString = #filePath, - function: StaticString = #function, - line: UInt = #line, - for task: () async throws -> Void - ) async rethrows where R.Bound: DivisiveArithmetic { - let second: R.Bound = 1_000_000_000 - let time = DispatchTime.now().uptimeNanoseconds - try await task() - guard - let span = R.Bound( - exactly: DispatchTime.now().uptimeNanoseconds - time - ), - case let duration = span / second - else { - XCTFail("Invalid range type: \(R.self)", file: file, line: line) - return - } - - let assertions = { - XCTAssertTrue( - range.contains(duration), - "\(duration) not present in \(range)", - file: file, line: line - ) - } - runAssertions(with: name, assertions) - } - - static func sleep(seconds: T) async throws { - let second: T = 1_000_000_000 - try await Task.sleep(nanoseconds: UInt64(exactly: seconds * second)!) - } - - static func sleep(seconds: T) async throws { - let second: T = 1_000_000_000 - try await Task.sleep(nanoseconds: UInt64(exactly: seconds * second)!) - } -} - -extension AsyncObject { - @Sendable - @inlinable - func wait(forSeconds seconds: UInt64) async throws { - return try await self.wait(forNanoseconds: seconds * 1_000_000_000) - } -} - -#if swift(>=5.7) -@available(macOS 13, iOS 16, macCatalyst 16, tvOS 16, watchOS 9, *) -@MainActor -extension XCTestCase { - - static func checkExecInterval( - name: String? = nil, - duration: C.Instant.Duration = .zero, - clock: C, - file: StaticString = #filePath, - function: StaticString = #function, - line: UInt = #line, - for task: () async throws -> Void - ) async rethrows where C.Duration == Duration { - let result = try await clock.measure { try await task() } - let assertions = { - XCTAssertLessThanOrEqual( - abs(duration.components.seconds - result.components.seconds), 3, - file: file, line: line - ) - } - runAssertions(with: name, assertions) - } - - static func sleep( - seconds: T, - clock: C - ) async throws where C.Duration == Duration { - try await Task.sleep( - until: clock.now.advanced(by: .seconds(seconds)), - clock: clock - ) - } - - static func sleep( - seconds: Double, - clock: C - ) async throws where C.Duration == Duration { - try await Task.sleep( - until: clock.now.advanced(by: .seconds(seconds)), - clock: clock - ) - } -} - -@available(macOS 13, iOS 16, macCatalyst 16, tvOS 16, watchOS 9, *) -extension AsyncObject { - @Sendable - @inlinable - func wait( - forSeconds seconds: T, - clock: C - ) async throws where C.Duration == Duration { - return try await self.wait( - until: clock.now.advanced(by: .seconds(seconds)), - tolerance: .microseconds(1), - clock: clock - ) - } -} -#endif - -protocol DivisiveArithmetic: Numeric { - static func / (lhs: Self, rhs: Self) -> Self - static func /= (lhs: inout Self, rhs: Self) -} - -extension Int: DivisiveArithmetic {} -extension Double: DivisiveArithmetic {} -extension UInt64: DivisiveArithmetic {}