From 37273e4580361f13e09ca7c2d0dab3985ab4c6f2 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Thu, 17 Jul 2025 10:26:21 -0700 Subject: [PATCH 1/6] Part 1 --- FirebaseAI/Tests/Unit/ChatTests.swift | 99 +++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/FirebaseAI/Tests/Unit/ChatTests.swift b/FirebaseAI/Tests/Unit/ChatTests.swift index 40373a47494..1f2c40d1e67 100644 --- a/FirebaseAI/Tests/Unit/ChatTests.swift +++ b/FirebaseAI/Tests/Unit/ChatTests.swift @@ -94,4 +94,103 @@ final class ChatTests: XCTestCase { XCTAssertEqual(chat.history[1], assembledExpectation) #endif // os(watchOS) } + + func testSendMessage_unary_appendsHistory() async throws { + let expectedInput = "Test input" + MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler( + forResource: "unary-success-basic-reply-short", + withExtension: "json", + subdirectory: "mock-responses/googleai" + ) + let model = GenerativeModel( + modelName: modelName, + modelResourceName: modelResourceName, + firebaseInfo: GenerativeModelTestUtil.testFirebaseInfo(), + apiConfig: FirebaseAI.defaultVertexAIAPIConfig, + tools: nil, + requestOptions: RequestOptions(), + urlSession: urlSession + ) + let chat = model.startChat() + + // Pre-condition: History should be empty. + XCTAssertTrue(chat.history.isEmpty) + + let response = try await chat.sendMessage(expectedInput) + + XCTAssertNotNil(response.text) + XCTAssertFalse(response.text!.isEmpty) + + // Post-condition: History should have the user's message and the model's response. + XCTAssertEqual(chat.history.count, 2) + let userInput = try XCTUnwrap(chat.history.first) + XCTAssertEqual(userInput.role, "user") + XCTAssertEqual(userInput.parts.count, 1) + let userInputText = try XCTUnwrap(userInput.parts.first as? TextPart) + XCTAssertEqual(userInputText.text, expectedInput) + + let modelResponse = try XCTUnwrap(chat.history.last) + XCTAssertEqual(modelResponse.role, "model") + XCTAssertEqual(modelResponse.parts.count, 1) + let modelResponseText = try XCTUnwrap(modelResponse.parts.first as? TextPart) + XCTAssertFalse(modelResponseText.text.isEmpty) + } + + func testSendMessageStream_error_doesNotAppendHistory() async throws { + MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler( + forResource: "streaming-failure-finish-reason-safety", + withExtension: "txt", + subdirectory: "mock-responses/vertexai" + ) + let model = GenerativeModel( + modelName: modelName, + modelResourceName: modelResourceName, + firebaseInfo: GenerativeModelTestUtil.testFirebaseInfo(), + apiConfig: FirebaseAI.defaultVertexAIAPIConfig, + tools: nil, + requestOptions: RequestOptions(), + urlSession: urlSession + ) + let chat = model.startChat() + let input = "Test input" + + // Pre-condition: History should be empty. + XCTAssertTrue(chat.history.isEmpty) + + do { + let stream = try chat.sendMessageStream(input) + for try await _ in stream { + // Consume the stream. + } + XCTFail("Should have thrown a responseStoppedEarly error.") + } catch let GenerateContentError.responseStoppedEarly(reason, _) { + XCTAssertEqual(reason, .safety) + } catch { + XCTFail("Unexpected error thrown: \(error)") + } + + // Post-condition: History should still be empty. + XCTAssertEqual(chat.history.count, 0) + } + + func testStartChat_withHistory_initializesCorrectly() async throws { + let history = [ + ModelContent(role: "user", parts: "Question 1"), + ModelContent(role: "model", parts: "Answer 1"), + ] + let model = GenerativeModel( + modelName: modelName, + modelResourceName: modelResourceName, + firebaseInfo: GenerativeModelTestUtil.testFirebaseInfo(), + apiConfig: FirebaseAI.defaultVertexAIAPIConfig, + tools: nil, + requestOptions: RequestOptions(), + urlSession: urlSession + ) + + let chat = model.startChat(history: history) + + XCTAssertEqual(chat.history.count, 2) + XCTAssertEqual(chat.history, history) + } } From 8dcfc182d67818d50cdd6b1f0e505cc9dbf5e95c Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Thu, 17 Jul 2025 11:12:35 -0700 Subject: [PATCH 2/6] part 2 --- ...enerativeModelGoogleAITests+Coverage.swift | 124 ++++++++++++++++++ .../Tests/Unit/JSONValueTests+Coverage.swift | 96 ++++++++++++++ .../PartsRepresentableTests+Coverage.swift | 102 ++++++++++++++ FirebaseAI/Tests/Unit/SafetyTests.swift | 110 ++++++++++++++++ 4 files changed, 432 insertions(+) create mode 100644 FirebaseAI/Tests/Unit/GenerativeModelGoogleAITests+Coverage.swift create mode 100644 FirebaseAI/Tests/Unit/JSONValueTests+Coverage.swift create mode 100644 FirebaseAI/Tests/Unit/PartsRepresentableTests+Coverage.swift create mode 100644 FirebaseAI/Tests/Unit/SafetyTests.swift diff --git a/FirebaseAI/Tests/Unit/GenerativeModelGoogleAITests+Coverage.swift b/FirebaseAI/Tests/Unit/GenerativeModelGoogleAITests+Coverage.swift new file mode 100644 index 00000000000..2a7c7157c80 --- /dev/null +++ b/FirebaseAI/Tests/Unit/GenerativeModelGoogleAITests+Coverage.swift @@ -0,0 +1,124 @@ +// Copyright 2025 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 +// +// http://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 FirebaseAppCheckInterop +import FirebaseAuthInterop +import FirebaseCore +import XCTest + +@testable import FirebaseAI + +@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) +extension GenerativeModelGoogleAITests { + // MARK: - Tool/Function Calling + + func testGenerateContent_success_functionCall_emptyArguments() async throws { + MockURLProtocol + .requestHandler = try GenerativeModelTestUtil.httpRequestHandler( + forResource: "unary-success-function-call-empty-arguments", + withExtension: "json", + subdirectory: "mock-responses/vertexai" + ) + + let response = try await model.generateContent(testPrompt) + + XCTAssertEqual(response.candidates.count, 1) + let candidate = try XCTUnwrap(response.candidates.first) + XCTAssertEqual(candidate.content.parts.count, 1) + let part = try XCTUnwrap(candidate.content.parts.first) + guard let functionCall = part as? FunctionCallPart else { + XCTFail("Part is not a FunctionCall.") + return + } + XCTAssertEqual(functionCall.name, "current_time") + XCTAssertTrue(functionCall.args.isEmpty) + XCTAssertEqual(response.functionCalls, [functionCall]) + } + + func testGenerateContent_success_functionCall_withArguments() async throws { + MockURLProtocol + .requestHandler = try GenerativeModelTestUtil.httpRequestHandler( + forResource: "unary-success-function-call-with-arguments", + withExtension: "json", + subdirectory: "mock-responses/vertexai" + ) + + let response = try await model.generateContent(testPrompt) + + XCTAssertEqual(response.candidates.count, 1) + let candidate = try XCTUnwrap(response.candidates.first) + XCTAssertEqual(candidate.content.parts.count, 1) + let part = try XCTUnwrap(candidate.content.parts.first) + guard let functionCall = part as? FunctionCallPart else { + XCTFail("Part is not a FunctionCall.") + return + } + XCTAssertEqual(functionCall.name, "sum") + XCTAssertEqual(functionCall.args.count, 2) + let argX = try XCTUnwrap(functionCall.args["x"]) + XCTAssertEqual(argX, .number(4)) + let argY = try XCTUnwrap(functionCall.args["y"]) + XCTAssertEqual(argY, .number(5)) + XCTAssertEqual(response.functionCalls, [functionCall]) + } + + func testGenerateContent_success_functionCall_parallelCalls() async throws { + MockURLProtocol + .requestHandler = try GenerativeModelTestUtil.httpRequestHandler( + forResource: "unary-success-function-call-parallel-calls", + withExtension: "json", + subdirectory: "mock-responses/vertexai" + ) + + let response = try await model.generateContent(testPrompt) + + XCTAssertEqual(response.candidates.count, 1) + let candidate = try XCTUnwrap(response.candidates.first) + XCTAssertEqual(candidate.content.parts.count, 3) + let functionCalls = response.functionCalls + XCTAssertEqual(functionCalls.count, 3) + } + + func testGenerateContent_success_functionCall_mixedContent() async throws { + MockURLProtocol + .requestHandler = try GenerativeModelTestUtil.httpRequestHandler( + forResource: "unary-success-function-call-mixed-content", + withExtension: "json", + subdirectory: "mock-responses/vertexai" + ) + + let response = try await model.generateContent(testPrompt) + + XCTAssertEqual(response.candidates.count, 1) + let candidate = try XCTUnwrap(response.candidates.first) + XCTAssertEqual(candidate.content.parts.count, 4) + let functionCalls = response.functionCalls + XCTAssertEqual(functionCalls.count, 2) + let text = try XCTUnwrap(response.text) + XCTAssertEqual(text, "The sum of [1, 2, 3] is") + } + + // MARK: - Count Tokens + + func testCountTokens_success() async throws { + MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler( + forResource: "unary-success-total-tokens", + withExtension: "json", + subdirectory: "mock-responses/vertexai" + ) + + let response = try await model.countTokens("Why is the sky blue?") + XCTAssertEqual(response.totalTokens, 6) + } +} diff --git a/FirebaseAI/Tests/Unit/JSONValueTests+Coverage.swift b/FirebaseAI/Tests/Unit/JSONValueTests+Coverage.swift new file mode 100644 index 00000000000..0db36fcbfc5 --- /dev/null +++ b/FirebaseAI/Tests/Unit/JSONValueTests+Coverage.swift @@ -0,0 +1,96 @@ +// Copyright 2025 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 +// +// http://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 XCTest + +@testable import FirebaseAI + +@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) +extension JSONValueTests { + func testDecodeNestedObject() throws { + let nestedObject: JSONObject = [ + "nestedKey": .string("nestedValue"), + ] + let expectedObject: JSONObject = [ + "numberKey": .number(numberValue), + "objectKey": .object(nestedObject), + ] + let json = """ + { + "numberKey": \(numberValue), + "objectKey": { + "nestedKey": "nestedValue" + } + } + """ + let jsonData = try XCTUnwrap(json.data(using: .utf8)) + + let jsonObject = try XCTUnwrap(decoder.decode(JSONValue.self, from: jsonData)) + + XCTAssertEqual(jsonObject, .object(expectedObject)) + } + + func testDecodeNestedArray() throws { + let nestedArray: [JSONValue] = [.string("a"), .string("b")] + let expectedObject: JSONObject = [ + "numberKey": .number(numberValue), + "arrayKey": .array(nestedArray), + ] + let json = """ + { + "numberKey": \(numberValue), + "arrayKey": ["a", "b"] + } + """ + let jsonData = try XCTUnwrap(json.data(using: .utf8)) + + let jsonObject = try XCTUnwrap(decoder.decode(JSONValue.self, from: jsonData)) + + XCTAssertEqual(jsonObject, .object(expectedObject)) + } + + func testEncodeNestedObject() throws { + let nestedObject: JSONObject = [ + "nestedKey": .string("nestedValue"), + ] + let objectValue: JSONObject = [ + "numberKey": .number(numberValue), + "objectKey": .object(nestedObject), + ] + + let jsonData = try encoder.encode(JSONValue.object(objectValue)) + + let json = try XCTUnwrap(String(data: jsonData, encoding: .utf8)) + XCTAssertEqual( + json, + "{\"numberKey\":\(numberValueEncoded),\"objectKey\":{\"nestedKey\":\"nestedValue\"}}" + ) + } + + func testEncodeNestedArray() throws { + let nestedArray: [JSONValue] = [.string("a"), .string("b")] + let objectValue: JSONObject = [ + "numberKey": .number(numberValue), + "arrayKey": .array(nestedArray), + ] + + let jsonData = try encoder.encode(JSONValue.object(objectValue)) + + let json = try XCTUnwrap(String(data: jsonData, encoding: .utf8)) + XCTAssertEqual( + json, + "{\"arrayKey\":[\"a\",\"b\"],\"numberKey\":\(numberValueEncoded)}" + ) + } +} diff --git a/FirebaseAI/Tests/Unit/PartsRepresentableTests+Coverage.swift b/FirebaseAI/Tests/Unit/PartsRepresentableTests+Coverage.swift new file mode 100644 index 00000000000..0665a5c55b1 --- /dev/null +++ b/FirebaseAI/Tests/Unit/PartsRepresentableTests+Coverage.swift @@ -0,0 +1,102 @@ +// Copyright 2025 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 +// +// http://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 XCTest +#if canImport(UIKit) + import UIKit +#elseif canImport(AppKit) + import AppKit +#endif + +@testable import FirebaseAI + +@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) +extension PartsRepresentableTests { + func testMixedParts() throws { + let text = "This is a test" + let data = try XCTUnwrap("This is some data".data(using: .utf8)) + let inlineData = InlineDataPart(data: data, mimeType: "text/plain") + + let parts: [any PartsRepresentable] = [text, inlineData] + let modelContent = ModelContent(parts: parts) + + XCTAssertEqual(modelContent.parts.count, 2) + let textPart = try XCTUnwrap(modelContent.parts[0] as? TextPart) + XCTAssertEqual(textPart.text, text) + let dataPart = try XCTUnwrap(modelContent.parts[1] as? InlineDataPart) + XCTAssertEqual(dataPart, inlineData) + } + + #if canImport(UIKit) && !os(visionOS) + func testMixedParts_withImage() throws { + let text = "This is a test" + let image = try XCTUnwrap(UIImage(systemName: "star")) + let parts: [any PartsRepresentable] = [text, image] + let modelContent = ModelContent(parts: parts) + + XCTAssertEqual(modelContent.parts.count, 2) + let textPart = try XCTUnwrap(modelContent.parts[0] as? TextPart) + XCTAssertEqual(textPart.text, text) + let imagePart = try XCTUnwrap(modelContent.parts[1] as? InlineDataPart) + XCTAssertEqual(imagePart.mimeType, "image/jpeg") + XCTAssertFalse(imagePart.data.isEmpty) + } + + func SKIPtestMixedParts_withInvalidImage() throws { + let text = "This is a test" + let invalidImage = UIImage() + let parts: [any PartsRepresentable] = [text, invalidImage] + let modelContent = ModelContent(parts: parts) // Generates fatalError + + XCTAssertEqual(modelContent.parts.count, 2) + let textPart = try XCTUnwrap(modelContent.parts[0] as? TextPart) + XCTAssertEqual(textPart.text, text) + let errorPart = try XCTUnwrap(modelContent.parts[1] as? ErrorPart) + XCTAssert(errorPart.error is ImageConversionError) + } + + #elseif canImport(AppKit) + func testMixedParts_withImage() throws { + let text = "This is a test" + let coreImage = CIImage(color: CIColor.blue) + .cropped(to: CGRect(origin: CGPoint.zero, size: CGSize(width: 16, height: 16))) + let rep = NSCIImageRep(ciImage: coreImage) + let image = NSImage(size: rep.size) + image.addRepresentation(rep) + + let parts: [any PartsRepresentable] = [text, image] + let modelContent = ModelContent(parts: parts) + + XCTAssertEqual(modelContent.parts.count, 2) + let textPart = try XCTUnwrap(modelContent.parts[0] as? TextPart) + XCTAssertEqual(textPart.text, text) + let imagePart = try XCTUnwrap(modelContent.parts[1] as? InlineDataPart) + XCTAssertEqual(imagePart.mimeType, "image/jpeg") + XCTAssertFalse(imagePart.data.isEmpty) + } + + func testMixedParts_withInvalidImage() throws { + let text = "This is a test" + let invalidImage = NSImage() + let parts: [any PartsRepresentable] = [text, invalidImage] + let modelContent = ModelContent(parts: parts) + + XCTAssertEqual(modelContent.parts.count, 2) + let textPart = try XCTUnwrap(modelContent.parts[0] as? TextPart) + XCTAssertEqual(textPart.text, text) + let errorPart = try XCTUnwrap(modelContent.parts[1] as? ErrorPart) + XCTAssert(errorPart.error is ImageConversionError) + } + #endif +} diff --git a/FirebaseAI/Tests/Unit/SafetyTests.swift b/FirebaseAI/Tests/Unit/SafetyTests.swift new file mode 100644 index 00000000000..031c0b9a96e --- /dev/null +++ b/FirebaseAI/Tests/Unit/SafetyTests.swift @@ -0,0 +1,110 @@ +// Copyright 2025 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 +// +// http://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 XCTest + +@testable import FirebaseAI + +@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) +final class SafetyTests: XCTestCase { + let decoder = JSONDecoder() + let encoder = JSONEncoder() + + // MARK: - SafetyRating Decoding + + func testDecodeSafetyRating_allFieldsPresent() throws { + let json = """ + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE", + "probabilityScore": 0.1, + "severity": "HARM_SEVERITY_LOW", + "severityScore": 0.2, + "blocked": true + } + """ + let jsonData = try XCTUnwrap(json.data(using: .utf8)) + let rating = try decoder.decode(SafetyRating.self, from: jsonData) + + XCTAssertEqual(rating.category, .dangerousContent) + XCTAssertEqual(rating.probability, .negligible) + XCTAssertEqual(rating.probabilityScore, 0.1) + XCTAssertEqual(rating.severity, .low) + XCTAssertEqual(rating.severityScore, 0.2) + XCTAssertTrue(rating.blocked) + } + + func testDecodeSafetyRating_missingOptionalFields() throws { + let json = """ + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "LOW" + } + """ + let jsonData = try XCTUnwrap(json.data(using: .utf8)) + let rating = try decoder.decode(SafetyRating.self, from: jsonData) + + XCTAssertEqual(rating.category, .harassment) + XCTAssertEqual(rating.probability, .low) + XCTAssertEqual(rating.probabilityScore, 0.0) + XCTAssertEqual(rating.severity, .unspecified) + XCTAssertEqual(rating.severityScore, 0.0) + XCTAssertFalse(rating.blocked) + } + + func testDecodeSafetyRating_unknownEnums() throws { + let json = """ + { + "category": "HARM_CATEGORY_UNKNOWN", + "probability": "UNKNOWN_PROBABILITY", + "severity": "UNKNOWN_SEVERITY" + } + """ + let jsonData = try XCTUnwrap(json.data(using: .utf8)) + let rating = try decoder.decode(SafetyRating.self, from: jsonData) + + XCTAssertEqual(rating.category.rawValue, "HARM_CATEGORY_UNKNOWN") + XCTAssertEqual(rating.probability.rawValue, "UNKNOWN_PROBABILITY") + XCTAssertEqual(rating.severity.rawValue, "UNKNOWN_SEVERITY") + } + + // MARK: - SafetySetting Encoding + + func testEncodeSafetySetting_allFields() throws { + let setting = SafetySetting( + harmCategory: .hateSpeech, + threshold: .blockMediumAndAbove, + method: .severity + ) + let jsonData = try encoder.encode(setting) + let jsonObject = try XCTUnwrap(JSONSerialization.jsonObject(with: jsonData) as? [String: Any]) + + XCTAssertEqual(jsonObject["category"] as? String, "HARM_CATEGORY_HATE_SPEECH") + XCTAssertEqual(jsonObject["threshold"] as? String, "BLOCK_MEDIUM_AND_ABOVE") + XCTAssertEqual(jsonObject["method"] as? String, "SEVERITY") + } + + func testEncodeSafetySetting_nilMethod() throws { + let setting = SafetySetting( + harmCategory: .sexuallyExplicit, + threshold: .blockOnlyHigh + ) + let jsonData = try encoder.encode(setting) + let jsonObject = try XCTUnwrap(JSONSerialization.jsonObject(with: jsonData) as? [String: Any]) + + XCTAssertEqual(jsonObject["category"] as? String, "HARM_CATEGORY_SEXUALLY_EXPLICIT") + XCTAssertEqual(jsonObject["threshold"] as? String, "BLOCK_ONLY_HIGH") + XCTAssertNil(jsonObject["method"]) + } +} From d769436fc6a225cee16dec1e8b0814411636fe96 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Thu, 17 Jul 2025 13:45:35 -0700 Subject: [PATCH 3/6] Review --- FirebaseAI/Tests/Unit/ChatTests.swift | 3 ++- .../Tests/Unit/JSONValueTests+Coverage.swift | 16 +++--------- .../PartsRepresentableTests+Coverage.swift | 26 ------------------- 3 files changed, 6 insertions(+), 39 deletions(-) diff --git a/FirebaseAI/Tests/Unit/ChatTests.swift b/FirebaseAI/Tests/Unit/ChatTests.swift index 1f2c40d1e67..4bf89f8cb51 100644 --- a/FirebaseAI/Tests/Unit/ChatTests.swift +++ b/FirebaseAI/Tests/Unit/ChatTests.swift @@ -119,7 +119,8 @@ final class ChatTests: XCTestCase { let response = try await chat.sendMessage(expectedInput) XCTAssertNotNil(response.text) - XCTAssertFalse(response.text!.isEmpty) + let text = try XCTUnwrap(response.text) + XCTAssertFalse(text.isEmpty) // Post-condition: History should have the user's message and the model's response. XCTAssertEqual(chat.history.count, 2) diff --git a/FirebaseAI/Tests/Unit/JSONValueTests+Coverage.swift b/FirebaseAI/Tests/Unit/JSONValueTests+Coverage.swift index 0db36fcbfc5..fce13ed8f3a 100644 --- a/FirebaseAI/Tests/Unit/JSONValueTests+Coverage.swift +++ b/FirebaseAI/Tests/Unit/JSONValueTests+Coverage.swift @@ -70,12 +70,8 @@ extension JSONValueTests { ] let jsonData = try encoder.encode(JSONValue.object(objectValue)) - - let json = try XCTUnwrap(String(data: jsonData, encoding: .utf8)) - XCTAssertEqual( - json, - "{\"numberKey\":\(numberValueEncoded),\"objectKey\":{\"nestedKey\":\"nestedValue\"}}" - ) + let jsonObject = try XCTUnwrap(decoder.decode(JSONValue.self, from: jsonData)) + XCTAssertEqual(jsonObject, .object(objectValue)) } func testEncodeNestedArray() throws { @@ -86,11 +82,7 @@ extension JSONValueTests { ] let jsonData = try encoder.encode(JSONValue.object(objectValue)) - - let json = try XCTUnwrap(String(data: jsonData, encoding: .utf8)) - XCTAssertEqual( - json, - "{\"arrayKey\":[\"a\",\"b\"],\"numberKey\":\(numberValueEncoded)}" - ) + let jsonObject = try XCTUnwrap(decoder.decode(JSONValue.self, from: jsonData)) + XCTAssertEqual(jsonObject, .object(objectValue)) } } diff --git a/FirebaseAI/Tests/Unit/PartsRepresentableTests+Coverage.swift b/FirebaseAI/Tests/Unit/PartsRepresentableTests+Coverage.swift index 0665a5c55b1..66802721c45 100644 --- a/FirebaseAI/Tests/Unit/PartsRepresentableTests+Coverage.swift +++ b/FirebaseAI/Tests/Unit/PartsRepresentableTests+Coverage.swift @@ -53,19 +53,6 @@ extension PartsRepresentableTests { XCTAssertFalse(imagePart.data.isEmpty) } - func SKIPtestMixedParts_withInvalidImage() throws { - let text = "This is a test" - let invalidImage = UIImage() - let parts: [any PartsRepresentable] = [text, invalidImage] - let modelContent = ModelContent(parts: parts) // Generates fatalError - - XCTAssertEqual(modelContent.parts.count, 2) - let textPart = try XCTUnwrap(modelContent.parts[0] as? TextPart) - XCTAssertEqual(textPart.text, text) - let errorPart = try XCTUnwrap(modelContent.parts[1] as? ErrorPart) - XCTAssert(errorPart.error is ImageConversionError) - } - #elseif canImport(AppKit) func testMixedParts_withImage() throws { let text = "This is a test" @@ -85,18 +72,5 @@ extension PartsRepresentableTests { XCTAssertEqual(imagePart.mimeType, "image/jpeg") XCTAssertFalse(imagePart.data.isEmpty) } - - func testMixedParts_withInvalidImage() throws { - let text = "This is a test" - let invalidImage = NSImage() - let parts: [any PartsRepresentable] = [text, invalidImage] - let modelContent = ModelContent(parts: parts) - - XCTAssertEqual(modelContent.parts.count, 2) - let textPart = try XCTUnwrap(modelContent.parts[0] as? TextPart) - XCTAssertEqual(textPart.text, text) - let errorPart = try XCTUnwrap(modelContent.parts[1] as? ErrorPart) - XCTAssert(errorPart.error is ImageConversionError) - } #endif } From 1eef6135fb9bc45478271cd2036d767d9397b7cf Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Thu, 17 Jul 2025 20:14:09 -0700 Subject: [PATCH 4/6] address review comments --- ...enerativeModelGoogleAITests+Coverage.swift | 124 ------------------ .../PartsRepresentableTests+Coverage.swift | 15 ++- FirebaseAI/Tests/Unit/SafetyTests.swift | 18 +-- 3 files changed, 24 insertions(+), 133 deletions(-) delete mode 100644 FirebaseAI/Tests/Unit/GenerativeModelGoogleAITests+Coverage.swift diff --git a/FirebaseAI/Tests/Unit/GenerativeModelGoogleAITests+Coverage.swift b/FirebaseAI/Tests/Unit/GenerativeModelGoogleAITests+Coverage.swift deleted file mode 100644 index 2a7c7157c80..00000000000 --- a/FirebaseAI/Tests/Unit/GenerativeModelGoogleAITests+Coverage.swift +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright 2025 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 -// -// http://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 FirebaseAppCheckInterop -import FirebaseAuthInterop -import FirebaseCore -import XCTest - -@testable import FirebaseAI - -@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) -extension GenerativeModelGoogleAITests { - // MARK: - Tool/Function Calling - - func testGenerateContent_success_functionCall_emptyArguments() async throws { - MockURLProtocol - .requestHandler = try GenerativeModelTestUtil.httpRequestHandler( - forResource: "unary-success-function-call-empty-arguments", - withExtension: "json", - subdirectory: "mock-responses/vertexai" - ) - - let response = try await model.generateContent(testPrompt) - - XCTAssertEqual(response.candidates.count, 1) - let candidate = try XCTUnwrap(response.candidates.first) - XCTAssertEqual(candidate.content.parts.count, 1) - let part = try XCTUnwrap(candidate.content.parts.first) - guard let functionCall = part as? FunctionCallPart else { - XCTFail("Part is not a FunctionCall.") - return - } - XCTAssertEqual(functionCall.name, "current_time") - XCTAssertTrue(functionCall.args.isEmpty) - XCTAssertEqual(response.functionCalls, [functionCall]) - } - - func testGenerateContent_success_functionCall_withArguments() async throws { - MockURLProtocol - .requestHandler = try GenerativeModelTestUtil.httpRequestHandler( - forResource: "unary-success-function-call-with-arguments", - withExtension: "json", - subdirectory: "mock-responses/vertexai" - ) - - let response = try await model.generateContent(testPrompt) - - XCTAssertEqual(response.candidates.count, 1) - let candidate = try XCTUnwrap(response.candidates.first) - XCTAssertEqual(candidate.content.parts.count, 1) - let part = try XCTUnwrap(candidate.content.parts.first) - guard let functionCall = part as? FunctionCallPart else { - XCTFail("Part is not a FunctionCall.") - return - } - XCTAssertEqual(functionCall.name, "sum") - XCTAssertEqual(functionCall.args.count, 2) - let argX = try XCTUnwrap(functionCall.args["x"]) - XCTAssertEqual(argX, .number(4)) - let argY = try XCTUnwrap(functionCall.args["y"]) - XCTAssertEqual(argY, .number(5)) - XCTAssertEqual(response.functionCalls, [functionCall]) - } - - func testGenerateContent_success_functionCall_parallelCalls() async throws { - MockURLProtocol - .requestHandler = try GenerativeModelTestUtil.httpRequestHandler( - forResource: "unary-success-function-call-parallel-calls", - withExtension: "json", - subdirectory: "mock-responses/vertexai" - ) - - let response = try await model.generateContent(testPrompt) - - XCTAssertEqual(response.candidates.count, 1) - let candidate = try XCTUnwrap(response.candidates.first) - XCTAssertEqual(candidate.content.parts.count, 3) - let functionCalls = response.functionCalls - XCTAssertEqual(functionCalls.count, 3) - } - - func testGenerateContent_success_functionCall_mixedContent() async throws { - MockURLProtocol - .requestHandler = try GenerativeModelTestUtil.httpRequestHandler( - forResource: "unary-success-function-call-mixed-content", - withExtension: "json", - subdirectory: "mock-responses/vertexai" - ) - - let response = try await model.generateContent(testPrompt) - - XCTAssertEqual(response.candidates.count, 1) - let candidate = try XCTUnwrap(response.candidates.first) - XCTAssertEqual(candidate.content.parts.count, 4) - let functionCalls = response.functionCalls - XCTAssertEqual(functionCalls.count, 2) - let text = try XCTUnwrap(response.text) - XCTAssertEqual(text, "The sum of [1, 2, 3] is") - } - - // MARK: - Count Tokens - - func testCountTokens_success() async throws { - MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler( - forResource: "unary-success-total-tokens", - withExtension: "json", - subdirectory: "mock-responses/vertexai" - ) - - let response = try await model.countTokens("Why is the sky blue?") - XCTAssertEqual(response.totalTokens, 6) - } -} diff --git a/FirebaseAI/Tests/Unit/PartsRepresentableTests+Coverage.swift b/FirebaseAI/Tests/Unit/PartsRepresentableTests+Coverage.swift index 66802721c45..d41411877e0 100644 --- a/FirebaseAI/Tests/Unit/PartsRepresentableTests+Coverage.swift +++ b/FirebaseAI/Tests/Unit/PartsRepresentableTests+Coverage.swift @@ -38,7 +38,7 @@ extension PartsRepresentableTests { XCTAssertEqual(dataPart, inlineData) } - #if canImport(UIKit) && !os(visionOS) + #if canImport(UIKit) func testMixedParts_withImage() throws { let text = "This is a test" let image = try XCTUnwrap(UIImage(systemName: "star")) @@ -53,6 +53,19 @@ extension PartsRepresentableTests { XCTAssertFalse(imagePart.data.isEmpty) } + func testMixedParts_withInvalidImage() throws { + let text = "This is a test" + let invalidImage = UIImage() + let parts: [any PartsRepresentable] = [text, invalidImage] + let modelContent = ModelContent(parts: parts) + + XCTAssertEqual(modelContent.parts.count, 2) + let textPart = try XCTUnwrap(modelContent.parts[0] as? TextPart) + XCTAssertEqual(textPart.text, text) + let errorPart = try XCTUnwrap(modelContent.parts[1] as? ErrorPart) + XCTAssert(errorPart.error is ImageConversionError) + } + #elseif canImport(AppKit) func testMixedParts_withImage() throws { let text = "This is a test" diff --git a/FirebaseAI/Tests/Unit/SafetyTests.swift b/FirebaseAI/Tests/Unit/SafetyTests.swift index 031c0b9a96e..680567617c4 100644 --- a/FirebaseAI/Tests/Unit/SafetyTests.swift +++ b/FirebaseAI/Tests/Unit/SafetyTests.swift @@ -87,12 +87,13 @@ final class SafetyTests: XCTestCase { threshold: .blockMediumAndAbove, method: .severity ) + encoder.outputFormatting = .sortedKeys let jsonData = try encoder.encode(setting) - let jsonObject = try XCTUnwrap(JSONSerialization.jsonObject(with: jsonData) as? [String: Any]) + let jsonString = try XCTUnwrap(String(data: jsonData, encoding: .utf8)) - XCTAssertEqual(jsonObject["category"] as? String, "HARM_CATEGORY_HATE_SPEECH") - XCTAssertEqual(jsonObject["threshold"] as? String, "BLOCK_MEDIUM_AND_ABOVE") - XCTAssertEqual(jsonObject["method"] as? String, "SEVERITY") + XCTAssertEqual(jsonString, """ + {"category":"HARM_CATEGORY_HATE_SPEECH","method":"SEVERITY","threshold":"BLOCK_MEDIUM_AND_ABOVE"} + """) } func testEncodeSafetySetting_nilMethod() throws { @@ -100,11 +101,12 @@ final class SafetyTests: XCTestCase { harmCategory: .sexuallyExplicit, threshold: .blockOnlyHigh ) + encoder.outputFormatting = .sortedKeys let jsonData = try encoder.encode(setting) - let jsonObject = try XCTUnwrap(JSONSerialization.jsonObject(with: jsonData) as? [String: Any]) + let jsonString = try XCTUnwrap(String(data: jsonData, encoding: .utf8)) - XCTAssertEqual(jsonObject["category"] as? String, "HARM_CATEGORY_SEXUALLY_EXPLICIT") - XCTAssertEqual(jsonObject["threshold"] as? String, "BLOCK_ONLY_HIGH") - XCTAssertNil(jsonObject["method"]) + XCTAssertEqual(jsonString, """ + {"category":"HARM_CATEGORY_SEXUALLY_EXPLICIT","threshold":"BLOCK_ONLY_HIGH"} + """) } } From 76bf729743c6b408dae57413137868231a15992e Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Fri, 18 Jul 2025 08:12:07 -0700 Subject: [PATCH 5/6] fix tests --- .../Unit/PartsRepresentableTests+Coverage.swift | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/FirebaseAI/Tests/Unit/PartsRepresentableTests+Coverage.swift b/FirebaseAI/Tests/Unit/PartsRepresentableTests+Coverage.swift index d41411877e0..e7c001f2c17 100644 --- a/FirebaseAI/Tests/Unit/PartsRepresentableTests+Coverage.swift +++ b/FirebaseAI/Tests/Unit/PartsRepresentableTests+Coverage.swift @@ -53,19 +53,6 @@ extension PartsRepresentableTests { XCTAssertFalse(imagePart.data.isEmpty) } - func testMixedParts_withInvalidImage() throws { - let text = "This is a test" - let invalidImage = UIImage() - let parts: [any PartsRepresentable] = [text, invalidImage] - let modelContent = ModelContent(parts: parts) - - XCTAssertEqual(modelContent.parts.count, 2) - let textPart = try XCTUnwrap(modelContent.parts[0] as? TextPart) - XCTAssertEqual(textPart.text, text) - let errorPart = try XCTUnwrap(modelContent.parts[1] as? ErrorPart) - XCTAssert(errorPart.error is ImageConversionError) - } - #elseif canImport(AppKit) func testMixedParts_withImage() throws { let text = "This is a test" From 7c3cb3af88e85b371f88c9048803c532cca4df2c Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Mon, 21 Jul 2025 22:05:33 -0400 Subject: [PATCH 6/6] [Firebase AI] Merge `+Coverage` files into existing test files (#15136) --- .../Tests/Unit/JSONValueTests+Coverage.swift | 88 ------------------- FirebaseAI/Tests/Unit/JSONValueTests.swift | 68 ++++++++++++++ .../PartsRepresentableTests+Coverage.swift | 76 ---------------- .../Tests/Unit/PartsRepresentableTests.swift | 51 +++++++++++ FirebaseAI/Tests/Unit/SafetyTests.swift | 19 +++- 5 files changed, 134 insertions(+), 168 deletions(-) delete mode 100644 FirebaseAI/Tests/Unit/JSONValueTests+Coverage.swift delete mode 100644 FirebaseAI/Tests/Unit/PartsRepresentableTests+Coverage.swift diff --git a/FirebaseAI/Tests/Unit/JSONValueTests+Coverage.swift b/FirebaseAI/Tests/Unit/JSONValueTests+Coverage.swift deleted file mode 100644 index fce13ed8f3a..00000000000 --- a/FirebaseAI/Tests/Unit/JSONValueTests+Coverage.swift +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2025 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 -// -// http://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 XCTest - -@testable import FirebaseAI - -@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) -extension JSONValueTests { - func testDecodeNestedObject() throws { - let nestedObject: JSONObject = [ - "nestedKey": .string("nestedValue"), - ] - let expectedObject: JSONObject = [ - "numberKey": .number(numberValue), - "objectKey": .object(nestedObject), - ] - let json = """ - { - "numberKey": \(numberValue), - "objectKey": { - "nestedKey": "nestedValue" - } - } - """ - let jsonData = try XCTUnwrap(json.data(using: .utf8)) - - let jsonObject = try XCTUnwrap(decoder.decode(JSONValue.self, from: jsonData)) - - XCTAssertEqual(jsonObject, .object(expectedObject)) - } - - func testDecodeNestedArray() throws { - let nestedArray: [JSONValue] = [.string("a"), .string("b")] - let expectedObject: JSONObject = [ - "numberKey": .number(numberValue), - "arrayKey": .array(nestedArray), - ] - let json = """ - { - "numberKey": \(numberValue), - "arrayKey": ["a", "b"] - } - """ - let jsonData = try XCTUnwrap(json.data(using: .utf8)) - - let jsonObject = try XCTUnwrap(decoder.decode(JSONValue.self, from: jsonData)) - - XCTAssertEqual(jsonObject, .object(expectedObject)) - } - - func testEncodeNestedObject() throws { - let nestedObject: JSONObject = [ - "nestedKey": .string("nestedValue"), - ] - let objectValue: JSONObject = [ - "numberKey": .number(numberValue), - "objectKey": .object(nestedObject), - ] - - let jsonData = try encoder.encode(JSONValue.object(objectValue)) - let jsonObject = try XCTUnwrap(decoder.decode(JSONValue.self, from: jsonData)) - XCTAssertEqual(jsonObject, .object(objectValue)) - } - - func testEncodeNestedArray() throws { - let nestedArray: [JSONValue] = [.string("a"), .string("b")] - let objectValue: JSONObject = [ - "numberKey": .number(numberValue), - "arrayKey": .array(nestedArray), - ] - - let jsonData = try encoder.encode(JSONValue.object(objectValue)) - let jsonObject = try XCTUnwrap(decoder.decode(JSONValue.self, from: jsonData)) - XCTAssertEqual(jsonObject, .object(objectValue)) - } -} diff --git a/FirebaseAI/Tests/Unit/JSONValueTests.swift b/FirebaseAI/Tests/Unit/JSONValueTests.swift index 1ffe88eaf55..54ac3520e77 100644 --- a/FirebaseAI/Tests/Unit/JSONValueTests.swift +++ b/FirebaseAI/Tests/Unit/JSONValueTests.swift @@ -97,6 +97,48 @@ final class JSONValueTests: XCTestCase { XCTAssertEqual(json, "null") } + func testDecodeNestedObject() throws { + let nestedObject: JSONObject = [ + "nestedKey": .string("nestedValue"), + ] + let expectedObject: JSONObject = [ + "numberKey": .number(numberValue), + "objectKey": .object(nestedObject), + ] + let json = """ + { + "numberKey": \(numberValue), + "objectKey": { + "nestedKey": "nestedValue" + } + } + """ + let jsonData = try XCTUnwrap(json.data(using: .utf8)) + + let jsonObject = try XCTUnwrap(decoder.decode(JSONValue.self, from: jsonData)) + + XCTAssertEqual(jsonObject, .object(expectedObject)) + } + + func testDecodeNestedArray() throws { + let nestedArray: [JSONValue] = [.string("a"), .string("b")] + let expectedObject: JSONObject = [ + "numberKey": .number(numberValue), + "arrayKey": .array(nestedArray), + ] + let json = """ + { + "numberKey": \(numberValue), + "arrayKey": ["a", "b"] + } + """ + let jsonData = try XCTUnwrap(json.data(using: .utf8)) + + let jsonObject = try XCTUnwrap(decoder.decode(JSONValue.self, from: jsonData)) + + XCTAssertEqual(jsonObject, .object(expectedObject)) + } + func testEncodeNumber() throws { let jsonData = try encoder.encode(JSONValue.number(numberValue)) @@ -143,4 +185,30 @@ final class JSONValueTests: XCTestCase { let json = try XCTUnwrap(String(data: jsonData, encoding: .utf8)) XCTAssertEqual(json, "[null,\(numberValueEncoded)]") } + + func testEncodeNestedObject() throws { + let nestedObject: JSONObject = [ + "nestedKey": .string("nestedValue"), + ] + let objectValue: JSONObject = [ + "numberKey": .number(numberValue), + "objectKey": .object(nestedObject), + ] + + let jsonData = try encoder.encode(JSONValue.object(objectValue)) + let jsonObject = try XCTUnwrap(decoder.decode(JSONValue.self, from: jsonData)) + XCTAssertEqual(jsonObject, .object(objectValue)) + } + + func testEncodeNestedArray() throws { + let nestedArray: [JSONValue] = [.string("a"), .string("b")] + let objectValue: JSONObject = [ + "numberKey": .number(numberValue), + "arrayKey": .array(nestedArray), + ] + + let jsonData = try encoder.encode(JSONValue.object(objectValue)) + let jsonObject = try XCTUnwrap(decoder.decode(JSONValue.self, from: jsonData)) + XCTAssertEqual(jsonObject, .object(objectValue)) + } } diff --git a/FirebaseAI/Tests/Unit/PartsRepresentableTests+Coverage.swift b/FirebaseAI/Tests/Unit/PartsRepresentableTests+Coverage.swift deleted file mode 100644 index e7c001f2c17..00000000000 --- a/FirebaseAI/Tests/Unit/PartsRepresentableTests+Coverage.swift +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2025 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 -// -// http://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 XCTest -#if canImport(UIKit) - import UIKit -#elseif canImport(AppKit) - import AppKit -#endif - -@testable import FirebaseAI - -@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) -extension PartsRepresentableTests { - func testMixedParts() throws { - let text = "This is a test" - let data = try XCTUnwrap("This is some data".data(using: .utf8)) - let inlineData = InlineDataPart(data: data, mimeType: "text/plain") - - let parts: [any PartsRepresentable] = [text, inlineData] - let modelContent = ModelContent(parts: parts) - - XCTAssertEqual(modelContent.parts.count, 2) - let textPart = try XCTUnwrap(modelContent.parts[0] as? TextPart) - XCTAssertEqual(textPart.text, text) - let dataPart = try XCTUnwrap(modelContent.parts[1] as? InlineDataPart) - XCTAssertEqual(dataPart, inlineData) - } - - #if canImport(UIKit) - func testMixedParts_withImage() throws { - let text = "This is a test" - let image = try XCTUnwrap(UIImage(systemName: "star")) - let parts: [any PartsRepresentable] = [text, image] - let modelContent = ModelContent(parts: parts) - - XCTAssertEqual(modelContent.parts.count, 2) - let textPart = try XCTUnwrap(modelContent.parts[0] as? TextPart) - XCTAssertEqual(textPart.text, text) - let imagePart = try XCTUnwrap(modelContent.parts[1] as? InlineDataPart) - XCTAssertEqual(imagePart.mimeType, "image/jpeg") - XCTAssertFalse(imagePart.data.isEmpty) - } - - #elseif canImport(AppKit) - func testMixedParts_withImage() throws { - let text = "This is a test" - let coreImage = CIImage(color: CIColor.blue) - .cropped(to: CGRect(origin: CGPoint.zero, size: CGSize(width: 16, height: 16))) - let rep = NSCIImageRep(ciImage: coreImage) - let image = NSImage(size: rep.size) - image.addRepresentation(rep) - - let parts: [any PartsRepresentable] = [text, image] - let modelContent = ModelContent(parts: parts) - - XCTAssertEqual(modelContent.parts.count, 2) - let textPart = try XCTUnwrap(modelContent.parts[0] as? TextPart) - XCTAssertEqual(textPart.text, text) - let imagePart = try XCTUnwrap(modelContent.parts[1] as? InlineDataPart) - XCTAssertEqual(imagePart.mimeType, "image/jpeg") - XCTAssertFalse(imagePart.data.isEmpty) - } - #endif -} diff --git a/FirebaseAI/Tests/Unit/PartsRepresentableTests.swift b/FirebaseAI/Tests/Unit/PartsRepresentableTests.swift index e7531d1da9e..658db79a50e 100644 --- a/FirebaseAI/Tests/Unit/PartsRepresentableTests.swift +++ b/FirebaseAI/Tests/Unit/PartsRepresentableTests.swift @@ -121,4 +121,55 @@ final class PartsRepresentableTests: XCTestCase { } } #endif + + func testMixedParts() throws { + let text = "This is a test" + let data = try XCTUnwrap("This is some data".data(using: .utf8)) + let inlineData = InlineDataPart(data: data, mimeType: "text/plain") + + let parts: [any PartsRepresentable] = [text, inlineData] + let modelContent = ModelContent(parts: parts) + + XCTAssertEqual(modelContent.parts.count, 2) + let textPart = try XCTUnwrap(modelContent.parts[0] as? TextPart) + XCTAssertEqual(textPart.text, text) + let dataPart = try XCTUnwrap(modelContent.parts[1] as? InlineDataPart) + XCTAssertEqual(dataPart, inlineData) + } + + #if canImport(UIKit) + func testMixedParts_withImage() throws { + let text = "This is a test" + let image = try XCTUnwrap(UIImage(systemName: "star")) + let parts: [any PartsRepresentable] = [text, image] + let modelContent = ModelContent(parts: parts) + + XCTAssertEqual(modelContent.parts.count, 2) + let textPart = try XCTUnwrap(modelContent.parts[0] as? TextPart) + XCTAssertEqual(textPart.text, text) + let imagePart = try XCTUnwrap(modelContent.parts[1] as? InlineDataPart) + XCTAssertEqual(imagePart.mimeType, "image/jpeg") + XCTAssertFalse(imagePart.data.isEmpty) + } + + #elseif canImport(AppKit) + func testMixedParts_withImage() throws { + let text = "This is a test" + let coreImage = CIImage(color: CIColor.blue) + .cropped(to: CGRect(origin: CGPoint.zero, size: CGSize(width: 16, height: 16))) + let rep = NSCIImageRep(ciImage: coreImage) + let image = NSImage(size: rep.size) + image.addRepresentation(rep) + + let parts: [any PartsRepresentable] = [text, image] + let modelContent = ModelContent(parts: parts) + + XCTAssertEqual(modelContent.parts.count, 2) + let textPart = try XCTUnwrap(modelContent.parts[0] as? TextPart) + XCTAssertEqual(textPart.text, text) + let imagePart = try XCTUnwrap(modelContent.parts[1] as? InlineDataPart) + XCTAssertEqual(imagePart.mimeType, "image/jpeg") + XCTAssertFalse(imagePart.data.isEmpty) + } + #endif } diff --git a/FirebaseAI/Tests/Unit/SafetyTests.swift b/FirebaseAI/Tests/Unit/SafetyTests.swift index 680567617c4..4a1e07e04e3 100644 --- a/FirebaseAI/Tests/Unit/SafetyTests.swift +++ b/FirebaseAI/Tests/Unit/SafetyTests.swift @@ -21,6 +21,12 @@ final class SafetyTests: XCTestCase { let decoder = JSONDecoder() let encoder = JSONEncoder() + override func setUp() { + encoder.outputFormatting = .init( + arrayLiteral: .prettyPrinted, .sortedKeys, .withoutEscapingSlashes + ) + } + // MARK: - SafetyRating Decoding func testDecodeSafetyRating_allFieldsPresent() throws { @@ -87,12 +93,15 @@ final class SafetyTests: XCTestCase { threshold: .blockMediumAndAbove, method: .severity ) - encoder.outputFormatting = .sortedKeys let jsonData = try encoder.encode(setting) let jsonString = try XCTUnwrap(String(data: jsonData, encoding: .utf8)) XCTAssertEqual(jsonString, """ - {"category":"HARM_CATEGORY_HATE_SPEECH","method":"SEVERITY","threshold":"BLOCK_MEDIUM_AND_ABOVE"} + { + "category" : "HARM_CATEGORY_HATE_SPEECH", + "method" : "SEVERITY", + "threshold" : "BLOCK_MEDIUM_AND_ABOVE" + } """) } @@ -101,12 +110,14 @@ final class SafetyTests: XCTestCase { harmCategory: .sexuallyExplicit, threshold: .blockOnlyHigh ) - encoder.outputFormatting = .sortedKeys let jsonData = try encoder.encode(setting) let jsonString = try XCTUnwrap(String(data: jsonData, encoding: .utf8)) XCTAssertEqual(jsonString, """ - {"category":"HARM_CATEGORY_SEXUALLY_EXPLICIT","threshold":"BLOCK_ONLY_HIGH"} + { + "category" : "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "threshold" : "BLOCK_ONLY_HIGH" + } """) } }