diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/javatime.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/javatime.kt index 12f780d2883..a826f1d9038 100644 --- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/javatime.kt +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/javatime.kt @@ -24,6 +24,7 @@ import com.google.firebase.dataconnect.testutil.property.arbitrary.JavaTimeEdgeC import com.google.firebase.dataconnect.testutil.property.arbitrary.JavaTimeEdgeCases.MIN_NANO import com.google.firebase.dataconnect.testutil.property.arbitrary.JavaTimeEdgeCases.MIN_YEAR import com.google.firebase.dataconnect.testutil.toTimestamp +import io.kotest.common.mapError import io.kotest.property.Arb import io.kotest.property.arbitrary.arbitrary import io.kotest.property.arbitrary.choice @@ -31,7 +32,6 @@ import io.kotest.property.arbitrary.enum import io.kotest.property.arbitrary.int import io.kotest.property.arbitrary.of import io.kotest.property.arbitrary.orNull -import kotlin.random.nextInt import org.threeten.bp.Instant import org.threeten.bp.OffsetDateTime import org.threeten.bp.ZoneOffset @@ -153,7 +153,11 @@ private fun Instant.toFdcFieldRegex(): Regex { return Regex(pattern) } -data class Nanoseconds(val nanoseconds: Int, val string: String) +data class Nanoseconds( + val nanoseconds: Int, + val string: String, + val digitCounts: JavaTimeArbs.NanosecondComponents +) sealed interface TimeOffset { @@ -177,8 +181,12 @@ sealed interface TimeOffset { data class HhMm(val hours: Int, val minutes: Int, val sign: Sign) : TimeOffset { init { - require(hours in 0..18) { "invalid hours: $hours (must be in the closed range 0..23)" } - require(minutes in 0..59) { "invalid minutes: $minutes (must be in the closed range 0..59)" } + require(hours in validHours) { + "invalid hours: $hours (must be in the closed range $validHours)" + } + require(minutes in validMinutes) { + "invalid minutes: $minutes (must be in the closed range $validMinutes)" + } require(hours != 18 || minutes == 0) { "invalid minutes: $minutes (must be 0 when hours=18)" } } @@ -192,15 +200,44 @@ sealed interface TimeOffset { append("$minutes".padStart(2, '0')) } + fun toSeconds(): Int { + val absValue = (hours * SECONDS_PER_HOUR) + (minutes * SECONDS_PER_MINUTE) + return when (sign) { + Sign.Positive -> absValue + Sign.Negative -> -absValue + } + } + override fun toString() = "HhMm(hours=$hours, minutes=$minutes, sign=$sign, " + "zoneOffset=$zoneOffset, rfc3339String=$rfc3339String)" + operator fun compareTo(other: HhMm): Int = toSeconds() - other.toSeconds() + @Suppress("unused") enum class Sign(val char: Char, val multiplier: Int) { Positive('+', 1), Negative('-', -1), } + + companion object { + private const val SECONDS_PER_MINUTE: Int = 60 + private const val SECONDS_PER_HOUR: Int = 60 * SECONDS_PER_MINUTE + + val validHours = 0..18 + val validMinutes = 0..59 + + val maxSeconds: Int = 18 * SECONDS_PER_HOUR + + fun forSeconds(seconds: Int, sign: Sign): HhMm { + require(seconds in 0..maxSeconds) { + "invalid seconds: $seconds (must be between 0 and $maxSeconds, inclusive)" + } + val hours = seconds / SECONDS_PER_HOUR + val minutes = (seconds - (hours * SECONDS_PER_HOUR)) / SECONDS_PER_MINUTE + return HhMm(hours = hours, minutes = minutes, sign = sign) + } + } } } @@ -215,7 +252,6 @@ object JavaTimeArbs { val minuteArb = minute() val secondArb = second() val nanosecondArb = nanosecond().orNull(nullProbability = 0.15) - val timeOffsetArb = timeOffset() return arbitrary(JavaTimeInstantEdgeCases.all) { val year = yearArb.bind() @@ -226,7 +262,55 @@ object JavaTimeArbs { val minute = minuteArb.bind() val second = secondArb.bind() val nanosecond = nanosecondArb.bind() - val timeOffset = timeOffsetArb.bind() + + val instantUtc = + OffsetDateTime.of( + year, + month, + day, + hour, + minute, + second, + nanosecond?.nanoseconds ?: 0, + ZoneOffset.UTC, + ) + .toInstant() + + // The valid range below was copied from: + // com.google.firebase.Timestamp.Timestamp.validateRange() 253_402_300_800 + val validEpochSecondRange = -62_135_596_800..253_402_300_800 + + val numSecondsBelowMaxEpochSecond = validEpochSecondRange.last - instantUtc.epochSecond + require(numSecondsBelowMaxEpochSecond > 0) { + "internal error gh98nqedss: " + + "invalid numSecondsBelowMaxEpochSecond: $numSecondsBelowMaxEpochSecond" + } + val minTimeZoneOffset = + if (numSecondsBelowMaxEpochSecond >= TimeOffset.HhMm.maxSeconds) { + null + } else { + TimeOffset.HhMm.forSeconds( + numSecondsBelowMaxEpochSecond.toInt(), + TimeOffset.HhMm.Sign.Negative + ) + } + + val numSecondsAboveMinEpochSecond = instantUtc.epochSecond - validEpochSecondRange.first + require(numSecondsAboveMinEpochSecond > 0) { + "internal error mje6a4mrbm: " + + "invalid numSecondsAboveMinEpochSecond: $numSecondsAboveMinEpochSecond" + } + val maxTimeZoneOffset = + if (numSecondsAboveMinEpochSecond >= TimeOffset.HhMm.maxSeconds) { + null + } else { + TimeOffset.HhMm.forSeconds( + numSecondsAboveMinEpochSecond.toInt(), + TimeOffset.HhMm.Sign.Positive + ) + } + + val timeOffset = timeOffset(min = minTimeZoneOffset, max = maxTimeZoneOffset).bind() val instant = OffsetDateTime.of( @@ -241,6 +325,27 @@ object JavaTimeArbs { ) .toInstant() + require(instant.epochSecond >= validEpochSecondRange.first) { + "internal error weppxzqj2y: " + + "instant.epochSecond out of range by " + + "${validEpochSecondRange.first - instant.epochSecond}: ${instant.epochSecond} (" + + "validEpochSecondRange.first=${validEpochSecondRange.first}, " + + "year=$year, month=$month, day=$day, " + + "hour=$hour, minute=$minute, second=$second, " + + "nanosecond=$nanosecond timeOffset=$timeOffset, " + + "minTimeZoneOffset=$minTimeZoneOffset, maxTimeZoneOffset=$maxTimeZoneOffset)" + } + require(instant.epochSecond <= validEpochSecondRange.last) { + "internal error yxga5xy9bm: " + + "instant.epochSecond out of range by " + + "${instant.epochSecond - validEpochSecondRange.last}: ${instant.epochSecond} (" + + "validEpochSecondRange.last=${validEpochSecondRange.last}, " + + "year=$year, month=$month, day=$day, " + + "hour=$hour, minute=$minute, second=$second, " + + "nanosecond=$nanosecond timeOffset=$timeOffset, " + + "minTimeZoneOffset=$minTimeZoneOffset, maxTimeZoneOffset=$maxTimeZoneOffset)" + } + val string = buildString { append(year) append('-') @@ -268,7 +373,10 @@ object JavaTimeArbs { } } - fun timeOffset(): Arb = Arb.choice(timeOffsetUtc(), timeOffsetHhMm()) + fun timeOffset( + min: TimeOffset.HhMm?, + max: TimeOffset.HhMm?, + ): Arb = Arb.choice(timeOffsetUtc(), timeOffsetHhMm(min = min, max = max)) fun timeOffsetUtc( case: Arb = Arb.enum(), @@ -278,20 +386,45 @@ object JavaTimeArbs { sign: Arb = Arb.enum(), hour: Arb = Arb.positiveIntWithUniformNumDigitsProbability(0..18), minute: Arb = minute(), - ): Arb = - arbitrary( + min: TimeOffset.HhMm?, + max: TimeOffset.HhMm?, + ): Arb { + require(min === null || max === null || min.toSeconds() < max.toSeconds()) { + "min must be strictly less than max, but got: " + + "min=$min (${min!!.toSeconds()} seconds), " + + "max=$max (${max!!.toSeconds()} seconds), " + + "a difference of ${min.toSeconds() - max.toSeconds()} seconds" + } + + fun isBetweenMinAndMax(other: TimeOffset.HhMm): Boolean = + (min === null || other >= min) && (max === null || other <= max) + + return arbitrary( edgecases = listOf( - TimeOffset.HhMm(hours = 0, minutes = 0, sign = TimeOffset.HhMm.Sign.Positive), - TimeOffset.HhMm(hours = 0, minutes = 0, sign = TimeOffset.HhMm.Sign.Negative), - TimeOffset.HhMm(hours = 17, minutes = 59, sign = TimeOffset.HhMm.Sign.Positive), - TimeOffset.HhMm(hours = 17, minutes = 59, sign = TimeOffset.HhMm.Sign.Negative), - TimeOffset.HhMm(hours = 18, minutes = 0, sign = TimeOffset.HhMm.Sign.Positive), - TimeOffset.HhMm(hours = 18, minutes = 0, sign = TimeOffset.HhMm.Sign.Negative), - ) + TimeOffset.HhMm(hours = 0, minutes = 0, sign = TimeOffset.HhMm.Sign.Positive), + TimeOffset.HhMm(hours = 0, minutes = 0, sign = TimeOffset.HhMm.Sign.Negative), + TimeOffset.HhMm(hours = 17, minutes = 59, sign = TimeOffset.HhMm.Sign.Positive), + TimeOffset.HhMm(hours = 17, minutes = 59, sign = TimeOffset.HhMm.Sign.Negative), + TimeOffset.HhMm(hours = 18, minutes = 0, sign = TimeOffset.HhMm.Sign.Positive), + TimeOffset.HhMm(hours = 18, minutes = 0, sign = TimeOffset.HhMm.Sign.Negative), + ) + .filter(::isBetweenMinAndMax) ) { - TimeOffset.HhMm(hours = hour.bind(), minutes = minute.bind(), sign = sign.bind()) + var count = 0 + var hhmm: TimeOffset.HhMm + while (true) { + count++ + hhmm = TimeOffset.HhMm(hours = hour.bind(), minutes = minute.bind(), sign = sign.bind()) + if (isBetweenMinAndMax(hhmm)) { + break + } else if (count > 1000) { + throw Exception("internal error j878fp4gmr: exhausted attempts to generate HhMm") + } + } + hhmm } + } fun year(): Arb = Arb.int(MIN_YEAR..MAX_YEAR) @@ -316,8 +449,12 @@ object JavaTimeArbs { repeat(digitCounts.leadingZeroes) { append('0') } if (digitCounts.proper > 0) { append(nonZeroDigits.bind()) - repeat(digitCounts.proper - 2) { append(digits.bind()) } - append(nonZeroDigits.bind()) + if (digitCounts.proper > 1) { + if (digitCounts.proper > 2) { + repeat(digitCounts.proper - 2) { append(digits.bind()) } + } + append(nonZeroDigits.bind()) + } } repeat(digitCounts.trailingZeroes) { append('0') } } @@ -327,18 +464,29 @@ object JavaTimeArbs { if (nanosecondsStringTrimmed.isEmpty()) { 0 } else { - nanosecondsStringTrimmed.toInt() + val toIntResult = nanosecondsStringTrimmed.runCatching { toInt() } + toIntResult.mapError { exception -> + Exception( + "internal error qbdgapmye2: " + + "failed to parse nanosecondsStringTrimmed as an int: " + + "\"$nanosecondsStringTrimmed\" (digitCounts=$digitCounts)", + exception + ) + } + toIntResult.getOrThrow() } - Nanoseconds(nanosecondsInt, nanosecondsString) + check(nanosecondsInt in 0..999_999_999) { + "internal error c7j2myw6bd: " + + "nanosecondsStringTrimmed parsed to a value outside the valid range: " + + "$nanosecondsInt (digitCounts=$digitCounts)" + } + + Nanoseconds(nanosecondsInt, nanosecondsString, digitCounts) } } - private data class NanosecondComponents( - val leadingZeroes: Int, - val proper: Int, - val trailingZeroes: Int - ) + data class NanosecondComponents(val leadingZeroes: Int, val proper: Int, val trailingZeroes: Int) private fun nanosecondComponents(): Arb = arbitrary( diff --git a/firebase-functions/CHANGELOG.md b/firebase-functions/CHANGELOG.md index c26be0a15b6..e9fe66c897d 100644 --- a/firebase-functions/CHANGELOG.md +++ b/firebase-functions/CHANGELOG.md @@ -1,4 +1,6 @@ # Unreleased +* [fixed] Resolve Kotlin migration visibility issues + ([#6522](//github.com/firebase/firebase-android-sdk/pull/6522)) # 21.1.0 diff --git a/firebase-vertexai/api.txt b/firebase-vertexai/api.txt index 7bb2f629c51..02faa3674a4 100644 --- a/firebase-vertexai/api.txt +++ b/firebase-vertexai/api.txt @@ -183,14 +183,14 @@ package com.google.firebase.vertexai.type { } public final class CountTokensResponse { - ctor public CountTokensResponse(int totalTokens, Integer? totalBillableCharacters = null, java.util.List? promptTokensDetails = null); + ctor public CountTokensResponse(int totalTokens, Integer? totalBillableCharacters = null, java.util.List promptTokensDetails = emptyList()); method public operator int component1(); method public operator Integer? component2(); method public operator java.util.List? component3(); - method public java.util.List? getPromptTokensDetails(); + method public java.util.List getPromptTokensDetails(); method public Integer? getTotalBillableCharacters(); method public int getTotalTokens(); - property public final java.util.List? promptTokensDetails; + property public final java.util.List promptTokensDetails; property public final Integer? totalBillableCharacters; property public final int totalTokens; } @@ -582,16 +582,16 @@ package com.google.firebase.vertexai.type { } public final class UsageMetadata { - ctor public UsageMetadata(int promptTokenCount, Integer? candidatesTokenCount, int totalTokenCount, java.util.List? promptTokensDetails, java.util.List? candidatesTokensDetails); + ctor public UsageMetadata(int promptTokenCount, Integer? candidatesTokenCount, int totalTokenCount, java.util.List promptTokensDetails, java.util.List candidatesTokensDetails); method public Integer? getCandidatesTokenCount(); - method public java.util.List? getCandidatesTokensDetails(); + method public java.util.List getCandidatesTokensDetails(); method public int getPromptTokenCount(); - method public java.util.List? getPromptTokensDetails(); + method public java.util.List getPromptTokensDetails(); method public int getTotalTokenCount(); property public final Integer? candidatesTokenCount; - property public final java.util.List? candidatesTokensDetails; + property public final java.util.List candidatesTokensDetails; property public final int promptTokenCount; - property public final java.util.List? promptTokensDetails; + property public final java.util.List promptTokensDetails; property public final int totalTokenCount; } diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/CountTokensResponse.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/CountTokensResponse.kt index a6fe492862b..49f6b0433e0 100644 --- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/CountTokensResponse.kt +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/CountTokensResponse.kt @@ -36,7 +36,7 @@ import kotlinx.serialization.Serializable public class CountTokensResponse( public val totalTokens: Int, public val totalBillableCharacters: Int? = null, - public val promptTokensDetails: List? = null, + public val promptTokensDetails: List = emptyList(), ) { public operator fun component1(): Int = totalTokens @@ -55,7 +55,7 @@ public class CountTokensResponse( return CountTokensResponse( totalTokens, totalBillableCharacters ?: 0, - promptTokensDetails?.map { it.toPublic() } + promptTokensDetails?.map { it.toPublic() } ?: emptyList() ) } } diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/UsageMetadata.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/UsageMetadata.kt index 5ebbc3639d9..16200792f9c 100644 --- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/UsageMetadata.kt +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/UsageMetadata.kt @@ -33,8 +33,8 @@ public class UsageMetadata( public val promptTokenCount: Int, public val candidatesTokenCount: Int?, public val totalTokenCount: Int, - public val promptTokensDetails: List?, - public val candidatesTokensDetails: List?, + public val promptTokensDetails: List, + public val candidatesTokensDetails: List, ) { @Serializable @@ -51,8 +51,8 @@ public class UsageMetadata( promptTokenCount ?: 0, candidatesTokenCount ?: 0, totalTokenCount ?: 0, - promptTokensDetails = promptTokensDetails?.map { it.toPublic() }, - candidatesTokensDetails = candidatesTokensDetails?.map { it.toPublic() } + promptTokensDetails = promptTokensDetails?.map { it.toPublic() } ?: emptyList(), + candidatesTokensDetails = candidatesTokensDetails?.map { it.toPublic() } ?: emptyList() ) } } diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/UnarySnapshotTests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/UnarySnapshotTests.kt index e176fd8f7eb..11d5a0df052 100644 --- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/UnarySnapshotTests.kt +++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/UnarySnapshotTests.kt @@ -289,6 +289,7 @@ internal class UnarySnapshotTests { response.candidates.first().finishReason shouldBe FinishReason.STOP response.usageMetadata shouldNotBe null response.usageMetadata?.totalTokenCount shouldBe 363 + response.usageMetadata?.promptTokensDetails?.isEmpty() shouldBe true } } @@ -478,6 +479,7 @@ internal class UnarySnapshotTests { response.totalTokens shouldBe 6 response.totalBillableCharacters shouldBe 16 + response.promptTokensDetails.isEmpty() shouldBe true } }