From e325b4ffe9f70c867805ef9bd8c3a4c2dc32d1ea Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Sun, 3 May 2026 06:03:31 -0700 Subject: [PATCH 1/5] Updated `collect(_:sequenceTransform:)` to throw typed errors --- .../AsyncReadUpToCountSequence.swift | 40 +++++++++++++------ .../AsyncReadUpToCountSequenceTests.swift | 18 ++++----- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/Sources/AsyncSequenceReader/AsyncReadUpToCountSequence.swift b/Sources/AsyncSequenceReader/AsyncReadUpToCountSequence.swift index f879457..2ee6d65 100644 --- a/Sources/AsyncSequenceReader/AsyncReadUpToCountSequence.swift +++ b/Sources/AsyncSequenceReader/AsyncReadUpToCountSequence.swift @@ -50,7 +50,9 @@ extension AsyncIteratorProtocol { return result } - +} + +extension AsyncIteratorProtocol { /// Collect the specified number of elements into a sequence, and transform it using the provided closure. /// /// In this example, an asynchronous sequence of Strings encodes sentences by prefixing each word sequence with a number. @@ -82,10 +84,13 @@ extension AsyncIteratorProtocol { /// - Parameter sequenceTransform: A transformation that accepts a sequence of the specified size that can be read from, or stopped prematurely by returning early. The receiving iterator will have moved forward by the same amount of items consumed within `sequenceTransform`. /// - Returns: A transformed value as returned by `sequenceTransform`, or `nil` if the sequence was already finished. /// - Throws: `AsyncSequenceReaderError.insufficientElements` if a complete byte sequence could not be returned by the time the sequence ended. - public mutating func collect( + public mutating func collect< + Transformed, + TransformFailure + >( _ count: Int, - sequenceTransform: sending (sending AsyncReadUpToCountSequence) async throws -> Transformed - ) async throws -> Transformed? { + sequenceTransform: sending (sending AsyncReadUpToCountSequence) async throws(TransformFailure) -> Transformed + ) async throws(TransformFailure) -> Transformed? { assert(count >= 0, "count must be larger than or equal to 0") return try await collect(min: count, max: count, sequenceTransform: sequenceTransform) } @@ -124,11 +129,14 @@ extension AsyncIteratorProtocol { /// - Parameter sequenceTransform: A transformation that accepts a sequence of the specified size that can be read from, or stopped prematurely by returning early. The receiving iterator will have moved forward by the same amount of items consumed within `sequenceTransform`. /// - Returns: A transformed value as returned by `sequenceTransform`, or `nil` if the sequence was already finished. /// - Throws: `AsyncSequenceReaderError.insufficientElements` if a complete byte sequence could not be returned by the time the sequence ended. - public mutating func collect( + public mutating func collect< + Transformed, + TransformFailure + >( min minCount: Int = 1, max maxCount: Int, - sequenceTransform: sending (sending AsyncReadUpToCountSequence) async throws -> Transformed - ) async throws -> Transformed? { + sequenceTransform: sending (sending AsyncReadUpToCountSequence) async throws(TransformFailure) -> Transformed + ) async throws(TransformFailure) -> Transformed? { /// It is unsafe to read ahead in this case, so exit early if we know we won't need to read. if maxCount == 0 { return nil } assert(minCount >= 1, "minCount must be larger than or equal to 1, or the first value risks getting dropped") @@ -168,10 +176,13 @@ extension AsyncBufferedIterator { /// - Parameter sequenceTransform: A transformation that accepts a sequence of the specified size that can be read from, or stopped prematurely by returning early. The receiving iterator will have moved forward by the same amount of items consumed within `sequenceTransform`. /// - Returns: A transformed value as returned by `sequenceTransform`, or `nil` if the sequence was already finished. /// - Throws: `AsyncSequenceReaderError.insufficientElements` if a complete byte sequence could not be returned by the time the sequence ended. - public mutating func collect( + public mutating func collect< + Transformed, + TransformFailure + >( _ count: Int, - sequenceTransform: sending (sending AsyncReadUpToCountSequence) async throws -> Transformed - ) async throws -> Transformed? { + sequenceTransform: sending (sending AsyncReadUpToCountSequence) async throws(TransformFailure) -> Transformed + ) async throws(TransformFailure) -> Transformed? { assert(count >= 0, "count must be larger than 0") return try await collect(min: count, max: count, sequenceTransform: sequenceTransform) } @@ -208,11 +219,14 @@ extension AsyncBufferedIterator { /// - Parameter sequenceTransform: A transformation that accepts a sequence of the specified size that can be read from, or stopped prematurely by returning early. The receiving iterator will have moved forward by the same amount of items consumed within `sequenceTransform`. /// - Returns: A transformed value as returned by `sequenceTransform`, or `nil` if the sequence was already finished. /// - Throws: `AsyncSequenceReaderError.insufficientElements` if a complete byte sequence could not be returned by the time the sequence ended. - public mutating func collect( + public mutating func collect< + Transformed, + TransformFailure: Error + >( min minCount: Int = 0, max maxCount: Int, - sequenceTransform: sending (sending AsyncReadUpToCountSequence) async throws -> Transformed - ) async throws -> Transformed? { + sequenceTransform: sending (sending AsyncReadUpToCountSequence) async throws(TransformFailure) -> Transformed + ) async throws(TransformFailure) -> Transformed? { try await transform(with: sequenceTransform) { .init($0, minCount: minCount, maxCount: maxCount) } } } diff --git a/Tests/AsyncSequenceReaderTests/AsyncReadUpToCountSequenceTests.swift b/Tests/AsyncSequenceReaderTests/AsyncReadUpToCountSequenceTests.swift index 2b0f8e1..bfec4aa 100644 --- a/Tests/AsyncSequenceReaderTests/AsyncReadUpToCountSequenceTests.swift +++ b/Tests/AsyncSequenceReaderTests/AsyncReadUpToCountSequenceTests.swift @@ -221,7 +221,7 @@ import Testing }.reduce(into: [], { $0.append($1) }) } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:175: Assertion failed: count must be larger than 0") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:186: Assertion failed: count must be larger than 0") #endif } @@ -236,7 +236,7 @@ import Testing }.reduce(into: [], { $0.append($1) }) } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:240: Precondition failed: minCount must be larger than or equal to 0") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:254: Precondition failed: minCount must be larger than or equal to 0") #endif } @@ -251,7 +251,7 @@ import Testing }.reduce(into: [], { $0.append($1) }) } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:240: Precondition failed: minCount must be larger than or equal to 0") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:254: Precondition failed: minCount must be larger than or equal to 0") #endif } @@ -266,7 +266,7 @@ import Testing }.reduce(into: [], { $0.append($1) }) } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:239: Precondition failed: maxCount must be larger than or equal to minCount") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:253: Precondition failed: maxCount must be larger than or equal to minCount") #endif } @@ -280,7 +280,7 @@ import Testing } } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:89: Assertion failed: count must be larger than or equal to 0") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:94: Assertion failed: count must be larger than or equal to 0") #endif } @@ -306,7 +306,7 @@ import Testing try await sequence.reduce(into: "") { $0 += ($0.isEmpty ? "" : " ") + String($1) } } } - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:134: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:142: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") #endif } @@ -320,7 +320,7 @@ import Testing } } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:134: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:142: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") #endif } @@ -346,7 +346,7 @@ import Testing } } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:134: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:142: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") #endif } @@ -372,7 +372,7 @@ import Testing } } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:134: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:142: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") #endif } From 0dfa05f67f4856f021dc89be1b74fd3ed9395608 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Sun, 3 May 2026 06:09:40 -0700 Subject: [PATCH 2/5] Updated documentation references to link to the related errors --- .../AsyncReadUpToCountSequence.swift | 12 ++++++------ .../AsyncReadUpToElementsSequence.swift | 8 ++++---- .../AsyncSequenceReader.docc/AsyncSequenceReader.md | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/AsyncSequenceReader/AsyncReadUpToCountSequence.swift b/Sources/AsyncSequenceReader/AsyncReadUpToCountSequence.swift index 2ee6d65..3a8598a 100644 --- a/Sources/AsyncSequenceReader/AsyncReadUpToCountSequence.swift +++ b/Sources/AsyncSequenceReader/AsyncReadUpToCountSequence.swift @@ -13,7 +13,7 @@ extension AsyncIteratorProtocol { /// If a complete array could not be collected, an error is thrown and the sequence should be considered finished. /// - Parameter count: The number of elements to collect. /// - Returns: A collection with exactly `count` elements, or `nil` if the sequence is finished. - /// - Throws: `AsyncSequenceReaderError.insufficientElements` if a complete byte sequence could not be returned by the time the sequence ended. + /// - Throws: ``AsyncSequenceReaderError/insufficientElements(minimum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. public mutating func collect(_ count: Int) async throws -> [Element]? { assert(count >= 0, "count must be larger than or equal to 0") return try await collect(min: count, max: count) @@ -25,7 +25,7 @@ extension AsyncIteratorProtocol { /// - Parameter minCount: The minimum number of elements to collect. /// - Parameter maxCount: The maximum number of elements to collect. /// - Returns: A collection with at least `minCount` and at most `maxCount` elements, or `nil` if the sequence is finished. - /// - Throws: `AsyncSequenceReaderError.insufficientElements` if a complete byte sequence could not be returned by the time the sequence ended. + /// - Throws: ``AsyncSequenceReaderError/insufficientElements(minimum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. public mutating func collect(min minCount: Int = 0, max maxCount: Int) async throws -> [Element]? { precondition(minCount <= maxCount, "maxCount must be larger than or equal to minCount") precondition(minCount >= 0, "minCount must be larger than or equal to 0") @@ -83,7 +83,7 @@ extension AsyncIteratorProtocol { /// - Parameter count: The number of elements the `sequenceTransform` closure will have access to. /// - Parameter sequenceTransform: A transformation that accepts a sequence of the specified size that can be read from, or stopped prematurely by returning early. The receiving iterator will have moved forward by the same amount of items consumed within `sequenceTransform`. /// - Returns: A transformed value as returned by `sequenceTransform`, or `nil` if the sequence was already finished. - /// - Throws: `AsyncSequenceReaderError.insufficientElements` if a complete byte sequence could not be returned by the time the sequence ended. + /// - Throws: ``AsyncSequenceReaderError/insufficientElements(minimum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. public mutating func collect< Transformed, TransformFailure @@ -128,7 +128,7 @@ extension AsyncIteratorProtocol { /// - Parameter maxCount: The maximum number of elements the `sequenceTransform` closure will have access to. /// - Parameter sequenceTransform: A transformation that accepts a sequence of the specified size that can be read from, or stopped prematurely by returning early. The receiving iterator will have moved forward by the same amount of items consumed within `sequenceTransform`. /// - Returns: A transformed value as returned by `sequenceTransform`, or `nil` if the sequence was already finished. - /// - Throws: `AsyncSequenceReaderError.insufficientElements` if a complete byte sequence could not be returned by the time the sequence ended. + /// - Throws: ``AsyncSequenceReaderError/insufficientElements(minimum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. public mutating func collect< Transformed, TransformFailure @@ -175,7 +175,7 @@ extension AsyncBufferedIterator { /// - Parameter count: The number of elements the `sequenceTransform` closure will have access to. /// - Parameter sequenceTransform: A transformation that accepts a sequence of the specified size that can be read from, or stopped prematurely by returning early. The receiving iterator will have moved forward by the same amount of items consumed within `sequenceTransform`. /// - Returns: A transformed value as returned by `sequenceTransform`, or `nil` if the sequence was already finished. - /// - Throws: `AsyncSequenceReaderError.insufficientElements` if a complete byte sequence could not be returned by the time the sequence ended. + /// - Throws: ``AsyncSequenceReaderError/insufficientElements(minimum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. public mutating func collect< Transformed, TransformFailure @@ -218,7 +218,7 @@ extension AsyncBufferedIterator { /// - Parameter maxCount: The maximum number of elements the `sequenceTransform` closure will have access to. /// - Parameter sequenceTransform: A transformation that accepts a sequence of the specified size that can be read from, or stopped prematurely by returning early. The receiving iterator will have moved forward by the same amount of items consumed within `sequenceTransform`. /// - Returns: A transformed value as returned by `sequenceTransform`, or `nil` if the sequence was already finished. - /// - Throws: `AsyncSequenceReaderError.insufficientElements` if a complete byte sequence could not be returned by the time the sequence ended. + /// - Throws: ``AsyncSequenceReaderError/insufficientElements(minimum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. public mutating func collect< Transformed, TransformFailure: Error diff --git a/Sources/AsyncSequenceReader/AsyncReadUpToElementsSequence.swift b/Sources/AsyncSequenceReader/AsyncReadUpToElementsSequence.swift index 377c0d7..6c18be3 100644 --- a/Sources/AsyncSequenceReader/AsyncReadUpToElementsSequence.swift +++ b/Sources/AsyncSequenceReader/AsyncReadUpToElementsSequence.swift @@ -33,7 +33,7 @@ extension AsyncIteratorProtocol { /// - Parameter termination: The element marking the end of the sequence that will be collected. /// - Parameter throwsIfOver: The maximum amount of elements that will be read before an error is thrown if a termination is not detected. /// - Returns: An array of the collected elements, or `nil` if the sequence was already finished. - /// - Throws: `AsyncSequenceReaderError.terminationNotFound` if a complete byte sequence could not be returned by the time the sequence ended. + /// - Throws: ``AsyncSequenceReaderError/terminationNotFound(maximum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. public mutating func collect( upToIncluding termination: Element, throwsIfOver maximumBufferSize: Int @@ -66,7 +66,7 @@ extension AsyncIteratorProtocol { /// - Parameter termination: The sequence of elements marking the end of the sequence that will be collected. /// - Parameter throwsIfOver: The maximum amount of elements that will be read before an error is thrown if a termination is not detected. /// - Returns: An array of the collected elements, or `nil` if the sequence was already finished. - /// - Throws: `AsyncSequenceReaderError.terminationNotFound` if a complete byte sequence could not be returned by the time the sequence ended. + /// - Throws: ``AsyncSequenceReaderError/terminationNotFound(maximum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. public mutating func collect( upToIncluding termination: some Collection, throwsIfOver maximumBufferSize: Int @@ -116,7 +116,7 @@ extension AsyncIteratorProtocol { /// - Parameter termination: The element marking the end of the sequence that will be collected. /// - Parameter throwsIfOver: The maximum amount of elements that will be read before an error is thrown if a termination is not detected. /// - Returns: An array of the collected elements, or `nil` if the sequence was already finished. - /// - Throws: `AsyncSequenceReaderError.terminationNotFound` if a complete byte sequence could not be returned by the time the sequence ended. + /// - Throws: ``AsyncSequenceReaderError/terminationNotFound(maximum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. public mutating func collect( upToExcluding termination: Element, throwsIfOver maximumBufferSize: Int @@ -149,7 +149,7 @@ extension AsyncIteratorProtocol { /// - Parameter termination: The sequence of elements marking the end of the sequence that will be collected. /// - Parameter throwsIfOver: The maximum amount of elements that will be read before an error is thrown if a termination is not detected. /// - Returns: An array of the collected elements, or `nil` if the sequence was already finished. - /// - Throws: `AsyncSequenceReaderError.terminationNotFound` if a complete byte sequence could not be returned by the time the sequence ended. + /// - Throws: ``AsyncSequenceReaderError/terminationNotFound(maximum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. public mutating func collect( upToExcluding termination: some Collection, throwsIfOver maximumBufferSize: Int diff --git a/Sources/AsyncSequenceReader/AsyncSequenceReader.docc/AsyncSequenceReader.md b/Sources/AsyncSequenceReader/AsyncSequenceReader.docc/AsyncSequenceReader.md index 2a23942..b045e31 100644 --- a/Sources/AsyncSequenceReader/AsyncSequenceReader.docc/AsyncSequenceReader.md +++ b/Sources/AsyncSequenceReader/AsyncSequenceReader.docc/AsyncSequenceReader.md @@ -72,7 +72,7 @@ var limitedSequence = try await iterator.collect(min: 128, max: 256) // Array of For that last example, do note that the `limitedSequence` will only become available if and when all the bytes have been read. ie. you will not get results back if only 128 bytes are available _right now_, if the sequence is still ongoing. -If the minimum number of bytes cannot be collected, an `AsyncSequenceReaderError.insufficientElements` error will be thrown. +If the minimum number of bytes cannot be collected, an ``AsyncSequenceReaderError/insufficientElements(minimum:actual:)`` error will be thrown. You can also collect elements into another async sequence using a **sequence transform**: @@ -104,7 +104,7 @@ var httpHeaderEntry = try await iterator.collect(upToExcluding: ["\r".asciiValue This is especially useful when scanning for strings or other known boundaries, allowing you get get an array of elements either including or excluding the terminator you specified. -Note how a ``throwsIfOver`` parameter is necessary — this is to prevent un-bounded reads from running out of control. If the terminator is not detected, or your maximum element allowance has been reached, an ``AsyncSequenceReaderError/terminationNotFound`` error will be thrown. +Note how a `throwsIfOver` parameter is necessary — this is to prevent un-bounded reads from running out of control. If the terminator is not detected, or your maximum element allowance has been reached, an ``AsyncSequenceReaderError/terminationNotFound(maximum:actual:)`` error will be thrown. You can bypass the `throwsIfOver` parameter if you use a **sequence transform** instead, which may be a better option if your algorithm deals with large amounts of data. If you stop reading early, elements can still be read by subsequent requests, giving you more control over how to read your data. From d4df7a543db68a49971ce44680ad59f10a3a9808 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Sun, 3 May 2026 06:30:40 -0700 Subject: [PATCH 3/5] =?UTF-8?q?Added=20variations=20of=20`collect(?= =?UTF-8?q?=E2=80=A6)`=20that=20throws=20typed=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AsyncReadUpToCountSequence.swift | 46 ++++++ .../AsyncReadUpToElementsSequence.swift | 152 ++++++++++++++++++ .../AsyncReadUpToCountSequenceTests.swift | 18 +-- .../AsyncReadUpToElementsSequenceTests.swift | 4 +- 4 files changed, 209 insertions(+), 11 deletions(-) diff --git a/Sources/AsyncSequenceReader/AsyncReadUpToCountSequence.swift b/Sources/AsyncSequenceReader/AsyncReadUpToCountSequence.swift index 3a8598a..f93542d 100644 --- a/Sources/AsyncSequenceReader/AsyncReadUpToCountSequence.swift +++ b/Sources/AsyncSequenceReader/AsyncReadUpToCountSequence.swift @@ -52,6 +52,52 @@ extension AsyncIteratorProtocol { } } +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension AsyncIteratorProtocol where Failure == Never { + /// Asynchronously advances by the specified number of elements, or ends the sequence if there is no next element. + /// + /// If a complete array could not be collected, an error is thrown and the sequence should be considered finished. + /// - Parameter count: The number of elements to collect. + /// - Returns: A collection with exactly `count` elements, or `nil` if the sequence is finished. + /// - Throws: ``AsyncSequenceReaderError/insufficientElements(minimum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + public mutating func collect(_ count: Int) async throws(AsyncSequenceReaderError) -> [Element]? { + assert(count >= 0, "count must be larger than or equal to 0") + return try await collect(min: count, max: count) + } + + /// Asynchronously advances by the specified minimum number of elements, continuing until the specified maximum number of elements, or ends the sequence if there is no next element. + /// + /// If a complete array larger than `minCount` could not be constructed, an error is thrown and the sequence should be considered finished. + /// - Parameter minCount: The minimum number of elements to collect. + /// - Parameter maxCount: The maximum number of elements to collect. + /// - Returns: A collection with at least `minCount` and at most `maxCount` elements, or `nil` if the sequence is finished. + /// - Throws: ``AsyncSequenceReaderError/insufficientElements(minimum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + public mutating func collect(min minCount: Int = 0, max maxCount: Int) async throws(AsyncSequenceReaderError) -> [Element]? { + precondition(minCount <= maxCount, "maxCount must be larger than or equal to minCount") + precondition(minCount >= 0, "minCount must be larger than or equal to 0") + if maxCount == 0 { return [] } + + var result = [Element]() + result.reserveCapacity(minCount) + + while let next = await _nextIsolated() { + result.append(next) + + if result.count == maxCount { + return result + } + } + + guard !result.isEmpty else { return nil } + + guard result.count >= minCount else { + throw AsyncSequenceReaderError.insufficientElements(minimum: minCount, actual: result.count) + } + + return result + } +} + extension AsyncIteratorProtocol { /// Collect the specified number of elements into a sequence, and transform it using the provided closure. /// diff --git a/Sources/AsyncSequenceReader/AsyncReadUpToElementsSequence.swift b/Sources/AsyncSequenceReader/AsyncReadUpToElementsSequence.swift index 6c18be3..6a4f6df 100644 --- a/Sources/AsyncSequenceReader/AsyncReadUpToElementsSequence.swift +++ b/Sources/AsyncSequenceReader/AsyncReadUpToElementsSequence.swift @@ -158,6 +158,158 @@ extension AsyncIteratorProtocol { } } +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension AsyncIteratorProtocol where Failure == Never { + /// Collect elements into a sequence until the termination sequence is encountered, and return them as an array, including the termination sequence. + /// + /// If the termination sequence was not detected before the end of the stream, or more than the specified maximum elements are read, an error will be thrown. + /// + /// In this example, an asynchronous sequence of Characters represents a list of words. + /// + /// The closure provided to the `iteratorMap(_:)` reads characters up to and inluding the termination provided, splitting the sequence into an array of words. + /// + /// ```swift + /// let dataStream = ... // "apple orange banana kiwi kumquat pear pineapple " + /// + /// let wordStream = dataStream.iteratorMap { iterator -> String? in + /// (try await iterator.collect(upToIncluding: " ", throwsIfOver: 100)) + /// .map { String($0.dropLast()) } + /// } + /// + /// for await word in wordStream { + /// print("\"\(word)\"", terminator: ", ") + /// } + /// // Prints: "apple", "orange", "banana", "kiwi", "kumquat", "pear", "pineapple", + /// ``` + /// + /// - Parameter termination: The element marking the end of the sequence that will be collected. + /// - Parameter throwsIfOver: The maximum amount of elements that will be read before an error is thrown if a termination is not detected. + /// - Returns: An array of the collected elements, or `nil` if the sequence was already finished. + /// - Throws: ``AsyncSequenceReaderError/terminationNotFound(maximum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + public mutating func collect( + upToIncluding termination: Element, + throwsIfOver maximumBufferSize: Int + ) async throws(AsyncSequenceReaderError) -> [Element]? where Element: Equatable { + try await collect(upToIncluding: [termination], throwsIfOver: maximumBufferSize) + } + + /// Collect elements into a sequence until the termination sequence is encountered, and return them as an array, including the termination sequence. + /// + /// If the termination sequence was not detected before the end of the stream, or more than the specified maximum elements are read, an error will be thrown. + /// + /// In this example, an asynchronous sequence of Characters represents a list of words. + /// + /// The closure provided to the `iteratorMap(_:)` reads characters up to and inluding the termination provided, splitting the sequence into an array of words. + /// + /// ```swift + /// let dataStream = ... // "apple orange banana kiwi kumquat pear pineapple " + /// + /// let wordStream = dataStream.iteratorMap { iterator -> String? in + /// (try await iterator.collect(upToIncluding: [" "], throwsIfOver: 100)) + /// .map { String($0.dropLast()) } + /// } + /// + /// for await word in wordStream { + /// print("\"\(word)\"", terminator: ", ") + /// } + /// // Prints: "apple", "orange", "banana", "kiwi", "kumquat", "pear", "pineapple", + /// ``` + /// + /// - Parameter termination: The sequence of elements marking the end of the sequence that will be collected. + /// - Parameter throwsIfOver: The maximum amount of elements that will be read before an error is thrown if a termination is not detected. + /// - Returns: An array of the collected elements, or `nil` if the sequence was already finished. + /// - Throws: ``AsyncSequenceReaderError/terminationNotFound(maximum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + public mutating func collect( + upToIncluding termination: some Collection, + throwsIfOver maximumBufferSize: Int + ) async throws(AsyncSequenceReaderError) -> [Element]? where Element: Equatable { + precondition(!termination.isEmpty, "termination must not be empty") + var result = [Element]() + + while let next = await _nextIsolated() { + if result.count == maximumBufferSize { + throw AsyncSequenceReaderError.terminationNotFound(maximum: maximumBufferSize, actual: result.count + 1) + } + + result.append(next) + + if result.suffix(termination.count).elementsEqual(termination) { + return result + } + } + + guard !result.isEmpty else { return nil } + + throw AsyncSequenceReaderError.terminationNotFound(maximum: maximumBufferSize, actual: result.count) + } + + /// Collect elements into a sequence until the termination sequence is encountered, and return them as an array, excluding the termination sequence. + /// + /// If the termination sequence was not detected before the end of the stream, or more than the specified maximum elements are read, an error will be thrown. + /// + /// In this example, an asynchronous sequence of Characters represents a list of words. + /// + /// The closure provided to the `iteratorMap(_:)` reads characters up to and inluding the termination provided, splitting the sequence into an array of words. + /// + /// ```swift + /// let dataStream = ... // "apple orange banana kiwi kumquat pear pineapple " + /// + /// let wordStream = dataStream.iteratorMap { iterator -> String? in + /// (try await iterator.collect(upToExcluding: " ", throwsIfOver: 100)) + /// .map { String($0) } + /// } + /// + /// for await word in wordStream { + /// print("\"\(word)\"", terminator: ", ") + /// } + /// // Prints: "apple", "orange", "banana", "kiwi", "kumquat", "pear", "pineapple", + /// ``` + /// + /// - Parameter termination: The element marking the end of the sequence that will be collected. + /// - Parameter throwsIfOver: The maximum amount of elements that will be read before an error is thrown if a termination is not detected. + /// - Returns: An array of the collected elements, or `nil` if the sequence was already finished. + /// - Throws: ``AsyncSequenceReaderError/terminationNotFound(maximum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + public mutating func collect( + upToExcluding termination: Element, + throwsIfOver maximumBufferSize: Int + ) async throws(AsyncSequenceReaderError) -> [Element]? where Element: Equatable { + try await collect(upToExcluding: [termination], throwsIfOver: maximumBufferSize) + } + + /// Collect elements into a sequence until the termination sequence is encountered, and return them as an array, excluding the termination sequence. + /// + /// If the termination sequence was not detected before the end of the stream, or more than the specified maximum elements are read, an error will be thrown. + /// + /// In this example, an asynchronous sequence of Characters represents a list of words. + /// + /// The closure provided to the `iteratorMap(_:)` reads characters up to and inluding the termination provided, splitting the sequence into an array of words. + /// + /// ```swift + /// let dataStream = ... // "apple orange banana kiwi kumquat pear pineapple " + /// + /// let wordStream = dataStream.iteratorMap { iterator -> String? in + /// (try await iterator.collect(upToExcluding: [" "], throwsIfOver: 100)) + /// .map { String($0) } + /// } + /// + /// for await word in wordStream { + /// print("\"\(word)\"", terminator: ", ") + /// } + /// // Prints: "apple", "orange", "banana", "kiwi", "kumquat", "pear", "pineapple", + /// ``` + /// + /// - Parameter termination: The sequence of elements marking the end of the sequence that will be collected. + /// - Parameter throwsIfOver: The maximum amount of elements that will be read before an error is thrown if a termination is not detected. + /// - Returns: An array of the collected elements, or `nil` if the sequence was already finished. + /// - Throws: ``AsyncSequenceReaderError/terminationNotFound(maximum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + public mutating func collect( + upToExcluding termination: some Collection, + throwsIfOver maximumBufferSize: Int + ) async throws(AsyncSequenceReaderError) -> [Element]? where Element: Equatable { + try await collect(upToIncluding: termination, throwsIfOver: maximumBufferSize)?.dropLast(termination.count) + } +} + extension AsyncIteratorProtocol { /// Collect elements into a sequence until the termination sequence is encountered, and transform it using the provided closure. diff --git a/Tests/AsyncSequenceReaderTests/AsyncReadUpToCountSequenceTests.swift b/Tests/AsyncSequenceReaderTests/AsyncReadUpToCountSequenceTests.swift index bfec4aa..0762876 100644 --- a/Tests/AsyncSequenceReaderTests/AsyncReadUpToCountSequenceTests.swift +++ b/Tests/AsyncSequenceReaderTests/AsyncReadUpToCountSequenceTests.swift @@ -221,7 +221,7 @@ import Testing }.reduce(into: [], { $0.append($1) }) } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:186: Assertion failed: count must be larger than 0") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:232: Assertion failed: count must be larger than 0") #endif } @@ -236,7 +236,7 @@ import Testing }.reduce(into: [], { $0.append($1) }) } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:254: Precondition failed: minCount must be larger than or equal to 0") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:300: Precondition failed: minCount must be larger than or equal to 0") #endif } @@ -251,7 +251,7 @@ import Testing }.reduce(into: [], { $0.append($1) }) } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:254: Precondition failed: minCount must be larger than or equal to 0") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:300: Precondition failed: minCount must be larger than or equal to 0") #endif } @@ -266,7 +266,7 @@ import Testing }.reduce(into: [], { $0.append($1) }) } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:253: Precondition failed: maxCount must be larger than or equal to minCount") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:299: Precondition failed: maxCount must be larger than or equal to minCount") #endif } @@ -280,7 +280,7 @@ import Testing } } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:94: Assertion failed: count must be larger than or equal to 0") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:140: Assertion failed: count must be larger than or equal to 0") #endif } @@ -306,7 +306,7 @@ import Testing try await sequence.reduce(into: "") { $0 += ($0.isEmpty ? "" : " ") + String($1) } } } - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:142: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:188: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") #endif } @@ -320,7 +320,7 @@ import Testing } } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:142: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:188: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") #endif } @@ -346,7 +346,7 @@ import Testing } } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:142: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:188: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") #endif } @@ -372,7 +372,7 @@ import Testing } } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:142: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:188: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") #endif } diff --git a/Tests/AsyncSequenceReaderTests/AsyncReadUpToElementsSequenceTests.swift b/Tests/AsyncSequenceReaderTests/AsyncReadUpToElementsSequenceTests.swift index 66a0d4b..71b564b 100644 --- a/Tests/AsyncSequenceReaderTests/AsyncReadUpToElementsSequenceTests.swift +++ b/Tests/AsyncSequenceReaderTests/AsyncReadUpToElementsSequenceTests.swift @@ -363,7 +363,7 @@ import Testing } } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:350: Precondition failed: termination must not be empty") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:502: Precondition failed: termination must not be empty") #endif } @@ -431,7 +431,7 @@ import Testing } } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:350: Precondition failed: termination must not be empty") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:502: Precondition failed: termination must not be empty") #endif } } From fe037289316126f8ebe2909cf42b3deca1d4122c Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Sun, 3 May 2026 06:45:29 -0700 Subject: [PATCH 4/5] Fixed issues where a different test path was used on Linux --- .../AsyncReadUpToCountSequenceTests.swift | 16 ++++++++++++++++ .../AsyncReadUpToElementsSequenceTests.swift | 8 ++++++++ 2 files changed, 24 insertions(+) diff --git a/Tests/AsyncSequenceReaderTests/AsyncReadUpToCountSequenceTests.swift b/Tests/AsyncSequenceReaderTests/AsyncReadUpToCountSequenceTests.swift index 0762876..665dc6b 100644 --- a/Tests/AsyncSequenceReaderTests/AsyncReadUpToCountSequenceTests.swift +++ b/Tests/AsyncSequenceReaderTests/AsyncReadUpToCountSequenceTests.swift @@ -292,7 +292,11 @@ import Testing _ = try await iterator.collect(-1) } #if DEBUG + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:18: Assertion failed: count must be larger than or equal to 0") + #else + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:64: Assertion failed: count must be larger than or equal to 0") + #endif #endif } @@ -332,7 +336,11 @@ import Testing _ = try await iterator.collect(min: -1, max: 1) } #if DEBUG + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:31: Precondition failed: minCount must be larger than or equal to 0") + #else + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:77: Precondition failed: minCount must be larger than or equal to 0") + #endif #endif } @@ -358,7 +366,11 @@ import Testing _ = try await iterator.collect(min: -1, max: -1) } #if DEBUG + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:31: Precondition failed: minCount must be larger than or equal to 0") + #else + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:77: Precondition failed: minCount must be larger than or equal to 0") + #endif #endif } @@ -384,7 +396,11 @@ import Testing _ = try await iterator.collect(min: 0, max: -1) } #if DEBUG + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:30: Precondition failed: maxCount must be larger than or equal to minCount") + #else + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:76: Precondition failed: maxCount must be larger than or equal to minCount") + #endif #endif } diff --git a/Tests/AsyncSequenceReaderTests/AsyncReadUpToElementsSequenceTests.swift b/Tests/AsyncSequenceReaderTests/AsyncReadUpToElementsSequenceTests.swift index 71b564b..5b99117 100644 --- a/Tests/AsyncSequenceReaderTests/AsyncReadUpToElementsSequenceTests.swift +++ b/Tests/AsyncSequenceReaderTests/AsyncReadUpToElementsSequenceTests.swift @@ -158,7 +158,11 @@ import Testing let _ = try await inputSequence.collect(upToIncluding: "", throwsIfOver: 10) } #if DEBUG + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:74: Precondition failed: termination must not be empty") + #else + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:226: Precondition failed: termination must not be empty") + #endif #endif } @@ -309,7 +313,11 @@ import Testing let _ = try await inputSequence.collect(upToExcluding: "", throwsIfOver: 10) } #if DEBUG + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:74: Precondition failed: termination must not be empty") + #else + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:226: Precondition failed: termination must not be empty") + #endif #endif } From 296f11287dd3444669d65781b9a2471bf8cf52b7 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Sun, 3 May 2026 23:29:53 -0700 Subject: [PATCH 5/5] Fixed a compiler crash on Swift 6.1.3 --- .../AsyncReadUpToCountSequence.swift | 31 ++++++ .../AsyncReadUpToElementsSequence.swift | 101 +++++++++++++++--- .../AsyncReadUpToCountSequenceTests.swift | 34 +++--- .../AsyncReadUpToElementsSequenceTests.swift | 12 +-- 4 files changed, 141 insertions(+), 37 deletions(-) diff --git a/Sources/AsyncSequenceReader/AsyncReadUpToCountSequence.swift b/Sources/AsyncSequenceReader/AsyncReadUpToCountSequence.swift index f93542d..810bc85 100644 --- a/Sources/AsyncSequenceReader/AsyncReadUpToCountSequence.swift +++ b/Sources/AsyncSequenceReader/AsyncReadUpToCountSequence.swift @@ -14,6 +14,7 @@ extension AsyncIteratorProtocol { /// - Parameter count: The number of elements to collect. /// - Returns: A collection with exactly `count` elements, or `nil` if the sequence is finished. /// - Throws: ``AsyncSequenceReaderError/insufficientElements(minimum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable public mutating func collect(_ count: Int) async throws -> [Element]? { assert(count >= 0, "count must be larger than or equal to 0") return try await collect(min: count, max: count) @@ -26,6 +27,7 @@ extension AsyncIteratorProtocol { /// - Parameter maxCount: The maximum number of elements to collect. /// - Returns: A collection with at least `minCount` and at most `maxCount` elements, or `nil` if the sequence is finished. /// - Throws: ``AsyncSequenceReaderError/insufficientElements(minimum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable public mutating func collect(min minCount: Int = 0, max maxCount: Int) async throws -> [Element]? { precondition(minCount <= maxCount, "maxCount must be larger than or equal to minCount") precondition(minCount >= 0, "minCount must be larger than or equal to 0") @@ -60,9 +62,33 @@ extension AsyncIteratorProtocol where Failure == Never { /// - Parameter count: The number of elements to collect. /// - Returns: A collection with exactly `count` elements, or `nil` if the sequence is finished. /// - Throws: ``AsyncSequenceReaderError/insufficientElements(minimum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable public mutating func collect(_ count: Int) async throws(AsyncSequenceReaderError) -> [Element]? { assert(count >= 0, "count must be larger than or equal to 0") + #if compiler(<6.1.3) || compiler(>=6.2) return try await collect(min: count, max: count) + #else /// The above crashes the Swift 6.1.3 compiler. Make sure to inline it manually: + if count == 0 { return [] } + + var result = [Element]() + result.reserveCapacity(count) + + while let next = await _nextIsolated() { + result.append(next) + + if result.count == count { + return result + } + } + + guard !result.isEmpty else { return nil } + + guard result.count >= count else { + throw AsyncSequenceReaderError.insufficientElements(minimum: count, actual: result.count) + } + + return result + #endif } /// Asynchronously advances by the specified minimum number of elements, continuing until the specified maximum number of elements, or ends the sequence if there is no next element. @@ -72,6 +98,7 @@ extension AsyncIteratorProtocol where Failure == Never { /// - Parameter maxCount: The maximum number of elements to collect. /// - Returns: A collection with at least `minCount` and at most `maxCount` elements, or `nil` if the sequence is finished. /// - Throws: ``AsyncSequenceReaderError/insufficientElements(minimum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable public mutating func collect(min minCount: Int = 0, max maxCount: Int) async throws(AsyncSequenceReaderError) -> [Element]? { precondition(minCount <= maxCount, "maxCount must be larger than or equal to minCount") precondition(minCount >= 0, "minCount must be larger than or equal to 0") @@ -130,6 +157,7 @@ extension AsyncIteratorProtocol { /// - Parameter sequenceTransform: A transformation that accepts a sequence of the specified size that can be read from, or stopped prematurely by returning early. The receiving iterator will have moved forward by the same amount of items consumed within `sequenceTransform`. /// - Returns: A transformed value as returned by `sequenceTransform`, or `nil` if the sequence was already finished. /// - Throws: ``AsyncSequenceReaderError/insufficientElements(minimum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable public mutating func collect< Transformed, TransformFailure @@ -175,6 +203,7 @@ extension AsyncIteratorProtocol { /// - Parameter sequenceTransform: A transformation that accepts a sequence of the specified size that can be read from, or stopped prematurely by returning early. The receiving iterator will have moved forward by the same amount of items consumed within `sequenceTransform`. /// - Returns: A transformed value as returned by `sequenceTransform`, or `nil` if the sequence was already finished. /// - Throws: ``AsyncSequenceReaderError/insufficientElements(minimum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable public mutating func collect< Transformed, TransformFailure @@ -222,6 +251,7 @@ extension AsyncBufferedIterator { /// - Parameter sequenceTransform: A transformation that accepts a sequence of the specified size that can be read from, or stopped prematurely by returning early. The receiving iterator will have moved forward by the same amount of items consumed within `sequenceTransform`. /// - Returns: A transformed value as returned by `sequenceTransform`, or `nil` if the sequence was already finished. /// - Throws: ``AsyncSequenceReaderError/insufficientElements(minimum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable public mutating func collect< Transformed, TransformFailure @@ -265,6 +295,7 @@ extension AsyncBufferedIterator { /// - Parameter sequenceTransform: A transformation that accepts a sequence of the specified size that can be read from, or stopped prematurely by returning early. The receiving iterator will have moved forward by the same amount of items consumed within `sequenceTransform`. /// - Returns: A transformed value as returned by `sequenceTransform`, or `nil` if the sequence was already finished. /// - Throws: ``AsyncSequenceReaderError/insufficientElements(minimum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable public mutating func collect< Transformed, TransformFailure: Error diff --git a/Sources/AsyncSequenceReader/AsyncReadUpToElementsSequence.swift b/Sources/AsyncSequenceReader/AsyncReadUpToElementsSequence.swift index 6a4f6df..24cc90f 100644 --- a/Sources/AsyncSequenceReader/AsyncReadUpToElementsSequence.swift +++ b/Sources/AsyncSequenceReader/AsyncReadUpToElementsSequence.swift @@ -7,7 +7,7 @@ // async-sequence-reader-watermark: 7E20A9CAB0604E89B17C6747A34F00C0 // -extension AsyncIteratorProtocol { +extension AsyncIteratorProtocol where Element: Equatable { /// Collect elements into a sequence until the termination sequence is encountered, and return them as an array, including the termination sequence. /// /// If the termination sequence was not detected before the end of the stream, or more than the specified maximum elements are read, an error will be thrown. @@ -34,10 +34,11 @@ extension AsyncIteratorProtocol { /// - Parameter throwsIfOver: The maximum amount of elements that will be read before an error is thrown if a termination is not detected. /// - Returns: An array of the collected elements, or `nil` if the sequence was already finished. /// - Throws: ``AsyncSequenceReaderError/terminationNotFound(maximum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable public mutating func collect( upToIncluding termination: Element, throwsIfOver maximumBufferSize: Int - ) async throws -> [Element]? where Element: Equatable { + ) async throws -> [Element]? { try await collect(upToIncluding: [termination], throwsIfOver: maximumBufferSize) } @@ -67,10 +68,11 @@ extension AsyncIteratorProtocol { /// - Parameter throwsIfOver: The maximum amount of elements that will be read before an error is thrown if a termination is not detected. /// - Returns: An array of the collected elements, or `nil` if the sequence was already finished. /// - Throws: ``AsyncSequenceReaderError/terminationNotFound(maximum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable public mutating func collect( upToIncluding termination: some Collection, throwsIfOver maximumBufferSize: Int - ) async throws -> [Element]? where Element: Equatable { + ) async throws -> [Element]? { precondition(!termination.isEmpty, "termination must not be empty") var result = [Element]() @@ -117,10 +119,11 @@ extension AsyncIteratorProtocol { /// - Parameter throwsIfOver: The maximum amount of elements that will be read before an error is thrown if a termination is not detected. /// - Returns: An array of the collected elements, or `nil` if the sequence was already finished. /// - Throws: ``AsyncSequenceReaderError/terminationNotFound(maximum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable public mutating func collect( upToExcluding termination: Element, throwsIfOver maximumBufferSize: Int - ) async throws -> [Element]? where Element: Equatable { + ) async throws -> [Element]? { try await collect(upToExcluding: [termination], throwsIfOver: maximumBufferSize) } @@ -150,16 +153,17 @@ extension AsyncIteratorProtocol { /// - Parameter throwsIfOver: The maximum amount of elements that will be read before an error is thrown if a termination is not detected. /// - Returns: An array of the collected elements, or `nil` if the sequence was already finished. /// - Throws: ``AsyncSequenceReaderError/terminationNotFound(maximum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable public mutating func collect( upToExcluding termination: some Collection, throwsIfOver maximumBufferSize: Int - ) async throws -> [Element]? where Element: Equatable { + ) async throws -> [Element]? { try await collect(upToIncluding: termination, throwsIfOver: maximumBufferSize)?.dropLast(termination.count) } } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension AsyncIteratorProtocol where Failure == Never { +extension AsyncIteratorProtocol where Element: Equatable, Failure == Never { /// Collect elements into a sequence until the termination sequence is encountered, and return them as an array, including the termination sequence. /// /// If the termination sequence was not detected before the end of the stream, or more than the specified maximum elements are read, an error will be thrown. @@ -186,11 +190,33 @@ extension AsyncIteratorProtocol where Failure == Never { /// - Parameter throwsIfOver: The maximum amount of elements that will be read before an error is thrown if a termination is not detected. /// - Returns: An array of the collected elements, or `nil` if the sequence was already finished. /// - Throws: ``AsyncSequenceReaderError/terminationNotFound(maximum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable public mutating func collect( upToIncluding termination: Element, throwsIfOver maximumBufferSize: Int - ) async throws(AsyncSequenceReaderError) -> [Element]? where Element: Equatable { + ) async throws(AsyncSequenceReaderError) -> [Element]? { + #if compiler(<6.1.3) || compiler(>=6.2) try await collect(upToIncluding: [termination], throwsIfOver: maximumBufferSize) + #else /// The above crashes the Swift 6.1.3 compiler. Make sure to inline it manually: + let termination = [termination] + var result = [Element]() + + while let next = await _nextIsolated() { + if result.count == maximumBufferSize { + throw AsyncSequenceReaderError.terminationNotFound(maximum: maximumBufferSize, actual: result.count + 1) + } + + result.append(next) + + if result.suffix(termination.count).elementsEqual(termination) { + return result + } + } + + guard !result.isEmpty else { return nil } + + throw AsyncSequenceReaderError.terminationNotFound(maximum: maximumBufferSize, actual: result.count) + #endif } /// Collect elements into a sequence until the termination sequence is encountered, and return them as an array, including the termination sequence. @@ -219,10 +245,11 @@ extension AsyncIteratorProtocol where Failure == Never { /// - Parameter throwsIfOver: The maximum amount of elements that will be read before an error is thrown if a termination is not detected. /// - Returns: An array of the collected elements, or `nil` if the sequence was already finished. /// - Throws: ``AsyncSequenceReaderError/terminationNotFound(maximum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable public mutating func collect( upToIncluding termination: some Collection, throwsIfOver maximumBufferSize: Int - ) async throws(AsyncSequenceReaderError) -> [Element]? where Element: Equatable { + ) async throws(AsyncSequenceReaderError) -> [Element]? { precondition(!termination.isEmpty, "termination must not be empty") var result = [Element]() @@ -269,11 +296,33 @@ extension AsyncIteratorProtocol where Failure == Never { /// - Parameter throwsIfOver: The maximum amount of elements that will be read before an error is thrown if a termination is not detected. /// - Returns: An array of the collected elements, or `nil` if the sequence was already finished. /// - Throws: ``AsyncSequenceReaderError/terminationNotFound(maximum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable public mutating func collect( upToExcluding termination: Element, throwsIfOver maximumBufferSize: Int - ) async throws(AsyncSequenceReaderError) -> [Element]? where Element: Equatable { + ) async throws(AsyncSequenceReaderError) -> [Element]? { + #if compiler(<6.1.3) || compiler(>=6.2) try await collect(upToExcluding: [termination], throwsIfOver: maximumBufferSize) + #else /// The above crashes the Swift 6.1.3 compiler. Make sure to inline it manually: + let termination = [termination] + var result = [Element]() + + while let next = await _nextIsolated() { + if result.count == maximumBufferSize { + throw AsyncSequenceReaderError.terminationNotFound(maximum: maximumBufferSize, actual: result.count + 1) + } + + result.append(next) + + if result.suffix(termination.count).elementsEqual(termination) { + return result.dropLast(termination.count) + } + } + + guard !result.isEmpty else { return nil } + + throw AsyncSequenceReaderError.terminationNotFound(maximum: maximumBufferSize, actual: result.count) + #endif } /// Collect elements into a sequence until the termination sequence is encountered, and return them as an array, excluding the termination sequence. @@ -302,16 +351,36 @@ extension AsyncIteratorProtocol where Failure == Never { /// - Parameter throwsIfOver: The maximum amount of elements that will be read before an error is thrown if a termination is not detected. /// - Returns: An array of the collected elements, or `nil` if the sequence was already finished. /// - Throws: ``AsyncSequenceReaderError/terminationNotFound(maximum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable public mutating func collect( upToExcluding termination: some Collection, throwsIfOver maximumBufferSize: Int - ) async throws(AsyncSequenceReaderError) -> [Element]? where Element: Equatable { + ) async throws(AsyncSequenceReaderError) -> [Element]? { + #if compiler(<6.1.3) || compiler(>=6.2) try await collect(upToIncluding: termination, throwsIfOver: maximumBufferSize)?.dropLast(termination.count) + #else /// The above crashes the Swift 6.1.3 compiler. Make sure to inline it manually: + var result = [Element]() + + while let next = await _nextIsolated() { + if result.count == maximumBufferSize { + throw AsyncSequenceReaderError.terminationNotFound(maximum: maximumBufferSize, actual: result.count + 1) + } + + result.append(next) + + if result.suffix(termination.count).elementsEqual(termination) { + return result.dropLast(termination.count) + } + } + + guard !result.isEmpty else { return nil } + + throw AsyncSequenceReaderError.terminationNotFound(maximum: maximumBufferSize, actual: result.count) + #endif } } -extension AsyncIteratorProtocol { - +extension AsyncIteratorProtocol where Element: Equatable { /// Collect elements into a sequence until the termination sequence is encountered, and transform it using the provided closure. /// /// - Note: It is up to the caller to verify if the termination sequence was encountered or not, which can easily be done by checking `result.suffix(termination.count).elementsEqual(termination)`. @@ -344,13 +413,14 @@ extension AsyncIteratorProtocol { /// - Parameter termination: The element marking the end of the sequence the `sequenceTransform` closure will have access to. /// - Parameter sequenceTransform: A transformation that accepts a sequence containing elements up to the termination that can be read from, or stopped prematurely by returning early. The receiving iterator will have moved forward by the same amount of items consumed within `sequenceTransform`. /// - Returns: A transformed value as returned by `sequenceTransform`, or `nil` if the sequence was already finished. + @inlinable public mutating func collect< Transformed, TransformFailure: Error >( upToIncluding termination: Element, sequenceTransform: sending (AsyncReadUpToElementsSequence>) async throws(TransformFailure) -> Transformed - ) async throws(TransformFailure) -> Transformed? where Element: Equatable { + ) async throws(TransformFailure) -> Transformed? { try await collect(upToIncluding: [termination], sequenceTransform: sequenceTransform) } @@ -386,6 +456,7 @@ extension AsyncIteratorProtocol { /// - Parameter termination: The sequence of elements marking the end of the sequence the `sequenceTransform` closure will have access to. /// - Parameter sequenceTransform: A transformation that accepts a sequence containing elements up to the termination that can be read from, or stopped prematurely by returning early. The receiving iterator will have moved forward by the same amount of items consumed within `sequenceTransform`. /// - Returns: A transformed value as returned by `sequenceTransform`, or `nil` if the sequence was already finished. + @inlinable public mutating func collect< TerminationCollection: Collection, Transformed, @@ -393,7 +464,7 @@ extension AsyncIteratorProtocol { >( upToIncluding termination: TerminationCollection, sequenceTransform: sending (AsyncReadUpToElementsSequence) async throws(TransformFailure) -> Transformed - ) async throws(TransformFailure) -> Transformed? where Element: Equatable { + ) async throws(TransformFailure) -> Transformed? { try await transform(with: sequenceTransform) { .init($0, termination: termination) } } } @@ -431,6 +502,7 @@ extension AsyncBufferedIterator where Element: Equatable { /// - Parameter termination: The element marking the end of the sequence the `sequenceTransform` closure will have access to. /// - Parameter sequenceTransform: A transformation that accepts a sequence containing elements up to the termination that can be read from, or stopped prematurely by returning early. The receiving iterator will have moved forward by the same amount of items consumed within `sequenceTransform`. /// - Returns: A transformed value as returned by `sequenceTransform`, or `nil` if the sequence was already finished. + @inlinable public mutating func collect( upToIncluding termination: Element, sequenceTransform: sending (AsyncReadUpToElementsSequence>) async throws -> Transformed @@ -470,6 +542,7 @@ extension AsyncBufferedIterator where Element: Equatable { /// - Parameter termination: The sequence of elements marking the end of the sequence the `sequenceTransform` closure will have access to. /// - Parameter sequenceTransform: A transformation that accepts a sequence containing elements up to the termination that can be read from, or stopped prematurely by returning early. The receiving iterator will have moved forward by the same amount of items consumed within `sequenceTransform`. /// - Returns: A transformed value as returned by `sequenceTransform`, or `nil` if the sequence was already finished. + @inlinable public mutating func collect< TerminationCollection: Collection, Transformed diff --git a/Tests/AsyncSequenceReaderTests/AsyncReadUpToCountSequenceTests.swift b/Tests/AsyncSequenceReaderTests/AsyncReadUpToCountSequenceTests.swift index 665dc6b..9712099 100644 --- a/Tests/AsyncSequenceReaderTests/AsyncReadUpToCountSequenceTests.swift +++ b/Tests/AsyncSequenceReaderTests/AsyncReadUpToCountSequenceTests.swift @@ -221,7 +221,7 @@ import Testing }.reduce(into: [], { $0.append($1) }) } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:232: Assertion failed: count must be larger than 0") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:262: Assertion failed: count must be larger than 0") #endif } @@ -236,7 +236,7 @@ import Testing }.reduce(into: [], { $0.append($1) }) } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:300: Precondition failed: minCount must be larger than or equal to 0") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:331: Precondition failed: minCount must be larger than or equal to 0") #endif } @@ -251,7 +251,7 @@ import Testing }.reduce(into: [], { $0.append($1) }) } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:300: Precondition failed: minCount must be larger than or equal to 0") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:331: Precondition failed: minCount must be larger than or equal to 0") #endif } @@ -266,7 +266,7 @@ import Testing }.reduce(into: [], { $0.append($1) }) } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:299: Precondition failed: maxCount must be larger than or equal to minCount") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:330: Precondition failed: maxCount must be larger than or equal to minCount") #endif } @@ -280,7 +280,7 @@ import Testing } } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:140: Assertion failed: count must be larger than or equal to 0") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:168: Assertion failed: count must be larger than or equal to 0") #endif } @@ -293,9 +293,9 @@ import Testing } #if DEBUG #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:18: Assertion failed: count must be larger than or equal to 0") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:19: Assertion failed: count must be larger than or equal to 0") #else - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:64: Assertion failed: count must be larger than or equal to 0") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:67: Assertion failed: count must be larger than or equal to 0") #endif #endif } @@ -310,7 +310,7 @@ import Testing try await sequence.reduce(into: "") { $0 += ($0.isEmpty ? "" : " ") + String($1) } } } - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:188: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:217: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") #endif } @@ -324,7 +324,7 @@ import Testing } } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:188: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:217: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") #endif } @@ -337,9 +337,9 @@ import Testing } #if DEBUG #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:31: Precondition failed: minCount must be larger than or equal to 0") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:33: Precondition failed: minCount must be larger than or equal to 0") #else - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:77: Precondition failed: minCount must be larger than or equal to 0") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:104: Precondition failed: minCount must be larger than or equal to 0") #endif #endif } @@ -354,7 +354,7 @@ import Testing } } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:188: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:217: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") #endif } @@ -367,9 +367,9 @@ import Testing } #if DEBUG #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:31: Precondition failed: minCount must be larger than or equal to 0") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:33: Precondition failed: minCount must be larger than or equal to 0") #else - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:77: Precondition failed: minCount must be larger than or equal to 0") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:104: Precondition failed: minCount must be larger than or equal to 0") #endif #endif } @@ -384,7 +384,7 @@ import Testing } } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:188: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:217: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") #endif } @@ -397,9 +397,9 @@ import Testing } #if DEBUG #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:30: Precondition failed: maxCount must be larger than or equal to minCount") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:32: Precondition failed: maxCount must be larger than or equal to minCount") #else - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:76: Precondition failed: maxCount must be larger than or equal to minCount") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:103: Precondition failed: maxCount must be larger than or equal to minCount") #endif #endif } diff --git a/Tests/AsyncSequenceReaderTests/AsyncReadUpToElementsSequenceTests.swift b/Tests/AsyncSequenceReaderTests/AsyncReadUpToElementsSequenceTests.swift index 5b99117..c8d359f 100644 --- a/Tests/AsyncSequenceReaderTests/AsyncReadUpToElementsSequenceTests.swift +++ b/Tests/AsyncSequenceReaderTests/AsyncReadUpToElementsSequenceTests.swift @@ -159,9 +159,9 @@ import Testing } #if DEBUG #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:74: Precondition failed: termination must not be empty") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:76: Precondition failed: termination must not be empty") #else - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:226: Precondition failed: termination must not be empty") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:253: Precondition failed: termination must not be empty") #endif #endif } @@ -314,9 +314,9 @@ import Testing } #if DEBUG #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:74: Precondition failed: termination must not be empty") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:76: Precondition failed: termination must not be empty") #else - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:226: Precondition failed: termination must not be empty") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:253: Precondition failed: termination must not be empty") #endif #endif } @@ -371,7 +371,7 @@ import Testing } } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:502: Precondition failed: termination must not be empty") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:575: Precondition failed: termination must not be empty") #endif } @@ -439,7 +439,7 @@ import Testing } } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:502: Precondition failed: termination must not be empty") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:575: Precondition failed: termination must not be empty") #endif } }