From 4ecc37208797aeb4116741c880f0d34734294b5b Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 21:51:44 +0000 Subject: [PATCH 1/7] feat: Add inlineDataParts property to GenerateContentResponse Adds a new computed property `inlineDataParts` to `GenerateContentResponse` to provide easier access to inline data parts (like images) within the model's response. This property iterates through the parts of the first candidate and filters for `InlineDataPart` instances. Error handling is included for cases with no candidates or no inline data parts. Unit tests have been added in `GenerativeModelTests.swift` to validate the functionality of the new property, covering scenarios with and without inline data, and responses with no candidates. --- .../Sources/GenerateContentResponse.swift | 26 ++++++ .../Tests/Unit/GenerativeModelTests.swift | 86 +++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/FirebaseVertexAI/Sources/GenerateContentResponse.swift b/FirebaseVertexAI/Sources/GenerateContentResponse.swift index a8a11a21e1f..27e45d0da98 100644 --- a/FirebaseVertexAI/Sources/GenerateContentResponse.swift +++ b/FirebaseVertexAI/Sources/GenerateContentResponse.swift @@ -88,6 +88,32 @@ public struct GenerateContentResponse: Sendable { } } + /// Returns inline data parts found in any `Part`s of the first candidate of the response, if any. + public var inlineDataParts: [InlineDataPart] { + guard let candidate = candidates.first else { + VertexLog.error( + code: .generateContentResponseNoCandidates, + "Could not get inline data parts from a response that had no candidates." + ) + return [] + } + let inlineData: [InlineDataPart] = candidate.content.parts.compactMap { part in + switch part { + case let inlineDataPart as InlineDataPart: + return inlineDataPart + default: + return nil + } + } + if inlineData.isEmpty { + VertexLog.warning( + code: .generateContentResponseNoInlineData, + "Could not find any inline data parts in the first candidate." + ) + } + return inlineData + } + /// Initializer for SwiftUI previews or tests. public init(candidates: [Candidate], promptFeedback: PromptFeedback? = nil, usageMetadata: UsageMetadata? = nil) { diff --git a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift index 8dd2cf07e42..f55dac26434 100644 --- a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift +++ b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift @@ -1538,6 +1538,92 @@ final class GenerativeModelTests: XCTestCase { XCTAssertEqual(response.totalTokens, 6) } + // MARK: - GenerateContentResponse Computed Properties + + func testGenerateContentResponse_inlineDataParts_success() throws { + // 1. Create mock parts + let imageData = Data("sample image data".utf8) // Placeholder data + let inlineDataPart = InlineDataPart(mimeType: "image/png", data: imageData) + let textPart = TextPart("This is the text part.") + + // 2. Create ModelContent + let modelContent = ModelContent(parts: [textPart, inlineDataPart]) // Mixed parts + + // 3. Create Candidate + let candidate = Candidate( + content: modelContent, + safetyRatings: [], // Assuming negligible for this test + finishReason: .stop, + citationMetadata: nil + ) + + // 4. Create GenerateContentResponse + let response = GenerateContentResponse(candidates: [candidate]) + + // 5. Assertions for inlineDataParts + let inlineParts = response.inlineDataParts + XCTAssertFalse(inlineParts.isEmpty, "inlineDataParts should not be empty.") + XCTAssertEqual(inlineParts.count, 1, "There should be exactly one InlineDataPart.") + + let firstInlinePart = try XCTUnwrap(inlineParts.first, "Could not get the first inline part.") + XCTAssertEqual(firstInlinePart.mimeType, "image/png", "MimeType should match.") + XCTAssertFalse(firstInlinePart.data.isEmpty, "Inline data should not be empty.") + XCTAssertEqual(firstInlinePart.data, imageData) // Verify data content + + // 6. Assertion for text (ensure other properties still work) + XCTAssertEqual(response.text, "This is the text part.") + + // 7. Assertion for function calls (ensure it's empty) + XCTAssertTrue(response.functionCalls.isEmpty, "functionCalls should be empty.") + } + + func testGenerateContentResponse_inlineDataParts_noInlineData() throws { + // 1. Create mock parts (only text) + let textPart = TextPart("This is the text part.") + let funcCallPart = FunctionCallPart(name: "testFunc", args: nil) // Add another part type + + // 2. Create ModelContent + let modelContent = ModelContent(parts: [textPart, funcCallPart]) + + // 3. Create Candidate + let candidate = Candidate( + content: modelContent, + safetyRatings: [], + finishReason: .stop, + citationMetadata: nil + ) + + // 4. Create GenerateContentResponse + let response = GenerateContentResponse(candidates: [candidate]) + + // 5. Assertions for inlineDataParts + let inlineParts = response.inlineDataParts + XCTAssertTrue(inlineParts.isEmpty, "inlineDataParts should be empty.") + + // 6. Assertion for text + XCTAssertEqual(response.text, "This is the text part.") + + // 7. Assertion for function calls + XCTAssertEqual(response.functionCalls.count, 1) + XCTAssertEqual(response.functionCalls.first?.name, "testFunc") + } + + func testGenerateContentResponse_inlineDataParts_noCandidates() throws { + // 1. Create GenerateContentResponse with no candidates + let response = GenerateContentResponse(candidates: []) + + // 2. Assertions for inlineDataParts + let inlineParts = response.inlineDataParts + XCTAssertTrue(inlineParts.isEmpty, "inlineDataParts should be empty when there are no candidates.") + + // 3. Assertion for text + XCTAssertNil(response.text, "Text should be nil when there are no candidates.") + + // 4. Assertion for function calls + XCTAssertTrue(response.functionCalls.isEmpty, "functionCalls should be empty when there are no candidates.") + } + + // MARK: - Helpers private func testFirebaseInfo(appCheck: AppCheckInterop? = nil, From a2e7116407ea78de8bb89cd4ce0675e9e52ed3c3 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Tue, 22 Apr 2025 15:14:49 -0700 Subject: [PATCH 2/7] Integration test, minor build fixes, and style --- .../Sources/GenerateContentResponse.swift | 6 -- .../GenerateContentIntegrationTests.swift | 4 + .../Tests/Unit/GenerativeModelTests.swift | 73 ++++++++++--------- 3 files changed, 43 insertions(+), 40 deletions(-) diff --git a/FirebaseVertexAI/Sources/GenerateContentResponse.swift b/FirebaseVertexAI/Sources/GenerateContentResponse.swift index 27e45d0da98..597a7f08292 100644 --- a/FirebaseVertexAI/Sources/GenerateContentResponse.swift +++ b/FirebaseVertexAI/Sources/GenerateContentResponse.swift @@ -105,12 +105,6 @@ public struct GenerateContentResponse: Sendable { return nil } } - if inlineData.isEmpty { - VertexLog.warning( - code: .generateContentResponseNoInlineData, - "Could not find any inline data parts in the first candidate." - ) - } return inlineData } diff --git a/FirebaseVertexAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift b/FirebaseVertexAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift index ca0d5bd7100..13136c93bc3 100644 --- a/FirebaseVertexAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift +++ b/FirebaseVertexAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift @@ -149,6 +149,10 @@ struct GenerateContentIntegrationTests { let candidate = try #require(response.candidates.first) let inlineDataPart = try #require(candidate.content.parts .first { $0 is InlineDataPart } as? InlineDataPart) + let inlineDataPartsViaAccessor = response.inlineDataParts + #expect(inlineDataPartsViaAccessor.count == 1) + let inlineDataPartViaAccessor = try #require(inlineDataPartsViaAccessor.first) + #expect(inlineDataPart == inlineDataPartViaAccessor) #expect(inlineDataPart.mimeType == "image/png") #expect(inlineDataPart.data.count > 0) #if canImport(UIKit) diff --git a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift index f55dac26434..e3e9a07e1eb 100644 --- a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift +++ b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift @@ -1543,7 +1543,7 @@ final class GenerativeModelTests: XCTestCase { func testGenerateContentResponse_inlineDataParts_success() throws { // 1. Create mock parts let imageData = Data("sample image data".utf8) // Placeholder data - let inlineDataPart = InlineDataPart(mimeType: "image/png", data: imageData) + let inlineDataPart = InlineDataPart(data: imageData, mimeType: "image/png") let textPart = TextPart("This is the text part.") // 2. Create ModelContent @@ -1574,56 +1574,61 @@ final class GenerativeModelTests: XCTestCase { XCTAssertEqual(response.text, "This is the text part.") // 7. Assertion for function calls (ensure it's empty) - XCTAssertTrue(response.functionCalls.isEmpty, "functionCalls should be empty.") + XCTAssertTrue(response.functionCalls.isEmpty, "functionCalls should be empty.") } func testGenerateContentResponse_inlineDataParts_noInlineData() throws { - // 1. Create mock parts (only text) - let textPart = TextPart("This is the text part.") - let funcCallPart = FunctionCallPart(name: "testFunc", args: nil) // Add another part type + // 1. Create mock parts (only text) + let textPart = TextPart("This is the text part.") + let funcCallPart = FunctionCallPart(name: "testFunc", args: [:]) // Add another part type - // 2. Create ModelContent - let modelContent = ModelContent(parts: [textPart, funcCallPart]) + // 2. Create ModelContent + let modelContent = ModelContent(parts: [textPart, funcCallPart]) - // 3. Create Candidate - let candidate = Candidate( - content: modelContent, - safetyRatings: [], - finishReason: .stop, - citationMetadata: nil - ) + // 3. Create Candidate + let candidate = Candidate( + content: modelContent, + safetyRatings: [], + finishReason: .stop, + citationMetadata: nil + ) - // 4. Create GenerateContentResponse - let response = GenerateContentResponse(candidates: [candidate]) + // 4. Create GenerateContentResponse + let response = GenerateContentResponse(candidates: [candidate]) - // 5. Assertions for inlineDataParts - let inlineParts = response.inlineDataParts - XCTAssertTrue(inlineParts.isEmpty, "inlineDataParts should be empty.") + // 5. Assertions for inlineDataParts + let inlineParts = response.inlineDataParts + XCTAssertTrue(inlineParts.isEmpty, "inlineDataParts should be empty.") - // 6. Assertion for text - XCTAssertEqual(response.text, "This is the text part.") + // 6. Assertion for text + XCTAssertEqual(response.text, "This is the text part.") - // 7. Assertion for function calls - XCTAssertEqual(response.functionCalls.count, 1) - XCTAssertEqual(response.functionCalls.first?.name, "testFunc") + // 7. Assertion for function calls + XCTAssertEqual(response.functionCalls.count, 1) + XCTAssertEqual(response.functionCalls.first?.name, "testFunc") } func testGenerateContentResponse_inlineDataParts_noCandidates() throws { - // 1. Create GenerateContentResponse with no candidates - let response = GenerateContentResponse(candidates: []) + // 1. Create GenerateContentResponse with no candidates + let response = GenerateContentResponse(candidates: []) - // 2. Assertions for inlineDataParts - let inlineParts = response.inlineDataParts - XCTAssertTrue(inlineParts.isEmpty, "inlineDataParts should be empty when there are no candidates.") + // 2. Assertions for inlineDataParts + let inlineParts = response.inlineDataParts + XCTAssertTrue( + inlineParts.isEmpty, + "inlineDataParts should be empty when there are no candidates." + ) - // 3. Assertion for text - XCTAssertNil(response.text, "Text should be nil when there are no candidates.") + // 3. Assertion for text + XCTAssertNil(response.text, "Text should be nil when there are no candidates.") - // 4. Assertion for function calls - XCTAssertTrue(response.functionCalls.isEmpty, "functionCalls should be empty when there are no candidates.") + // 4. Assertion for function calls + XCTAssertTrue( + response.functionCalls.isEmpty, + "functionCalls should be empty when there are no candidates." + ) } - // MARK: - Helpers private func testFirebaseInfo(appCheck: AppCheckInterop? = nil, From c4f685da779ac9543850b16d0514f500700ae87b Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Tue, 22 Apr 2025 15:19:28 -0700 Subject: [PATCH 3/7] Update FirebaseVertexAI/Sources/GenerateContentResponse.swift Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- FirebaseVertexAI/Sources/GenerateContentResponse.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FirebaseVertexAI/Sources/GenerateContentResponse.swift b/FirebaseVertexAI/Sources/GenerateContentResponse.swift index 597a7f08292..bad0129c945 100644 --- a/FirebaseVertexAI/Sources/GenerateContentResponse.swift +++ b/FirebaseVertexAI/Sources/GenerateContentResponse.swift @@ -97,7 +97,7 @@ public struct GenerateContentResponse: Sendable { ) return [] } - let inlineData: [InlineDataPart] = candidate.content.parts.compactMap { part in + let inlineDataParts: [InlineDataPart] = candidate.content.parts.compactMap { part in switch part { case let inlineDataPart as InlineDataPart: return inlineDataPart @@ -105,7 +105,7 @@ public struct GenerateContentResponse: Sendable { return nil } } - return inlineData + return inlineDataParts } /// Initializer for SwiftUI previews or tests. From 4015b329909dbd1b41c4de3b5b0d1e863a6bdcf2 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Tue, 22 Apr 2025 15:19:43 -0700 Subject: [PATCH 4/7] Update FirebaseVertexAI/Sources/GenerateContentResponse.swift Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- FirebaseVertexAI/Sources/GenerateContentResponse.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseVertexAI/Sources/GenerateContentResponse.swift b/FirebaseVertexAI/Sources/GenerateContentResponse.swift index bad0129c945..17ea7c55c42 100644 --- a/FirebaseVertexAI/Sources/GenerateContentResponse.swift +++ b/FirebaseVertexAI/Sources/GenerateContentResponse.swift @@ -93,7 +93,7 @@ public struct GenerateContentResponse: Sendable { guard let candidate = candidates.first else { VertexLog.error( code: .generateContentResponseNoCandidates, - "Could not get inline data parts from a response that had no candidates." + "Could not get inline data parts because the response has no candidates. The accessor only checks the first candidate." ) return [] } From 678e3ca0e8879783ea57fe6eb87659ded5a144fe Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Tue, 22 Apr 2025 16:30:22 -0700 Subject: [PATCH 5/7] Update FirebaseVertexAI/Sources/GenerateContentResponse.swift Co-authored-by: Andrew Heard --- FirebaseVertexAI/Sources/GenerateContentResponse.swift | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/FirebaseVertexAI/Sources/GenerateContentResponse.swift b/FirebaseVertexAI/Sources/GenerateContentResponse.swift index 17ea7c55c42..505f78b63ca 100644 --- a/FirebaseVertexAI/Sources/GenerateContentResponse.swift +++ b/FirebaseVertexAI/Sources/GenerateContentResponse.swift @@ -97,15 +97,7 @@ public struct GenerateContentResponse: Sendable { ) return [] } - let inlineDataParts: [InlineDataPart] = candidate.content.parts.compactMap { part in - switch part { - case let inlineDataPart as InlineDataPart: - return inlineDataPart - default: - return nil - } - } - return inlineDataParts + return candidate.content.parts.compactMap { $0 as? InlineDataPart } } /// Initializer for SwiftUI previews or tests. From 7ffdacbe62e48931b6e009ca3392b3066a0b326a Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Tue, 22 Apr 2025 16:30:28 -0700 Subject: [PATCH 6/7] Update FirebaseVertexAI/Sources/GenerateContentResponse.swift Co-authored-by: Andrew Heard --- FirebaseVertexAI/Sources/GenerateContentResponse.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/FirebaseVertexAI/Sources/GenerateContentResponse.swift b/FirebaseVertexAI/Sources/GenerateContentResponse.swift index 505f78b63ca..0ca92040091 100644 --- a/FirebaseVertexAI/Sources/GenerateContentResponse.swift +++ b/FirebaseVertexAI/Sources/GenerateContentResponse.swift @@ -91,10 +91,10 @@ public struct GenerateContentResponse: Sendable { /// Returns inline data parts found in any `Part`s of the first candidate of the response, if any. public var inlineDataParts: [InlineDataPart] { guard let candidate = candidates.first else { - VertexLog.error( - code: .generateContentResponseNoCandidates, - "Could not get inline data parts because the response has no candidates. The accessor only checks the first candidate." - ) + VertexLog.error(code: .generateContentResponseNoCandidates, """ + Could not get inline data parts because the response has no candidates. The accessor only \ + checks the first candidate. + """) return [] } return candidate.content.parts.compactMap { $0 as? InlineDataPart } From 73eac1afa4b7a83cd151533a3ded4df5883c94bd Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Wed, 23 Apr 2025 17:43:54 -0400 Subject: [PATCH 7/7] Move `inlineDataParts` tests to new `GenerateContentResponseTests` file --- .../Tests/Unit/GenerativeModelTests.swift | 91 --------------- .../Types/GenerateContentResponseTests.swift | 109 ++++++++++++++++++ 2 files changed, 109 insertions(+), 91 deletions(-) create mode 100644 FirebaseVertexAI/Tests/Unit/Types/GenerateContentResponseTests.swift diff --git a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift index e3e9a07e1eb..8dd2cf07e42 100644 --- a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift +++ b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift @@ -1538,97 +1538,6 @@ final class GenerativeModelTests: XCTestCase { XCTAssertEqual(response.totalTokens, 6) } - // MARK: - GenerateContentResponse Computed Properties - - func testGenerateContentResponse_inlineDataParts_success() throws { - // 1. Create mock parts - let imageData = Data("sample image data".utf8) // Placeholder data - let inlineDataPart = InlineDataPart(data: imageData, mimeType: "image/png") - let textPart = TextPart("This is the text part.") - - // 2. Create ModelContent - let modelContent = ModelContent(parts: [textPart, inlineDataPart]) // Mixed parts - - // 3. Create Candidate - let candidate = Candidate( - content: modelContent, - safetyRatings: [], // Assuming negligible for this test - finishReason: .stop, - citationMetadata: nil - ) - - // 4. Create GenerateContentResponse - let response = GenerateContentResponse(candidates: [candidate]) - - // 5. Assertions for inlineDataParts - let inlineParts = response.inlineDataParts - XCTAssertFalse(inlineParts.isEmpty, "inlineDataParts should not be empty.") - XCTAssertEqual(inlineParts.count, 1, "There should be exactly one InlineDataPart.") - - let firstInlinePart = try XCTUnwrap(inlineParts.first, "Could not get the first inline part.") - XCTAssertEqual(firstInlinePart.mimeType, "image/png", "MimeType should match.") - XCTAssertFalse(firstInlinePart.data.isEmpty, "Inline data should not be empty.") - XCTAssertEqual(firstInlinePart.data, imageData) // Verify data content - - // 6. Assertion for text (ensure other properties still work) - XCTAssertEqual(response.text, "This is the text part.") - - // 7. Assertion for function calls (ensure it's empty) - XCTAssertTrue(response.functionCalls.isEmpty, "functionCalls should be empty.") - } - - func testGenerateContentResponse_inlineDataParts_noInlineData() throws { - // 1. Create mock parts (only text) - let textPart = TextPart("This is the text part.") - let funcCallPart = FunctionCallPart(name: "testFunc", args: [:]) // Add another part type - - // 2. Create ModelContent - let modelContent = ModelContent(parts: [textPart, funcCallPart]) - - // 3. Create Candidate - let candidate = Candidate( - content: modelContent, - safetyRatings: [], - finishReason: .stop, - citationMetadata: nil - ) - - // 4. Create GenerateContentResponse - let response = GenerateContentResponse(candidates: [candidate]) - - // 5. Assertions for inlineDataParts - let inlineParts = response.inlineDataParts - XCTAssertTrue(inlineParts.isEmpty, "inlineDataParts should be empty.") - - // 6. Assertion for text - XCTAssertEqual(response.text, "This is the text part.") - - // 7. Assertion for function calls - XCTAssertEqual(response.functionCalls.count, 1) - XCTAssertEqual(response.functionCalls.first?.name, "testFunc") - } - - func testGenerateContentResponse_inlineDataParts_noCandidates() throws { - // 1. Create GenerateContentResponse with no candidates - let response = GenerateContentResponse(candidates: []) - - // 2. Assertions for inlineDataParts - let inlineParts = response.inlineDataParts - XCTAssertTrue( - inlineParts.isEmpty, - "inlineDataParts should be empty when there are no candidates." - ) - - // 3. Assertion for text - XCTAssertNil(response.text, "Text should be nil when there are no candidates.") - - // 4. Assertion for function calls - XCTAssertTrue( - response.functionCalls.isEmpty, - "functionCalls should be empty when there are no candidates." - ) - } - // MARK: - Helpers private func testFirebaseInfo(appCheck: AppCheckInterop? = nil, diff --git a/FirebaseVertexAI/Tests/Unit/Types/GenerateContentResponseTests.swift b/FirebaseVertexAI/Tests/Unit/Types/GenerateContentResponseTests.swift new file mode 100644 index 00000000000..08a2dbab61e --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/Types/GenerateContentResponseTests.swift @@ -0,0 +1,109 @@ +// 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 FirebaseVertexAI +import XCTest + +@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) +final class GenerateContentResponseTests: XCTestCase { + // MARK: - GenerateContentResponse Computed Properties + + func testGenerateContentResponse_inlineDataParts_success() throws { + let imageData = Data("sample image data".utf8) + let inlineDataPart = InlineDataPart(data: imageData, mimeType: "image/png") + let textPart = TextPart("This is the text part.") + let modelContent = ModelContent(parts: [textPart, inlineDataPart]) + let candidate = Candidate( + content: modelContent, + safetyRatings: [], + finishReason: nil, + citationMetadata: nil + ) + let response = GenerateContentResponse(candidates: [candidate]) + + let inlineParts = response.inlineDataParts + + XCTAssertFalse(inlineParts.isEmpty, "inlineDataParts should not be empty.") + XCTAssertEqual(inlineParts.count, 1, "There should be exactly one InlineDataPart.") + let firstInlinePart = try XCTUnwrap(inlineParts.first, "Could not get the first inline part.") + XCTAssertEqual(firstInlinePart.mimeType, inlineDataPart.mimeType, "MimeType should match.") + XCTAssertEqual(firstInlinePart.data, imageData) + XCTAssertEqual(response.text, textPart.text) + XCTAssertTrue(response.functionCalls.isEmpty, "functionCalls should be empty.") + } + + func testGenerateContentResponse_inlineDataParts_multipleInlineDataParts_success() throws { + let imageData1 = Data("sample image data 1".utf8) + let inlineDataPart1 = InlineDataPart(data: imageData1, mimeType: "image/png") + let imageData2 = Data("sample image data 2".utf8) + let inlineDataPart2 = InlineDataPart(data: imageData2, mimeType: "image/jpeg") + let modelContent = ModelContent(parts: [inlineDataPart1, inlineDataPart2]) + let candidate = Candidate( + content: modelContent, + safetyRatings: [], + finishReason: nil, + citationMetadata: nil + ) + let response = GenerateContentResponse(candidates: [candidate]) + + let inlineParts = response.inlineDataParts + + XCTAssertFalse(inlineParts.isEmpty, "inlineDataParts should not be empty.") + XCTAssertEqual(inlineParts.count, 2, "There should be exactly two InlineDataParts.") + let firstInlinePart = try XCTUnwrap(inlineParts.first, "Could not get the first inline part.") + XCTAssertEqual(firstInlinePart.mimeType, inlineDataPart1.mimeType, "MimeType should match.") + XCTAssertEqual(firstInlinePart.data, imageData1) + let secondInlinePart = try XCTUnwrap(inlineParts.last, "Could not get the second inline part.") + XCTAssertEqual(secondInlinePart.mimeType, inlineDataPart2.mimeType, "MimeType should match.") + XCTAssertEqual(secondInlinePart.data, imageData2) + XCTAssertNil(response.text) + XCTAssertTrue(response.functionCalls.isEmpty, "functionCalls should be empty.") + } + + func testGenerateContentResponse_inlineDataParts_noInlineData() throws { + let textPart = TextPart("This is the text part.") + let functionCallPart = FunctionCallPart(name: "testFunc", args: [:]) + let modelContent = ModelContent(parts: [textPart, functionCallPart]) + let candidate = Candidate( + content: modelContent, + safetyRatings: [], + finishReason: nil, + citationMetadata: nil + ) + let response = GenerateContentResponse(candidates: [candidate]) + + let inlineParts = response.inlineDataParts + + XCTAssertTrue(inlineParts.isEmpty, "inlineDataParts should be empty.") + XCTAssertEqual(response.text, "This is the text part.") + XCTAssertEqual(response.functionCalls.count, 1) + XCTAssertEqual(response.functionCalls.first?.name, "testFunc") + } + + func testGenerateContentResponse_inlineDataParts_noCandidates() throws { + let response = GenerateContentResponse(candidates: []) + + let inlineParts = response.inlineDataParts + + XCTAssertTrue( + inlineParts.isEmpty, + "inlineDataParts should be empty when there are no candidates." + ) + XCTAssertNil(response.text, "Text should be nil when there are no candidates.") + XCTAssertTrue( + response.functionCalls.isEmpty, + "functionCalls should be empty when there are no candidates." + ) + } +}