From d537c1e5e0b953189de9f0319abd1d2c89442199 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Mon, 22 Sep 2025 11:41:44 -0400 Subject: [PATCH 1/2] Revert `MockURLProtocol` changes and inline JSON tests --- .../Unit/GenerativeModelGoogleAITests.swift | 21 ------- .../Unit/GenerativeModelVertexAITests.swift | 21 ------- FirebaseAI/Tests/Unit/MockURLProtocol.swift | 56 +++++++------------ .../Types/GenerateContentResponseTests.swift | 19 +++++++ 4 files changed, 40 insertions(+), 77 deletions(-) diff --git a/FirebaseAI/Tests/Unit/GenerativeModelGoogleAITests.swift b/FirebaseAI/Tests/Unit/GenerativeModelGoogleAITests.swift index 3ab32724ca4..14944dae5e3 100644 --- a/FirebaseAI/Tests/Unit/GenerativeModelGoogleAITests.swift +++ b/FirebaseAI/Tests/Unit/GenerativeModelGoogleAITests.swift @@ -384,27 +384,6 @@ final class GenerativeModelGoogleAITests: XCTestCase { XCTAssertEqual(errorURLMetadata.retrievalStatus, .error) } - func testGenerateContent_success_urlContext_emptyURLMetadata() async throws { - let json = """ - { - "candidates": [ - { - "content": { "role": "model", "parts": [ { "text": "Some text." } ] }, - "finishReason": "STOP", - "urlContextMetadata": { "urlMetadata": [] } - } - ] - } - """ - MockURLProtocol.requestHandler = nil - MockURLProtocol.dataRequestHandler = try GenerativeModelTestUtil.httpRequestHandler(json: json) - - let response = try await model.generateContent(testPrompt) - - let candidate = try XCTUnwrap(response.candidates.first) - XCTAssertNil(candidate.urlContextMetadata) - } - func testGenerateContent_failure_invalidAPIKey() async throws { let expectedStatusCode = 400 MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler( diff --git a/FirebaseAI/Tests/Unit/GenerativeModelVertexAITests.swift b/FirebaseAI/Tests/Unit/GenerativeModelVertexAITests.swift index acae82be7e5..b0a23c0a164 100644 --- a/FirebaseAI/Tests/Unit/GenerativeModelVertexAITests.swift +++ b/FirebaseAI/Tests/Unit/GenerativeModelVertexAITests.swift @@ -556,27 +556,6 @@ final class GenerativeModelVertexAITests: XCTestCase { XCTAssertEqual(urlMetadata.retrievalStatus, .error) } - func testGenerateContent_success_urlContext_emptyURLMetadata() async throws { - let json = """ - { - "candidates": [ - { - "content": { "role": "model", "parts": [ { "text": "Some text." } ] }, - "finishReason": "STOP", - "urlContextMetadata": { "urlMetadata": [] } - } - ] - } - """ - MockURLProtocol.requestHandler = nil - MockURLProtocol.dataRequestHandler = try GenerativeModelTestUtil.httpRequestHandler(json: json) - - let response = try await model.generateContent(testPrompt) - - let candidate = try XCTUnwrap(response.candidates.first) - XCTAssertNil(candidate.urlContextMetadata) - } - func testGenerateContent_success_image_invalidSafetyRatingsIgnored() async throws { MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler( forResource: "unary-success-image-invalid-safety-ratings", diff --git a/FirebaseAI/Tests/Unit/MockURLProtocol.swift b/FirebaseAI/Tests/Unit/MockURLProtocol.swift index 076c5ad3056..6db227d5cfb 100644 --- a/FirebaseAI/Tests/Unit/MockURLProtocol.swift +++ b/FirebaseAI/Tests/Unit/MockURLProtocol.swift @@ -22,11 +22,6 @@ class MockURLProtocol: URLProtocol, @unchecked Sendable { AsyncLineSequence? ))? - nonisolated(unsafe) static var dataRequestHandler: ((URLRequest) throws -> ( - URLResponse, - Data? - ))? - override class func canInit(with request: URLRequest) -> Bool { #if os(watchOS) print("MockURLProtocol cannot be used on watchOS.") @@ -43,40 +38,31 @@ class MockURLProtocol: URLProtocol, @unchecked Sendable { fatalError("`client` is nil.") } - if let requestHandler = MockURLProtocol.requestHandler { - Task { - let (response, stream) = try requestHandler(self.request) - client.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) - if let stream = stream { - do { - for try await line in stream { - guard let data = line.data(using: .utf8) else { - fatalError("Failed to convert \"\(line)\" to UTF8 data.") - } - client.urlProtocol(self, didLoad: data) - // Add a newline character since AsyncLineSequence strips them when reading line by - // line; - // without the following, the whole file is delivered as a single line. - client.urlProtocol(self, didLoad: "\n".data(using: .utf8)!) + guard let requestHandler = MockURLProtocol.requestHandler else { + fatalError("No request handler set.") + } + + Task { + let (response, stream) = try requestHandler(self.request) + client.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + if let stream = stream { + do { + for try await line in stream { + guard let data = line.data(using: .utf8) else { + fatalError("Failed to convert \"\(line)\" to UTF8 data.") } - } catch { - client.urlProtocol(self, didFailWithError: error) - XCTFail("Unexpected failure reading lines from stream: \(error.localizedDescription)") + client.urlProtocol(self, didLoad: data) + // Add a newline character since AsyncLineSequence strips them when reading line by + // line; + // without the following, the whole file is delivered as a single line. + client.urlProtocol(self, didLoad: "\n".data(using: .utf8)!) } + } catch { + client.urlProtocol(self, didFailWithError: error) + XCTFail("Unexpected failure reading lines from stream: \(error.localizedDescription)") } - client.urlProtocolDidFinishLoading(self) - } - } else if let dataRequestHandler = MockURLProtocol.dataRequestHandler { - Task { - let (response, data) = try dataRequestHandler(self.request) - client.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) - if let data = data { - client.urlProtocol(self, didLoad: data) - } - client.urlProtocolDidFinishLoading(self) } - } else { - fatalError("No request handler set.") + client.urlProtocolDidFinishLoading(self) } } diff --git a/FirebaseAI/Tests/Unit/Types/GenerateContentResponseTests.swift b/FirebaseAI/Tests/Unit/Types/GenerateContentResponseTests.swift index a53d215359f..f3726b4490e 100644 --- a/FirebaseAI/Tests/Unit/Types/GenerateContentResponseTests.swift +++ b/FirebaseAI/Tests/Unit/Types/GenerateContentResponseTests.swift @@ -106,4 +106,23 @@ final class GenerateContentResponseTests: XCTestCase { "functionCalls should be empty when there are no candidates." ) } + + func testURLContextMetadata_withEmptyURLMetadata_isNil() throws { + let json = """ + { + "candidates": [ + { + "content": { "role": "model", "parts": [ { "text": "Some text." } ] }, + "finishReason": "STOP", + "urlContextMetadata": { "urlMetadata": [] } + } + ] + } + """.data(using: .utf8)! + + let response = try JSONDecoder().decode(GenerateContentResponse.self, from: json) + + let candidate = try XCTUnwrap(response.candidates.first) + XCTAssertNil(candidate.urlContextMetadata) + } } From bd2501a19eb43641c160bc780e6752c498bb8274 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Mon, 22 Sep 2025 11:45:22 -0400 Subject: [PATCH 2/2] Add `Candidate` decoding tests for empty/nil `urlMetadata` cases --- .../Types/GenerateContentResponseTests.swift | 56 +++++++++++++++---- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/FirebaseAI/Tests/Unit/Types/GenerateContentResponseTests.swift b/FirebaseAI/Tests/Unit/Types/GenerateContentResponseTests.swift index f3726b4490e..276308f63aa 100644 --- a/FirebaseAI/Tests/Unit/Types/GenerateContentResponseTests.swift +++ b/FirebaseAI/Tests/Unit/Types/GenerateContentResponseTests.swift @@ -17,6 +17,8 @@ import XCTest @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) final class GenerateContentResponseTests: XCTestCase { + let jsonDecoder = JSONDecoder() + // MARK: - GenerateContentResponse Computed Properties func testGenerateContentResponse_inlineDataParts_success() throws { @@ -107,22 +109,52 @@ final class GenerateContentResponseTests: XCTestCase { ) } - func testURLContextMetadata_withEmptyURLMetadata_isNil() throws { + // MARK: - Decoding Tests + + func testDecodeCandidate_emptyURLMetadata_urlContextMetadataIsNil() throws { let json = """ { - "candidates": [ - { - "content": { "role": "model", "parts": [ { "text": "Some text." } ] }, - "finishReason": "STOP", - "urlContextMetadata": { "urlMetadata": [] } - } - ] + "content": { "role": "model", "parts": [ { "text": "Some text." } ] }, + "finishReason": "STOP", + "urlContextMetadata": { "urlMetadata": [] } } - """.data(using: .utf8)! + """ + let jsonData = try XCTUnwrap(json.data(using: .utf8)) + + let candidate = try jsonDecoder.decode(Candidate.self, from: jsonData) - let response = try JSONDecoder().decode(GenerateContentResponse.self, from: json) + XCTAssertNil( + candidate.urlContextMetadata, + "urlContextMetadata should be nil if the `urlMetadata` array is empty in the candidate." + ) + XCTAssertEqual(candidate.content.role, "model") + let part = try XCTUnwrap(candidate.content.parts.first) + let textPart = try XCTUnwrap(part as? TextPart) + XCTAssertEqual(textPart.text, "Some text.") + XCTAssertFalse(textPart.isThought) + XCTAssertEqual(candidate.finishReason, .stop) + } - let candidate = try XCTUnwrap(response.candidates.first) - XCTAssertNil(candidate.urlContextMetadata) + func testDecodeCandidate_missingURLMetadata_urlContextMetadataIsNil() throws { + let json = """ + { + "content": { "role": "model", "parts": [ { "text": "Some text." } ] }, + "finishReason": "STOP" + } + """ + let jsonData = try XCTUnwrap(json.data(using: .utf8)) + + let candidate = try jsonDecoder.decode(Candidate.self, from: jsonData) + + XCTAssertNil( + candidate.urlContextMetadata, + "urlContextMetadata should be nil if `urlMetadata` is not provided in the candidate." + ) + XCTAssertEqual(candidate.content.role, "model") + let part = try XCTUnwrap(candidate.content.parts.first) + let textPart = try XCTUnwrap(part as? TextPart) + XCTAssertEqual(textPart.text, "Some text.") + XCTAssertFalse(textPart.isThought) + XCTAssertEqual(candidate.finishReason, .stop) } }