From 0f18ed3bff918b49ed6702bd29c2d3b664e06740 Mon Sep 17 00:00:00 2001 From: chingor13 Date: Mon, 17 Aug 2026 20:42:11 +0000 Subject: [PATCH 1/4] feat: download with stream of data create internal type to wrap AsyncHttpClient body type skip checksumming for now remove checksumming stuff for now --- .../GoogleCloudGax/HTTPClientResponse.swift | 32 ++++++++ .../GoogleCloudStorage/DownloadOptions.swift | 73 ++++++++++++++++--- .../StorageClient+Download.swift | 18 +---- .../StorageClientOptions.swift | 7 +- .../storage/Tests/DownloadOptionsTests.swift | 9 +-- packages/storage/Tests/DownloadTests.swift | 46 +++++++++++- packages/storage/Tests/UrlMocks.swift | 28 ++++++- 7 files changed, 180 insertions(+), 33 deletions(-) diff --git a/packages/gax/Sources/GoogleCloudGax/HTTPClientResponse.swift b/packages/gax/Sources/GoogleCloudGax/HTTPClientResponse.swift index 39ad1ecc5..9bb6a5871 100644 --- a/packages/gax/Sources/GoogleCloudGax/HTTPClientResponse.swift +++ b/packages/gax/Sources/GoogleCloudGax/HTTPClientResponse.swift @@ -19,6 +19,34 @@ import struct NIOHTTP1.HTTPHeaders import enum NIOHTTP1.HTTPResponseStatus import struct NIOCore.ByteBuffer +/// An asynchronous sequence of response body chunks. +@_spi(GoogleCloudInternal) public struct _HTTPResponseBody: AsyncSequence, Sendable { + public typealias Element = Data + + let body: AsyncHTTPClient.HTTPClientResponse.Body + + public init(_ body: AsyncHTTPClient.HTTPClientResponse.Body) { + self.body = body + } + + public struct AsyncIterator: AsyncIteratorProtocol { + var iterator: AsyncHTTPClient.HTTPClientResponse.Body.AsyncIterator + + public init(_ iterator: AsyncHTTPClient.HTTPClientResponse.Body.AsyncIterator) { + self.iterator = iterator + } + + public mutating func next() async throws -> Data? { + guard let buffer = try await self.iterator.next() else { return nil } + return Data(buffer: buffer) + } + } + + public func makeAsyncIterator() -> AsyncIterator { + AsyncIterator(self.body.makeAsyncIterator()) + } +} + /// Represents an HTTP response. /// /// The generated code uses this type directly. It exposes the methods we @@ -43,6 +71,10 @@ import struct NIOCore.ByteBuffer self.response.headers } + public var body: _HTTPResponseBody { + _HTTPResponseBody(self.response.body) + } + public func data(upTo: Int) async throws -> Data { let buffer = try await self.response.body.collect(upTo: upTo) return Data(buffer: buffer) diff --git a/packages/storage/Sources/GoogleCloudStorage/DownloadOptions.swift b/packages/storage/Sources/GoogleCloudStorage/DownloadOptions.swift index f9bd80f6d..98556d58e 100644 --- a/packages/storage/Sources/GoogleCloudStorage/DownloadOptions.swift +++ b/packages/storage/Sources/GoogleCloudStorage/DownloadOptions.swift @@ -13,6 +13,7 @@ // limitations under the License. import Foundation +@_spi(GoogleCloudInternal) import GoogleCloudGax /// Specifies a byte range for ranged reads. public enum ReadObjectRange: Sendable, Hashable, Equatable { @@ -168,7 +169,7 @@ public struct ReadObjectOptions: Sendable { /// Flag to enable automatic decompressive transcoding by GCS. Defaults to `true`. public var enableDecompressiveTranscoding: Bool = true - /// Configuration options for download checksum validation. + /// Checksum options for validating data integrity. public var checksums: ChecksumOptions = .default /// Flag to enable transparent auto-resumption on transient network failures. Defaults to `true`. @@ -253,7 +254,11 @@ public struct ReadObjectSequence: AsyncSequence, Sendable { /// Configuration options used for this object download. public var options: ReadObjectOptions = .init() - internal var stream: AsyncThrowingStream = AsyncThrowingStream { $0.finish() } + /// Object metadata extracted from initial HTTP response headers. + public var metadata: ReadObjectMetadata = .init() + + package var initialBody: _HTTPResponseBody? + package var stream: AsyncThrowingStream? /// Creates a new `ReadObjectSequence` instance. public init() {} @@ -269,25 +274,75 @@ public struct ReadObjectSequence: AsyncSequence, Sendable { public struct AsyncIterator: AsyncIteratorProtocol, Sendable { public typealias Element = Data - private struct Storage: @unchecked Sendable { - var iterator: AsyncThrowingStream.AsyncIterator + package final class Storage: @unchecked Sendable { + let options: ReadObjectOptions + var bodyIterator: _HTTPResponseBody.AsyncIterator? + var streamIterator: AsyncThrowingStream.AsyncIterator? + var isFinished: Bool = false + + init( + options: ReadObjectOptions, + initialBody: _HTTPResponseBody?, + stream: AsyncThrowingStream? + ) { + self.options = options + self.bodyIterator = initialBody?.makeAsyncIterator() + self.streamIterator = stream?.makeAsyncIterator() + } + + func next() async throws -> Data? { + guard !isFinished else { return nil } + + if case .prefix(0) = options.range { + isFinished = true + return nil + } + if case .suffix(0) = options.range { + isFinished = true + return nil + } + + if var it = streamIterator { + let chunk = try await it.next() + self.streamIterator = it + if chunk == nil { + isFinished = true + } + return chunk + } else if var it = bodyIterator { + let chunk = try await it.next() + self.bodyIterator = it + if chunk == nil { + isFinished = true + } + return chunk + } else { + isFinished = true + return nil + } + } } - private var storage: Storage + package var storage: Storage - internal init(iterator: AsyncThrowingStream.AsyncIterator) { - self.storage = Storage(iterator: iterator) + package init(storage: Storage) { + self.storage = storage } /// Advances to the next `Data` chunk in the downloaded object payload stream. public mutating func next() async throws -> Data? { - try await storage.iterator.next() + try await storage.next() } } /// Creates an asynchronous iterator for iterating over object payload chunks. public func makeAsyncIterator() -> AsyncIterator { - AsyncIterator(iterator: stream.makeAsyncIterator()) + let storage = AsyncIterator.Storage( + options: options, + initialBody: initialBody, + stream: stream + ) + return AsyncIterator(storage: storage) } } diff --git a/packages/storage/Sources/GoogleCloudStorage/StorageClient+Download.swift b/packages/storage/Sources/GoogleCloudStorage/StorageClient+Download.swift index a9d1bda38..8b066bf65 100644 --- a/packages/storage/Sources/GoogleCloudStorage/StorageClient+Download.swift +++ b/packages/storage/Sources/GoogleCloudStorage/StorageClient+Download.swift @@ -40,9 +40,9 @@ extension StorageClient { bucket: bucket, object: object, options: options) let response = try await request.execute() let statusCode = Int(response.status.code) - let data = try await response.data() guard (200..<300).contains(statusCode) else { + let data = try await response.data() let message = String(data: data, encoding: .utf8) ?? "" throw DownloadError.unexpectedServerResponse( statusCode: statusCode, message: message) @@ -51,22 +51,12 @@ extension StorageClient { let metadata = try Self.parseReadObjectMetadata( from: response.headers, bucket: bucket, object: object) - let stream = AsyncThrowingStream { continuation in - if case .prefix(0) = options.range { - // Requested 0 bytes - } else if case .suffix(0) = options.range { - // Requested 0 bytes - } else if !data.isEmpty { - continuation.yield(data) - } - continuation.finish() - } - let sequence = ReadObjectSequence().with { $0.bucket = bucket $0.object = object $0.options = options - $0.stream = stream + $0.metadata = metadata + $0.initialBody = response.body } return ReadObjectResult().with { $0.metadata = metadata @@ -169,7 +159,7 @@ extension StorageClient { } extension GoogleCloudGax._HTTPClient { - fileprivate func buildReadObjectRequest( + package func buildReadObjectRequest( bucket: String, object: String, options: ReadObjectOptions diff --git a/packages/storage/Sources/GoogleCloudStorage/StorageClientOptions.swift b/packages/storage/Sources/GoogleCloudStorage/StorageClientOptions.swift index cc074c4b3..b36893a36 100644 --- a/packages/storage/Sources/GoogleCloudStorage/StorageClientOptions.swift +++ b/packages/storage/Sources/GoogleCloudStorage/StorageClientOptions.swift @@ -22,12 +22,17 @@ public struct StorageClientOptions: Sendable { /// Default configuration inherited by data-plane operations (e.g., Uploads). public var upload: UploadOptions + /// Default configuration inherited by data-plane download operations (e.g., ReadObject). + public var download: ReadObjectOptions + public init( client: GoogleCloudGax.ClientOptions = .init(), - upload: UploadOptions = .default + upload: UploadOptions = .default, + download: ReadObjectOptions = .default ) { self.client = client self.upload = upload + self.download = download } /// Override specific values using closure modification. diff --git a/packages/storage/Tests/DownloadOptionsTests.swift b/packages/storage/Tests/DownloadOptionsTests.swift index be9763997..8a68daf8c 100644 --- a/packages/storage/Tests/DownloadOptionsTests.swift +++ b/packages/storage/Tests/DownloadOptionsTests.swift @@ -35,8 +35,8 @@ import Testing #expect(defaultOptions.customerEncryptionKey == nil) #expect(defaultOptions.range == .entire) #expect(defaultOptions.enableDecompressiveTranscoding == true) - #expect(defaultOptions.checksums == .default) #expect(defaultOptions.autoResume == true) + #expect(defaultOptions.checksums == .default) } @Test func readObjectOptionsWithBuilder() throws { @@ -50,8 +50,8 @@ import Testing $0.customerEncryptionKey = csek $0.range = .bounded(start: 0, end: 1024) $0.enableDecompressiveTranscoding = false - $0.checksums = .none $0.autoResume = false + $0.checksums = .none } #expect(options.generation == 456) @@ -59,8 +59,8 @@ import Testing #expect(options.customerEncryptionKey == csek) #expect(options.range == .bounded(start: 0, end: 1024)) #expect(options.enableDecompressiveTranscoding == false) - #expect(options.checksums == .none) #expect(options.autoResume == false) + #expect(options.checksums == .none) } @Test func readObjectMetadataProperties() { @@ -101,16 +101,13 @@ import Testing $0.bucket = "bkt" $0.object = "obj" } - let options = ReadObjectOptions().with { $0.autoResume = false } let sequence = ReadObjectSequence().with { $0.bucket = "bkt" $0.object = "obj" - $0.options = options } #expect(sequence.bucket == "bkt") #expect(sequence.object == "obj") - #expect(sequence.options.autoResume == false) var iterator = sequence.makeAsyncIterator() let firstChunk = try await iterator.next() diff --git a/packages/storage/Tests/DownloadTests.swift b/packages/storage/Tests/DownloadTests.swift index beae82e10..195accfc2 100644 --- a/packages/storage/Tests/DownloadTests.swift +++ b/packages/storage/Tests/DownloadTests.swift @@ -51,7 +51,7 @@ import Testing "x-goog-generation": "17123456789", "x-goog-metageneration": "3", "ETag": "\"CPv1234\"", - "x-goog-hash": "crc32c=mcw+ng==, md5=N1YvABC==", + "x-goog-hash": "crc32c=AdiAvw==, md5=N1YvABC==", "x-goog-storage-class": "STANDARD", "Last-Modified": "Fri, 07 Aug 2026 01:00:00 GMT", ] @@ -70,7 +70,7 @@ import Testing #expect(result.metadata.generation == 17123456789) #expect(result.metadata.metageneration == 3) #expect(result.metadata.etag == "\"CPv1234\"") - #expect(result.metadata.crc32c == "mcw+ng==") + #expect(result.metadata.crc32c == "AdiAvw==") #expect(result.metadata.md5Hash == "N1YvABC==") #expect(result.metadata.contentType == "text/plain; charset=utf-8") #expect(result.metadata.storageClass == "STANDARD") @@ -714,4 +714,46 @@ import Testing } #expect(downloaded == payload) } + + @Test func downloadObjectStreaming() async throws { + let registry = MockRegistry.create() + let bucket = "test-bucket" + let objectName = "stream-object.bin" + let chunk1 = Data("Chunk-1-".utf8) + let chunk2 = Data("Chunk-2-".utf8) + let chunk3 = Data("Chunk-3".utf8) + let fullPayload = chunk1 + chunk2 + chunk3 + let computedCrc = _CRC32C.compute(fullPayload) + let crcBase64 = withUnsafeBytes(of: computedCrc.bigEndian) { Data($0).base64EncodedString() } + + let downloadUrl = registry.url("/storage/v1/b/\(bucket)/o/\(objectName)?alt=media") + let headers = [ + "Content-Type": "application/octet-stream", + "Content-Length": String(fullPayload.count), + "x-goog-generation": "999", + "x-goog-hash": "crc32c=\(crcBase64)", + ] + + registry.register( + response: .stream(statusCode: 200, chunks: [chunk1, chunk2, chunk3], headers: headers), + for: downloadUrl + ) + + let client = try makeClient(registry: registry) + let result = try await client.readObject(from: bucket, object: objectName) + + #expect(result.metadata.size == UInt64(fullPayload.count)) + #expect(result.metadata.generation == 999) + + var receivedChunks: [Data] = [] + for try await chunk in result.body { + receivedChunks.append(chunk) + } + + #expect(receivedChunks.count == 3) + #expect(receivedChunks[0] == chunk1) + #expect(receivedChunks[1] == chunk2) + #expect(receivedChunks[2] == chunk3) + #expect(receivedChunks.reduce(Data(), +) == fullPayload) + } } diff --git a/packages/storage/Tests/UrlMocks.swift b/packages/storage/Tests/UrlMocks.swift index c38c24e5f..cfabe9488 100644 --- a/packages/storage/Tests/UrlMocks.swift +++ b/packages/storage/Tests/UrlMocks.swift @@ -20,12 +20,15 @@ import Testing @_spi(GoogleCloudInternal) @testable import GoogleCloudGax @_spi(GoogleCloudInternal) @testable import GoogleCloudStorage -/// Defines what a mock response should return enum MockResponse: Sendable { case success(statusCode: Int, data: Data, headers: [String: String]? = nil) + case stream( + statusCode: Int, chunks: [Data], error: (any Error)? = nil, headers: [String: String]? = nil) case failure(any Error) } +struct MockNetworkError: Error, Sendable, Equatable {} + /// Recorded HTTP request during testing struct RecordedRequest: Sendable { let urlString: String @@ -241,6 +244,29 @@ final class MockRegistry: _HTTPClientProtocol, @unchecked Sendable { headers: nioHeaders, body: .bytes(NIOCore.ByteBuffer(data: data)) ) + case .stream(let statusCode, let chunks, let error, let headers): + var nioHeaders = NIOHTTP1.HTTPHeaders() + if let headers { + for (key, value) in headers { + nioHeaders.add(name: key, value: value) + } + } + let stream = AsyncThrowingStream { continuation in + for chunk in chunks { + continuation.yield(NIOCore.ByteBuffer(data: chunk)) + } + if let error { + continuation.finish(throwing: error) + } else { + continuation.finish() + } + } + return AsyncHTTPClient.HTTPClientResponse( + version: .http1_1, + status: NIOHTTP1.HTTPResponseStatus(statusCode: statusCode), + headers: nioHeaders, + body: .stream(stream) + ) case .failure(let error): throw error } From d54e3cc229fc7ee7d3cd00249a84c0242ae0dcf7 Mon Sep 17 00:00:00 2001 From: chingor13 Date: Mon, 17 Aug 2026 21:31:11 +0000 Subject: [PATCH 2/4] move _HTTPResponseBody to its own file --- .../GoogleCloudGax/HTTPClientResponse.swift | 28 ------------ .../GoogleCloudGax/HTTPResponseBody.swift | 44 +++++++++++++++++++ 2 files changed, 44 insertions(+), 28 deletions(-) create mode 100644 packages/gax/Sources/GoogleCloudGax/HTTPResponseBody.swift diff --git a/packages/gax/Sources/GoogleCloudGax/HTTPClientResponse.swift b/packages/gax/Sources/GoogleCloudGax/HTTPClientResponse.swift index 9bb6a5871..6ff02eea6 100644 --- a/packages/gax/Sources/GoogleCloudGax/HTTPClientResponse.swift +++ b/packages/gax/Sources/GoogleCloudGax/HTTPClientResponse.swift @@ -19,34 +19,6 @@ import struct NIOHTTP1.HTTPHeaders import enum NIOHTTP1.HTTPResponseStatus import struct NIOCore.ByteBuffer -/// An asynchronous sequence of response body chunks. -@_spi(GoogleCloudInternal) public struct _HTTPResponseBody: AsyncSequence, Sendable { - public typealias Element = Data - - let body: AsyncHTTPClient.HTTPClientResponse.Body - - public init(_ body: AsyncHTTPClient.HTTPClientResponse.Body) { - self.body = body - } - - public struct AsyncIterator: AsyncIteratorProtocol { - var iterator: AsyncHTTPClient.HTTPClientResponse.Body.AsyncIterator - - public init(_ iterator: AsyncHTTPClient.HTTPClientResponse.Body.AsyncIterator) { - self.iterator = iterator - } - - public mutating func next() async throws -> Data? { - guard let buffer = try await self.iterator.next() else { return nil } - return Data(buffer: buffer) - } - } - - public func makeAsyncIterator() -> AsyncIterator { - AsyncIterator(self.body.makeAsyncIterator()) - } -} - /// Represents an HTTP response. /// /// The generated code uses this type directly. It exposes the methods we diff --git a/packages/gax/Sources/GoogleCloudGax/HTTPResponseBody.swift b/packages/gax/Sources/GoogleCloudGax/HTTPResponseBody.swift new file mode 100644 index 000000000..96385674a --- /dev/null +++ b/packages/gax/Sources/GoogleCloudGax/HTTPResponseBody.swift @@ -0,0 +1,44 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation +import struct AsyncHTTPClient.HTTPClientResponse + +/// An asynchronous sequence of response body chunks. +@_spi(GoogleCloudInternal) public struct _HTTPResponseBody: AsyncSequence, Sendable { + public typealias Element = Data + + let body: AsyncHTTPClient.HTTPClientResponse.Body + + public init(_ body: AsyncHTTPClient.HTTPClientResponse.Body) { + self.body = body + } + + public struct AsyncIterator: AsyncIteratorProtocol { + var iterator: AsyncHTTPClient.HTTPClientResponse.Body.AsyncIterator + + public init(_ iterator: AsyncHTTPClient.HTTPClientResponse.Body.AsyncIterator) { + self.iterator = iterator + } + + public mutating func next() async throws -> Data? { + guard let buffer = try await self.iterator.next() else { return nil } + return Data(buffer: buffer) + } + } + + public func makeAsyncIterator() -> AsyncIterator { + AsyncIterator(self.body.makeAsyncIterator()) + } +} From 98d1e8eb0e66de930d6d88d9fb1f1c8a62237785 Mon Sep 17 00:00:00 2001 From: chingor13 Date: Mon, 17 Aug 2026 22:13:26 +0000 Subject: [PATCH 3/4] return NIOCore.ByteBuffer to avoid extra copies --- .../GoogleCloudGax/HTTPResponseBody.swift | 9 +++---- .../GoogleCloudStorage/DownloadOptions.swift | 19 +++++++------- packages/storage/Tests/DownloadTests.swift | 26 +++++++++---------- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/gax/Sources/GoogleCloudGax/HTTPResponseBody.swift b/packages/gax/Sources/GoogleCloudGax/HTTPResponseBody.swift index 96385674a..c4f4d9505 100644 --- a/packages/gax/Sources/GoogleCloudGax/HTTPResponseBody.swift +++ b/packages/gax/Sources/GoogleCloudGax/HTTPResponseBody.swift @@ -12,12 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Foundation import struct AsyncHTTPClient.HTTPClientResponse +import struct NIOCore.ByteBuffer /// An asynchronous sequence of response body chunks. @_spi(GoogleCloudInternal) public struct _HTTPResponseBody: AsyncSequence, Sendable { - public typealias Element = Data + public typealias Element = NIOCore.ByteBuffer let body: AsyncHTTPClient.HTTPClientResponse.Body @@ -32,9 +32,8 @@ import struct AsyncHTTPClient.HTTPClientResponse self.iterator = iterator } - public mutating func next() async throws -> Data? { - guard let buffer = try await self.iterator.next() else { return nil } - return Data(buffer: buffer) + public mutating func next() async throws -> NIOCore.ByteBuffer? { + try await self.iterator.next() } } diff --git a/packages/storage/Sources/GoogleCloudStorage/DownloadOptions.swift b/packages/storage/Sources/GoogleCloudStorage/DownloadOptions.swift index 98556d58e..2d8cebc57 100644 --- a/packages/storage/Sources/GoogleCloudStorage/DownloadOptions.swift +++ b/packages/storage/Sources/GoogleCloudStorage/DownloadOptions.swift @@ -14,6 +14,7 @@ import Foundation @_spi(GoogleCloudInternal) import GoogleCloudGax +import struct NIOCore.ByteBuffer /// Specifies a byte range for ranged reads. public enum ReadObjectRange: Sendable, Hashable, Equatable { @@ -241,9 +242,9 @@ public struct ReadObjectMetadata: Sendable, Hashable, Equatable { } } -/// An asynchronous sequence of `Data` chunks representing an object payload being downloaded. +/// An asynchronous sequence of `ByteBuffer` chunks representing an object payload being downloaded. public struct ReadObjectSequence: AsyncSequence, Sendable { - public typealias Element = Data + public typealias Element = NIOCore.ByteBuffer /// Name of the bucket containing the object being read. public var bucket: String = "" @@ -258,7 +259,7 @@ public struct ReadObjectSequence: AsyncSequence, Sendable { public var metadata: ReadObjectMetadata = .init() package var initialBody: _HTTPResponseBody? - package var stream: AsyncThrowingStream? + package var stream: AsyncThrowingStream? /// Creates a new `ReadObjectSequence` instance. public init() {} @@ -272,25 +273,25 @@ public struct ReadObjectSequence: AsyncSequence, Sendable { /// An asynchronous iterator for iterating over chunks of downloaded object payload data. public struct AsyncIterator: AsyncIteratorProtocol, Sendable { - public typealias Element = Data + public typealias Element = NIOCore.ByteBuffer package final class Storage: @unchecked Sendable { let options: ReadObjectOptions var bodyIterator: _HTTPResponseBody.AsyncIterator? - var streamIterator: AsyncThrowingStream.AsyncIterator? + var streamIterator: AsyncThrowingStream.AsyncIterator? var isFinished: Bool = false init( options: ReadObjectOptions, initialBody: _HTTPResponseBody?, - stream: AsyncThrowingStream? + stream: AsyncThrowingStream? ) { self.options = options self.bodyIterator = initialBody?.makeAsyncIterator() self.streamIterator = stream?.makeAsyncIterator() } - func next() async throws -> Data? { + func next() async throws -> NIOCore.ByteBuffer? { guard !isFinished else { return nil } if case .prefix(0) = options.range { @@ -329,8 +330,8 @@ public struct ReadObjectSequence: AsyncSequence, Sendable { self.storage = storage } - /// Advances to the next `Data` chunk in the downloaded object payload stream. - public mutating func next() async throws -> Data? { + /// Advances to the next `ByteBuffer` chunk in the downloaded object payload stream. + public mutating func next() async throws -> NIOCore.ByteBuffer? { try await storage.next() } } diff --git a/packages/storage/Tests/DownloadTests.swift b/packages/storage/Tests/DownloadTests.swift index 195accfc2..3a4064e09 100644 --- a/packages/storage/Tests/DownloadTests.swift +++ b/packages/storage/Tests/DownloadTests.swift @@ -78,7 +78,7 @@ import Testing var downloaded = Data() for try await chunk in result.body { - downloaded.append(chunk) + downloaded.append(contentsOf: chunk.readableBytesView) } #expect(downloaded == payload) } @@ -126,7 +126,7 @@ import Testing var downloaded = Data() for try await chunk in result.body { - downloaded.append(chunk) + downloaded.append(contentsOf: chunk.readableBytesView) } #expect(downloaded == payload) } @@ -224,7 +224,7 @@ import Testing var downloaded = Data() for try await chunk in result.body { - downloaded.append(chunk) + downloaded.append(contentsOf: chunk.readableBytesView) } #expect(downloaded == payload) } @@ -293,7 +293,7 @@ import Testing var downloaded = Data() for try await chunk in result.body { - downloaded.append(chunk) + downloaded.append(contentsOf: chunk.readableBytesView) } #expect(downloaded == payload) } @@ -360,7 +360,7 @@ import Testing var downloaded = Data() for try await chunk in result.body { - downloaded.append(chunk) + downloaded.append(contentsOf: chunk.readableBytesView) } #expect(downloaded == rangePayload) } @@ -400,7 +400,7 @@ import Testing var downloaded = Data() for try await chunk in result.body { - downloaded.append(chunk) + downloaded.append(contentsOf: chunk.readableBytesView) } #expect(downloaded == rangePayload) } @@ -440,7 +440,7 @@ import Testing var downloaded = Data() for try await chunk in result.body { - downloaded.append(chunk) + downloaded.append(contentsOf: chunk.readableBytesView) } #expect(downloaded == rangePayload) } @@ -480,7 +480,7 @@ import Testing var downloaded = Data() for try await chunk in result.body { - downloaded.append(chunk) + downloaded.append(contentsOf: chunk.readableBytesView) } #expect(downloaded == rangePayload) } @@ -560,7 +560,7 @@ import Testing var prefixData = Data() for try await chunk in prefixResult.body { - prefixData.append(chunk) + prefixData.append(contentsOf: chunk.readableBytesView) } #expect(prefixData.isEmpty) @@ -591,7 +591,7 @@ import Testing var suffixData = Data() for try await chunk in suffixResult.body { - suffixData.append(chunk) + suffixData.append(contentsOf: chunk.readableBytesView) } #expect(suffixData.isEmpty) @@ -666,7 +666,7 @@ import Testing var downloaded = Data() for try await chunk in result.body { - downloaded.append(chunk) + downloaded.append(contentsOf: chunk.readableBytesView) } #expect(downloaded == payload) } @@ -710,7 +710,7 @@ import Testing var downloaded = Data() for try await chunk in result.body { - downloaded.append(chunk) + downloaded.append(contentsOf: chunk.readableBytesView) } #expect(downloaded == payload) } @@ -747,7 +747,7 @@ import Testing var receivedChunks: [Data] = [] for try await chunk in result.body { - receivedChunks.append(chunk) + receivedChunks.append(Data(buffer: chunk)) } #expect(receivedChunks.count == 3) From 2ef7c48a74166837df3629821be89f079f7f719b Mon Sep 17 00:00:00 2001 From: chingor13 Date: Mon, 17 Aug 2026 23:00:44 +0000 Subject: [PATCH 4/4] fix integration tests --- packages/storage/Package.swift | 5 ++++- .../StorageClientIntegrationTests.swift | 19 ++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/storage/Package.swift b/packages/storage/Package.swift index b655a4b3f..f8f3f7f00 100644 --- a/packages/storage/Package.swift +++ b/packages/storage/Package.swift @@ -84,7 +84,10 @@ let package = Package( .testTarget( name: "GoogleCloudStorageIntegrationTests", dependencies: [ - "GoogleCloudStorage" + "GoogleCloudStorage", + .product(name: "GoogleCloudAuth", package: "auth"), + .product(name: "GoogleCloudGax", package: "gax"), + .product(name: "NIOCore", package: "swift-nio"), ], path: "Tests/IntegrationTests" ), diff --git a/packages/storage/Tests/IntegrationTests/StorageClientIntegrationTests.swift b/packages/storage/Tests/IntegrationTests/StorageClientIntegrationTests.swift index d169ee453..34cfc9660 100644 --- a/packages/storage/Tests/IntegrationTests/StorageClientIntegrationTests.swift +++ b/packages/storage/Tests/IntegrationTests/StorageClientIntegrationTests.swift @@ -14,6 +14,7 @@ import Foundation @testable import GoogleCloudStorage +import NIOCore import Testing #if IntegrationTests @@ -76,7 +77,7 @@ import Testing var downloadedData = Data() for try await chunk in result.body { - downloadedData.append(chunk) + downloadedData.append(contentsOf: chunk.readableBytesView) } #expect(downloadedData == data) let downloadedString = String(data: downloadedData, encoding: .utf8) @@ -145,7 +146,7 @@ import Testing var downloadedData = Data() for try await chunk in result.body { - downloadedData.append(chunk) + downloadedData.append(contentsOf: chunk.readableBytesView) } #expect(downloadedData == data) let downloadedString = String(data: downloadedData, encoding: .utf8) @@ -534,7 +535,7 @@ import Testing var downloadedData = Data() for try await chunk in result.body { - downloadedData.append(chunk) + downloadedData.append(contentsOf: chunk.readableBytesView) } #expect(downloadedData == data) let downloadedString = String(data: downloadedData, encoding: .utf8) @@ -607,7 +608,7 @@ import Testing var downloadedData = Data() for try await chunk in result.body { - downloadedData.append(chunk) + downloadedData.append(contentsOf: chunk.readableBytesView) } #expect(downloadedData == data) @@ -771,7 +772,7 @@ import Testing var downloadedData = Data() for try await chunk in result.body { - downloadedData.append(chunk) + downloadedData.append(contentsOf: chunk.readableBytesView) } // GCS serves uncompressed raw content #expect(downloadedData == Self.rawGzipData) @@ -812,7 +813,7 @@ import Testing var downloadedData = Data() for try await chunk in result.body { - downloadedData.append(chunk) + downloadedData.append(contentsOf: chunk.readableBytesView) } // Downloader receives the original gzip-compressed file #expect(downloadedData == Self.compressedGzipData) @@ -852,7 +853,7 @@ import Testing var downloadedData = Data() for try await chunk in result.body { - downloadedData.append(chunk) + downloadedData.append(contentsOf: chunk.readableBytesView) } // Downloader receives the original gzip-compressed file because of Cache-Control: no-transform #expect(downloadedData == Self.compressedGzipData) @@ -921,7 +922,7 @@ import Testing var downloadedData = Data() for try await chunk in result.body { - downloadedData.append(chunk) + downloadedData.append(contentsOf: chunk.readableBytesView) } let downloadedString = String(data: downloadedData, encoding: .utf8) ?? "" #expect(downloadedString == expectedContent) @@ -941,7 +942,7 @@ import Testing var downloadedData = Data() for try await chunk in result.body { - downloadedData.append(chunk) + downloadedData.append(contentsOf: chunk.readableBytesView) } #expect(downloadedData.isEmpty) }