Skip to content

Commit d62802a

Browse files
authored
fix(ai): Fix fraction seconds bug with ProtoDuration (#15410)
1 parent 455d291 commit d62802a

File tree

4 files changed

+134
-3
lines changed

4 files changed

+134
-3
lines changed

FirebaseAI/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Unreleased
22
- [fixed] Fixed various links in the Live API doc comments not mapping correctly.
3+
- [fixed] Fixed minor translation issue for nanosecond conversion when receiving
4+
`LiveServerGoingAwayNotice`. (#15410)
35

46
# 12.4.0
57
- [feature] Added support for the URL context tool, which allows the model to access content

FirebaseAI/Sources/Types/Internal/ProtoDuration.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,10 @@ extension ProtoDuration: Decodable {
9494
))
9595
}
9696

97-
guard let nanos = Int32(nanoseconds) else {
97+
guard let fractionalSeconds = Double("0.\(nanoseconds)") else {
9898
AILog.warning(
9999
code: .decodedInvalidProtoDurationNanoseconds,
100-
"Failed to parse the nanoseconds to an Int32: \(nanoseconds)."
100+
"Failed to parse the nanoseconds to a Double: \(nanoseconds)."
101101
)
102102

103103
throw DecodingError.dataCorrupted(.init(
@@ -107,6 +107,6 @@ extension ProtoDuration: Decodable {
107107
}
108108

109109
self.seconds = secs
110-
self.nanos = nanos
110+
nanos = Int32(fractionalSeconds * 1_000_000_000)
111111
}
112112
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import XCTest
16+
17+
/// Asserts that a string contains another string.
18+
///
19+
/// ```swift
20+
/// XCTAssertContains("my name is", "name")
21+
/// ```
22+
///
23+
/// - Parameters:
24+
/// - string: The source string that should contain the other.
25+
/// - contains: The string that should be contained in the source string.
26+
func XCTAssertContains(_ string: String, _ contains: String) {
27+
if !string.contains(contains) {
28+
XCTFail("(\"\(string)\") does not contain (\"\(contains)\")")
29+
}
30+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import XCTest
16+
17+
@testable import FirebaseAI
18+
19+
final class ProtoDurationTests: XCTestCase {
20+
let decoder = JSONDecoder()
21+
22+
private func decodeProtoDuration(_ jsonString: String) throws -> ProtoDuration {
23+
let escapedString = "\"\(jsonString)\""
24+
let jsonData = try XCTUnwrap(escapedString.data(using: .utf8))
25+
26+
return try decoder.decode(ProtoDuration.self, from: jsonData)
27+
}
28+
29+
private func expectDecodeFailure(_ jsonString: String) throws -> DecodingError.Context? {
30+
do {
31+
let _ = try decodeProtoDuration(jsonString)
32+
XCTFail("Expected decoding to fail")
33+
return nil
34+
} catch {
35+
let decodingError = try XCTUnwrap(error as? DecodingError)
36+
guard case let .dataCorrupted(dataCorrupted) = decodingError else {
37+
XCTFail("Error was not a data corrupted error")
38+
return nil
39+
}
40+
41+
return dataCorrupted
42+
}
43+
}
44+
45+
func testDecodeProtoDuration_standardDuration() throws {
46+
let duration = try decodeProtoDuration("120.000000123s")
47+
XCTAssertEqual(duration.seconds, 120)
48+
XCTAssertEqual(duration.nanos, 123)
49+
50+
XCTAssertEqual(duration.timeInterval, 120.000000123, accuracy: 1e-9)
51+
}
52+
53+
func testDecodeProtoDuration_withoutNanoseconds() throws {
54+
let duration = try decodeProtoDuration("120s")
55+
XCTAssertEqual(duration.seconds, 120)
56+
XCTAssertEqual(duration.nanos, 0)
57+
58+
XCTAssertEqual(duration.timeInterval, 120, accuracy: 1e-9)
59+
}
60+
61+
func testDecodeProtoDuration_maxNanosecondDigits() throws {
62+
let duration = try decodeProtoDuration("15.123456789s")
63+
XCTAssertEqual(duration.seconds, 15)
64+
XCTAssertEqual(duration.nanos, 123_456_789)
65+
66+
XCTAssertEqual(duration.timeInterval, 15.123456789, accuracy: 1e-9)
67+
}
68+
69+
func testDecodeProtoDuration_withMilliseconds() throws {
70+
let duration = try decodeProtoDuration("15.123s")
71+
XCTAssertEqual(duration.seconds, 15)
72+
XCTAssertEqual(duration.nanos, 123_000_000)
73+
74+
XCTAssertEqual(duration.timeInterval, 15.123, accuracy: 1e-9)
75+
}
76+
77+
func testDecodeProtoDuration_invalidSeconds() throws {
78+
guard let error = try expectDecodeFailure("invalid.123s") else { return }
79+
XCTAssertContains(error.debugDescription, "Invalid proto duration seconds")
80+
}
81+
82+
func testDecodeProtoDuration_invalidNanoseconds() throws {
83+
guard let error = try expectDecodeFailure("123.invalid") else { return }
84+
XCTAssertContains(error.debugDescription, "Invalid proto duration nanoseconds")
85+
}
86+
87+
func testDecodeProtoDuration_tooManyDecimals() throws {
88+
guard let error = try expectDecodeFailure("123.45.67") else { return }
89+
XCTAssertContains(error.debugDescription, "Invalid proto duration string")
90+
}
91+
92+
func testDecodeProtoDuration_withoutSuffix() throws {
93+
let duration = try decodeProtoDuration("123.456")
94+
XCTAssertEqual(duration.seconds, 123)
95+
XCTAssertEqual(duration.nanos, 456_000_000)
96+
97+
XCTAssertEqual(duration.timeInterval, 123.456, accuracy: 1e-9)
98+
}
99+
}

0 commit comments

Comments
 (0)