diff --git a/Package.swift b/Package.swift index c6a2894c..82dae0fa 100644 --- a/Package.swift +++ b/Package.swift @@ -6,9 +6,13 @@ import CompilerPluginSupport let AsyncAlgorithms_v1_0 = "AvailabilityMacro=AsyncAlgorithms 1.0:macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0" #if compiler(>=6.0) && swift(>=6.0) // 5.10 doesnt support visionOS availability let AsyncAlgorithms_v1_1 = - "AvailabilityMacro=AsyncAlgorithms 1.1:macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0" + "AvailabilityMacro=AsyncAlgorithms 1.1:macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, visionOS 1.0" +let AsyncAlgorithms_v1_2 = + "AvailabilityMacro=AsyncAlgorithms 1.2:macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0" #else let AsyncAlgorithms_v1_1 = "AvailabilityMacro=AsyncAlgorithms 1.1:macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0" +let AsyncAlgorithms_v1_2 = + "AvailabilityMacro=AsyncAlgorithms 1.2:macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0" #endif let availabilityMacros: [SwiftSetting] = [ @@ -18,6 +22,9 @@ let availabilityMacros: [SwiftSetting] = [ .enableExperimentalFeature( AsyncAlgorithms_v1_1 ), + .enableExperimentalFeature( + AsyncAlgorithms_v1_2 + ) ] let package = Package( diff --git a/Sources/AsyncAlgorithms/AsyncFailureBackportable.swift b/Sources/AsyncAlgorithms/AsyncFailureBackportable.swift new file mode 100644 index 00000000..0feeb47b --- /dev/null +++ b/Sources/AsyncAlgorithms/AsyncFailureBackportable.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// A backportable protocol / hack to allow `Failure` associated type on older iOS/macOS/etc. versions. +/// +/// By assigning this protocol to any value conforming to `AsyncSequence`, they will both have access to `Failure` +/// > There could be a possible issue with mangled name of the entire object as discussed +/// [here](https://forums.swift.org/t/how-to-use-asyncsequence-on-macos-14-5-in-xcode-16-beta-need-help-with-availability-check-since-failure-is-unavailb-e/72439/5). +/// However, the issue should only happen if the object conforming to this protocol follows (_Concurrency, AsyncSequence) +/// in lexicographic order. (AsyncAlgorithms, MySequence) should always be after it. +/// +/// Example: +/// ```swift +/// class MySequence: AsyncSequence, AsyncFailureBackportable { ... } +/// +/// ``` +@available(AsyncAlgorithms 1.1, *) +public protocol AsyncFailureBackportable { + typealias BackportableFailure = Failure + associatedtype Failure: Error +} diff --git a/Sources/AsyncAlgorithms/AsyncShareSequence.swift b/Sources/AsyncAlgorithms/AsyncShareSequence.swift index 6c76a4d1..4d6743f2 100644 --- a/Sources/AsyncAlgorithms/AsyncShareSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncShareSequence.swift @@ -8,7 +8,6 @@ // See https://swift.org/LICENSE.txt for license information // //===----------------------------------------------------------------------===// - #if compiler(>=6.2) import Synchronization @@ -16,7 +15,7 @@ import DequeModule @available(AsyncAlgorithms 1.1, *) extension AsyncSequence -where Element: Sendable, Self: SendableMetatype, AsyncIterator: SendableMetatype { +where Element: Sendable, Self: _SendableMetatype, AsyncIterator: _SendableMetatype { /// Creates a shared async sequence that allows multiple concurrent iterations over a single source. /// /// The `share` method transforms an async sequence into a shareable sequence that can be safely @@ -67,7 +66,7 @@ where Element: Sendable, Self: SendableMetatype, AsyncIterator: SendableMetatype /// public func share( bufferingPolicy: AsyncBufferSequencePolicy = .bounded(1) - ) -> some AsyncSequence & Sendable { + ) -> AsyncShareSequence { // The iterator is transferred to the isolation of the iterating task // this has to be done "unsafely" since we cannot annotate the transfer // however since iterating an AsyncSequence types twice has been defined @@ -115,8 +114,8 @@ where Element: Sendable, Self: SendableMetatype, AsyncIterator: SendableMetatype // This type is typically not used directly; instead, use the `share()` method on any // async sequence that meets the sendability requirements. @available(AsyncAlgorithms 1.1, *) -struct AsyncShareSequence: Sendable -where Base.Element: Sendable, Base: SendableMetatype, Base.AsyncIterator: SendableMetatype { +public struct AsyncShareSequence: Sendable +where Base.Element: Sendable, Base: _SendableMetatype, Base.AsyncIterator: _SendableMetatype { // Represents a single consumer's connection to the shared sequence. // // Each iterator of the shared sequence creates its own `Side` instance, which tracks @@ -135,7 +134,7 @@ where Base.Element: Sendable, Base: SendableMetatype, Base.AsyncIterator: Sendab // - `continuation`: The continuation waiting for the next element (nil if not waiting) // - `position`: The consumer's current position in the shared buffer struct State { - var continuation: UnsafeContinuation, Never>? + var continuation: UnsafeContinuation, Never>? var position = 0 // Creates a new state with the position adjusted by the given offset. @@ -162,7 +161,7 @@ where Base.Element: Sendable, Base: SendableMetatype, Base.AsyncIterator: Sendab iteration.unregisterSide(id) } - func next(isolation actor: isolated (any Actor)?) async throws(Failure) -> Element? { + func next(isolation actor: isolated (any Actor)?) async throws(Failure) -> Base.Element? { try await iteration.next(isolation: actor, id: id) } } @@ -230,7 +229,7 @@ where Base.Element: Sendable, Base: SendableMetatype, Base.AsyncIterator: Sendab var generation = 0 var sides = [Int: Side.State]() var iteratingTask: IteratingTask - private(set) var buffer = Deque() + private(set) var buffer = Deque() private(set) var finished = false private(set) var failure: Failure? var cancelled = false @@ -311,7 +310,7 @@ where Base.Element: Sendable, Base: SendableMetatype, Base.AsyncIterator: Sendab // **Buffering Newest**: Appends if under the limit, otherwise removes the oldest and appends // // - Parameter element: The element to add to the buffer - mutating func enqueue(_ element: Element) { + mutating func enqueue(_ element: Base.Element) { let count = buffer.count switch storagePolicy { @@ -341,14 +340,14 @@ where Base.Element: Sendable, Base: SendableMetatype, Base.AsyncIterator: Sendab } } - let state: Mutex + let state: ManagedCriticalState let limit: Int? init( _ iteratorFactory: @escaping @Sendable () -> sending Base.AsyncIterator, bufferingPolicy: AsyncBufferSequencePolicy ) { - state = Mutex(State(iteratorFactory, bufferingPolicy: bufferingPolicy)) + state = ManagedCriticalState(State(iteratorFactory, bufferingPolicy: bufferingPolicy)) switch bufferingPolicy.policy { case .bounded(let limit): self.limit = limit @@ -478,15 +477,15 @@ where Base.Element: Sendable, Base: SendableMetatype, Base.AsyncIterator: Sendab } struct Resumption { - let continuation: UnsafeContinuation, Never> - let result: Result + let continuation: UnsafeContinuation, Never> + let result: Result func resume() { continuation.resume(returning: result) } } - func emit(_ result: Result) { + func emit(_ result: Result) { let (resumptions, limitContinuation, demandContinuation, cancelled) = state.withLock { state -> ([Resumption], UnsafeContinuation?, UnsafeContinuation?, Bool) in var resumptions = [Resumption]() @@ -533,12 +532,12 @@ where Base.Element: Sendable, Base: SendableMetatype, Base.AsyncIterator: Sendab private func nextIteration( _ id: Int - ) async -> Result.Element?, AsyncShareSequence.Failure> { + ) async -> Result { return await withTaskCancellationHandler { await withUnsafeContinuation { continuation in let (res, limitContinuation, demandContinuation, cancelled) = state.withLock { state -> ( - Result?, UnsafeContinuation?, UnsafeContinuation?, Bool + Result?, UnsafeContinuation?, UnsafeContinuation?, Bool ) in guard let side = state.sides[id] else { return state.emit(.success(nil), limit: limit) @@ -591,20 +590,19 @@ where Base.Element: Sendable, Base: SendableMetatype, Base.AsyncIterator: Sendab } } - func next(isolation actor: isolated (any Actor)?, id: Int) async throws(Failure) -> Element? { - let (factory, cancelled) = state.withLock { state -> ((@Sendable () -> sending Base.AsyncIterator)?, Bool) in - switch state.iteratingTask { - case .pending(let factory): - state.iteratingTask = .starting - return (factory, false) - case .cancelled: - return (nil, true) - default: - return (nil, false) + func next(isolation actor: isolated (any Actor)?, id: Int) async throws(Failure) -> Base.Element? { + let iteratingTask = state.withLock { state -> IteratingTask in + defer { + if case .pending = state.iteratingTask { + state.iteratingTask = .starting + } } + return state.iteratingTask } - if cancelled { return nil } - if let factory { + + if case .cancelled = iteratingTask { return nil } + + if case .pending(let factory) = iteratingTask { let task: Task // for the fancy dance of availability and canImport see the comment on the next check for details #if swift(>=6.2) @@ -659,7 +657,6 @@ where Base.Element: Sendable, Base: SendableMetatype, Base.AsyncIterator: Sendab #else return try await nextIteration(id).get() #endif - } } @@ -698,29 +695,33 @@ where Base.Element: Sendable, Base: SendableMetatype, Base.AsyncIterator: Sendab } @available(AsyncAlgorithms 1.1, *) -extension AsyncShareSequence: AsyncSequence { - typealias Element = Base.Element - typealias Failure = Base.Failure - - struct Iterator: AsyncIteratorProtocol { +extension AsyncShareSequence: AsyncSequence, AsyncFailureBackportable { + public typealias Element = Base.Element + public struct Iterator: AsyncIteratorProtocol, _SendableMetatype { let side: Side init(_ iteration: Iteration) { side = Side(iteration) } - - mutating func next() async rethrows -> Element? { + + mutating public func next() async rethrows -> Element? { try await side.next(isolation: nil) } - - mutating func next(isolation actor: isolated (any Actor)?) async throws(Failure) -> Element? { - try await side.next(isolation: actor) - } + +// mutating public func next(isolation actor: isolated (any Actor)?) async throws(Self.BackportableFailure) -> Element? { +// try await side.next(isolation: actor) +// } } - func makeAsyncIterator() -> Iterator { + public func makeAsyncIterator() -> Iterator { Iterator(extent.iteration) } } +@available(AsyncAlgorithms 1.2, *) +extension AsyncShareSequence.Iterator { + mutating public func next(isolation actor: isolated (any Actor)?) async throws(Base.Failure) -> Base.Element? { + try await side.next(isolation: actor) + } +} #endif diff --git a/Sources/AsyncAlgorithms/Shims.swift b/Sources/AsyncAlgorithms/Shims.swift new file mode 100644 index 00000000..3e33b76b --- /dev/null +++ b/Sources/AsyncAlgorithms/Shims.swift @@ -0,0 +1,18 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import Foundation + +#if compiler(>=6.2) +public typealias _SendableMetatype = SendableMetatype +#else +public typealias _SendableMetatype = Any +#endif diff --git a/Tests/AsyncAlgorithmsTests/TestShare.swift b/Tests/AsyncAlgorithmsTests/TestShare.swift index ac817536..4c2cec83 100644 --- a/Tests/AsyncAlgorithmsTests/TestShare.swift +++ b/Tests/AsyncAlgorithmsTests/TestShare.swift @@ -9,18 +9,18 @@ // //===----------------------------------------------------------------------===// -#if compiler(>=6.2) +#if compiler(>=6.0) import XCTest import AsyncAlgorithms import Synchronization -@available(macOS 15.0, *) +@available(AsyncAlgorithms 1.1, *) final class TestShare: XCTestCase { // MARK: - Basic Functionality Tests - func test_share_delivers_elements_to_multiple_consumers() async { + func test_share_delivers_elements_to_multiple_consumers() async throws { let source = [1, 2, 3, 4, 5] let shared = source.async.share() let gate1 = Gate() @@ -31,7 +31,7 @@ final class TestShare: XCTestCase { var iterator = shared.makeAsyncIterator() gate1.open() await gate2.enter() - while let value = await iterator.next(isolation: nil) { + while let value = await iterator.next() { results.append(value) } return results @@ -42,7 +42,7 @@ final class TestShare: XCTestCase { var iterator = shared.makeAsyncIterator() gate2.open() await gate1.enter() - while let value = await iterator.next(isolation: nil) { + while let value = await iterator.next() { results.append(value) } return results @@ -80,12 +80,12 @@ final class TestShare: XCTestCase { // MARK: - Buffering Policy Tests - func test_share_with_bounded_buffering() async { + func test_share_with_bounded_buffering() async throws { var gated = GatedSequence([1, 2, 3, 4, 5]) let shared = gated.share(bufferingPolicy: .bounded(2)) - let results1 = Mutex([Int]()) - let results2 = Mutex([Int]()) + let results1 = ManagedCriticalState([Int]()) + let results2 = ManagedCriticalState([Int]()) let gate1 = Gate() let gate2 = Gate() @@ -94,13 +94,13 @@ final class TestShare: XCTestCase { gate1.open() await gate2.enter() // Consumer 1 reads first element - if let value = await iterator.next(isolation: nil) { + if let value = await iterator.next() { results1.withLock { $0.append(value) } } // Delay to allow consumer 2 to get ahead - try? await Task.sleep(for: .milliseconds(10)) + try? await Task.sleep(nanoseconds: 10_000_000) // Continue reading - while let value = await iterator.next(isolation: nil) { + while let value = await iterator.next() { results1.withLock { $0.append(value) } } } @@ -110,7 +110,7 @@ final class TestShare: XCTestCase { gate2.open() await gate1.enter() // Consumer 2 reads all elements quickly - while let value = await iterator.next(isolation: nil) { + while let value = await iterator.next() { results2.withLock { $0.append(value) } } } @@ -130,12 +130,12 @@ final class TestShare: XCTestCase { XCTAssertEqual(results2.withLock { $0 }.sorted(), [1, 2, 3, 4, 5]) } - func test_share_with_unbounded_buffering() async { + func test_share_with_unbounded_buffering() async throws { let source = [1, 2, 3, 4, 5] let shared = source.async.share(bufferingPolicy: .unbounded) - let results1 = Mutex([Int]()) - let results2 = Mutex([Int]()) + let results1 = ManagedCriticalState([Int]()) + let results2 = ManagedCriticalState([Int]()) let gate1 = Gate() let gate2 = Gate() @@ -143,10 +143,10 @@ final class TestShare: XCTestCase { var iterator = shared.makeAsyncIterator() gate2.open() await gate1.enter() - while let value = await iterator.next(isolation: nil) { + while let value = await iterator.next() { results1.withLock { $0.append(value) } // Add some delay to consumer 1 - try? await Task.sleep(for: .milliseconds(1)) + try? await Task.sleep(nanoseconds: 1_000_000) } } @@ -154,7 +154,7 @@ final class TestShare: XCTestCase { var iterator = shared.makeAsyncIterator() gate1.open() await gate2.enter() - while let value = await iterator.next(isolation: nil) { + while let value = await iterator.next() { results2.withLock { $0.append(value) } } } @@ -166,12 +166,12 @@ final class TestShare: XCTestCase { XCTAssertEqual(results2.withLock { $0 }, [1, 2, 3, 4, 5]) } - func test_share_with_bufferingLatest_buffering() async { + func test_share_with_bufferingLatest_buffering() async throws { var gated = GatedSequence([1, 2, 3, 4, 5]) let shared = gated.share(bufferingPolicy: .bufferingLatest(2)) - let fastResults = Mutex([Int]()) - let slowResults = Mutex([Int]()) + let fastResults = ManagedCriticalState([Int]()) + let slowResults = ManagedCriticalState([Int]()) let gate1 = Gate() let gate2 = Gate() @@ -179,7 +179,7 @@ final class TestShare: XCTestCase { var iterator = shared.makeAsyncIterator() gate2.open() await gate1.enter() - while let value = await iterator.next(isolation: nil) { + while let value = await iterator.next() { fastResults.withLock { $0.append(value) } } } @@ -189,26 +189,26 @@ final class TestShare: XCTestCase { gate1.open() await gate2.enter() // Read first element immediately - if let value = await iterator.next(isolation: nil) { + if let value = await iterator.next() { slowResults.withLock { $0.append(value) } } // Add significant delay to let buffer fill up and potentially overflow - try? await Task.sleep(for: .milliseconds(50)) + try? await Task.sleep(nanoseconds: 50_000_000) // Continue reading remaining elements - while let value = await iterator.next(isolation: nil) { + while let value = await iterator.next() { slowResults.withLock { $0.append(value) } } } // Release all elements quickly to test buffer overflow behavior gated.advance() // 1 - try? await Task.sleep(for: .milliseconds(5)) + try? await Task.sleep(nanoseconds: 5_000_000) gated.advance() // 2 - try? await Task.sleep(for: .milliseconds(5)) + try? await Task.sleep(nanoseconds: 5_000_000) gated.advance() // 3 - try? await Task.sleep(for: .milliseconds(5)) + try? await Task.sleep(nanoseconds: 5_000_000) gated.advance() // 4 - try? await Task.sleep(for: .milliseconds(5)) + try? await Task.sleep(nanoseconds: 5_000_000) gated.advance() // 5 await fastConsumer.value @@ -238,12 +238,12 @@ final class TestShare: XCTestCase { } } - func test_share_with_bufferingOldest_buffering() async { + func test_share_with_bufferingOldest_buffering() async throws { var gated = GatedSequence([1, 2, 3, 4, 5]) let shared = gated.share(bufferingPolicy: .bufferingOldest(2)) - let fastResults = Mutex([Int]()) - let slowResults = Mutex([Int]()) + let fastResults = ManagedCriticalState([Int]()) + let slowResults = ManagedCriticalState([Int]()) let gate1 = Gate() let gate2 = Gate() @@ -251,7 +251,7 @@ final class TestShare: XCTestCase { var iterator = shared.makeAsyncIterator() gate2.open() await gate1.enter() - while let value = await iterator.next(isolation: nil) { + while let value = await iterator.next() { fastResults.withLock { $0.append(value) } } } @@ -261,26 +261,26 @@ final class TestShare: XCTestCase { gate1.open() await gate2.enter() // Read first element immediately - if let value = await iterator.next(isolation: nil) { + if let value = await iterator.next() { slowResults.withLock { $0.append(value) } } // Add significant delay to let buffer fill up and potentially overflow - try? await Task.sleep(for: .milliseconds(50)) + try? await Task.sleep(nanoseconds: 50_000_000) // Continue reading remaining elements - while let value = await iterator.next(isolation: nil) { + while let value = await iterator.next() { slowResults.withLock { $0.append(value) } } } // Release all elements quickly to test buffer overflow behavior gated.advance() // 1 - try? await Task.sleep(for: .milliseconds(5)) + try? await Task.sleep(nanoseconds: 5_000_000) gated.advance() // 2 - try? await Task.sleep(for: .milliseconds(5)) + try? await Task.sleep(nanoseconds: 5_000_000) gated.advance() // 3 - try? await Task.sleep(for: .milliseconds(5)) + try? await Task.sleep(nanoseconds: 5_000_000) gated.advance() // 4 - try? await Task.sleep(for: .milliseconds(5)) + try? await Task.sleep(nanoseconds: 5_000_000) gated.advance() // 5 await fastConsumer.value @@ -388,7 +388,7 @@ final class TestShare: XCTestCase { await fulfillment(of: [consumer2Finished], timeout: 1.0) } - func test_share_cancellation_cancels_source_when_no_consumers() async { + func test_share_cancellation_cancels_source_when_no_consumers() async throws { let source = Indefinite(value: 1).async let shared = source.share() @@ -397,11 +397,11 @@ final class TestShare: XCTestCase { let task = Task { var iterator = shared.makeAsyncIterator() - if await iterator.next(isolation: nil) != nil { + if await iterator.next() != nil { iterated.fulfill() } // Task will be cancelled here, so iteration should stop - while await iterator.next(isolation: nil) != nil { + while await iterator.next() != nil { // Continue iterating until cancelled } finished.fulfill() @@ -423,10 +423,10 @@ final class TestShare: XCTestCase { } let shared = source.share() - let consumer1Results = Mutex([Int]()) - let consumer2Results = Mutex([Int]()) - let consumer1Error = Mutex(nil) - let consumer2Error = Mutex(nil) + let consumer1Results = ManagedCriticalState([Int]()) + let consumer2Results = ManagedCriticalState([Int]()) + let consumer1Error = ManagedCriticalState(nil) + let consumer2Error = ManagedCriticalState(nil) let gate1 = Gate() let gate2 = Gate() @@ -470,17 +470,17 @@ final class TestShare: XCTestCase { // MARK: - Timing and Race Condition Tests - func test_share_with_late_joining_consumer() async { + func test_share_with_late_joining_consumer() async throws { var gated = GatedSequence([1, 2, 3, 4, 5]) let shared = gated.share(bufferingPolicy: .unbounded) - let earlyResults = Mutex([Int]()) - let lateResults = Mutex([Int]()) + let earlyResults = ManagedCriticalState([Int]()) + let lateResults = ManagedCriticalState([Int]()) // Start early consumer let earlyConsumer = Task { var iterator = shared.makeAsyncIterator() - while let value = await iterator.next(isolation: nil) { + while let value = await iterator.next() { earlyResults.withLock { $0.append(value) } } } @@ -490,12 +490,12 @@ final class TestShare: XCTestCase { gated.advance() // 2 // Give early consumer time to consume - try? await Task.sleep(for: .milliseconds(10)) + try? await Task.sleep(nanoseconds: 10_000_000) // Start late consumer let lateConsumer = Task { var iterator = shared.makeAsyncIterator() - while let value = await iterator.next(isolation: nil) { + while let value = await iterator.next() { lateResults.withLock { $0.append(value) } } } @@ -514,7 +514,7 @@ final class TestShare: XCTestCase { XCTAssertTrue(lateResults.withLock { $0.count <= 5 }) } - func test_share_iterator_independence() async { + func test_share_iterator_independence() async throws { let source = [1, 2, 3, 4, 5] let shared = source.async.share() @@ -522,11 +522,11 @@ final class TestShare: XCTestCase { var iterator2 = shared.makeAsyncIterator() // Both iterators should independently get the same elements - let value1a = await iterator1.next(isolation: nil) - let value2a = await iterator2.next(isolation: nil) + let value1a = await iterator1.next() + let value2a = await iterator2.next() - let value1b = await iterator1.next(isolation: nil) - let value2b = await iterator2.next(isolation: nil) + let value1b = await iterator1.next() + let value2b = await iterator2.next() XCTAssertEqual(value1a, 1) XCTAssertEqual(value2a, 1) @@ -536,7 +536,7 @@ final class TestShare: XCTestCase { // MARK: - Memory and Resource Management Tests - func test_share_cleans_up_when_all_consumers_finish() async { + func test_share_cleans_up_when_all_consumers_finish() async throws { let source = [1, 2, 3] let shared = source.async.share() @@ -549,7 +549,7 @@ final class TestShare: XCTestCase { // Create a new iterator after the sequence finished var newIterator = shared.makeAsyncIterator() - let value = await newIterator.next(isolation: nil) + let value = await newIterator.next() XCTAssertNil(value) // Should return nil since source is exhausted } @@ -572,6 +572,37 @@ final class TestShare: XCTestCase { XCTAssertEqual(results1, [1, 2, 3, 4, 5]) XCTAssertEqual(results2, []) // Should be empty since source is exhausted } + + @available(AsyncAlgorithms 1.1, *) + func test_share_rethrows_failure_type_on_backported() async { + let shared = AsyncThrowingStream { + $0.finish(throwing: TestError.failure) + }.share() + do { + for try await _ in shared { + XCTFail("Expected to not get here") + } + } catch { + XCTAssertEqual(error as? TestError, .failure) + } + } + + func test_share_rethrows_failure_type_without_falling_back_to_any_error() async { + if #available(AsyncAlgorithms 1.2, *) { + // Ensure - at compile time - that error is effectively a TestError + let shared: some AsyncSequence = AlwaysFailingSequence().share() + do { + for try await _ in shared { + XCTFail("Expected to not get here") + } + } catch { + + XCTAssertEqual(error, TestError.failure) + } + } else { + // not available + } + } } // MARK: - Helper Types @@ -580,4 +611,21 @@ private enum TestError: Error, Equatable { case failure } +@available(AsyncAlgorithms 1.2, *) +/// A sequence used to properly test concrete error on 1.2 +private struct AlwaysFailingSequence: AsyncSequence, Sendable { + init() {} + + func makeAsyncIterator() -> AsyncIterator { AsyncIterator() } + + struct AsyncIterator: AsyncIteratorProtocol, Sendable { + + func next() async throws(TestError) -> Void? { + throw TestError.failure + } + mutating func next(completion: @escaping (Result) -> Void) async throws(TestError) -> Element? { + throw TestError.failure + } + } +} #endif