From 2b41dff3836c1ffec604ac333f4c7b9dd519e377 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare <dconeybe@google.com> Date: Wed, 13 Nov 2024 19:47:10 +0000 Subject: [PATCH 01/13] dataconnect: DataConnectExecutableVersions.json updated with version 1.7.0 --- .../plugin/DataConnectExecutableVersions.json | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/firebase-dataconnect/gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/plugin/DataConnectExecutableVersions.json b/firebase-dataconnect/gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/plugin/DataConnectExecutableVersions.json index fc1c279feca..886c8eeacb1 100644 --- a/firebase-dataconnect/gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/plugin/DataConnectExecutableVersions.json +++ b/firebase-dataconnect/gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/plugin/DataConnectExecutableVersions.json @@ -1,5 +1,5 @@ { - "defaultVersion": "1.6.1", + "defaultVersion": "1.7.0", "versions": [ { "version": "1.3.4", @@ -270,6 +270,24 @@ "os": "linux", "size": 25223320, "sha512DigestHex": "5e8002a048b55a358d6d4a8c59c30ef8c3615461fd400bf4c8e86e84cc1b6482da604cb2635ec1639276c3b9c9f22a9c570f6729934e4fc77b09c8ccb6f3b986" + }, + { + "version": "1.7.0", + "os": "windows", + "size": 25783808, + "sha512DigestHex": "9fc0bf918ea2c20bc8dcf26efd101e7a567a13bf7b0967c16f35989e3557d0055edce6522b23fb70361f26f3ad1abd93458af5b33d3fa3019333ca72680353a2" + }, + { + "version": "1.7.0", + "os": "macos", + "size": 25350912, + "sha512DigestHex": "f887290a6083c3c88ee92f532c6fceb993a64714d5703ea7c389381222eab05998e8a25e0bee0770686433d5e7915fe6bfd58b3a0098d13ad0800c4e515fee0d" + }, + { + "version": "1.7.0", + "os": "linux", + "size": 25272472, + "sha512DigestHex": "795c3f63a3c78b94204ae8c525227f3295a02cd90e553f52bde543029a91f68da0d17653cc6b4c863ed778104fd2baa97a729f80ab4bd54dd5dd4f5e15354b7a" } ] } \ No newline at end of file From 799f33d731bdde04393c84100d197fd8b42dc133 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare <dconeybe@google.com> Date: Wed, 13 Nov 2024 19:53:36 +0000 Subject: [PATCH 02/13] ListVariablesAndDataIntegrationTest.kt fixed --- .../demo/ListVariablesAndDataIntegrationTest.kt | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/ListVariablesAndDataIntegrationTest.kt b/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/ListVariablesAndDataIntegrationTest.kt index 0125ae2cb15..98adca0a0e9 100644 --- a/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/ListVariablesAndDataIntegrationTest.kt +++ b/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/ListVariablesAndDataIntegrationTest.kt @@ -17,12 +17,12 @@ package com.google.firebase.dataconnect.connectors.demo import com.google.firebase.Timestamp +import com.google.firebase.dataconnect.LocalDate import com.google.firebase.dataconnect.connectors.demo.testutil.DemoConnectorIntegrationTestBase import com.google.firebase.dataconnect.testutil.property.arbitrary.DataConnectArb import com.google.firebase.dataconnect.testutil.property.arbitrary.EdgeCases import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect -import com.google.firebase.dataconnect.testutil.property.arbitrary.dateTestData -import com.google.firebase.dataconnect.testutil.property.arbitrary.toJavaUtilDate +import com.google.firebase.dataconnect.testutil.property.arbitrary.localDate import com.google.firebase.dataconnect.testutil.withMicrosecondPrecision import io.kotest.common.ExperimentalKotest import io.kotest.matchers.shouldBe @@ -37,7 +37,6 @@ import io.kotest.property.arbitrary.long import io.kotest.property.arbitrary.map import io.kotest.property.arbitrary.uuid import io.kotest.property.checkAll -import java.util.Date import java.util.UUID import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.test.runTest @@ -475,7 +474,7 @@ class ListVariablesAndDataIntegrationTest : DemoConnectorIntegrationTestBase() { val booleans: List<Boolean>, val uuids: List<UUID>, val int64s: List<Long>, - val dates: List<Date>, + val dates: List<LocalDate>, val timestamps: List<Timestamp>, ) { @@ -524,7 +523,7 @@ class ListVariablesAndDataIntegrationTest : DemoConnectorIntegrationTestBase() { booleans = EdgeCases.booleans, uuids = EdgeCases.uuids, int64s = EdgeCases.int64s, - dates = EdgeCases.dates.all().map { it.toJavaUtilDate() }, + dates = EdgeCases.dates.all().map { it.date }, timestamps = EdgeCases.javaTime.instants.all.map { it.timestamp }, ) } @@ -546,8 +545,7 @@ class ListVariablesAndDataIntegrationTest : DemoConnectorIntegrationTestBase() { booleans: Arb<List<Boolean>> = Arb.list(Arb.boolean(), 1..100), uuids: Arb<List<UUID>> = Arb.list(Arb.uuid(), 1..100), int64s: Arb<List<Long>> = Arb.list(Arb.long(), 1..100), - dates: Arb<List<Date>> = - Arb.list(Arb.dataConnect.dateTestData().map { it.toJavaUtilDate() }, 1..100), + dates: Arb<List<LocalDate>> = Arb.list(Arb.dataConnect.localDate(), 1..100), timestamps: Arb<List<Timestamp>> = Arb.list(Arb.dataConnect.javaTime.instantTestCase().map { it.timestamp }, 1..100), ): Arb<Lists> = arbitrary { From 941c39804dce790a078c14725222a389c5fb0734 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare <dconeybe@google.com> Date: Wed, 13 Nov 2024 19:54:23 +0000 Subject: [PATCH 03/13] KeyVariablesIntegrationTest.kt fixed --- .../dataconnect/connectors/demo/KeyVariablesIntegrationTest.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/KeyVariablesIntegrationTest.kt b/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/KeyVariablesIntegrationTest.kt index 0ec62809cf0..231be08834f 100644 --- a/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/KeyVariablesIntegrationTest.kt +++ b/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/KeyVariablesIntegrationTest.kt @@ -19,7 +19,6 @@ package com.google.firebase.dataconnect.connectors.demo import com.google.firebase.dataconnect.connectors.demo.testutil.DemoConnectorIntegrationTestBase import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect import com.google.firebase.dataconnect.testutil.property.arbitrary.dateTestData -import com.google.firebase.dataconnect.testutil.property.arbitrary.toJavaUtilDate import com.google.firebase.dataconnect.testutil.randomTimestamp import com.google.firebase.dataconnect.testutil.withMicrosecondPrecision import io.kotest.matchers.shouldBe @@ -86,7 +85,7 @@ class KeyVariablesIntegrationTest : DemoConnectorIntegrationTestBase() { @Test fun primaryKeyIsDate() = runTest { - val id = Arb.dataConnect.dateTestData().next(rs).toJavaUtilDate() + val id = Arb.dataConnect.dateTestData().next(rs).date val value = Arb.dataConnect.string().next(rs) val key = connector.insertPrimaryKeyIsDate.execute(foo = id, value = value).data.key From 76528b240bbf70832716fcd1b5d12ec8b7d7b554 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare <dconeybe@google.com> Date: Wed, 13 Nov 2024 20:15:49 +0000 Subject: [PATCH 04/13] DateScalarIntegrationTest.kt fixed --- .../demo/DateScalarIntegrationTest.kt | 84 +++++++++---------- 1 file changed, 40 insertions(+), 44 deletions(-) diff --git a/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/DateScalarIntegrationTest.kt b/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/DateScalarIntegrationTest.kt index dca905d755d..67eac700af0 100644 --- a/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/DateScalarIntegrationTest.kt +++ b/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/DateScalarIntegrationTest.kt @@ -20,15 +20,14 @@ package com.google.firebase.dataconnect.connectors.demo import com.google.firebase.dataconnect.DataConnectException import com.google.firebase.dataconnect.ExperimentalFirebaseDataConnect +import com.google.firebase.dataconnect.LocalDate import com.google.firebase.dataconnect.connectors.demo.testutil.DemoConnectorIntegrationTestBase import com.google.firebase.dataconnect.generated.GeneratedMutation import com.google.firebase.dataconnect.generated.GeneratedQuery -import com.google.firebase.dataconnect.testutil.dateFromYearMonthDayUTC import com.google.firebase.dataconnect.testutil.executeWithEmptyVariables import com.google.firebase.dataconnect.testutil.property.arbitrary.EdgeCases import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect import com.google.firebase.dataconnect.testutil.property.arbitrary.dateTestData -import com.google.firebase.dataconnect.testutil.property.arbitrary.toJavaUtilDate import io.kotest.assertions.assertSoftly import io.kotest.assertions.throwables.shouldThrow import io.kotest.assertions.withClue @@ -47,7 +46,7 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() { @Test fun nonNullDate_insert_NormalCases() = runTest { checkAll(20, Arb.dataConnect.dateTestData()) { - val key = connector.insertNonNullDate.execute(it.toJavaUtilDate()).data.key + val key = connector.insertNonNullDate.execute(it.date).data.key assertNonNullDateByKeyEquals(key, it.string) } } @@ -56,7 +55,7 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() { fun nonNullDate_insert_EdgeCases() = runTest { assertSoftly { EdgeCases.dates.all().forEach { - val key = connector.insertNonNullDate.execute(it.toJavaUtilDate()).data.key + val key = connector.insertNonNullDate.execute(it.date).data.key assertNonNullDateByKeyEquals(key, it.string) } } @@ -70,14 +69,14 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() { // "2024-03-27" if it did the erroneous conversion to UTC before taking the YYYY-MM-DD. val date = "2024-03-26T19:48:00.144-07:00" val key = connector.insertNonNullDate.executeWithStringVariables(date).data.key - assertNonNullDateByKeyEquals(key, dateFromYearMonthDayUTC(2024, 3, 26)) + assertNonNullDateByKeyEquals(key, LocalDate(2024, 3, 26)) } @Test fun nonNullDate_insert_ShouldIgnoreTime() = runTest { val date = "2024-03-26T19:48:00.144Z" val key = connector.insertNonNullDate.executeWithStringVariables(date).data.key - assertNonNullDateByKeyEquals(key, dateFromYearMonthDayUTC(2024, 3, 26)) + assertNonNullDateByKeyEquals(key, LocalDate(2024, 3, 26)) } @Test @@ -92,9 +91,9 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() { queryResult.data shouldBe GetNonNullDatesWithDefaultsByKeyQuery.Data( GetNonNullDatesWithDefaultsByKeyQuery.Data.NonNullDatesWithDefaults( - valueWithVariableDefault = dateFromYearMonthDayUTC(6904, 11, 30), - valueWithSchemaDefault = dateFromYearMonthDayUTC(2112, 1, 31), - epoch = EdgeCases.dates.epoch.toJavaUtilDate(), + valueWithVariableDefault = LocalDate(6904, 11, 30), + valueWithSchemaDefault = LocalDate(2112, 1, 31), + epoch = EdgeCases.dates.epoch.date, requestTime1 = expectedRequestTime, requestTime2 = expectedRequestTime, ) @@ -134,8 +133,8 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() { @Test fun nonNullDate_update_NormalCases() = runTest { checkAll(20, Arb.dataConnect.dateTestData(), Arb.dataConnect.dateTestData()) { date1, date2 -> - val key = connector.insertNonNullDate.execute(date1.toJavaUtilDate()).data.key - connector.updateNonNullDate.execute(key) { value = date2.toJavaUtilDate() } + val key = connector.insertNonNullDate.execute(date1.date).data.key + connector.updateNonNullDate.execute(key) { value = date2.date } assertNonNullDateByKeyEquals(key, date2.string) } } @@ -151,8 +150,8 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() { assertSoftly { for ((date1, date2) in dates1.zip(dates2)) { withClue("date1=${date1.string} date2=${date2.string}") { - val key = connector.insertNonNullDate.execute(date1.toJavaUtilDate()).data.key - connector.updateNonNullDate.execute(key) { value = date2.toJavaUtilDate() } + val key = connector.insertNonNullDate.execute(date1.date).data.key + connector.updateNonNullDate.execute(key) { value = date2.date } assertNonNullDateByKeyEquals(key, date2.string) } } @@ -162,15 +161,15 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() { @Test fun nonNullDate_update_DateVariableOmitted() = runTest { val date = Arb.dataConnect.dateTestData().next(rs) - val key = connector.insertNonNullDate.execute(date.toJavaUtilDate()).data.key + val key = connector.insertNonNullDate.execute(date.date).data.key connector.updateNonNullDate.execute(key) {} - assertNonNullDateByKeyEquals(key, date.toJavaUtilDate()) + assertNonNullDateByKeyEquals(key, date.date) } @Test fun nullableDate_insert_NormalCases() = runTest { checkAll(20, Arb.dataConnect.dateTestData()) { - val key = connector.insertNullableDate.execute { value = it.toJavaUtilDate() }.data.key + val key = connector.insertNullableDate.execute { value = it.date }.data.key assertNullableDateByKeyEquals(key, it.string) } } @@ -180,7 +179,7 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() { val edgeCases = EdgeCases.dates.all() + listOf(null) assertSoftly { edgeCases.forEach { - val key = connector.insertNullableDate.execute { value = it?.toJavaUtilDate() }.data.key + val key = connector.insertNullableDate.execute { value = it?.date }.data.key if (it === null) { assertNullableDateByKeyHasNullInnerValue(key) } else { @@ -204,14 +203,14 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() { // "2024-03-27" if it did the erroneous conversion to UTC before taking the YYYY-MM-DD. val date = "2024-03-26T19:48:00.144-07:00" val key = connector.insertNullableDate.executeWithStringVariables(date).data.key - assertNullableDateByKeyEquals(key, dateFromYearMonthDayUTC(2024, 3, 26)) + assertNullableDateByKeyEquals(key, LocalDate(2024, 3, 26)) } @Test fun nullableDate_insert_ShouldIgnoreTime() = runTest { val date = "2024-03-26T19:48:00.144Z" val key = connector.insertNullableDate.executeWithStringVariables(date).data.key - assertNullableDateByKeyEquals(key, dateFromYearMonthDayUTC(2024, 3, 26)) + assertNullableDateByKeyEquals(key, LocalDate(2024, 3, 26)) } @Test @@ -242,9 +241,9 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() { queryResult.data shouldBe GetNullableDatesWithDefaultsByKeyQuery.Data( GetNullableDatesWithDefaultsByKeyQuery.Data.NullableDatesWithDefaults( - valueWithVariableDefault = dateFromYearMonthDayUTC(8113, 2, 9), - valueWithSchemaDefault = dateFromYearMonthDayUTC(1921, 12, 2), - epoch = EdgeCases.dates.epoch.toJavaUtilDate(), + valueWithVariableDefault = LocalDate(8113, 2, 9), + valueWithSchemaDefault = LocalDate(1921, 12, 2), + epoch = EdgeCases.dates.epoch.date, requestTime1 = expectedRequestTime, requestTime2 = expectedRequestTime, ) @@ -254,8 +253,8 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() { @Test fun nullableDate_update_NormalCases() = runTest { checkAll(20, Arb.dataConnect.dateTestData(), Arb.dataConnect.dateTestData()) { date1, date2 -> - val key = connector.insertNullableDate.execute { value = date1.toJavaUtilDate() }.data.key - connector.updateNullableDate.execute(key) { value = date2.toJavaUtilDate() } + val key = connector.insertNullableDate.execute { value = date1.date }.data.key + connector.updateNullableDate.execute(key) { value = date2.date } assertNullableDateByKeyEquals(key, date2.string) } } @@ -271,8 +270,8 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() { assertSoftly { for ((date1, date2) in dates1.zip(dates2)) { withClue("date1=${date1.string} date2=${date2.string}") { - val key = connector.insertNullableDate.execute { value = date1.toJavaUtilDate() }.data.key - connector.updateNullableDate.execute(key) { value = date2.toJavaUtilDate() } + val key = connector.insertNullableDate.execute { value = date1.date }.data.key + connector.updateNullableDate.execute(key) { value = date2.date } assertNullableDateByKeyEquals(key, date2.string) } } @@ -281,26 +280,26 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() { @Test fun nullableDate_update_UpdateNonNullValueToNull() = runTest { - val date = Arb.dataConnect.dateTestData().next(rs).toJavaUtilDate() - val key = connector.insertNullableDate.execute { value = date }.data.key + val date = Arb.dataConnect.dateTestData().next(rs) + val key = connector.insertNullableDate.execute { value = date.date }.data.key connector.updateNullableDate.execute(key) { value = null } assertNullableDateByKeyHasNullInnerValue(key) } @Test fun nullableDate_update_UpdateNullValueToNonNull() = runTest { - val date = Arb.dataConnect.dateTestData().next(rs).toJavaUtilDate() + val date = Arb.dataConnect.dateTestData().next(rs) val key = connector.insertNullableDate.execute { value = null }.data.key - connector.updateNullableDate.execute(key) { value = date } - assertNullableDateByKeyEquals(key, date) + connector.updateNullableDate.execute(key) { value = date.date } + assertNullableDateByKeyEquals(key, date.date) } @Test fun nullableDate_update_DateVariableOmitted() = runTest { - val date = Arb.dataConnect.dateTestData().next(rs).toJavaUtilDate() - val key = connector.insertNullableDate.execute { value = date }.data.key + val date = Arb.dataConnect.dateTestData().next(rs) + val key = connector.insertNullableDate.execute { value = date.date }.data.key connector.updateNullableDate.execute(key) {} - assertNullableDateByKeyEquals(key, date) + assertNullableDateByKeyEquals(key, date.date) } private suspend fun assertNonNullDateByKeyEquals(key: NonNullDateKey, expected: String) { @@ -311,7 +310,7 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() { queryResult.data shouldBe GetDateByKeyQueryStringData(expected) } - private suspend fun assertNonNullDateByKeyEquals(key: NonNullDateKey, expected: java.util.Date) { + private suspend fun assertNonNullDateByKeyEquals(key: NonNullDateKey, expected: LocalDate) { val queryResult = connector.getNonNullDateByKey.execute(key) queryResult.data shouldBe GetNonNullDateByKeyQuery.Data(GetNonNullDateByKeyQuery.Data.Value(expected)) @@ -334,10 +333,7 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() { queryResult.data shouldBe GetDateByKeyQueryStringData(expected) } - private suspend fun assertNullableDateByKeyEquals( - key: NullableDateKey, - expected: java.util.Date - ) { + private suspend fun assertNullableDateByKeyEquals(key: NullableDateKey, expected: LocalDate) { val queryResult = connector.getNullableDateByKey.execute(key) queryResult.data shouldBe GetNullableDateByKeyQuery.Data(GetNullableDateByKeyQuery.Data.Value(expected)) @@ -345,8 +341,8 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() { /** * A `Data` type that can be used in place of [GetNonNullDateByKeyQuery.Data] that types the value - * as a [String] instead of a [java.util.Date], allowing verification of the data sent over the - * wire without possible confounding from date deserialization. + * as a [String] instead of a [LocalDate], allowing verification of the data sent over the wire + * without possible confounding from date deserialization. */ @Serializable private data class GetDateByKeyQueryStringData(val value: DateStringValue?) { @@ -357,14 +353,14 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() { /** * A `Variables` type that can be used in place of [InsertNonNullDateMutation.Variables] that - * types the value as a [String] instead of a [java.util.Date], allowing verification of the data - * sent over the wire without possible confounding from date serialization. + * types the value as a [String] instead of a [LocalDate], allowing verification of the data sent + * over the wire without possible confounding from date serialization. */ @Serializable private data class InsertDateStringVariables(val value: String?) /** * A `Variables` type that can be used in place of [InsertNonNullDateMutation.Variables] that - * types the value as a [Int] instead of a [java.util.Date], allowing verification that the server + * types the value as a [Int] instead of a [LocalDate], allowing verification that the server * fails with an expected error (rather than crashing, for example). */ @Serializable private data class InsertDateIntVariables(val value: Int) From 8addb04550719ce007f50ee35a1ed035de32875f Mon Sep 17 00:00:00 2001 From: Denver Coneybeare <dconeybe@google.com> Date: Wed, 13 Nov 2024 20:16:24 +0000 Subject: [PATCH 05/13] Remove unused test helper functions related to java.util.Date --- .../dataconnect/testutil/DateTimeTestUtils.kt | 21 ------------------- .../testutil/property/arbitrary/dates.kt | 4 ---- 2 files changed, 25 deletions(-) diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/DateTimeTestUtils.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/DateTimeTestUtils.kt index f3d3018d2dd..a0cfdcbcf3c 100644 --- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/DateTimeTestUtils.kt +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/DateTimeTestUtils.kt @@ -18,31 +18,10 @@ package com.google.firebase.dataconnect.testutil import com.google.firebase.Timestamp import java.util.Calendar -import java.util.Date import java.util.GregorianCalendar import java.util.TimeZone import kotlin.random.Random -/** - * Creates and returns a new [Date] object that represents the given year, month, and day in UTC. - * - * @param year The year; must be between 0 and 9999, inclusive. - * @param month The month; must be between 1 and 12, inclusive. - * @param day The day of the month; must be between 1 and 31, inclusive. - */ -fun dateFromYearMonthDayUTC(year: Int, month: Int, day: Int): Date { - require(year in 0..9999) { "year must be between 0 and 9999, inclusive" } - require(month in 1..12) { "month must be between 1 and 12, inclusive" } - require(day in 1..31) { "day must be between 1 and 31, inclusive" } - - return GregorianCalendar(TimeZone.getTimeZone("UTC")) - .apply { - set(year, month - 1, day, 0, 0, 0) - set(Calendar.MILLISECOND, 0) - } - .time -} - /** Generates and returns a random [Timestamp] object. */ fun randomTimestamp(): Timestamp { val nanoseconds = Random.nextInt(1_000_000_000) diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/dates.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/dates.kt index 6959ccbd50b..6ed685f0476 100644 --- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/dates.kt +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/dates.kt @@ -20,7 +20,6 @@ package com.google.firebase.dataconnect.testutil.property.arbitrary import com.google.firebase.dataconnect.LocalDate import com.google.firebase.dataconnect.testutil.NullableReference -import com.google.firebase.dataconnect.testutil.dateFromYearMonthDayUTC import com.google.firebase.dataconnect.testutil.dayRangeInYear import com.google.firebase.dataconnect.testutil.property.arbitrary.DateEdgeCases.MAX_YEAR import com.google.firebase.dataconnect.testutil.property.arbitrary.DateEdgeCases.MIN_YEAR @@ -102,9 +101,6 @@ data class DateTestData( val string: String, ) -fun DateTestData.toJavaUtilDate(): java.util.Date = - dateFromYearMonthDayUTC(year = date.year, month = date.month, day = date.day) - @Suppress("MemberVisibilityCanBePrivate") object DateEdgeCases { // See https://en.wikipedia.org/wiki/ISO_8601#Years for rationale of lower bound of 1583. From ef7b70b1eca8d00a8f226078a1c7dfaf7eecd243 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare <dconeybe@google.com> Date: Wed, 13 Nov 2024 21:21:59 +0000 Subject: [PATCH 06/13] DateSerializer removed, as it is superceded by LocalDateSerializer --- .../dataconnect/serializers/DateSerializer.kt | 76 ------------------- 1 file changed, 76 deletions(-) delete mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/DateSerializer.kt diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/DateSerializer.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/DateSerializer.kt deleted file mode 100644 index 6ff9cb79c13..00000000000 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/DateSerializer.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2024 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. - */ - -package com.google.firebase.dataconnect.serializers - -import java.util.Calendar -import java.util.Date -import java.util.GregorianCalendar -import java.util.TimeZone -import java.util.regex.Matcher -import java.util.regex.Pattern -import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder - -/** - * An implementation of [KSerializer] for serializing and deserializing [Date] objects in the wire - * format expected by the Firebase Data Connect backend. - */ -public object DateSerializer : KSerializer<Date> { - - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("Date", PrimitiveKind.STRING) - - override fun serialize(encoder: Encoder, value: Date) { - val calendar = GregorianCalendar(TimeZone.getTimeZone("UTC")) - calendar.time = value - - val year = calendar.get(Calendar.YEAR) - val month = calendar.get(Calendar.MONTH) + 1 - val day = calendar.get(Calendar.DAY_OF_MONTH) - - val serializedDate = - "$year".padStart(4, '0') + '-' + "$month".padStart(2, '0') + '-' + "$day".padStart(2, '0') - encoder.encodeString(serializedDate) - } - - override fun deserialize(decoder: Decoder): Date { - val serializedDate = decoder.decodeString() - - val matcher = Pattern.compile("^(\\d{4})-(\\d{2})-(\\d{2})$").matcher(serializedDate) - require(matcher.matches()) { "date does not match regular expression: ${matcher.pattern()}" } - - fun Matcher.groupToIntIgnoringLeadingZeroes(index: Int): Int { - val groupText = group(index)!!.trimStart('0') - return if (groupText.isEmpty()) 0 else groupText.toInt() - } - - val year = matcher.groupToIntIgnoringLeadingZeroes(1) - val month = matcher.groupToIntIgnoringLeadingZeroes(2) - val day = matcher.groupToIntIgnoringLeadingZeroes(3) - - return GregorianCalendar(TimeZone.getTimeZone("UTC")) - .apply { - set(year, month - 1, day, 0, 0, 0) - set(Calendar.MILLISECOND, 0) - } - .time - } -} From 4574d4e4c2edb012d987b135b974c470e66b3b71 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare <dconeybe@google.com> Date: Wed, 13 Nov 2024 21:25:41 +0000 Subject: [PATCH 07/13] firebase-dataconnect/api.txt updated by running ./gradlew :firebase-dataconnect:generateApiTxtFile --- firebase-dataconnect/api.txt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/firebase-dataconnect/api.txt b/firebase-dataconnect/api.txt index e1ebc29d5b8..11e44aa0dae 100644 --- a/firebase-dataconnect/api.txt +++ b/firebase-dataconnect/api.txt @@ -310,14 +310,6 @@ package com.google.firebase.dataconnect.serializers { field @NonNull public static final com.google.firebase.dataconnect.serializers.AnyValueSerializer INSTANCE; } - public final class DateSerializer implements kotlinx.serialization.KSerializer<java.util.Date> { - method @NonNull public java.util.Date deserialize(@NonNull kotlinx.serialization.encoding.Decoder decoder); - method @NonNull public kotlinx.serialization.descriptors.SerialDescriptor getDescriptor(); - method public void serialize(@NonNull kotlinx.serialization.encoding.Encoder encoder, @NonNull java.util.Date value); - property @NonNull public kotlinx.serialization.descriptors.SerialDescriptor descriptor; - field @NonNull public static final com.google.firebase.dataconnect.serializers.DateSerializer INSTANCE; - } - public final class LocalDateSerializer implements kotlinx.serialization.KSerializer<com.google.firebase.dataconnect.LocalDate> { method @NonNull public com.google.firebase.dataconnect.LocalDate deserialize(@NonNull kotlinx.serialization.encoding.Decoder decoder); method @NonNull public kotlinx.serialization.descriptors.SerialDescriptor getDescriptor(); From b806a077d07e867fd7e3e9637471c5f7f8a3e0fc Mon Sep 17 00:00:00 2001 From: Denver Coneybeare <dconeybe@google.com> Date: Wed, 13 Nov 2024 21:34:18 +0000 Subject: [PATCH 08/13] firebase-dataconnect/CHANGELOG.md updated --- firebase-dataconnect/CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/firebase-dataconnect/CHANGELOG.md b/firebase-dataconnect/CHANGELOG.md index 8acc4d43026..d7840f19c2b 100644 --- a/firebase-dataconnect/CHANGELOG.md +++ b/firebase-dataconnect/CHANGELOG.md @@ -23,6 +23,13 @@ * [changed] Replaced java.util.Date with com.google.firebase.dataconnect.LocalDate. ([#6434](https://github.com/firebase/firebase-android-sdk/pull/6434)) +* [changed] `DateSerializer` removed, as it is superceded by + `LocalDateSerializer`. + As of Data Connect emulator version 1.7.0, the generated Kotlin code uses + `com.google.firebase.dataconnect.LocalDate` instead of `java.util.Date`. + Therefore, this version of the SDK must be paired with an appropriate version + of the Data Connect emulator. + ([#NNNN](https://github.com/firebase/firebase-android-sdk/pull/NNNN)) # 16.0.0-beta02 * [changed] Updated protobuf dependency to `3.25.5` to fix From c2c9273a1435b3722731df35945e9f8a02616707 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare <dconeybe@google.com> Date: Wed, 13 Nov 2024 21:38:40 +0000 Subject: [PATCH 09/13] firebase-dataconnect/CHANGELOG.md: update PR number --- firebase-dataconnect/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-dataconnect/CHANGELOG.md b/firebase-dataconnect/CHANGELOG.md index d7840f19c2b..449f1ddd3a1 100644 --- a/firebase-dataconnect/CHANGELOG.md +++ b/firebase-dataconnect/CHANGELOG.md @@ -29,7 +29,7 @@ `com.google.firebase.dataconnect.LocalDate` instead of `java.util.Date`. Therefore, this version of the SDK must be paired with an appropriate version of the Data Connect emulator. - ([#NNNN](https://github.com/firebase/firebase-android-sdk/pull/NNNN)) + ([#6513](https://github.com/firebase/firebase-android-sdk/pull/6513)) # 16.0.0-beta02 * [changed] Updated protobuf dependency to `3.25.5` to fix From 990c43ad58ac9cd742aca8612713a8251009920e Mon Sep 17 00:00:00 2001 From: Denver Coneybeare <dconeybe@google.com> Date: Thu, 14 Nov 2024 03:16:22 +0000 Subject: [PATCH 10/13] DateScalarIntegrationTest.kt rewritten --- .../demo/DateScalarIntegrationTest.kt | 1137 ++++++++++++----- .../dataconnect/connector/demo/demo_ops.gql | 52 - .../dataconnect/schema/demo_schema.gql | 24 - .../testutil/property/arbitrary/dates.kt | 7 + 4 files changed, 830 insertions(+), 390 deletions(-) diff --git a/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/DateScalarIntegrationTest.kt b/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/DateScalarIntegrationTest.kt index 67eac700af0..31478621e69 100644 --- a/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/DateScalarIntegrationTest.kt +++ b/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/DateScalarIntegrationTest.kt @@ -14,28 +14,50 @@ * limitations under the License. */ -@file:OptIn(ExperimentalFirebaseDataConnect::class) +@file:OptIn(ExperimentalKotest::class, ExperimentalFirebaseDataConnect::class) package com.google.firebase.dataconnect.connectors.demo import com.google.firebase.dataconnect.DataConnectException import com.google.firebase.dataconnect.ExperimentalFirebaseDataConnect import com.google.firebase.dataconnect.LocalDate +import com.google.firebase.dataconnect.MutationResult +import com.google.firebase.dataconnect.QueryResult import com.google.firebase.dataconnect.connectors.demo.testutil.DemoConnectorIntegrationTestBase -import com.google.firebase.dataconnect.generated.GeneratedMutation import com.google.firebase.dataconnect.generated.GeneratedQuery -import com.google.firebase.dataconnect.testutil.executeWithEmptyVariables +import com.google.firebase.dataconnect.testutil.property.arbitrary.DateTestData import com.google.firebase.dataconnect.testutil.property.arbitrary.EdgeCases +import com.google.firebase.dataconnect.testutil.property.arbitrary.ThreeDateTestDatas +import com.google.firebase.dataconnect.testutil.property.arbitrary.ThreeDateTestDatas.ItemNumber import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect import com.google.firebase.dataconnect.testutil.property.arbitrary.dateTestData +import com.google.firebase.dataconnect.testutil.property.arbitrary.invalidDateScalarString +import com.google.firebase.dataconnect.testutil.property.arbitrary.localDate +import com.google.firebase.dataconnect.testutil.property.arbitrary.orNullableReference +import com.google.firebase.dataconnect.testutil.property.arbitrary.threeNonNullDatesTestData +import com.google.firebase.dataconnect.testutil.property.arbitrary.threePossiblyNullDatesTestData +import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingText +import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingTextIgnoringCase +import com.google.firebase.dataconnect.testutil.toTheeTenAbpJavaLocalDate +import io.kotest.assertions.asClue import io.kotest.assertions.assertSoftly import io.kotest.assertions.throwables.shouldThrow import io.kotest.assertions.withClue +import io.kotest.common.ExperimentalKotest +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldBeIn +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.kotest.property.Arb -import io.kotest.property.arbitrary.int +import io.kotest.property.EdgeConfig +import io.kotest.property.PropTestConfig import io.kotest.property.arbitrary.next +import io.kotest.property.arbitrary.withEdgecases import io.kotest.property.checkAll +import java.util.UUID +import kotlin.time.Duration.Companion.minutes import kotlinx.coroutines.test.runTest import kotlinx.serialization.Serializable import kotlinx.serialization.serializer @@ -43,416 +65,903 @@ import org.junit.Test class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() { + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for CRUD operations on this table: + // type DateNonNullable @table { value: Date!, tag: String } + ////////////////////////////////////////////////////////////////////////////////////////////////// + @Test - fun nonNullDate_insert_NormalCases() = runTest { - checkAll(20, Arb.dataConnect.dateTestData()) { - val key = connector.insertNonNullDate.execute(it.date).data.key - assertNonNullDateByKeyEquals(key, it.string) + fun dateNonNullable_MutationLocalDateVariable() = + runTest(timeout = 1.minutes) { + checkAll(propTestConfig, Arb.dataConnect.dateTestData()) { testData -> + val insertResult = connector.dateNonNullableInsert.execute(testData.date) + val returnedString = + connector.dateNonNullableGetByKey.executeWithStringData(insertResult.data.key) + returnedString shouldBe testData.string + } } - } @Test - fun nonNullDate_insert_EdgeCases() = runTest { - assertSoftly { - EdgeCases.dates.all().forEach { - val key = connector.insertNonNullDate.execute(it.date).data.key - assertNonNullDateByKeyEquals(key, it.string) + fun dateNonNullable_MutationStringVariable() = + runTest(timeout = 1.minutes) { + checkAll(propTestConfig, Arb.dataConnect.dateTestData()) { testData -> + val insertResult = connector.dateNonNullableInsert.execute(testData.string) + val queryResult = connector.dateNonNullableGetByKey.execute(insertResult.data.key) + queryResult.data.item?.value shouldBe testData.date } } - } @Test - fun nonNullDate_insert_ShouldIgnoreTimeZone() = runTest { - // Use a date that, when converted to UTC, in on a different date to verify that the server does - // the expected thing; that is, that it _drops_ the time zone information (rather than - // converting the date to UTC then taking the YYYY-MM-DD of that). The server would use the date - // "2024-03-27" if it did the erroneous conversion to UTC before taking the YYYY-MM-DD. - val date = "2024-03-26T19:48:00.144-07:00" - val key = connector.insertNonNullDate.executeWithStringVariables(date).data.key - assertNonNullDateByKeyEquals(key, LocalDate(2024, 3, 26)) - } + fun dateNonNullable_QueryLocalDateVariable() = + dateNonNullable_QueryVariable { tag, dateTestData -> + connector.dateNonNullableGetAllByTagAndValue.execute(tag = tag, dateTestData.date) + } @Test - fun nonNullDate_insert_ShouldIgnoreTime() = runTest { - val date = "2024-03-26T19:48:00.144Z" - val key = connector.insertNonNullDate.executeWithStringVariables(date).data.key - assertNonNullDateByKeyEquals(key, LocalDate(2024, 3, 26)) - } + fun dateNonNullable_QueryStringVariable() = dateNonNullable_QueryVariable { tag, dateTestData -> + connector.dateNonNullableGetAllByTagAndValue.execute(tag = tag, value = dateTestData.string) + } + + private fun dateNonNullable_QueryVariable( + executeQuery: + suspend (tag: String, date: DateTestData) -> QueryResult< + DateNonNullableGetAllByTagAndValueQuery.Data, * + > + ) = + runTest(timeout = 1.minutes) { + checkAll( + propTestConfig, + Arb.dataConnect.tag(), + Arb.dataConnect.threeNonNullDatesTestData() + ) { tag, testDatas -> + val insertResult = connector.dateNonNullableInsert3.execute(tag, testDatas) + val queryResult = executeQuery(tag, testDatas.selected!!) + val matchingIds = testDatas.idsMatchingSelected(insertResult) + queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds + } + } @Test - fun nonNullDatesWithDefaults_insert_ShouldUseDefaultValuesIfNoVariablesSpecified() = runTest { - val key = connector.insertNonNullDatesWithDefaults.execute {}.data.key - val queryResult = connector.getNonNullDatesWithDefaultsByKey.execute(key) + fun dateNonNullable_MutationNullVariableShouldThrow() = runTest { + val exception = + shouldThrow<DataConnectException> { connector.dateNonNullableInsert.execute(null) } + assertSoftly { + exception.message shouldContainWithNonAbuttingText "\$value" + exception.message shouldContainWithNonAbuttingText "is null" + } + } - // Since we can't know the exact value of `request.time` just make sure that the exact same - // value is used for both fields to which it is set. - val expectedRequestTime = queryResult.data.nonNullDatesWithDefaults!!.requestTime1 + @Test + fun dateNonNullable_QueryNullVariableShouldThrow() = runTest { + val tag = Arb.dataConnect.tag().next(rs) + val exception = + shouldThrow<DataConnectException> { + connector.dateNonNullableGetAllByTagAndValue.execute(tag = tag, value = null) + } + assertSoftly { + exception.message shouldContainWithNonAbuttingText "\$value" + exception.message shouldContainWithNonAbuttingText "is null" + } + } - queryResult.data shouldBe - GetNonNullDatesWithDefaultsByKeyQuery.Data( - GetNonNullDatesWithDefaultsByKeyQuery.Data.NonNullDatesWithDefaults( - valueWithVariableDefault = LocalDate(6904, 11, 30), - valueWithSchemaDefault = LocalDate(2112, 1, 31), - epoch = EdgeCases.dates.epoch.date, - requestTime1 = expectedRequestTime, - requestTime2 = expectedRequestTime, - ) + @Test + fun dateNonNullable_QueryOmittedVariableShouldMatchAll() = runTest { + val tag = Arb.dataConnect.tag().next(rs) + val testDatas = Arb.dataConnect.threeNonNullDatesTestData().next(rs) + val insertResult = connector.dateNonNullableInsert3.execute(tag, testDatas) + val queryResult = connector.dateNonNullableGetAllByTagAndMaybeValue.execute(tag) {} + queryResult.data.items + .map { it.id } + .shouldContainExactlyInAnyOrder( + insertResult.data.key1.id, + insertResult.data.key2.id, + insertResult.data.key3.id ) } @Test - fun nonNullDate_insert_ShouldFailIfDateVariableIsNull() = runTest { - shouldThrow<DataConnectException> { - connector.insertNonNullDate.executeWithStringVariables(null).data.key - } + fun dateNonNullable_QueryNullVariableShouldMatchNone() = runTest { + val tag = Arb.dataConnect.tag().next(rs) + val testDatas = Arb.dataConnect.threeNonNullDatesTestData().next(rs) + connector.dateNonNullableInsert3.execute(tag, testDatas) + val queryResult = + connector.dateNonNullableGetAllByTagAndMaybeValue.execute(tag) { value = null } + queryResult.data.items.shouldBeEmpty() } @Test - fun nonNullDate_insert_ShouldFailIfDateVariableIsAnInt() = runTest { - shouldThrow<DataConnectException> { - connector.insertNonNullDate.executeWithIntVariables(Arb.int().next(rs)).data.key + fun dateNonNullable_Update() = + runTest(timeout = 1.minutes) { + checkAll(propTestConfig, Arb.dataConnect.localDate(), Arb.dataConnect.localDate()) { + date1, + date2 -> + val insertResult = connector.dateNonNullableInsert.execute(date1) + val updateResult = + connector.dateNonNullableUpdateByKey.execute(insertResult.data.key) { value = date2 } + updateResult.asClue { it.data.key shouldBe insertResult.data.key } + val queryResult = connector.dateNonNullableGetByKey.execute(insertResult.data.key) + val item = withClue("queryResult.data.item") { queryResult.data.item.shouldNotBeNull() } + item.value shouldBe date2 + } } - } @Test - fun nonNullDate_insert_ShouldFailIfDateVariableIsOmitted() = runTest { - shouldThrow<DataConnectException> { - connector.insertNonNullDate.executeWithEmptyVariables().data.key + fun dateNonNullable_UpdateToNullShouldFail() = + runTest(timeout = 1.minutes) { + checkAll(propTestConfig, Arb.dataConnect.localDate()) { date -> + val insertResult = connector.dateNonNullableInsert.execute(date) + shouldThrow<DataConnectException> { + connector.dateNonNullableUpdateByKey.execute(insertResult.data.key) { value = null } + } + } } - } @Test - fun nonNullDate_insert_ShouldFailIfDateVariableIsMalformed() = runTest { - for (invalidDate in invalidDates) { - shouldThrow<DataConnectException> { - connector.insertNonNullDate.executeWithStringVariables(invalidDate).data.key + fun dateNonNullable_UpdateToOmittedShouldLeaveValueUnchanged() = + runTest(timeout = 1.minutes) { + checkAll(propTestConfig, Arb.dataConnect.localDate()) { date -> + val insertResult = connector.dateNonNullableInsert.execute(date) + val updateResult = connector.dateNonNullableUpdateByKey.execute(insertResult.data.key) {} + updateResult.asClue { it.data.key shouldBe insertResult.data.key } + val queryResult = connector.dateNonNullableGetByKey.execute(insertResult.data.key) + val item = withClue("queryResult.data.item") { queryResult.data.item.shouldNotBeNull() } + item.value shouldBe date } } - } @Test - fun nonNullDate_update_NormalCases() = runTest { - checkAll(20, Arb.dataConnect.dateTestData(), Arb.dataConnect.dateTestData()) { date1, date2 -> - val key = connector.insertNonNullDate.execute(date1.date).data.key - connector.updateNonNullDate.execute(key) { value = date2.date } - assertNonNullDateByKeyEquals(key, date2.string) + fun dateNonNullable_UpdateMany() = + runTest(timeout = 1.minutes) { + checkAll( + propTestConfig, + Arb.dataConnect.tag(), + Arb.dataConnect.threeNonNullDatesTestData(), + Arb.dataConnect.localDate() + ) { tag, testDatas, date2 -> + val insertResult = connector.dateNonNullableInsert3.execute(tag, testDatas) + val selectedDate = testDatas.selected!! + val updateResult = + connector.dateNonNullableUpdateByTagAndValue.execute(tag) { + value = selectedDate.date + newValue = date2 + } + withClue("updateResult.data.count") { + updateResult.data.count shouldBe testDatas.numMatchingSelected + } + val queryResult = connector.dateNonNullableGetAllByTagAndValue.execute(tag, date2) + val matchingIds1 = testDatas.idsMatchingSelected(insertResult) + val matchingIds2 = testDatas.idsMatching(insertResult, date2) + val matchingIds = (matchingIds1 + matchingIds2).distinct() + queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds + } } - } @Test - fun nonNullDate_update_EdgeCases() = runTest { - val edgeCases = EdgeCases.dates.all() - val dates1 = - edgeCases + List(edgeCases.size) { Arb.dataConnect.dateTestData().next(rs) } + edgeCases - val dates2 = - List(edgeCases.size) { Arb.dataConnect.dateTestData().next(rs) } + edgeCases + edgeCases + fun dateNonNullable_UpdateManyNullValueShouldUpdateNone() = + runTest(timeout = 1.minutes) { + checkAll( + propTestConfig, + Arb.dataConnect.tag(), + Arb.dataConnect.threeNonNullDatesTestData(), + Arb.dataConnect.localDate() + ) { tag, testDatas, date2 -> + val insertResult = connector.dateNonNullableInsert3.execute(tag, testDatas) + val updateResult = + connector.dateNonNullableUpdateByTagAndValue.execute(tag) { + value = null + newValue = date2 + } + withClue("updateResult.data.count") { updateResult.data.count shouldBe 0 } + val queryResult = connector.dateNonNullableGetAllByTagAndMaybeValue.execute(tag) {} + queryResult.data.items + .map { it.id } + .shouldContainExactlyInAnyOrder( + insertResult.data.key1.id, + insertResult.data.key2.id, + insertResult.data.key3.id, + ) + } + } - assertSoftly { - for ((date1, date2) in dates1.zip(dates2)) { - withClue("date1=${date1.string} date2=${date2.string}") { - val key = connector.insertNonNullDate.execute(date1.date).data.key - connector.updateNonNullDate.execute(key) { value = date2.date } - assertNonNullDateByKeyEquals(key, date2.string) + @Test + fun dateNonNullable_UpdateManyNullNewValueShouldThrow() = + runTest(timeout = 1.minutes) { + checkAll( + propTestConfig, + Arb.dataConnect.tag(), + Arb.dataConnect.threeNonNullDatesTestData(), + ) { tag, testDatas -> + connector.dateNonNullableInsert3.execute(tag, testDatas) + shouldThrow<DataConnectException> { + connector.dateNonNullableUpdateByTagAndValue.execute(tag) { newValue = null } } } } - } @Test - fun nonNullDate_update_DateVariableOmitted() = runTest { - val date = Arb.dataConnect.dateTestData().next(rs) - val key = connector.insertNonNullDate.execute(date.date).data.key - connector.updateNonNullDate.execute(key) {} - assertNonNullDateByKeyEquals(key, date.date) - } + fun dateNonNullable_UpdateManyOmittedValueShouldUpdateAll() = + runTest(timeout = 1.minutes) { + checkAll( + propTestConfig, + Arb.dataConnect.tag(), + Arb.dataConnect.threeNonNullDatesTestData(), + Arb.dataConnect.localDate() + ) { tag, testDatas, date2 -> + val insertResult = connector.dateNonNullableInsert3.execute(tag, testDatas) + val updateResult = + connector.dateNonNullableUpdateByTagAndValue.execute(tag) { newValue = date2 } + withClue("updateResult.data.count") { updateResult.data.count shouldBe 3 } + val queryResult = connector.dateNonNullableGetAllByTagAndValue.execute(tag, date2) + queryResult.data.items + .map { it.id } + .shouldContainExactlyInAnyOrder( + insertResult.data.key1.id, + insertResult.data.key2.id, + insertResult.data.key3.id, + ) + } + } @Test - fun nullableDate_insert_NormalCases() = runTest { - checkAll(20, Arb.dataConnect.dateTestData()) { - val key = connector.insertNullableDate.execute { value = it.date }.data.key - assertNullableDateByKeyEquals(key, it.string) + fun dateNonNullable_UpdateManyOmittedNewValueShouldNotChangeAny() = + runTest(timeout = 1.minutes) { + checkAll( + propTestConfig, + Arb.dataConnect.tag(), + Arb.dataConnect.threeNonNullDatesTestData() + ) { tag, testDatas -> + val insertResult = connector.dateNonNullableInsert3.execute(tag, testDatas) + val selectedDate = testDatas.selected!!.date + val updateResult = + connector.dateNonNullableUpdateByTagAndValue.execute(tag) { value = selectedDate } + withClue("updateResult.data.count") { + updateResult.data.count shouldBe testDatas.numMatchingSelected + } + val queryResult = connector.dateNonNullableGetAllByTagAndValue.execute(tag, selectedDate) + val matchingIds = testDatas.idsMatchingSelected(insertResult) + queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds + } } - } @Test - fun nullableDate_insert_EdgeCases() = runTest { - val edgeCases = EdgeCases.dates.all() + listOf(null) - assertSoftly { - edgeCases.forEach { - val key = connector.insertNullableDate.execute { value = it?.date }.data.key - if (it === null) { - assertNullableDateByKeyHasNullInnerValue(key) - } else { - assertNullableDateByKeyEquals(key, it.string) + fun dateNonNullable_DeleteMany() = + runTest(timeout = 1.minutes) { + checkAll( + propTestConfig, + Arb.dataConnect.tag(), + Arb.dataConnect.threeNonNullDatesTestData() + ) { tag, testDatas -> + val insertResult = connector.dateNonNullableInsert3.execute(tag, testDatas) + val selectedDate = testDatas.selected!!.date + val deleteResult = + connector.dateNonNullableDeleteByTagAndValue.execute(tag) { value = selectedDate } + withClue("deleteResult.data.count") { + deleteResult.data.count shouldBe testDatas.numMatchingSelected } + val queryResult = connector.dateNonNullableGetAllByTagAndMaybeValue.execute(tag) {} + val insertedIds = insertResult.data.run { listOf(key1, key2, key3).map { it.id } } + val matchingIds = testDatas.idsMatchingSelected(insertResult) + val remainingIds = insertedIds.filterNot { it in matchingIds } + queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder remainingIds } } - } @Test - fun nullableDate_insert_ShouldUseNullIfDateVariableIsOmitted() = runTest { - val key = connector.insertNullableDate.execute {}.data.key - assertNullableDateByKeyHasNullInnerValue(key) - } + fun dateNonNullable_DeleteManyNullValueShouldDeleteNone() = + runTest(timeout = 1.minutes) { + checkAll( + propTestConfig, + Arb.dataConnect.tag(), + Arb.dataConnect.threeNonNullDatesTestData() + ) { tag, testDatas -> + val insertResult = connector.dateNonNullableInsert3.execute(tag, testDatas) + val deleteResult = + connector.dateNonNullableDeleteByTagAndValue.execute(tag) { value = null } + withClue("deleteResult.data.count") { deleteResult.data.count shouldBe 0 } + val queryResult = connector.dateNonNullableGetAllByTagAndMaybeValue.execute(tag) {} + queryResult.data.items + .map { it.id } + .shouldContainExactlyInAnyOrder( + insertResult.data.key1.id, + insertResult.data.key2.id, + insertResult.data.key3.id, + ) + } + } @Test - fun nullableDate_insert_ShouldIgnoreTimeZone() = runTest { - // Use a date that, when converted to UTC, in on a different date to verify that the server does - // the expected thing; that is, that it _drops_ the time zone information (rather than - // converting the date to UTC then taking the YYYY-MM-DD of that). The server would use the date - // "2024-03-27" if it did the erroneous conversion to UTC before taking the YYYY-MM-DD. - val date = "2024-03-26T19:48:00.144-07:00" - val key = connector.insertNullableDate.executeWithStringVariables(date).data.key - assertNullableDateByKeyEquals(key, LocalDate(2024, 3, 26)) - } + fun dateNonNullable_DeleteManyOmittedValueShouldDeleteAll() = + runTest(timeout = 1.minutes) { + checkAll( + propTestConfig, + Arb.dataConnect.tag(), + Arb.dataConnect.threeNonNullDatesTestData(), + ) { tag, testDatas -> + connector.dateNonNullableInsert3.execute(tag, testDatas) + val deleteResult = connector.dateNonNullableDeleteByTagAndValue.execute(tag) {} + withClue("deleteResult.data.count") { deleteResult.data.count shouldBe 3 } + val queryResult = connector.dateNonNullableGetAllByTagAndMaybeValue.execute(tag) {} + queryResult.data.items.shouldBeEmpty() + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for inserting into and querying this table: + // type DateNullable @table { value: Date, tag: String } + ////////////////////////////////////////////////////////////////////////////////////////////////// @Test - fun nullableDate_insert_ShouldIgnoreTime() = runTest { - val date = "2024-03-26T19:48:00.144Z" - val key = connector.insertNullableDate.executeWithStringVariables(date).data.key - assertNullableDateByKeyEquals(key, LocalDate(2024, 3, 26)) - } + fun dateNullable_MutationLocalDateVariable() = + runTest(timeout = 1.minutes) { + val localDates = Arb.dataConnect.dateTestData().orNullableReference(nullProbability = 0.2) + checkAll(propTestConfig, localDates) { testData -> + val insertResult = connector.dateNullableInsert.execute { value = testData.ref?.date } + val returnedString = + connector.dateNullableGetByKey.executeWithStringData(insertResult.data.key) + returnedString shouldBe testData.ref?.string + } + } @Test - fun nullableDate_insert_ShouldFailIfDateVariableIsAnInt() = runTest { - shouldThrow<DataConnectException> { - connector.insertNullableDate.executeWithIntVariables(Arb.int().next(rs)).data.key + fun dateNullable_MutationStringVariable() = + runTest(timeout = 1.minutes) { + val localDates = Arb.dataConnect.dateTestData().orNullableReference(nullProbability = 0.2) + checkAll(propTestConfig, localDates) { testData -> + val insertResult = connector.dateNullableInsert.execute(testData.ref?.string) + val queryResult = connector.dateNullableGetByKey.execute(insertResult.data.key) + queryResult.data.item?.value shouldBe testData.ref?.date + } } + + @Test + fun dateNullable_QueryLocalDateVariable() = dateNullable_QueryVariable { tag, dateTestData -> + connector.dateNullableGetAllByTagAndValue.execute(tag = tag) { value = dateTestData?.date } } @Test - fun nullableDate_insert_ShouldFailIfDateVariableIsMalformed() = runTest { - for (invalidDate in invalidDates) { - shouldThrow<DataConnectException> { - connector.insertNonNullDate.executeWithStringVariables(invalidDate).data.key + fun dateNullable_QueryStringVariable() = dateNullable_QueryVariable { tag, dateTestData -> + connector.dateNullableGetAllByTagAndValue.execute(tag = tag, value = dateTestData?.string) + } + + private fun dateNullable_QueryVariable( + executeQuery: + suspend (tag: String, date: DateTestData?) -> QueryResult< + DateNullableGetAllByTagAndValueQuery.Data, * + > + ) = + runTest(timeout = 1.minutes) { + checkAll( + propTestConfig, + Arb.dataConnect.tag(), + Arb.dataConnect.threePossiblyNullDatesTestData() + ) { tag, testDatas -> + val insertResult = connector.dateNullableInsert3.execute(tag, testDatas) + val queryResult = executeQuery(tag, testDatas.selected) + val matchingIds = testDatas.idsMatchingSelected(insertResult) + queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds } } - } @Test - fun nullableDatesWithDefaults_insert_ShouldUseDefaultValuesIfNoVariablesSpecified() = runTest { - val key = connector.insertNullableDatesWithDefaults.execute {}.data.key - val queryResult = connector.getNullableDatesWithDefaultsByKey.execute(key) + fun dateNullable_QueryOmittedVariable() = + runTest(timeout = 1.minutes) { + checkAll( + propTestConfig, + Arb.dataConnect.tag(), + Arb.dataConnect.threePossiblyNullDatesTestData() + ) { tag, testDatas -> + val insertResult = connector.dateNullableInsert3.execute(tag, testDatas) + val queryResult = connector.dateNullableGetAllByTagAndValue.execute(tag) {} + queryResult.data.items + .map { it.id } + .shouldContainExactlyInAnyOrder( + insertResult.data.key1.id, + insertResult.data.key2.id, + insertResult.data.key3.id + ) + } + } - // Since we can't know the exact value of `request.time` just make sure that the exact same - // value is used for both fields to which it is set. - val expectedRequestTime = queryResult.data.nullableDatesWithDefaults!!.requestTime1 + @Test + fun dateNullable_Update() = + runTest(timeout = 1.minutes) { + val localDates = Arb.dataConnect.localDate().orNullableReference(nullProbability = 0.2) + checkAll(propTestConfig, localDates, localDates) { date1, date2 -> + val insertResult = connector.dateNullableInsert.execute { value = date1.ref } + val updateResult = + connector.dateNullableUpdateByKey.execute(insertResult.data.key) { value = date2.ref } + updateResult.asClue { it.data.key shouldBe insertResult.data.key } + val queryResult = connector.dateNullableGetByKey.execute(insertResult.data.key) + val item = withClue("queryResult.data.item") { queryResult.data.item.shouldNotBeNull() } + item.value shouldBe date2.ref + } + } - queryResult.data shouldBe - GetNullableDatesWithDefaultsByKeyQuery.Data( - GetNullableDatesWithDefaultsByKeyQuery.Data.NullableDatesWithDefaults( - valueWithVariableDefault = LocalDate(8113, 2, 9), - valueWithSchemaDefault = LocalDate(1921, 12, 2), - epoch = EdgeCases.dates.epoch.date, - requestTime1 = expectedRequestTime, - requestTime2 = expectedRequestTime, - ) - ) - } + @Test + fun dateNullable_UpdateToOmittedShouldLeaveValueUnchanged() = + runTest(timeout = 1.minutes) { + val localDates = Arb.dataConnect.localDate().orNullableReference(nullProbability = 0.2) + checkAll(propTestConfig, localDates) { date -> + val insertResult = connector.dateNullableInsert.execute { value = date.ref } + val updateResult = connector.dateNullableUpdateByKey.execute(insertResult.data.key) {} + updateResult.asClue { it.data.key shouldBe insertResult.data.key } + val queryResult = connector.dateNullableGetByKey.execute(insertResult.data.key) + val item = withClue("queryResult.data.item") { queryResult.data.item.shouldNotBeNull() } + item.value shouldBe date.ref + } + } @Test - fun nullableDate_update_NormalCases() = runTest { - checkAll(20, Arb.dataConnect.dateTestData(), Arb.dataConnect.dateTestData()) { date1, date2 -> - val key = connector.insertNullableDate.execute { value = date1.date }.data.key - connector.updateNullableDate.execute(key) { value = date2.date } - assertNullableDateByKeyEquals(key, date2.string) + fun dateNullable_UpdateMany() = + runTest(timeout = 1.minutes) { + val localDates = Arb.dataConnect.localDate().orNullableReference(nullProbability = 0.2) + checkAll( + propTestConfig, + Arb.dataConnect.tag(), + Arb.dataConnect.threePossiblyNullDatesTestData(), + localDates + ) { tag, testDatas, date2 -> + val insertResult = connector.dateNullableInsert3.execute(tag, testDatas) + val selectedDate = testDatas.selected?.date + val updateResult = + connector.dateNullableUpdateByTagAndValue.execute(tag) { + value = selectedDate + newValue = date2.ref + } + withClue("updateResult.data.count") { + updateResult.data.count shouldBe testDatas.numMatchingSelected + } + val queryResult = + connector.dateNullableGetAllByTagAndValue.execute(tag) { value = date2.ref } + val matchingIds1 = testDatas.idsMatchingSelected(insertResult) + val matchingIds2 = testDatas.idsMatching(insertResult, date2.ref) + val matchingIds = (matchingIds1 + matchingIds2).distinct() + queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds + } } - } @Test - fun nullableDate_update_EdgeCases() = runTest { - val edgeCases = EdgeCases.dates.all() - val dates1 = - edgeCases + List(edgeCases.size) { Arb.dataConnect.dateTestData().next(rs) } + edgeCases - val dates2 = - List(edgeCases.size) { Arb.dataConnect.dateTestData().next(rs) } + edgeCases + edgeCases + fun dateNullable_UpdateManyOmittedValueShouldUpdateAll() = + runTest(timeout = 1.minutes) { + val localDates = Arb.dataConnect.localDate().orNullableReference(nullProbability = 0.2) + checkAll( + propTestConfig, + Arb.dataConnect.tag(), + Arb.dataConnect.threePossiblyNullDatesTestData(), + localDates + ) { tag, testDatas, date2 -> + val insertResult = connector.dateNullableInsert3.execute(tag, testDatas) + val updateResult = + connector.dateNullableUpdateByTagAndValue.execute(tag) { newValue = date2.ref } + withClue("updateResult.data.count") { updateResult.data.count shouldBe 3 } + val queryResult = + connector.dateNullableGetAllByTagAndValue.execute(tag) { value = date2.ref } + queryResult.data.items + .map { it.id } + .shouldContainExactlyInAnyOrder( + insertResult.data.key1.id, + insertResult.data.key2.id, + insertResult.data.key3.id, + ) + } + } + + @Test + fun dateNullable_UpdateManyOmittedNewValueShouldNotChangeAny() = + runTest(timeout = 1.minutes) { + checkAll( + propTestConfig, + Arb.dataConnect.tag(), + Arb.dataConnect.threePossiblyNullDatesTestData() + ) { tag, testDatas -> + val insertResult = connector.dateNullableInsert3.execute(tag, testDatas) + val selectedDate = testDatas.selected?.date + val updateResult = + connector.dateNullableUpdateByTagAndValue.execute(tag) { value = selectedDate } + withClue("updateResult.data.count") { + updateResult.data.count shouldBe testDatas.numMatchingSelected + } + val queryResult = + connector.dateNullableGetAllByTagAndValue.execute(tag) { value = selectedDate } + val matchingIds = testDatas.idsMatchingSelected(insertResult) + queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds + } + } + + @Test + fun dateNullable_DeleteMany() = + runTest(timeout = 1.minutes) { + checkAll( + propTestConfig, + Arb.dataConnect.tag(), + Arb.dataConnect.threePossiblyNullDatesTestData(), + ) { tag, testDatas -> + val insertResult = connector.dateNullableInsert3.execute(tag, testDatas) + val selectedDate = testDatas.selected?.date + val deleteResult = + connector.dateNullableDeleteByTagAndValue.execute(tag) { value = selectedDate } + withClue("deleteResult.data.count") { + deleteResult.data.count shouldBe testDatas.numMatchingSelected + } + val queryResult = connector.dateNullableGetAllByTagAndValue.execute(tag) {} + val insertedIds = insertResult.data.run { listOf(key1, key2, key3).map { it.id } } + val matchingIds = testDatas.idsMatchingSelected(insertResult) + val remainingIds = insertedIds.filterNot { it in matchingIds } + queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder remainingIds + } + } + + @Test + fun dateNullable_DeleteManyOmittedValueShouldDeleteAll() = + runTest(timeout = 1.minutes) { + checkAll( + propTestConfig, + Arb.dataConnect.tag(), + Arb.dataConnect.threePossiblyNullDatesTestData(), + ) { tag, testDatas -> + connector.dateNullableInsert3.execute(tag, testDatas) + val deleteResult = connector.dateNullableDeleteByTagAndValue.execute(tag) {} + withClue("deleteResult.data.count") { deleteResult.data.count shouldBe 3 } + val queryResult = connector.dateNullableGetAllByTagAndValue.execute(tag) {} + queryResult.data.items.shouldBeEmpty() + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for default `Date` variable values. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun dateNonNullable_MutationVariableDefaults() = runTest { + val insertResult = connector.dateNonNullableWithDefaultsInsert.execute {} + val queryResult = connector.dateNonNullableWithDefaultsGetByKey.execute(insertResult.data.key) + val item = withClue("queryResult.data.item") { queryResult.data.item.shouldNotBeNull() } assertSoftly { - for ((date1, date2) in dates1.zip(dates2)) { - withClue("date1=${date1.string} date2=${date2.string}") { - val key = connector.insertNullableDate.execute { value = date1.date }.data.key - connector.updateNullableDate.execute(key) { value = date2.date } - assertNullableDateByKeyEquals(key, date2.string) + withClue(item) { + withClue("valueWithVariableDefault") { + item.valueWithVariableDefault shouldBe LocalDate(6904, 11, 30) + } + withClue("valueWithSchemaDefault") { + item.valueWithSchemaDefault shouldBe LocalDate(2112, 1, 31) } + withClue("epoch") { item.epoch shouldBe EdgeCases.dates.epoch.date } + withClue("requestTime2") { item.requestTime2 shouldBe item.requestTime1 } } } + + withClue("requestTime validation") { + val today = connector.requestTime().toTheeTenAbpJavaLocalDate() + val yesterday = today.minusDays(1) + val tomorrow = today.plusDays(1) + val requestTime = item.requestTime1.toTheeTenAbpJavaLocalDate() + requestTime.shouldBeIn(yesterday, today, tomorrow) + } } @Test - fun nullableDate_update_UpdateNonNullValueToNull() = runTest { - val date = Arb.dataConnect.dateTestData().next(rs) - val key = connector.insertNullableDate.execute { value = date.date }.data.key - connector.updateNullableDate.execute(key) { value = null } - assertNullableDateByKeyHasNullInnerValue(key) - } + fun dateNonNullable_QueryVariableDefaults() = + runTest(timeout = 1.minutes) { + val defaultTestData = DateTestData(LocalDate(2692, 5, 21), "2692-05-21") + val localDateArb = Arb.dataConnect.dateTestData().withEdgecases(defaultTestData) + checkAll( + propTestConfig, + Arb.dataConnect.threeNonNullDatesTestData(localDateArb), + Arb.dataConnect.tag() + ) { testDatas, tag -> + val insertResult = connector.dateNonNullableInsert3.execute(tag, testDatas) + val queryResult = connector.dateNonNullableGetAllByTagAndDefaultValue.execute(tag) {} + val matchingIds = testDatas.idsMatching(insertResult, defaultTestData.date) + queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds + } + } @Test - fun nullableDate_update_UpdateNullValueToNonNull() = runTest { - val date = Arb.dataConnect.dateTestData().next(rs) - val key = connector.insertNullableDate.execute { value = null }.data.key - connector.updateNullableDate.execute(key) { value = date.date } - assertNullableDateByKeyEquals(key, date.date) + fun dateNullable_MutationVariableDefaults() = runTest { + val insertResult = connector.dateNullableWithDefaultsInsert.execute {} + val queryResult = connector.dateNullableWithDefaultsGetByKey.execute(insertResult.data.key) + val item = withClue("queryResult.data.item") { queryResult.data.item.shouldNotBeNull() } + + assertSoftly { + withClue(item) { + withClue("valueWithVariableDefault") { + item.valueWithVariableDefault shouldBe LocalDate(8113, 2, 9) + } + withClue("valueWithVariableNullDefault") { + item.valueWithVariableNullDefault.shouldBeNull() + } + withClue("valueWithSchemaDefault") { + item.valueWithSchemaDefault shouldBe LocalDate(1921, 12, 2) + } + withClue("valueWithSchemaNullDefault") { item.valueWithSchemaNullDefault.shouldBeNull() } + withClue("valueWithNoDefault") { item.valueWithNoDefault.shouldBeNull() } + withClue("epoch") { item.epoch shouldBe EdgeCases.dates.epoch.date } + withClue("requestTime1") { item.requestTime1.shouldNotBeNull() } + withClue("requestTime2") { item.requestTime2 shouldBe item.requestTime1 } + } + } + + withClue("requestTime validation") { + val today = connector.requestTime().toTheeTenAbpJavaLocalDate() + val yesterday = today.minusDays(1) + val tomorrow = today.plusDays(1) + val requestTime = item.requestTime1!!.toTheeTenAbpJavaLocalDate() + requestTime.shouldBeIn(yesterday, today, tomorrow) + } } @Test - fun nullableDate_update_DateVariableOmitted() = runTest { - val date = Arb.dataConnect.dateTestData().next(rs) - val key = connector.insertNullableDate.execute { value = date.date }.data.key - connector.updateNullableDate.execute(key) {} - assertNullableDateByKeyEquals(key, date.date) - } + fun dateNullable_QueryVariableDefaults() = + runTest(timeout = 1.minutes) { + val defaultTestData = DateTestData(LocalDate(1771, 10, 28), "1771-10-28") + val dateTestDataArb = + Arb.dataConnect + .dateTestData() + .withEdgecases(defaultTestData) + .orNullableReference(nullProbability = 0.333) + checkAll( + propTestConfig, + Arb.dataConnect.threePossiblyNullDatesTestData(dateTestDataArb), + Arb.dataConnect.tag() + ) { testDatas, tag -> + val insertResult = connector.dateNullableInsert3.execute(tag, testDatas) + val queryResult = connector.dateNullableGetAllByTagAndDefaultValue.execute(tag) {} + val matchingIds = testDatas.idsMatching(insertResult, defaultTestData.date) + queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds + } + } - private suspend fun assertNonNullDateByKeyEquals(key: NonNullDateKey, expected: String) { - val queryResult = - connector.getNonNullDateByKey - .withDataDeserializer(serializer<GetDateByKeyQueryStringData>()) - .execute(key) - queryResult.data shouldBe GetDateByKeyQueryStringData(expected) - } + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Invalid Date String Tests + ////////////////////////////////////////////////////////////////////////////////////////////////// - private suspend fun assertNonNullDateByKeyEquals(key: NonNullDateKey, expected: LocalDate) { - val queryResult = connector.getNonNullDateByKey.execute(key) - queryResult.data shouldBe - GetNonNullDateByKeyQuery.Data(GetNonNullDateByKeyQuery.Data.Value(expected)) - } + @Test + fun dateNonNullable_MutationInvalidDateVariable() = + runTest(timeout = 1.minutes) { + checkAll(propTestConfig.copy(iterations = 500), Arb.dataConnect.invalidDateScalarString()) { + testData -> + val exception = + shouldThrow<DataConnectException> { + connector.dateNonNullableInsert.execute(testData.toDateScalarString()) + } + assertSoftly { + exception.message shouldContainWithNonAbuttingText "\$value" + exception.message shouldContainWithNonAbuttingTextIgnoringCase "invalid" + } + } + } - private suspend fun assertNullableDateByKeyHasNullInnerValue(key: NullableDateKey) { - val queryResult = - connector.getNullableDateByKey - .withDataDeserializer(serializer<GetDateByKeyQueryStringData>()) - .execute(key) - queryResult.data shouldBe - GetDateByKeyQueryStringData(GetDateByKeyQueryStringData.DateStringValue(null)) - } + @Test + fun dateNonNullable_QueryInvalidDateVariable() = + runTest(timeout = 1.minutes) { + checkAll( + propTestConfig.copy(iterations = 500), + Arb.dataConnect.tag(), + Arb.dataConnect.invalidDateScalarString() + ) { tag, testData -> + val exception = + shouldThrow<DataConnectException> { + connector.dateNonNullableGetAllByTagAndValue.execute( + tag = tag, + value = testData.toDateScalarString() + ) + } + assertSoftly { + exception.message shouldContainWithNonAbuttingText "\$value" + exception.message shouldContainWithNonAbuttingTextIgnoringCase "invalid" + } + } + } - private suspend fun assertNullableDateByKeyEquals(key: NullableDateKey, expected: String) { - val queryResult = - connector.getNullableDateByKey - .withDataDeserializer(serializer<GetDateByKeyQueryStringData>()) - .execute(key) - queryResult.data shouldBe GetDateByKeyQueryStringData(expected) - } + @Test + fun dateNullable_MutationInvalidDateVariable() = + runTest(timeout = 1.minutes) { + checkAll(propTestConfig.copy(iterations = 500), Arb.dataConnect.invalidDateScalarString()) { + testData -> + val exception = + shouldThrow<DataConnectException> { + connector.dateNullableInsert.execute(testData.toDateScalarString()) + } + assertSoftly { + exception.message shouldContainWithNonAbuttingText "\$value" + exception.message shouldContainWithNonAbuttingTextIgnoringCase "invalid" + } + } + } - private suspend fun assertNullableDateByKeyEquals(key: NullableDateKey, expected: LocalDate) { - val queryResult = connector.getNullableDateByKey.execute(key) - queryResult.data shouldBe - GetNullableDateByKeyQuery.Data(GetNullableDateByKeyQuery.Data.Value(expected)) - } + @Test + fun dateNullable_QueryInvalidDateVariable() = + runTest(timeout = 1.minutes) { + checkAll( + propTestConfig.copy(iterations = 500), + Arb.dataConnect.tag(), + Arb.dataConnect.invalidDateScalarString() + ) { tag, testData -> + val exception = + shouldThrow<DataConnectException> { + connector.dateNullableGetAllByTagAndValue.execute( + tag = tag, + value = testData.toDateScalarString() + ) + } + assertSoftly { + exception.message shouldContainWithNonAbuttingText "\$value" + exception.message shouldContainWithNonAbuttingTextIgnoringCase "invalid" + } + } + } - /** - * A `Data` type that can be used in place of [GetNonNullDateByKeyQuery.Data] that types the value - * as a [String] instead of a [LocalDate], allowing verification of the data sent over the wire - * without possible confounding from date deserialization. - */ - @Serializable - private data class GetDateByKeyQueryStringData(val value: DateStringValue?) { - constructor(value: String) : this(DateStringValue(value)) + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Helper methods and classes. + ////////////////////////////////////////////////////////////////////////////////////////////////// - @Serializable data class DateStringValue(val value: String?) + @Serializable + private data class StringItemData(val item: Item?) { + @Serializable data class Item(val value: String?) } - /** - * A `Variables` type that can be used in place of [InsertNonNullDateMutation.Variables] that - * types the value as a [String] instead of a [LocalDate], allowing verification of the data sent - * over the wire without possible confounding from date serialization. - */ - @Serializable private data class InsertDateStringVariables(val value: String?) + @Serializable private data class NullValueVariables(val value: Nothing?) + + @Serializable private data class StringValueVariables(val value: String?) + + @Serializable private data class TagAndStringValueVariables(val tag: String, val value: String?) - /** - * A `Variables` type that can be used in place of [InsertNonNullDateMutation.Variables] that - * types the value as a [Int] instead of a [LocalDate], allowing verification that the server - * fails with an expected error (rather than crashing, for example). - */ - @Serializable private data class InsertDateIntVariables(val value: Int) + @Serializable private data class TagAndNullValueVariables(val tag: String, val value: Nothing?) + + private suspend fun DemoConnector.requestTime(): LocalDate { + val insertResult = exprValuesInsert.execute() + val queryResult = exprValuesGetByKey.execute(insertResult.data.key) + return withClue("exprValuesGetByKey queryResult.data.item") { + queryResult.data.item.shouldNotBeNull() + } + .requestTimeAsDate + } private companion object { - suspend fun <Data> GeneratedMutation<*, Data, *>.executeWithStringVariables(value: String?) = - withVariablesSerializer(serializer<InsertDateStringVariables>()) - .ref(InsertDateStringVariables(value)) + val propTestConfig = + PropTestConfig(iterations = 20, edgeConfig = EdgeConfig(edgecasesGenerationProbability = 0.5)) + + suspend fun DateNullableInsert3Mutation.execute( + tag: String, + testDatas: ThreeDateTestDatas, + ): MutationResult<DateNullableInsert3Mutation.Data, DateNullableInsert3Mutation.Variables> = + execute(tag = tag) { + value1 = testDatas.testData1?.date + value2 = testDatas.testData2?.date + value3 = testDatas.testData3?.date + } + + suspend fun DateNonNullableInsert3Mutation.execute( + tag: String, + testDatas: ThreeDateTestDatas, + ): MutationResult< + DateNonNullableInsert3Mutation.Data, DateNonNullableInsert3Mutation.Variables + > = + execute( + tag = tag, + value1 = testDatas.testData1!!.date, + value2 = testDatas.testData2!!.date, + value3 = testDatas.testData3!!.date, + ) + + suspend fun DateNonNullableGetByKeyQuery.executeWithStringData( + key: DateNonNullableKey + ): String? = withDataDeserializer(serializer<StringItemData>()).execute(key).data.item?.value + + suspend fun DateNullableGetByKeyQuery.executeWithStringData(key: DateNullableKey): String? = + withDataDeserializer(serializer<StringItemData>()).execute(key).data.item?.value + + suspend fun DateNonNullableInsertMutation.execute( + date: String + ): MutationResult<DateNonNullableInsertMutation.Data, StringValueVariables> = + withVariablesSerializer(serializer<StringValueVariables>()) + .ref(StringValueVariables(date)) .execute() - suspend fun <Data> GeneratedMutation<*, Data, *>.executeWithIntVariables(value: Int) = - withVariablesSerializer(serializer<InsertDateIntVariables>()) - .ref(InsertDateIntVariables(value)) + suspend fun DateNullableInsertMutation.execute( + date: String? + ): MutationResult<DateNullableInsertMutation.Data, StringValueVariables> = + withVariablesSerializer(serializer<StringValueVariables>()) + .ref(StringValueVariables(date)) .execute() - suspend fun <Data> GeneratedQuery<*, Data, GetNonNullDateByKeyQuery.Variables>.execute( - key: NonNullDateKey - ) = ref(GetNonNullDateByKeyQuery.Variables(key)).execute() - - suspend fun <Data> GeneratedQuery<*, Data, GetNullableDateByKeyQuery.Variables>.execute( - key: NullableDateKey - ) = ref(GetNullableDateByKeyQuery.Variables(key)).execute() - - val invalidDates = - listOf( - // Partial dates - "2", - "20", - "202", - "2024", - "2024-", - "2024-0", - "2024-01", - "2024-01-", - "2024-01-0", - "2024-01-04T", - - // Missing components - "", - "2024-", - "-05-17", - "2024-05", - "2024--17", - "-05-", - - // Invalid year - "2-05-17", - "20-05-17", - "202-05-17", - "20245-05-17", - "02024-05-17", - "ABCD-05-17", - "-123-05-17", - - // Invalid month - "2024-1-17", - "2024-012-17", - "2024-123-17", - "2024-00-17", - "2024-13-17", - "2024-M-17", - "2024-MA-17", - - // Invalid day - "2024-05-1", - "2024-05-123", - "2024-05-012", - "2024-05-00", - "2024-05-32", - "2024-05-A", - "2024-05-AB", - "2024-05-ABC", - - // Out-of-range Values - "0000-01-01", - "2024-00-22", - "2024-13-22", - "2024-11-00", - "2024-01-32", - "2025-02-29", - "2024-02-30", - "2024-03-32", - "2024-04-31", - "2024-05-32", - "2024-06-31", - "2024-07-32", - "2024-08-32", - "2024-09-31", - "2024-10-32", - "2024-11-31", - "2024-12-32", - ) + suspend fun DateNonNullableInsertMutation.execute( + date: Nothing? + ): MutationResult<DateNonNullableInsertMutation.Data, NullValueVariables> = + withVariablesSerializer(serializer<NullValueVariables>()) + .ref(NullValueVariables(date)) + .execute() + + suspend fun DateNonNullableGetAllByTagAndValueQuery.execute( + tag: String, + value: String, + ): QueryResult<DateNonNullableGetAllByTagAndValueQuery.Data, TagAndStringValueVariables> = + withVariablesSerializer(serializer<TagAndStringValueVariables>()) + .ref(TagAndStringValueVariables(tag = tag, value = value)) + .execute() + + suspend fun DateNonNullableGetAllByTagAndValueQuery.execute( + tag: String, + value: Nothing?, + ): QueryResult<DateNonNullableGetAllByTagAndValueQuery.Data, TagAndNullValueVariables> = + withVariablesSerializer(serializer<TagAndNullValueVariables>()) + .ref(TagAndNullValueVariables(tag = tag, value = value)) + .execute() + + suspend fun DateNullableGetAllByTagAndValueQuery.execute( + tag: String, + value: String?, + ): QueryResult<DateNullableGetAllByTagAndValueQuery.Data, TagAndStringValueVariables> = + withVariablesSerializer(serializer<TagAndStringValueVariables>()) + .ref(TagAndStringValueVariables(tag = tag, value = value)) + .execute() + + @JvmName("idsMatching_DateNonNullable") + fun ThreeDateTestDatas.idsMatching( + result: MutationResult<DateNonNullableInsert3Mutation.Data, *>, + localDate: LocalDate?, + ): List<UUID> = idsMatching(result.data, localDate) + + @JvmName("idsMatching_DateNonNullable") + fun ThreeDateTestDatas.idsMatching( + data: DateNonNullableInsert3Mutation.Data, + localDate: LocalDate?, + ): List<UUID> = idsMatching(localDate) { data.uuidFromItemNumber(it) } + + @JvmName("idsMatchingSelected_DateNonNullable") + fun ThreeDateTestDatas.idsMatchingSelected( + result: MutationResult<DateNonNullableInsert3Mutation.Data, *> + ): List<UUID> = idsMatchingSelected(result.data) + + @JvmName("idsMatchingSelected_DateNonNullable") + fun ThreeDateTestDatas.idsMatchingSelected( + data: DateNonNullableInsert3Mutation.Data + ): List<UUID> = idsMatchingSelected { data.uuidFromItemNumber(it) } + + fun DateNonNullableInsert3Mutation.Data.uuidFromItemNumber(itemNumber: ItemNumber): UUID = + when (itemNumber) { + ItemNumber.ONE -> key1 + ItemNumber.TWO -> key2 + ItemNumber.THREE -> key3 + }.id + + @JvmName("idsMatching_DateNullable") + fun ThreeDateTestDatas.idsMatching( + result: MutationResult<DateNullableInsert3Mutation.Data, *>, + localDate: LocalDate?, + ): List<UUID> = idsMatching(result.data, localDate) + + @JvmName("idsMatching_DateNullable") + fun ThreeDateTestDatas.idsMatching( + data: DateNullableInsert3Mutation.Data, + localDate: LocalDate?, + ): List<UUID> = idsMatching(localDate) { data.uuidFromItemNumber(it) } + + @JvmName("idsMatchingSelected_DateNullable") + fun ThreeDateTestDatas.idsMatchingSelected( + result: MutationResult<DateNullableInsert3Mutation.Data, *> + ): List<UUID> = idsMatchingSelected(result.data) + + @JvmName("idsMatchingSelected_DateNullable") + fun ThreeDateTestDatas.idsMatchingSelected(data: DateNullableInsert3Mutation.Data): List<UUID> = + idsMatchingSelected { + data.uuidFromItemNumber(it) + } + + fun DateNullableInsert3Mutation.Data.uuidFromItemNumber(itemNumber: ItemNumber): UUID = + when (itemNumber) { + ItemNumber.ONE -> key1 + ItemNumber.TWO -> key2 + ItemNumber.THREE -> key3 + }.id + + suspend fun <Data> GeneratedQuery<*, Data, DateNonNullableGetByKeyQuery.Variables>.execute( + key: DateNonNullableKey + ) = ref(DateNonNullableGetByKeyQuery.Variables(key)).execute() + + suspend fun <Data> GeneratedQuery<*, Data, DateNullableGetByKeyQuery.Variables>.execute( + key: DateNullableKey + ) = ref(DateNullableGetByKeyQuery.Variables(key)).execute() } } diff --git a/firebase-dataconnect/emulator/dataconnect/connector/demo/demo_ops.gql b/firebase-dataconnect/emulator/dataconnect/connector/demo/demo_ops.gql index 2f22ed6210a..907f95778e4 100644 --- a/firebase-dataconnect/emulator/dataconnect/connector/demo/demo_ops.gql +++ b/firebase-dataconnect/emulator/dataconnect/connector/demo/demo_ops.gql @@ -1306,58 +1306,6 @@ query DateNullableWithDefaults_GetByKey($key: DateNullableWithDefaults_Key!) @au } } -mutation InsertNonNullDate($value: Date!) @auth(level: PUBLIC) { - nonNullDate_insert(data: { value: $value }) -} - -mutation UpdateNonNullDate($key: NonNullDate_Key!, $value: Date) @auth(level: PUBLIC) { - nonNullDate_update(key: $key, data: { value: $value }) -} - -query GetNonNullDateByKey($key: NonNullDate_Key!) @auth(level: PUBLIC) { - value: nonNullDate(key: $key) { value } -} - -mutation InsertNonNullDatesWithDefaults($value: Date! = "6904-11-30") @auth(level: PUBLIC) { - nonNullDatesWithDefaults_insert(data: { valueWithVariableDefault: $value }) -} - -query GetNonNullDatesWithDefaultsByKey($key: NonNullDatesWithDefaults_Key!) @auth(level: PUBLIC) { - nonNullDatesWithDefaults(key: $key) { - valueWithVariableDefault - valueWithSchemaDefault - epoch - requestTime1 - requestTime2 - } -} - -mutation InsertNullableDate($value: Date) @auth(level: PUBLIC) { - nullableDate_insert(data: { value: $value }) -} - -mutation UpdateNullableDate($key: NullableDate_Key!, $value: Date) @auth(level: PUBLIC) { - nullableDate_update(key: $key, data: { value: $value }) -} - -query GetNullableDateByKey($key: NullableDate_Key!) @auth(level: PUBLIC) { - value: nullableDate(key: $key) { value } -} - -mutation InsertNullableDatesWithDefaults($value: Date = "8113-02-09") @auth(level: PUBLIC) { - nullableDatesWithDefaults_insert(data: { valueWithVariableDefault: $value }) -} - -query GetNullableDatesWithDefaultsByKey($key: NullableDatesWithDefaults_Key!) @auth(level: PUBLIC) { - nullableDatesWithDefaults(key: $key) { - valueWithVariableDefault - valueWithSchemaDefault - epoch - requestTime1 - requestTime2 - } -} - ############################################################################### # Operations for table: NonNullTimestamp ############################################################################### diff --git a/firebase-dataconnect/emulator/dataconnect/schema/demo_schema.gql b/firebase-dataconnect/emulator/dataconnect/schema/demo_schema.gql index 49ba107a134..4bad02dc62f 100644 --- a/firebase-dataconnect/emulator/dataconnect/schema/demo_schema.gql +++ b/firebase-dataconnect/emulator/dataconnect/schema/demo_schema.gql @@ -287,30 +287,6 @@ type DateNullableWithDefaults @table { requestTime2: Date @default(expr: "request.time") } -type NonNullDate @table { - value: Date! -} - -type NullableDate @table { - value: Date -} - -type NonNullDatesWithDefaults @table { - valueWithVariableDefault: Date! - valueWithSchemaDefault: Date! @default(value: "2112-01-31") - epoch: Date! @default(sql: "'epoch'::date") - requestTime1: Date! @default(expr: "request.time") - requestTime2: Date! @default(expr: "request.time") -} - -type NullableDatesWithDefaults @table { - valueWithVariableDefault: Date - valueWithSchemaDefault: Date @default(value: "1921-12-02") - epoch: Date @default(sql: "'epoch'::date") - requestTime1: Date @default(expr: "request.time") - requestTime2: Date @default(expr: "request.time") -} - type NonNullTimestamp @table @index(fields: ["tag"]) { value: Timestamp! tag: String diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/dates.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/dates.kt index 6ed685f0476..1535b12a167 100644 --- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/dates.kt +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/dates.kt @@ -139,6 +139,13 @@ data class ThreeDateTestDatas( else -> throw Exception("internal error: unknown index: $index") } + val numMatchingSelected: Int = run { + val v1 = if (testData1 == selected) 1 else 0 + val v2 = if (testData2 == selected) 1 else 0 + val v3 = if (testData3 == selected) 1 else 0 + v1 + v2 + v3 + } + fun idsMatchingSelected(getter: (ItemNumber) -> UUID): List<UUID> = idsMatching(selected?.date, getter) From 75e552051147f72cd0ee8f238beff88c0b3d2e18 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare <dconeybe@google.com> Date: Thu, 14 Nov 2024 05:57:17 +0000 Subject: [PATCH 11/13] JavaTimeLocalDateSerializer.kt and KotlinxDatetimeLocalDateSerializer.kt added --- .../JavaTimeLocalDateIntegrationTest.kt | 720 ++++++++++++++++++ ...KotlinxDatetimeLocalDateIntegrationTest.kt | 720 ++++++++++++++++++ .../dataconnect/LocalDateIntegrationTest.kt | 12 + .../JavaTimeLocalDateSerializer.kt | 51 ++ .../KotlinxDatetimeLocalDateSerializer.kt | 52 ++ .../serializers/LocalDateSerializer.kt | 3 + .../JavaTimeLocalDateSerializerUnitTest.kt | 261 +++++++ ...linxDatetimeLocalDateSerializerUnitTest.kt | 264 +++++++ .../LocalDateSerializerUnitTest.kt | 12 + .../firebase/dataconnect/testutil/JavaTime.kt | 23 +- .../testutil/property/arbitrary/dates.kt | 28 +- 11 files changed, 2138 insertions(+), 8 deletions(-) create mode 100644 firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/JavaTimeLocalDateIntegrationTest.kt create mode 100644 firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/KotlinxDatetimeLocalDateIntegrationTest.kt create mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/JavaTimeLocalDateSerializer.kt create mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/KotlinxDatetimeLocalDateSerializer.kt create mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/JavaTimeLocalDateSerializerUnitTest.kt create mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/KotlinxDatetimeLocalDateSerializerUnitTest.kt diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/JavaTimeLocalDateIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/JavaTimeLocalDateIntegrationTest.kt new file mode 100644 index 00000000000..e99238cf00e --- /dev/null +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/JavaTimeLocalDateIntegrationTest.kt @@ -0,0 +1,720 @@ +/* + * Copyright 2024 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. + */ + +@file:OptIn(ExperimentalKotest::class) +@file:UseSerializers(UUIDSerializer::class, JavaTimeLocalDateSerializer::class) + +//////////////////////////////////////////////////////////////////////////////// +// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateIntegrationTest.kt +// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO +// LocalDateIntegrationTest.kt AND PORTED TO LocalDateIntegrationTest.kt, +// if appropriate. +//////////////////////////////////////////////////////////////////////////////// +package com.google.firebase.dataconnect + +import com.google.firebase.dataconnect.serializers.JavaTimeLocalDateSerializer +import com.google.firebase.dataconnect.serializers.UUIDSerializer +import com.google.firebase.dataconnect.testutil.DataConnectIntegrationTestBase +import com.google.firebase.dataconnect.testutil.property.arbitrary.DateTestData +import com.google.firebase.dataconnect.testutil.property.arbitrary.EdgeCases +import com.google.firebase.dataconnect.testutil.property.arbitrary.ThreeDateTestDatas +import com.google.firebase.dataconnect.testutil.property.arbitrary.ThreeDateTestDatas.ItemNumber +import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect +import com.google.firebase.dataconnect.testutil.property.arbitrary.dateTestData +import com.google.firebase.dataconnect.testutil.property.arbitrary.invalidDateScalarString +import com.google.firebase.dataconnect.testutil.property.arbitrary.localDate +import com.google.firebase.dataconnect.testutil.property.arbitrary.orNullableReference +import com.google.firebase.dataconnect.testutil.property.arbitrary.threeNonNullDatesTestData +import com.google.firebase.dataconnect.testutil.property.arbitrary.threePossiblyNullDatesTestData +import com.google.firebase.dataconnect.testutil.requestTimeAsDate +import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingText +import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingTextIgnoringCase +import com.google.firebase.dataconnect.testutil.toTheeTenAbpJavaLocalDate +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.assertions.withClue +import io.kotest.common.ExperimentalKotest +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldBeIn +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.EdgeConfig +import io.kotest.property.PropTestConfig +import io.kotest.property.arbitrary.map +import io.kotest.property.arbitrary.next +import io.kotest.property.arbitrary.withEdgecases +import io.kotest.property.checkAll +import java.util.UUID +import kotlin.time.Duration.Companion.minutes +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers +import kotlinx.serialization.serializer +import org.junit.Test + +//////////////////////////////////////////////////////////////////////////////// +// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateIntegrationTest.kt +// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO +// LocalDateIntegrationTest.kt AND PORTED TO LocalDateIntegrationTest.kt, +// if appropriate. +//////////////////////////////////////////////////////////////////////////////// +class JavaTimeLocalDateIntegrationTest : DataConnectIntegrationTestBase() { + + private val dataConnect: FirebaseDataConnect by lazy { + val connectorConfig = testConnectorConfig.copy(connector = "demo") + dataConnectFactory.newInstance(connectorConfig) + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for inserting into and querying this table: + // type DateNonNullable @table { value: Date!, tag: String } + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun dateNonNullable_MutationVariable() = + runTest(timeout = 1.minutes) { + checkAll(propTestConfig, Arb.dataConnect.localDate().map { it.toJavaLocalDate() }) { localDate + -> + val insertResult = nonNullableDate.insert(localDate) + val queryResult = nonNullableDate.getByKey(insertResult.data.key) + queryResult.data.item?.value shouldBe localDate + } + } + + @Test + fun dateNonNullable_QueryVariable() = + runTest(timeout = 1.minutes) { + checkAll( + propTestConfig, + Arb.dataConnect.tag(), + Arb.dataConnect.threeNonNullDatesTestData() + ) { tag, testDatas -> + val insertResult = nonNullableDate.insert3(tag, testDatas) + val queryResult = + nonNullableDate.getAllByTagAndValue(tag, testDatas.selected!!.date.toJavaLocalDate()) + val matchingIds = testDatas.idsMatchingSelected(insertResult) + queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds + } + } + + @Test + fun dateNonNullable_MutationNullVariableShouldThrow() = runTest { + val exception = shouldThrow<DataConnectException> { nonNullableDate.insert(null) } + assertSoftly { + exception.message shouldContainWithNonAbuttingText "\$value" + exception.message shouldContainWithNonAbuttingText "is null" + } + } + + @Test + fun dateNonNullable_QueryNullVariableShouldThrow() = runTest { + val tag = Arb.dataConnect.tag().next(rs) + val exception = + shouldThrow<DataConnectException> { nonNullableDate.getAllByTagAndValue(tag, null) } + assertSoftly { + exception.message shouldContainWithNonAbuttingText "\$value" + exception.message shouldContainWithNonAbuttingText "is null" + } + } + + @Test + fun dateNonNullable_QueryOmittedVariableShouldMatchAll() = runTest { + val tag = Arb.dataConnect.tag().next(rs) + val testDatas = Arb.dataConnect.threeNonNullDatesTestData().next(rs) + val insertResult = nonNullableDate.insert3(tag, testDatas) + val queryResult = nonNullableDate.getAllByTagAndMaybeValue(tag) + queryResult.data.items + .map { it.id } + .shouldContainExactlyInAnyOrder( + insertResult.data.key1.id, + insertResult.data.key2.id, + insertResult.data.key3.id + ) + } + + @Test + fun dateNonNullable_QueryNullVariableShouldMatchNone() = runTest { + val tag = Arb.dataConnect.tag().next(rs) + val testDatas = Arb.dataConnect.threeNonNullDatesTestData().next(rs) + nonNullableDate.insert3(tag, testDatas) + val queryResult = nonNullableDate.getAllByTagAndMaybeValue(tag, null) + queryResult.data.items.shouldBeEmpty() + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for inserting into and querying this table: + // type DateNullable @table { value: Date, tag: String } + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun dateNullable_MutationVariable() = + runTest(timeout = 1.minutes) { + val localDates = + Arb.dataConnect + .localDate() + .map { it.toJavaLocalDate() } + .orNullableReference(nullProbability = 0.2) + checkAll(propTestConfig, localDates) { localDate -> + val insertResult = nullableDate.insert(localDate.ref) + val queryResult = nullableDate.getByKey(insertResult.data.key) + queryResult.data.item?.value shouldBe localDate.ref + } + } + + @Test + fun dateNullable_QueryVariable() = + runTest(timeout = 1.minutes) { + checkAll( + propTestConfig, + Arb.dataConnect.tag(), + Arb.dataConnect.threePossiblyNullDatesTestData() + ) { tag, testDatas -> + val insertResult = nullableDate.insert3(tag, testDatas) + val queryResult = + nullableDate.getAllByTagAndValue(tag, testDatas.selected?.date?.toJavaLocalDate()) + val matchingIds = testDatas.idsMatchingSelected(insertResult) + queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds + } + } + + @Test + fun dateNullable_QueryOmittedVariable() = + runTest(timeout = 1.minutes) { + checkAll( + propTestConfig, + Arb.dataConnect.tag(), + Arb.dataConnect.threePossiblyNullDatesTestData() + ) { tag, testDatas -> + val insertResult = nullableDate.insert3(tag, testDatas) + val queryResult = nullableDate.getAllByTagAndMaybeValue(tag) + queryResult.data.items + .map { it.id } + .shouldContainExactlyInAnyOrder( + insertResult.data.key1.id, + insertResult.data.key2.id, + insertResult.data.key3.id + ) + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for default `Date` variable values. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun dateNonNullable_MutationVariableDefaults() = runTest { + val insertResult = nonNullableDate.insertWithDefaults() + val queryResult = nonNullableDate.getInsertedWithDefaultsByKey(insertResult.data.key) + val item = withClue("queryResult.data.item") { queryResult.data.item.shouldNotBeNull() } + + assertSoftly { + withClue(item) { + withClue("valueWithVariableDefault") { + item.valueWithVariableDefault shouldBe java.time.LocalDate.of(6904, 11, 30) + } + withClue("valueWithSchemaDefault") { + item.valueWithSchemaDefault shouldBe java.time.LocalDate.of(2112, 1, 31) + } + withClue("epoch") { item.epoch shouldBe EdgeCases.dates.epoch.date.toJavaLocalDate() } + withClue("requestTime1") { item.requestTime1.shouldNotBeNull() } + withClue("requestTime2") { item.requestTime2 shouldBe item.requestTime1 } + } + } + + withClue("requestTime validation") { + val today = dataConnect.requestTimeAsDate().toTheeTenAbpJavaLocalDate() + val yesterday = today.minusDays(1) + val tomorrow = today.plusDays(1) + val requestTime = item.requestTime1!!.toTheeTenAbpJavaLocalDate() + requestTime.shouldBeIn(yesterday, today, tomorrow) + } + } + + @Test + fun dateNonNullable_QueryVariableDefaults() = + runTest(timeout = 1.minutes) { + val defaultTestData = DateTestData(java.time.LocalDate.of(2692, 5, 21), "2692-05-21") + val localDateArb = Arb.dataConnect.dateTestData().withEdgecases(defaultTestData) + checkAll( + propTestConfig, + Arb.dataConnect.threeNonNullDatesTestData(localDateArb), + Arb.dataConnect.tag() + ) { testDatas, tag -> + val insertResult = nonNullableDate.insert3(tag, testDatas) + val queryResult = nonNullableDate.getAllByTagAndDefaultValue(tag) + val matchingIds = + testDatas.idsMatching(insertResult, defaultTestData.date.toJavaLocalDate()) + queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds + } + } + + @Test + fun dateNullable_MutationVariableDefaults() = runTest { + val insertResult = nullableDate.insertWithDefaults() + val queryResult = nullableDate.getInsertedWithDefaultsByKey(insertResult.data.key) + val item = withClue("queryResult.data.item") { queryResult.data.item.shouldNotBeNull() } + + assertSoftly { + withClue(item) { + withClue("valueWithVariableDefault") { + item.valueWithVariableDefault shouldBe java.time.LocalDate.of(8113, 2, 9) + } + withClue("valueWithVariableNullDefault") { + item.valueWithVariableNullDefault.shouldBeNull() + } + withClue("valueWithSchemaDefault") { + item.valueWithSchemaDefault shouldBe java.time.LocalDate.of(1921, 12, 2) + } + withClue("valueWithSchemaNullDefault") { item.valueWithSchemaNullDefault.shouldBeNull() } + withClue("valueWithNoDefault") { item.valueWithNoDefault.shouldBeNull() } + withClue("epoch") { item.epoch shouldBe EdgeCases.dates.epoch.date.toJavaLocalDate() } + withClue("requestTime1") { item.requestTime1.shouldNotBeNull() } + withClue("requestTime2") { item.requestTime2 shouldBe item.requestTime1 } + } + } + + withClue("requestTime validation") { + val today = dataConnect.requestTimeAsDate().toTheeTenAbpJavaLocalDate() + val yesterday = today.minusDays(1) + val tomorrow = today.plusDays(1) + val requestTime = item.requestTime1!!.toTheeTenAbpJavaLocalDate() + requestTime.shouldBeIn(yesterday, today, tomorrow) + } + } + + @Test + fun dateNullable_QueryVariableDefaults() = + runTest(timeout = 1.minutes) { + val defaultTestData = DateTestData(java.time.LocalDate.of(1771, 10, 28), "1771-10-28") + val dateTestDataArb = + Arb.dataConnect + .dateTestData() + .withEdgecases(defaultTestData) + .orNullableReference(nullProbability = 0.333) + checkAll( + propTestConfig, + Arb.dataConnect.threePossiblyNullDatesTestData(dateTestDataArb), + Arb.dataConnect.tag() + ) { testDatas, tag -> + val insertResult = nullableDate.insert3(tag, testDatas) + val queryResult = nullableDate.getAllByTagAndDefaultValue(tag) + val matchingIds = + testDatas.idsMatching(insertResult, defaultTestData.date.toJavaLocalDate()) + queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Invalid Date String Tests + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun dateNonNullable_MutationInvalidDateVariable() = + runTest(timeout = 1.minutes) { + checkAll(propTestConfig.copy(iterations = 500), Arb.dataConnect.invalidDateScalarString()) { + testData -> + val exception = + shouldThrow<DataConnectException> { + nonNullableDate.insert(testData.toDateScalarString()) + } + assertSoftly { + exception.message shouldContainWithNonAbuttingText "\$value" + exception.message shouldContainWithNonAbuttingTextIgnoringCase "invalid" + } + } + } + + @Test + fun dateNonNullable_QueryInvalidDateVariable() = + runTest(timeout = 1.minutes) { + checkAll( + propTestConfig.copy(iterations = 500), + Arb.dataConnect.tag(), + Arb.dataConnect.invalidDateScalarString() + ) { tag, testData -> + val exception = + shouldThrow<DataConnectException> { + nonNullableDate.getAllByTagAndValue(tag = tag, value = testData.toDateScalarString()) + } + assertSoftly { + exception.message shouldContainWithNonAbuttingText "\$value" + exception.message shouldContainWithNonAbuttingTextIgnoringCase "invalid" + } + } + } + + @Test + fun dateNullable_MutationInvalidDateVariable() = + runTest(timeout = 1.minutes) { + checkAll(propTestConfig.copy(iterations = 500), Arb.dataConnect.invalidDateScalarString()) { + testData -> + val exception = + shouldThrow<DataConnectException> { nullableDate.insert(testData.toDateScalarString()) } + assertSoftly { + exception.message shouldContainWithNonAbuttingText "\$value" + exception.message shouldContainWithNonAbuttingTextIgnoringCase "invalid" + } + } + } + + @Test + fun dateNullable_QueryInvalidDateVariable() = + runTest(timeout = 1.minutes) { + checkAll( + propTestConfig.copy(iterations = 500), + Arb.dataConnect.tag(), + Arb.dataConnect.invalidDateScalarString() + ) { tag, testData -> + val exception = + shouldThrow<DataConnectException> { + nullableDate.getAllByTagAndValue(tag = tag, value = testData.toDateScalarString()) + } + assertSoftly { + exception.message shouldContainWithNonAbuttingText "\$value" + exception.message shouldContainWithNonAbuttingTextIgnoringCase "invalid" + } + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Helper methods and classes. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Serializable private data class SingleKeyVariables(val key: Key) + + @Serializable private data class SingleKeyData(val key: Key) + + @Serializable + private data class MultipleKeysData(val items: List<Item>) { + @Serializable data class Item(val id: UUID) + } + + @Serializable private data class ThreeKeysData(val key1: Key, val key2: Key, val key3: Key) + + @Serializable private data class InsertVariables(val value: java.time.LocalDate?) + + @Serializable private data class InsertStringVariables(val value: String) + + @Serializable + private data class Insert3Variables( + val tag: String, + val value1: java.time.LocalDate?, + val value2: java.time.LocalDate?, + val value3: java.time.LocalDate?, + ) + + @Serializable private data class TagVariables(val tag: String) + + @Serializable + private data class TagAndValueVariables(val tag: String, val value: java.time.LocalDate?) + + @Serializable private data class TagAndStringValueVariables(val tag: String, val value: String) + + @Serializable + private data class QueryData(val item: Item?) { + @Serializable data class Item(val value: java.time.LocalDate?) + } + + @Serializable + private data class GetInsertedWithDefaultsByKeyQueryData(val item: Item?) { + @Serializable + data class Item( + val valueWithVariableDefault: java.time.LocalDate, + val valueWithVariableNullDefault: java.time.LocalDate?, + val valueWithSchemaDefault: java.time.LocalDate, + val valueWithSchemaNullDefault: java.time.LocalDate?, + val valueWithNoDefault: java.time.LocalDate?, + val epoch: java.time.LocalDate?, + val requestTime1: java.time.LocalDate?, + val requestTime2: java.time.LocalDate?, + ) + } + + @Serializable private data class Key(val id: UUID) + + /** Operations for querying and mutating the table that stores non-nullable Date scalar values. */ + private val nonNullableDate = + Operations( + getByKeyQueryName = "DateNonNullable_GetByKey", + getAllByTagAndValueQueryName = "DateNonNullable_GetAllByTagAndValue", + getAllByTagAndMaybeValueQueryName = "DateNonNullable_GetAllByTagAndMaybeValue", + getAllByTagAndDefaultValueQueryName = "DateNonNullable_GetAllByTagAndDefaultValue", + insertMutationName = "DateNonNullable_Insert", + insert3MutationName = "DateNonNullable_Insert3", + insertWithDefaultsMutationName = "DateNonNullableWithDefaults_Insert", + getInsertedWithDefaultsByKeyQueryName = "DateNonNullableWithDefaults_GetByKey", + ) + + /** Operations for querying and mutating the table that stores nullable Date scalar values. */ + private val nullableDate = + Operations( + getByKeyQueryName = "DateNullable_GetByKey", + getAllByTagAndValueQueryName = "DateNullable_GetAllByTagAndValue", + getAllByTagAndMaybeValueQueryName = "DateNullable_GetAllByTagAndValue", + getAllByTagAndDefaultValueQueryName = "DateNullable_GetAllByTagAndDefaultValue", + insertMutationName = "DateNullable_Insert", + insert3MutationName = "DateNullable_Insert3", + insertWithDefaultsMutationName = "DateNullableWithDefaults_Insert", + getInsertedWithDefaultsByKeyQueryName = "DateNullableWithDefaults_GetByKey", + ) + + private inner class Operations( + getByKeyQueryName: String, + getAllByTagAndValueQueryName: String, + getAllByTagAndMaybeValueQueryName: String, + getAllByTagAndDefaultValueQueryName: String, + insertMutationName: String, + insert3MutationName: String, + insertWithDefaultsMutationName: String, + getInsertedWithDefaultsByKeyQueryName: String, + ) { + + suspend fun insert( + localDate: java.time.LocalDate? + ): MutationResult<SingleKeyData, InsertVariables> = insert(InsertVariables(localDate)) + + suspend fun insert(variables: InsertVariables): MutationResult<SingleKeyData, InsertVariables> = + mutations.insert(variables).execute() + + suspend fun insert(localDate: String): MutationResult<SingleKeyData, InsertStringVariables> = + insert(InsertStringVariables(localDate)) + + suspend fun insert( + variables: InsertStringVariables + ): MutationResult<SingleKeyData, InsertStringVariables> = mutations.insert(variables).execute() + + suspend fun insert3( + tag: String, + testDatas: ThreeDateTestDatas, + ): MutationResult<ThreeKeysData, Insert3Variables> = + insert3( + tag = tag, + value1 = testDatas.testData1?.date?.toJavaLocalDate(), + value2 = testDatas.testData2?.date?.toJavaLocalDate(), + value3 = testDatas.testData3?.date?.toJavaLocalDate() + ) + + suspend fun insert3( + tag: String, + value1: java.time.LocalDate?, + value2: java.time.LocalDate?, + value3: java.time.LocalDate?, + ): MutationResult<ThreeKeysData, Insert3Variables> = + insert3(Insert3Variables(tag = tag, value1 = value1, value2 = value2, value3 = value3)) + + suspend fun insert3( + variables: Insert3Variables + ): MutationResult<ThreeKeysData, Insert3Variables> = mutations.insert3(variables).execute() + + suspend fun getByKey(key: Key): QueryResult<QueryData, SingleKeyVariables> = + getByKey(SingleKeyVariables(key)) + + suspend fun getByKey( + variables: SingleKeyVariables + ): QueryResult<QueryData, SingleKeyVariables> = queries.getByKey(variables).execute() + + suspend fun getAllByTagAndValue( + tag: String, + value: java.time.LocalDate? + ): QueryResult<MultipleKeysData, TagAndValueVariables> = + getAllByTagAndValue(TagAndValueVariables(tag, value)) + + suspend fun getAllByTagAndValue( + variables: TagAndValueVariables + ): QueryResult<MultipleKeysData, TagAndValueVariables> = + queries.getAllByTagAndValue(variables).execute() + + suspend fun getAllByTagAndValue( + tag: String, + value: String + ): QueryResult<MultipleKeysData, TagAndStringValueVariables> = + getAllByTagAndValue(TagAndStringValueVariables(tag, value)) + + suspend fun getAllByTagAndValue( + variables: TagAndStringValueVariables + ): QueryResult<MultipleKeysData, TagAndStringValueVariables> = + queries.getAllByTagAndValue(variables).execute() + + suspend fun getAllByTagAndMaybeValue( + tag: String, + ): QueryResult<MultipleKeysData, TagVariables> = getAllByTagAndMaybeValue(TagVariables(tag)) + + suspend fun getAllByTagAndMaybeValue( + variables: TagVariables + ): QueryResult<MultipleKeysData, TagVariables> = + queries.getAllByTagAndMaybeValue(variables).execute() + + suspend fun getAllByTagAndMaybeValue( + tag: String, + value: Nothing?, + ): QueryResult<MultipleKeysData, TagAndValueVariables> = + getAllByTagAndMaybeValue(TagAndValueVariables(tag, value)) + + suspend fun getAllByTagAndMaybeValue( + variables: TagAndValueVariables + ): QueryResult<MultipleKeysData, TagAndValueVariables> = + queries.getAllByTagAndMaybeValue(variables).execute() + + suspend fun getAllByTagAndDefaultValue( + tag: String + ): QueryResult<MultipleKeysData, TagVariables> = getAllByTagAndDefaultValue(TagVariables(tag)) + + suspend fun getAllByTagAndDefaultValue( + variables: TagVariables + ): QueryResult<MultipleKeysData, TagVariables> = + queries.getAllByTagAndDefaultValue(variables).execute() + + suspend fun insertWithDefaults(): MutationResult<SingleKeyData, Unit> = + mutations.insertWithDefaults().execute() + + suspend fun getInsertedWithDefaultsByKey( + key: Key + ): QueryResult<GetInsertedWithDefaultsByKeyQueryData, SingleKeyVariables> = + getInsertedWithDefaultsByKey(SingleKeyVariables(key)) + + suspend fun getInsertedWithDefaultsByKey( + variables: SingleKeyVariables + ): QueryResult<GetInsertedWithDefaultsByKeyQueryData, SingleKeyVariables> = + queries.getInsertedWithDefaultsByKey(variables).execute() + + private val queries = + object { + fun getByKey(variables: SingleKeyVariables): QueryRef<QueryData, SingleKeyVariables> = + dataConnect.query( + getByKeyQueryName, + variables, + serializer(), + serializer(), + ) + + inline fun <reified Variables> getAllByTagAndValue( + variables: Variables + ): QueryRef<MultipleKeysData, Variables> = + dataConnect.query( + getAllByTagAndValueQueryName, + variables, + serializer(), + serializer(), + ) + + fun getAllByTagAndMaybeValue( + variables: TagVariables + ): QueryRef<MultipleKeysData, TagVariables> = + dataConnect.query( + getAllByTagAndMaybeValueQueryName, + variables, + serializer(), + serializer(), + ) + + fun getAllByTagAndMaybeValue( + variables: TagAndValueVariables + ): QueryRef<MultipleKeysData, TagAndValueVariables> = + dataConnect.query( + getAllByTagAndMaybeValueQueryName, + variables, + serializer(), + serializer(), + ) + + fun getAllByTagAndDefaultValue( + variables: TagVariables + ): QueryRef<MultipleKeysData, TagVariables> = + dataConnect.query( + getAllByTagAndDefaultValueQueryName, + variables, + serializer(), + serializer(), + ) + + fun getInsertedWithDefaultsByKey( + variables: SingleKeyVariables + ): QueryRef<GetInsertedWithDefaultsByKeyQueryData, SingleKeyVariables> = + dataConnect.query( + getInsertedWithDefaultsByKeyQueryName, + variables, + serializer(), + serializer(), + ) + } + + private val mutations = + object { + inline fun <reified Variables> insert( + variables: Variables + ): MutationRef<SingleKeyData, Variables> = + dataConnect.mutation( + insertMutationName, + variables, + serializer(), + serializer(), + ) + + fun insert3(variables: Insert3Variables): MutationRef<ThreeKeysData, Insert3Variables> = + dataConnect.mutation( + insert3MutationName, + variables, + serializer(), + serializer(), + ) + + fun insertWithDefaults(): MutationRef<SingleKeyData, Unit> = + dataConnect.mutation( + insertWithDefaultsMutationName, + Unit, + serializer(), + serializer(), + ) + } + } + + private companion object { + val propTestConfig = + PropTestConfig( + iterations = 20, + edgeConfig = EdgeConfig(edgecasesGenerationProbability = 0.5), + ) + + fun ThreeDateTestDatas.idsMatchingSelected( + result: MutationResult<ThreeKeysData, *> + ): List<UUID> = idsMatchingSelected(result.data) + + fun ThreeDateTestDatas.idsMatchingSelected(data: ThreeKeysData): List<UUID> = + idsMatchingSelected { + data.uuidFromItemNumber(it) + } + + fun ThreeDateTestDatas.idsMatching( + result: MutationResult<ThreeKeysData, *>, + localDate: java.time.LocalDate?, + ): List<UUID> = idsMatching(result.data, localDate) + + fun ThreeDateTestDatas.idsMatching( + data: ThreeKeysData, + localDate: java.time.LocalDate?, + ): List<UUID> = idsMatching(localDate) { data.uuidFromItemNumber(it) } + + fun ThreeKeysData.uuidFromItemNumber(itemNumber: ItemNumber): UUID = + when (itemNumber) { + ItemNumber.ONE -> key1 + ItemNumber.TWO -> key2 + ItemNumber.THREE -> key3 + }.id + } +} diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/KotlinxDatetimeLocalDateIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/KotlinxDatetimeLocalDateIntegrationTest.kt new file mode 100644 index 00000000000..a4ea009a7c7 --- /dev/null +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/KotlinxDatetimeLocalDateIntegrationTest.kt @@ -0,0 +1,720 @@ +/* + * Copyright 2024 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. + */ + +@file:OptIn(ExperimentalKotest::class) +@file:UseSerializers(UUIDSerializer::class, KotlinxDatetimeLocalDateSerializer::class) + +//////////////////////////////////////////////////////////////////////////////// +// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateIntegrationTest.kt +// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO +// LocalDateIntegrationTest.kt AND PORTED TO JavaTimeLocalDateIntegrationTest.kt, +// if appropriate. +//////////////////////////////////////////////////////////////////////////////// +package com.google.firebase.dataconnect + +import com.google.firebase.dataconnect.serializers.KotlinxDatetimeLocalDateSerializer +import com.google.firebase.dataconnect.serializers.UUIDSerializer +import com.google.firebase.dataconnect.testutil.DataConnectIntegrationTestBase +import com.google.firebase.dataconnect.testutil.property.arbitrary.DateTestData +import com.google.firebase.dataconnect.testutil.property.arbitrary.EdgeCases +import com.google.firebase.dataconnect.testutil.property.arbitrary.ThreeDateTestDatas +import com.google.firebase.dataconnect.testutil.property.arbitrary.ThreeDateTestDatas.ItemNumber +import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect +import com.google.firebase.dataconnect.testutil.property.arbitrary.dateTestData +import com.google.firebase.dataconnect.testutil.property.arbitrary.invalidDateScalarString +import com.google.firebase.dataconnect.testutil.property.arbitrary.localDate +import com.google.firebase.dataconnect.testutil.property.arbitrary.orNullableReference +import com.google.firebase.dataconnect.testutil.property.arbitrary.threeNonNullDatesTestData +import com.google.firebase.dataconnect.testutil.property.arbitrary.threePossiblyNullDatesTestData +import com.google.firebase.dataconnect.testutil.requestTimeAsDate +import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingText +import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingTextIgnoringCase +import com.google.firebase.dataconnect.testutil.toTheeTenAbpJavaLocalDate +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.assertions.withClue +import io.kotest.common.ExperimentalKotest +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldBeIn +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.EdgeConfig +import io.kotest.property.PropTestConfig +import io.kotest.property.arbitrary.map +import io.kotest.property.arbitrary.next +import io.kotest.property.arbitrary.withEdgecases +import io.kotest.property.checkAll +import java.util.UUID +import kotlin.time.Duration.Companion.minutes +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers +import kotlinx.serialization.serializer +import org.junit.Test + +//////////////////////////////////////////////////////////////////////////////// +// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateIntegrationTest.kt +// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO +// LocalDateIntegrationTest.kt AND PORTED TO JavaTimeLocalDateIntegrationTest.kt, +// if appropriate. +//////////////////////////////////////////////////////////////////////////////// +class KotlinxDatetimeLocalDateIntegrationTest : DataConnectIntegrationTestBase() { + + private val dataConnect: FirebaseDataConnect by lazy { + val connectorConfig = testConnectorConfig.copy(connector = "demo") + dataConnectFactory.newInstance(connectorConfig) + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for inserting into and querying this table: + // type DateNonNullable @table { value: Date!, tag: String } + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun dateNonNullable_MutationVariable() = + runTest(timeout = 1.minutes) { + checkAll(propTestConfig, Arb.dataConnect.localDate().map { it.toKotlinxLocalDate() }) { + localDate -> + val insertResult = nonNullableDate.insert(localDate) + val queryResult = nonNullableDate.getByKey(insertResult.data.key) + queryResult.data.item?.value shouldBe localDate + } + } + + @Test + fun dateNonNullable_QueryVariable() = + runTest(timeout = 1.minutes) { + checkAll( + propTestConfig, + Arb.dataConnect.tag(), + Arb.dataConnect.threeNonNullDatesTestData() + ) { tag, testDatas -> + val insertResult = nonNullableDate.insert3(tag, testDatas) + val queryResult = + nonNullableDate.getAllByTagAndValue(tag, testDatas.selected!!.date.toKotlinxLocalDate()) + val matchingIds = testDatas.idsMatchingSelected(insertResult) + queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds + } + } + + @Test + fun dateNonNullable_MutationNullVariableShouldThrow() = runTest { + val exception = shouldThrow<DataConnectException> { nonNullableDate.insert(null) } + assertSoftly { + exception.message shouldContainWithNonAbuttingText "\$value" + exception.message shouldContainWithNonAbuttingText "is null" + } + } + + @Test + fun dateNonNullable_QueryNullVariableShouldThrow() = runTest { + val tag = Arb.dataConnect.tag().next(rs) + val exception = + shouldThrow<DataConnectException> { nonNullableDate.getAllByTagAndValue(tag, null) } + assertSoftly { + exception.message shouldContainWithNonAbuttingText "\$value" + exception.message shouldContainWithNonAbuttingText "is null" + } + } + + @Test + fun dateNonNullable_QueryOmittedVariableShouldMatchAll() = runTest { + val tag = Arb.dataConnect.tag().next(rs) + val testDatas = Arb.dataConnect.threeNonNullDatesTestData().next(rs) + val insertResult = nonNullableDate.insert3(tag, testDatas) + val queryResult = nonNullableDate.getAllByTagAndMaybeValue(tag) + queryResult.data.items + .map { it.id } + .shouldContainExactlyInAnyOrder( + insertResult.data.key1.id, + insertResult.data.key2.id, + insertResult.data.key3.id + ) + } + + @Test + fun dateNonNullable_QueryNullVariableShouldMatchNone() = runTest { + val tag = Arb.dataConnect.tag().next(rs) + val testDatas = Arb.dataConnect.threeNonNullDatesTestData().next(rs) + nonNullableDate.insert3(tag, testDatas) + val queryResult = nonNullableDate.getAllByTagAndMaybeValue(tag, null) + queryResult.data.items.shouldBeEmpty() + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for inserting into and querying this table: + // type DateNullable @table { value: Date, tag: String } + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun dateNullable_MutationVariable() = + runTest(timeout = 1.minutes) { + val localDates = + Arb.dataConnect + .localDate() + .map { it.toKotlinxLocalDate() } + .orNullableReference(nullProbability = 0.2) + checkAll(propTestConfig, localDates) { localDate -> + val insertResult = nullableDate.insert(localDate.ref) + val queryResult = nullableDate.getByKey(insertResult.data.key) + queryResult.data.item?.value shouldBe localDate.ref + } + } + + @Test + fun dateNullable_QueryVariable() = + runTest(timeout = 1.minutes) { + checkAll( + propTestConfig, + Arb.dataConnect.tag(), + Arb.dataConnect.threePossiblyNullDatesTestData() + ) { tag, testDatas -> + val insertResult = nullableDate.insert3(tag, testDatas) + val queryResult = + nullableDate.getAllByTagAndValue(tag, testDatas.selected?.date?.toKotlinxLocalDate()) + val matchingIds = testDatas.idsMatchingSelected(insertResult) + queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds + } + } + + @Test + fun dateNullable_QueryOmittedVariable() = + runTest(timeout = 1.minutes) { + checkAll( + propTestConfig, + Arb.dataConnect.tag(), + Arb.dataConnect.threePossiblyNullDatesTestData() + ) { tag, testDatas -> + val insertResult = nullableDate.insert3(tag, testDatas) + val queryResult = nullableDate.getAllByTagAndMaybeValue(tag) + queryResult.data.items + .map { it.id } + .shouldContainExactlyInAnyOrder( + insertResult.data.key1.id, + insertResult.data.key2.id, + insertResult.data.key3.id + ) + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for default `Date` variable values. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun dateNonNullable_MutationVariableDefaults() = runTest { + val insertResult = nonNullableDate.insertWithDefaults() + val queryResult = nonNullableDate.getInsertedWithDefaultsByKey(insertResult.data.key) + val item = withClue("queryResult.data.item") { queryResult.data.item.shouldNotBeNull() } + + assertSoftly { + withClue(item) { + withClue("valueWithVariableDefault") { + item.valueWithVariableDefault shouldBe kotlinx.datetime.LocalDate(6904, 11, 30) + } + withClue("valueWithSchemaDefault") { + item.valueWithSchemaDefault shouldBe kotlinx.datetime.LocalDate(2112, 1, 31) + } + withClue("epoch") { item.epoch shouldBe EdgeCases.dates.epoch.date.toKotlinxLocalDate() } + withClue("requestTime1") { item.requestTime1.shouldNotBeNull() } + withClue("requestTime2") { item.requestTime2 shouldBe item.requestTime1 } + } + } + + withClue("requestTime validation") { + val today = dataConnect.requestTimeAsDate().toTheeTenAbpJavaLocalDate() + val yesterday = today.minusDays(1) + val tomorrow = today.plusDays(1) + val requestTime = item.requestTime1!!.toTheeTenAbpJavaLocalDate() + requestTime.shouldBeIn(yesterday, today, tomorrow) + } + } + + @Test + fun dateNonNullable_QueryVariableDefaults() = + runTest(timeout = 1.minutes) { + val defaultTestData = DateTestData(kotlinx.datetime.LocalDate(2692, 5, 21), "2692-05-21") + val localDateArb = Arb.dataConnect.dateTestData().withEdgecases(defaultTestData) + checkAll( + propTestConfig, + Arb.dataConnect.threeNonNullDatesTestData(localDateArb), + Arb.dataConnect.tag() + ) { testDatas, tag -> + val insertResult = nonNullableDate.insert3(tag, testDatas) + val queryResult = nonNullableDate.getAllByTagAndDefaultValue(tag) + val matchingIds = + testDatas.idsMatching(insertResult, defaultTestData.date.toKotlinxLocalDate()) + queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds + } + } + + @Test + fun dateNullable_MutationVariableDefaults() = runTest { + val insertResult = nullableDate.insertWithDefaults() + val queryResult = nullableDate.getInsertedWithDefaultsByKey(insertResult.data.key) + val item = withClue("queryResult.data.item") { queryResult.data.item.shouldNotBeNull() } + + assertSoftly { + withClue(item) { + withClue("valueWithVariableDefault") { + item.valueWithVariableDefault shouldBe kotlinx.datetime.LocalDate(8113, 2, 9) + } + withClue("valueWithVariableNullDefault") { + item.valueWithVariableNullDefault.shouldBeNull() + } + withClue("valueWithSchemaDefault") { + item.valueWithSchemaDefault shouldBe kotlinx.datetime.LocalDate(1921, 12, 2) + } + withClue("valueWithSchemaNullDefault") { item.valueWithSchemaNullDefault.shouldBeNull() } + withClue("valueWithNoDefault") { item.valueWithNoDefault.shouldBeNull() } + withClue("epoch") { item.epoch shouldBe EdgeCases.dates.epoch.date.toKotlinxLocalDate() } + withClue("requestTime1") { item.requestTime1.shouldNotBeNull() } + withClue("requestTime2") { item.requestTime2 shouldBe item.requestTime1 } + } + } + + withClue("requestTime validation") { + val today = dataConnect.requestTimeAsDate().toTheeTenAbpJavaLocalDate() + val yesterday = today.minusDays(1) + val tomorrow = today.plusDays(1) + val requestTime = item.requestTime1!!.toTheeTenAbpJavaLocalDate() + requestTime.shouldBeIn(yesterday, today, tomorrow) + } + } + + @Test + fun dateNullable_QueryVariableDefaults() = + runTest(timeout = 1.minutes) { + val defaultTestData = DateTestData(kotlinx.datetime.LocalDate(1771, 10, 28), "1771-10-28") + val dateTestDataArb = + Arb.dataConnect + .dateTestData() + .withEdgecases(defaultTestData) + .orNullableReference(nullProbability = 0.333) + checkAll( + propTestConfig, + Arb.dataConnect.threePossiblyNullDatesTestData(dateTestDataArb), + Arb.dataConnect.tag() + ) { testDatas, tag -> + val insertResult = nullableDate.insert3(tag, testDatas) + val queryResult = nullableDate.getAllByTagAndDefaultValue(tag) + val matchingIds = + testDatas.idsMatching(insertResult, defaultTestData.date.toKotlinxLocalDate()) + queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Invalid Date String Tests + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun dateNonNullable_MutationInvalidDateVariable() = + runTest(timeout = 1.minutes) { + checkAll(propTestConfig.copy(iterations = 500), Arb.dataConnect.invalidDateScalarString()) { + testData -> + val exception = + shouldThrow<DataConnectException> { + nonNullableDate.insert(testData.toDateScalarString()) + } + assertSoftly { + exception.message shouldContainWithNonAbuttingText "\$value" + exception.message shouldContainWithNonAbuttingTextIgnoringCase "invalid" + } + } + } + + @Test + fun dateNonNullable_QueryInvalidDateVariable() = + runTest(timeout = 1.minutes) { + checkAll( + propTestConfig.copy(iterations = 500), + Arb.dataConnect.tag(), + Arb.dataConnect.invalidDateScalarString() + ) { tag, testData -> + val exception = + shouldThrow<DataConnectException> { + nonNullableDate.getAllByTagAndValue(tag = tag, value = testData.toDateScalarString()) + } + assertSoftly { + exception.message shouldContainWithNonAbuttingText "\$value" + exception.message shouldContainWithNonAbuttingTextIgnoringCase "invalid" + } + } + } + + @Test + fun dateNullable_MutationInvalidDateVariable() = + runTest(timeout = 1.minutes) { + checkAll(propTestConfig.copy(iterations = 500), Arb.dataConnect.invalidDateScalarString()) { + testData -> + val exception = + shouldThrow<DataConnectException> { nullableDate.insert(testData.toDateScalarString()) } + assertSoftly { + exception.message shouldContainWithNonAbuttingText "\$value" + exception.message shouldContainWithNonAbuttingTextIgnoringCase "invalid" + } + } + } + + @Test + fun dateNullable_QueryInvalidDateVariable() = + runTest(timeout = 1.minutes) { + checkAll( + propTestConfig.copy(iterations = 500), + Arb.dataConnect.tag(), + Arb.dataConnect.invalidDateScalarString() + ) { tag, testData -> + val exception = + shouldThrow<DataConnectException> { + nullableDate.getAllByTagAndValue(tag = tag, value = testData.toDateScalarString()) + } + assertSoftly { + exception.message shouldContainWithNonAbuttingText "\$value" + exception.message shouldContainWithNonAbuttingTextIgnoringCase "invalid" + } + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Helper methods and classes. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Serializable private data class SingleKeyVariables(val key: Key) + + @Serializable private data class SingleKeyData(val key: Key) + + @Serializable + private data class MultipleKeysData(val items: List<Item>) { + @Serializable data class Item(val id: UUID) + } + + @Serializable private data class ThreeKeysData(val key1: Key, val key2: Key, val key3: Key) + + @Serializable private data class InsertVariables(val value: kotlinx.datetime.LocalDate?) + + @Serializable private data class InsertStringVariables(val value: String) + + @Serializable + private data class Insert3Variables( + val tag: String, + val value1: kotlinx.datetime.LocalDate?, + val value2: kotlinx.datetime.LocalDate?, + val value3: kotlinx.datetime.LocalDate?, + ) + + @Serializable private data class TagVariables(val tag: String) + + @Serializable + private data class TagAndValueVariables(val tag: String, val value: kotlinx.datetime.LocalDate?) + + @Serializable private data class TagAndStringValueVariables(val tag: String, val value: String) + + @Serializable + private data class QueryData(val item: Item?) { + @Serializable data class Item(val value: kotlinx.datetime.LocalDate?) + } + + @Serializable + private data class GetInsertedWithDefaultsByKeyQueryData(val item: Item?) { + @Serializable + data class Item( + val valueWithVariableDefault: kotlinx.datetime.LocalDate, + val valueWithVariableNullDefault: kotlinx.datetime.LocalDate?, + val valueWithSchemaDefault: kotlinx.datetime.LocalDate, + val valueWithSchemaNullDefault: kotlinx.datetime.LocalDate?, + val valueWithNoDefault: kotlinx.datetime.LocalDate?, + val epoch: kotlinx.datetime.LocalDate?, + val requestTime1: kotlinx.datetime.LocalDate?, + val requestTime2: kotlinx.datetime.LocalDate?, + ) + } + + @Serializable private data class Key(val id: UUID) + + /** Operations for querying and mutating the table that stores non-nullable Date scalar values. */ + private val nonNullableDate = + Operations( + getByKeyQueryName = "DateNonNullable_GetByKey", + getAllByTagAndValueQueryName = "DateNonNullable_GetAllByTagAndValue", + getAllByTagAndMaybeValueQueryName = "DateNonNullable_GetAllByTagAndMaybeValue", + getAllByTagAndDefaultValueQueryName = "DateNonNullable_GetAllByTagAndDefaultValue", + insertMutationName = "DateNonNullable_Insert", + insert3MutationName = "DateNonNullable_Insert3", + insertWithDefaultsMutationName = "DateNonNullableWithDefaults_Insert", + getInsertedWithDefaultsByKeyQueryName = "DateNonNullableWithDefaults_GetByKey", + ) + + /** Operations for querying and mutating the table that stores nullable Date scalar values. */ + private val nullableDate = + Operations( + getByKeyQueryName = "DateNullable_GetByKey", + getAllByTagAndValueQueryName = "DateNullable_GetAllByTagAndValue", + getAllByTagAndMaybeValueQueryName = "DateNullable_GetAllByTagAndValue", + getAllByTagAndDefaultValueQueryName = "DateNullable_GetAllByTagAndDefaultValue", + insertMutationName = "DateNullable_Insert", + insert3MutationName = "DateNullable_Insert3", + insertWithDefaultsMutationName = "DateNullableWithDefaults_Insert", + getInsertedWithDefaultsByKeyQueryName = "DateNullableWithDefaults_GetByKey", + ) + + private inner class Operations( + getByKeyQueryName: String, + getAllByTagAndValueQueryName: String, + getAllByTagAndMaybeValueQueryName: String, + getAllByTagAndDefaultValueQueryName: String, + insertMutationName: String, + insert3MutationName: String, + insertWithDefaultsMutationName: String, + getInsertedWithDefaultsByKeyQueryName: String, + ) { + + suspend fun insert( + localDate: kotlinx.datetime.LocalDate? + ): MutationResult<SingleKeyData, InsertVariables> = insert(InsertVariables(localDate)) + + suspend fun insert(variables: InsertVariables): MutationResult<SingleKeyData, InsertVariables> = + mutations.insert(variables).execute() + + suspend fun insert(localDate: String): MutationResult<SingleKeyData, InsertStringVariables> = + insert(InsertStringVariables(localDate)) + + suspend fun insert( + variables: InsertStringVariables + ): MutationResult<SingleKeyData, InsertStringVariables> = mutations.insert(variables).execute() + + suspend fun insert3( + tag: String, + testDatas: ThreeDateTestDatas, + ): MutationResult<ThreeKeysData, Insert3Variables> = + insert3( + tag = tag, + value1 = testDatas.testData1?.date?.toKotlinxLocalDate(), + value2 = testDatas.testData2?.date?.toKotlinxLocalDate(), + value3 = testDatas.testData3?.date?.toKotlinxLocalDate() + ) + + suspend fun insert3( + tag: String, + value1: kotlinx.datetime.LocalDate?, + value2: kotlinx.datetime.LocalDate?, + value3: kotlinx.datetime.LocalDate?, + ): MutationResult<ThreeKeysData, Insert3Variables> = + insert3(Insert3Variables(tag = tag, value1 = value1, value2 = value2, value3 = value3)) + + suspend fun insert3( + variables: Insert3Variables + ): MutationResult<ThreeKeysData, Insert3Variables> = mutations.insert3(variables).execute() + + suspend fun getByKey(key: Key): QueryResult<QueryData, SingleKeyVariables> = + getByKey(SingleKeyVariables(key)) + + suspend fun getByKey( + variables: SingleKeyVariables + ): QueryResult<QueryData, SingleKeyVariables> = queries.getByKey(variables).execute() + + suspend fun getAllByTagAndValue( + tag: String, + value: kotlinx.datetime.LocalDate? + ): QueryResult<MultipleKeysData, TagAndValueVariables> = + getAllByTagAndValue(TagAndValueVariables(tag, value)) + + suspend fun getAllByTagAndValue( + variables: TagAndValueVariables + ): QueryResult<MultipleKeysData, TagAndValueVariables> = + queries.getAllByTagAndValue(variables).execute() + + suspend fun getAllByTagAndValue( + tag: String, + value: String + ): QueryResult<MultipleKeysData, TagAndStringValueVariables> = + getAllByTagAndValue(TagAndStringValueVariables(tag, value)) + + suspend fun getAllByTagAndValue( + variables: TagAndStringValueVariables + ): QueryResult<MultipleKeysData, TagAndStringValueVariables> = + queries.getAllByTagAndValue(variables).execute() + + suspend fun getAllByTagAndMaybeValue( + tag: String, + ): QueryResult<MultipleKeysData, TagVariables> = getAllByTagAndMaybeValue(TagVariables(tag)) + + suspend fun getAllByTagAndMaybeValue( + variables: TagVariables + ): QueryResult<MultipleKeysData, TagVariables> = + queries.getAllByTagAndMaybeValue(variables).execute() + + suspend fun getAllByTagAndMaybeValue( + tag: String, + value: Nothing?, + ): QueryResult<MultipleKeysData, TagAndValueVariables> = + getAllByTagAndMaybeValue(TagAndValueVariables(tag, value)) + + suspend fun getAllByTagAndMaybeValue( + variables: TagAndValueVariables + ): QueryResult<MultipleKeysData, TagAndValueVariables> = + queries.getAllByTagAndMaybeValue(variables).execute() + + suspend fun getAllByTagAndDefaultValue( + tag: String + ): QueryResult<MultipleKeysData, TagVariables> = getAllByTagAndDefaultValue(TagVariables(tag)) + + suspend fun getAllByTagAndDefaultValue( + variables: TagVariables + ): QueryResult<MultipleKeysData, TagVariables> = + queries.getAllByTagAndDefaultValue(variables).execute() + + suspend fun insertWithDefaults(): MutationResult<SingleKeyData, Unit> = + mutations.insertWithDefaults().execute() + + suspend fun getInsertedWithDefaultsByKey( + key: Key + ): QueryResult<GetInsertedWithDefaultsByKeyQueryData, SingleKeyVariables> = + getInsertedWithDefaultsByKey(SingleKeyVariables(key)) + + suspend fun getInsertedWithDefaultsByKey( + variables: SingleKeyVariables + ): QueryResult<GetInsertedWithDefaultsByKeyQueryData, SingleKeyVariables> = + queries.getInsertedWithDefaultsByKey(variables).execute() + + private val queries = + object { + fun getByKey(variables: SingleKeyVariables): QueryRef<QueryData, SingleKeyVariables> = + dataConnect.query( + getByKeyQueryName, + variables, + serializer(), + serializer(), + ) + + inline fun <reified Variables> getAllByTagAndValue( + variables: Variables + ): QueryRef<MultipleKeysData, Variables> = + dataConnect.query( + getAllByTagAndValueQueryName, + variables, + serializer(), + serializer(), + ) + + fun getAllByTagAndMaybeValue( + variables: TagVariables + ): QueryRef<MultipleKeysData, TagVariables> = + dataConnect.query( + getAllByTagAndMaybeValueQueryName, + variables, + serializer(), + serializer(), + ) + + fun getAllByTagAndMaybeValue( + variables: TagAndValueVariables + ): QueryRef<MultipleKeysData, TagAndValueVariables> = + dataConnect.query( + getAllByTagAndMaybeValueQueryName, + variables, + serializer(), + serializer(), + ) + + fun getAllByTagAndDefaultValue( + variables: TagVariables + ): QueryRef<MultipleKeysData, TagVariables> = + dataConnect.query( + getAllByTagAndDefaultValueQueryName, + variables, + serializer(), + serializer(), + ) + + fun getInsertedWithDefaultsByKey( + variables: SingleKeyVariables + ): QueryRef<GetInsertedWithDefaultsByKeyQueryData, SingleKeyVariables> = + dataConnect.query( + getInsertedWithDefaultsByKeyQueryName, + variables, + serializer(), + serializer(), + ) + } + + private val mutations = + object { + inline fun <reified Variables> insert( + variables: Variables + ): MutationRef<SingleKeyData, Variables> = + dataConnect.mutation( + insertMutationName, + variables, + serializer(), + serializer(), + ) + + fun insert3(variables: Insert3Variables): MutationRef<ThreeKeysData, Insert3Variables> = + dataConnect.mutation( + insert3MutationName, + variables, + serializer(), + serializer(), + ) + + fun insertWithDefaults(): MutationRef<SingleKeyData, Unit> = + dataConnect.mutation( + insertWithDefaultsMutationName, + Unit, + serializer(), + serializer(), + ) + } + } + + private companion object { + val propTestConfig = + PropTestConfig( + iterations = 20, + edgeConfig = EdgeConfig(edgecasesGenerationProbability = 0.5), + ) + + fun ThreeDateTestDatas.idsMatchingSelected( + result: MutationResult<ThreeKeysData, *> + ): List<UUID> = idsMatchingSelected(result.data) + + fun ThreeDateTestDatas.idsMatchingSelected(data: ThreeKeysData): List<UUID> = + idsMatchingSelected { + data.uuidFromItemNumber(it) + } + + fun ThreeDateTestDatas.idsMatching( + result: MutationResult<ThreeKeysData, *>, + localDate: kotlinx.datetime.LocalDate?, + ): List<UUID> = idsMatching(result.data, localDate) + + fun ThreeDateTestDatas.idsMatching( + data: ThreeKeysData, + localDate: kotlinx.datetime.LocalDate?, + ): List<UUID> = idsMatching(localDate) { data.uuidFromItemNumber(it) } + + fun ThreeKeysData.uuidFromItemNumber(itemNumber: ItemNumber): UUID = + when (itemNumber) { + ItemNumber.ONE -> key1 + ItemNumber.TWO -> key2 + ItemNumber.THREE -> key3 + }.id + } +} diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/LocalDateIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/LocalDateIntegrationTest.kt index 05055ba4e39..490fea04961 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/LocalDateIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/LocalDateIntegrationTest.kt @@ -17,6 +17,12 @@ @file:OptIn(ExperimentalKotest::class) @file:UseSerializers(UUIDSerializer::class) +//////////////////////////////////////////////////////////////////////////////// +// THIS FILE WAS COPIED TO JavaTimeLocalDateIntegrationTest.kt and +// KotlinxDatetimeLocalDateIntegrationTest.kt AND ADAPTED TO TEST THE +// CORRESPONDING IMPLEMENTATIONS OF LocalDate. ANY CHANGES MADE TO THIS FILE +// MUST ALSO BE PORTED TO THOSE OTHER FILES, IF APPROPRIATE. +//////////////////////////////////////////////////////////////////////////////// package com.google.firebase.dataconnect import com.google.firebase.dataconnect.serializers.UUIDSerializer @@ -60,6 +66,12 @@ import kotlinx.serialization.UseSerializers import kotlinx.serialization.serializer import org.junit.Test +//////////////////////////////////////////////////////////////////////////////// +// THIS FILE WAS COPIED TO JavaTimeLocalDateIntegrationTest.kt and +// KotlinxDatetimeLocalDateIntegrationTest.kt AND ADAPTED TO TEST THE +// CORRESPONDING IMPLEMENTATIONS OF LocalDate. ANY CHANGES MADE TO THIS FILE +// MUST ALSO BE PORTED TO THOSE OTHER FILES, IF APPROPRIATE. +//////////////////////////////////////////////////////////////////////////////// class LocalDateIntegrationTest : DataConnectIntegrationTestBase() { private val dataConnect: FirebaseDataConnect by lazy { diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/JavaTimeLocalDateSerializer.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/JavaTimeLocalDateSerializer.kt new file mode 100644 index 00000000000..3d7722b318c --- /dev/null +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/JavaTimeLocalDateSerializer.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2024 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. + */ + +package com.google.firebase.dataconnect.serializers + +import com.google.firebase.dataconnect.toDataConnectLocalDate +import com.google.firebase.dataconnect.toJavaLocalDate +import java.time.LocalDate +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +/** + * An implementation of [KSerializer] for serializing and deserializing [LocalDate] objects in the + * wire format expected by the Firebase Data Connect backend. + * + * Be sure to _only_ call this method if [java.time.LocalDate] is available. See the documentation + * for [toJavaLocalDate] for details. + * + * @see LocalDateSerializer + * @see KotlinxDatetimeLocalDateSerializer + */ +public object JavaTimeLocalDateSerializer : KSerializer<LocalDate> { + + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("java.time.LocalDate", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: LocalDate) { + LocalDateSerializer.serialize(encoder, value.toDataConnectLocalDate()) + } + + override fun deserialize(decoder: Decoder): LocalDate { + return LocalDateSerializer.deserialize(decoder).toJavaLocalDate() + } +} diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/KotlinxDatetimeLocalDateSerializer.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/KotlinxDatetimeLocalDateSerializer.kt new file mode 100644 index 00000000000..803fb3457e9 --- /dev/null +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/KotlinxDatetimeLocalDateSerializer.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2024 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. + */ + +package com.google.firebase.dataconnect.serializers + +import com.google.firebase.dataconnect.toDataConnectLocalDate +import com.google.firebase.dataconnect.toKotlinxLocalDate +import kotlinx.datetime.LocalDate +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +/** + * An implementation of [KSerializer] for serializing and deserializing [LocalDate] objects in the + * wire format expected by the Firebase Data Connect backend. + * + * Be sure to _only_ use this class if your application has a dependency on + * `org.jetbrains.kotlinx:kotlinx-datetime`. See the documentation for [toKotlinxLocalDate] for + * details. + * + * @see LocalDateSerializer + * @see JavaTimeLocalDateSerializer + */ +public object KotlinxDatetimeLocalDateSerializer : KSerializer<LocalDate> { + + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("java.time.LocalDate", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: LocalDate) { + LocalDateSerializer.serialize(encoder, value.toDataConnectLocalDate()) + } + + override fun deserialize(decoder: Decoder): LocalDate { + return LocalDateSerializer.deserialize(decoder).toKotlinxLocalDate() + } +} diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/LocalDateSerializer.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/LocalDateSerializer.kt index 0b699a1a6d3..22cb087cd3c 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/LocalDateSerializer.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/LocalDateSerializer.kt @@ -29,6 +29,9 @@ import kotlinx.serialization.encoding.Encoder /** * An implementation of [KSerializer] for serializing and deserializing [LocalDate] objects in the * wire format expected by the Firebase Data Connect backend. + * + * @see JavaTimeLocalDateSerializer + * @see KotlinxDatetimeLocalDateSerializer */ public object LocalDateSerializer : KSerializer<LocalDate> { diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/JavaTimeLocalDateSerializerUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/JavaTimeLocalDateSerializerUnitTest.kt new file mode 100644 index 00000000000..4b46994985f --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/JavaTimeLocalDateSerializerUnitTest.kt @@ -0,0 +1,261 @@ +/* + * Copyright 2024 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. + */ +@file:OptIn(ExperimentalKotest::class) + +//////////////////////////////////////////////////////////////////////////////// +// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateSerializerUnitTest.kt +// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO +// LocalDateIntegrationTest.kt AND PORTED TO KotlinxDatetimeLocalDateSerializerUnitTest.kt, +// if appropriate. +//////////////////////////////////////////////////////////////////////////////// +package com.google.firebase.dataconnect.serializers + +import com.google.firebase.dataconnect.testutil.dayRangeInYear +import com.google.firebase.dataconnect.testutil.property.arbitrary.intWithEvenNumDigitsDistribution +import com.google.firebase.dataconnect.util.ProtoUtil.decodeFromValue +import com.google.firebase.dataconnect.util.ProtoUtil.encodeToValue +import com.google.firebase.dataconnect.util.ProtoUtil.toValueProto +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.common.ExperimentalKotest +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.EdgeConfig +import io.kotest.property.PropTestConfig +import io.kotest.property.arbitrary.Codepoint +import io.kotest.property.arbitrary.arabic +import io.kotest.property.arbitrary.arbitrary +import io.kotest.property.arbitrary.ascii +import io.kotest.property.arbitrary.boolean +import io.kotest.property.arbitrary.booleanArray +import io.kotest.property.arbitrary.constant +import io.kotest.property.arbitrary.cyrillic +import io.kotest.property.arbitrary.egyptianHieroglyphs +import io.kotest.property.arbitrary.enum +import io.kotest.property.arbitrary.filterNot +import io.kotest.property.arbitrary.greekCoptic +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.katakana +import io.kotest.property.arbitrary.long +import io.kotest.property.arbitrary.merge +import io.kotest.property.arbitrary.next +import io.kotest.property.arbitrary.string +import io.kotest.property.arbitrary.triple +import io.kotest.property.arbitrary.withEdgecases +import io.kotest.property.checkAll +import io.mockk.every +import io.mockk.mockk +import kotlin.random.nextInt +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.encoding.Decoder +import org.junit.Test + +//////////////////////////////////////////////////////////////////////////////// +// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateSerializerUnitTest.kt +// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO +// LocalDateIntegrationTest.kt AND PORTED TO KotlinxDatetimeLocalDateSerializerUnitTest.kt, +// if appropriate. +//////////////////////////////////////////////////////////////////////////////// +class JavaTimeLocalDateSerializerUnitTest { + + @Test + fun `serialize() should produce the expected serialized string`() = runTest { + checkAll(propTestConfig, Arb.localDate()) { localDate -> + val value = encodeToValue(localDate, JavaTimeLocalDateSerializer, serializersModule = null) + value.stringValue shouldBe localDate.toYYYYMMDDWithZeroPadding() + } + } + + @Test + fun `deserialize() should produce the expected LocalDate object`() = runTest { + val numPaddingCharsArb = Arb.int(0..10) + val arb = Arb.triple(numPaddingCharsArb, numPaddingCharsArb, numPaddingCharsArb) + checkAll(propTestConfig, Arb.localDate(), arb) { localDate, paddingCharsTriple -> + val (yearPadding, monthPadding, dayPadding) = paddingCharsTriple + val value = + localDate + .toYYYYMMDDWithZeroPadding( + yearPadding = yearPadding, + monthPadding = monthPadding, + dayPadding = dayPadding + ) + .toValueProto() + + val decodedLocalDate = + decodeFromValue(value, JavaTimeLocalDateSerializer, serializersModule = null) + decodedLocalDate shouldBe localDate + } + } + + @Test + fun `deserialize() should throw IllegalArgumentException when given unparseable strings`() = + runTest { + checkAll(propTestConfig, Arb.unparseableDate()) { encodedDate -> + val decoder: Decoder = mockk { every { decodeString() } returns encodedDate } + shouldThrow<IllegalArgumentException> { JavaTimeLocalDateSerializer.deserialize(decoder) } + } + } + + private companion object { + val propTestConfig = + PropTestConfig( + iterations = 500, + edgeConfig = EdgeConfig(edgecasesGenerationProbability = 0.2) + ) + + fun java.time.LocalDate.toYYYYMMDDWithZeroPadding( + yearPadding: Int = 4, + monthPadding: Int = 2, + dayPadding: Int = 2, + ): String { + val yearString = year.toZeroPaddedString(yearPadding) + val monthString = month.value.toZeroPaddedString(monthPadding) + val dayString = dayOfMonth.toZeroPaddedString(dayPadding) + return "$yearString-$monthString-$dayString" + } + + fun Int.toZeroPaddedString(length: Int): String = buildString { + append(this@toZeroPaddedString) + val signChar = + firstOrNull()?.let { + if (it == '-') { + deleteCharAt(0) + it + } else { + null + } + } + + while (this.length < length) { + insert(0, '0') + } + + if (signChar !== null) { + insert(0, signChar) + } + } + + fun Arb.Companion.localDate( + year: Arb<Int> = + intWithEvenNumDigitsDistribution(java.time.Year.MIN_VALUE..java.time.Year.MAX_VALUE), + month: Arb<Int> = intWithEvenNumDigitsDistribution(1..12), + day: Arb<Int> = intWithEvenNumDigitsDistribution(1..31), + ): Arb<java.time.LocalDate> { + fun Int.coerceDayOfMonthIntoValidRangeFor(month: Int, year: Int): Int { + val monthObject = org.threeten.bp.Month.of(month) + val yearObject = org.threeten.bp.Year.of(year) + val dayRange = monthObject.dayRangeInYear(yearObject) + return coerceIn(dayRange) + } + return arbitrary( + edgecaseFn = { rs -> + val yearInt = if (rs.random.nextBoolean()) year.next(rs) else year.edgecase(rs)!! + val monthInt = if (rs.random.nextBoolean()) month.next(rs) else month.edgecase(rs)!! + val dayInt = if (rs.random.nextBoolean()) day.next(rs) else day.edgecase(rs)!! + val coercedDayInt = + dayInt.coerceDayOfMonthIntoValidRangeFor(month = monthInt, year = yearInt) + java.time.LocalDate.of(yearInt, monthInt, coercedDayInt) + }, + sampleFn = { + val yearInt = year.bind() + val monthInt = month.bind() + val dayInt = day.bind() + val coercedDayInt = + dayInt.coerceDayOfMonthIntoValidRangeFor(month = monthInt, year = yearInt) + java.time.LocalDate.of(yearInt, monthInt, coercedDayInt) + } + ) + } + + private enum class UnparseableNumberReason { + EmptyString, + InvalidChars, + GreaterThanIntMax, + LessThanIntMin, + } + + private val codepoints = + Codepoint.ascii() + .merge(Codepoint.egyptianHieroglyphs()) + .merge(Codepoint.arabic()) + .merge(Codepoint.cyrillic()) + .merge(Codepoint.greekCoptic()) + .merge(Codepoint.katakana()) + + fun Arb.Companion.unparseableNumber(): Arb<String> { + val reasonArb = enum<UnparseableNumberReason>() + val validIntArb = intWithEvenNumDigitsDistribution(0..Int.MAX_VALUE) + val validChars = listOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-').map { it.code } + val invalidString = + string(1..5, codepoints.filterNot { validChars.contains(it.value) }).withEdgecases("-") + val tooLargeValues = long(Int.MAX_VALUE.toLong() + 1L..Long.MAX_VALUE) + val tooSmallValues = long(Long.MIN_VALUE until Int.MIN_VALUE.toLong()) + return arbitrary { rs -> + when (reasonArb.bind()) { + UnparseableNumberReason.EmptyString -> "" + UnparseableNumberReason.GreaterThanIntMax -> "${tooLargeValues.bind()}" + UnparseableNumberReason.LessThanIntMin -> "${tooSmallValues.bind()}" + UnparseableNumberReason.InvalidChars -> { + val flags = Array(3) { rs.random.nextBoolean() } + if (!flags[0]) { + flags[2] = true + } + val prefix = if (flags[0]) invalidString.bind() else "" + val mid = if (flags[1]) validIntArb.bind() else "" + val suffix = if (flags[2]) invalidString.bind() else "" + "$prefix$mid$suffix" + } + } + } + } + + fun Arb.Companion.unparseableDash(): Arb<String> { + val invalidString = string(1..5, codepoints.filterNot { it.value == '-'.code }) + return arbitrary { rs -> + val flags = Array(3) { rs.random.nextBoolean() } + if (!flags[0]) { + flags[2] = true + } + + val prefix = if (flags[0]) invalidString.bind() else "" + val mid = if (flags[1]) "-" else "" + val suffix = if (flags[2]) invalidString.bind() else "" + + "$prefix$mid$suffix" + } + } + + fun Arb.Companion.unparseableDate(): Arb<String> { + val validNumber = intWithEvenNumDigitsDistribution(0..Int.MAX_VALUE) + val unparseableNumber = unparseableNumber() + val unparseableDash = unparseableDash() + val booleanArray = booleanArray(Arb.constant(5), Arb.boolean()) + return arbitrary(edgecases = listOf("", "-", "--", "---")) { rs -> + val invalidCharFlags = booleanArray.bind() + if (invalidCharFlags.count { it } == 0) { + invalidCharFlags[rs.random.nextInt(invalidCharFlags.indices)] = true + } + + val year = if (invalidCharFlags[0]) unparseableNumber.bind() else validNumber.bind() + val dash1 = if (invalidCharFlags[1]) unparseableDash.bind() else "-" + val month = if (invalidCharFlags[2]) unparseableNumber.bind() else validNumber.bind() + val dash2 = if (invalidCharFlags[3]) unparseableDash.bind() else "-" + val day = if (invalidCharFlags[4]) unparseableNumber.bind() else validNumber.bind() + + "$year$dash1$month$dash2$day" + } + } + } +} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/KotlinxDatetimeLocalDateSerializerUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/KotlinxDatetimeLocalDateSerializerUnitTest.kt new file mode 100644 index 00000000000..3ab96a648a9 --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/KotlinxDatetimeLocalDateSerializerUnitTest.kt @@ -0,0 +1,264 @@ +/* + * Copyright 2024 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. + */ +@file:OptIn(ExperimentalKotest::class) + +//////////////////////////////////////////////////////////////////////////////// +// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateSerializerUnitTest.kt +// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO +// LocalDateIntegrationTest.kt AND PORTED TO JavaTimeLocalDateSerializerUnitTest.kt, +// if appropriate. +//////////////////////////////////////////////////////////////////////////////// +package com.google.firebase.dataconnect.serializers + +import com.google.firebase.dataconnect.testutil.dayRangeInYear +import com.google.firebase.dataconnect.testutil.property.arbitrary.intWithEvenNumDigitsDistribution +import com.google.firebase.dataconnect.util.ProtoUtil.decodeFromValue +import com.google.firebase.dataconnect.util.ProtoUtil.encodeToValue +import com.google.firebase.dataconnect.util.ProtoUtil.toValueProto +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.common.ExperimentalKotest +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.EdgeConfig +import io.kotest.property.PropTestConfig +import io.kotest.property.arbitrary.Codepoint +import io.kotest.property.arbitrary.arabic +import io.kotest.property.arbitrary.arbitrary +import io.kotest.property.arbitrary.ascii +import io.kotest.property.arbitrary.boolean +import io.kotest.property.arbitrary.booleanArray +import io.kotest.property.arbitrary.constant +import io.kotest.property.arbitrary.cyrillic +import io.kotest.property.arbitrary.egyptianHieroglyphs +import io.kotest.property.arbitrary.enum +import io.kotest.property.arbitrary.filterNot +import io.kotest.property.arbitrary.greekCoptic +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.katakana +import io.kotest.property.arbitrary.long +import io.kotest.property.arbitrary.merge +import io.kotest.property.arbitrary.next +import io.kotest.property.arbitrary.string +import io.kotest.property.arbitrary.triple +import io.kotest.property.arbitrary.withEdgecases +import io.kotest.property.checkAll +import io.mockk.every +import io.mockk.mockk +import kotlin.random.nextInt +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.encoding.Decoder +import org.junit.Test + +//////////////////////////////////////////////////////////////////////////////// +// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateSerializerUnitTest.kt +// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO +// LocalDateIntegrationTest.kt AND PORTED TO JavaTimeLocalDateSerializerUnitTest.kt, +// if appropriate. +//////////////////////////////////////////////////////////////////////////////// +class KotlinxDatetimeLocalDateSerializerUnitTest { + + @Test + fun `serialize() should produce the expected serialized string`() = runTest { + checkAll(propTestConfig, Arb.localDate()) { localDate -> + val value = + encodeToValue(localDate, KotlinxDatetimeLocalDateSerializer, serializersModule = null) + value.stringValue shouldBe localDate.toYYYYMMDDWithZeroPadding() + } + } + + @Test + fun `deserialize() should produce the expected LocalDate object`() = runTest { + val numPaddingCharsArb = Arb.int(0..10) + val arb = Arb.triple(numPaddingCharsArb, numPaddingCharsArb, numPaddingCharsArb) + checkAll(propTestConfig, Arb.localDate(), arb) { localDate, paddingCharsTriple -> + val (yearPadding, monthPadding, dayPadding) = paddingCharsTriple + val value = + localDate + .toYYYYMMDDWithZeroPadding( + yearPadding = yearPadding, + monthPadding = monthPadding, + dayPadding = dayPadding + ) + .toValueProto() + + val decodedLocalDate = + decodeFromValue(value, KotlinxDatetimeLocalDateSerializer, serializersModule = null) + decodedLocalDate shouldBe localDate + } + } + + @Test + fun `deserialize() should throw IllegalArgumentException when given unparseable strings`() = + runTest { + checkAll(propTestConfig, Arb.unparseableDate()) { encodedDate -> + val decoder: Decoder = mockk { every { decodeString() } returns encodedDate } + shouldThrow<IllegalArgumentException> { + KotlinxDatetimeLocalDateSerializer.deserialize(decoder) + } + } + } + + private companion object { + val propTestConfig = + PropTestConfig( + iterations = 500, + edgeConfig = EdgeConfig(edgecasesGenerationProbability = 0.2) + ) + + fun kotlinx.datetime.LocalDate.toYYYYMMDDWithZeroPadding( + yearPadding: Int = 4, + monthPadding: Int = 2, + dayPadding: Int = 2, + ): String { + val yearString = year.toZeroPaddedString(yearPadding) + val monthString = month.value.toZeroPaddedString(monthPadding) + val dayString = dayOfMonth.toZeroPaddedString(dayPadding) + return "$yearString-$monthString-$dayString" + } + + fun Int.toZeroPaddedString(length: Int): String = buildString { + append(this@toZeroPaddedString) + val signChar = + firstOrNull()?.let { + if (it == '-') { + deleteCharAt(0) + it + } else { + null + } + } + + while (this.length < length) { + insert(0, '0') + } + + if (signChar !== null) { + insert(0, signChar) + } + } + + fun Arb.Companion.localDate( + year: Arb<Int> = + intWithEvenNumDigitsDistribution(java.time.Year.MIN_VALUE..java.time.Year.MAX_VALUE), + month: Arb<Int> = intWithEvenNumDigitsDistribution(1..12), + day: Arb<Int> = intWithEvenNumDigitsDistribution(1..31), + ): Arb<kotlinx.datetime.LocalDate> { + fun Int.coerceDayOfMonthIntoValidRangeFor(month: Int, year: Int): Int { + val monthObject = org.threeten.bp.Month.of(month) + val yearObject = org.threeten.bp.Year.of(year) + val dayRange = monthObject.dayRangeInYear(yearObject) + return coerceIn(dayRange) + } + return arbitrary( + edgecaseFn = { rs -> + val yearInt = if (rs.random.nextBoolean()) year.next(rs) else year.edgecase(rs)!! + val monthInt = if (rs.random.nextBoolean()) month.next(rs) else month.edgecase(rs)!! + val dayInt = if (rs.random.nextBoolean()) day.next(rs) else day.edgecase(rs)!! + val coercedDayInt = + dayInt.coerceDayOfMonthIntoValidRangeFor(month = monthInt, year = yearInt) + kotlinx.datetime.LocalDate(yearInt, monthInt, coercedDayInt) + }, + sampleFn = { + val yearInt = year.bind() + val monthInt = month.bind() + val dayInt = day.bind() + val coercedDayInt = + dayInt.coerceDayOfMonthIntoValidRangeFor(month = monthInt, year = yearInt) + kotlinx.datetime.LocalDate(yearInt, monthInt, coercedDayInt) + } + ) + } + + private enum class UnparseableNumberReason { + EmptyString, + InvalidChars, + GreaterThanIntMax, + LessThanIntMin, + } + + private val codepoints = + Codepoint.ascii() + .merge(Codepoint.egyptianHieroglyphs()) + .merge(Codepoint.arabic()) + .merge(Codepoint.cyrillic()) + .merge(Codepoint.greekCoptic()) + .merge(Codepoint.katakana()) + + fun Arb.Companion.unparseableNumber(): Arb<String> { + val reasonArb = enum<UnparseableNumberReason>() + val validIntArb = intWithEvenNumDigitsDistribution(0..Int.MAX_VALUE) + val validChars = listOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-').map { it.code } + val invalidString = + string(1..5, codepoints.filterNot { validChars.contains(it.value) }).withEdgecases("-") + val tooLargeValues = long(Int.MAX_VALUE.toLong() + 1L..Long.MAX_VALUE) + val tooSmallValues = long(Long.MIN_VALUE until Int.MIN_VALUE.toLong()) + return arbitrary { rs -> + when (reasonArb.bind()) { + UnparseableNumberReason.EmptyString -> "" + UnparseableNumberReason.GreaterThanIntMax -> "${tooLargeValues.bind()}" + UnparseableNumberReason.LessThanIntMin -> "${tooSmallValues.bind()}" + UnparseableNumberReason.InvalidChars -> { + val flags = Array(3) { rs.random.nextBoolean() } + if (!flags[0]) { + flags[2] = true + } + val prefix = if (flags[0]) invalidString.bind() else "" + val mid = if (flags[1]) validIntArb.bind() else "" + val suffix = if (flags[2]) invalidString.bind() else "" + "$prefix$mid$suffix" + } + } + } + } + + fun Arb.Companion.unparseableDash(): Arb<String> { + val invalidString = string(1..5, codepoints.filterNot { it.value == '-'.code }) + return arbitrary { rs -> + val flags = Array(3) { rs.random.nextBoolean() } + if (!flags[0]) { + flags[2] = true + } + + val prefix = if (flags[0]) invalidString.bind() else "" + val mid = if (flags[1]) "-" else "" + val suffix = if (flags[2]) invalidString.bind() else "" + + "$prefix$mid$suffix" + } + } + + fun Arb.Companion.unparseableDate(): Arb<String> { + val validNumber = intWithEvenNumDigitsDistribution(0..Int.MAX_VALUE) + val unparseableNumber = unparseableNumber() + val unparseableDash = unparseableDash() + val booleanArray = booleanArray(Arb.constant(5), Arb.boolean()) + return arbitrary(edgecases = listOf("", "-", "--", "---")) { rs -> + val invalidCharFlags = booleanArray.bind() + if (invalidCharFlags.count { it } == 0) { + invalidCharFlags[rs.random.nextInt(invalidCharFlags.indices)] = true + } + + val year = if (invalidCharFlags[0]) unparseableNumber.bind() else validNumber.bind() + val dash1 = if (invalidCharFlags[1]) unparseableDash.bind() else "-" + val month = if (invalidCharFlags[2]) unparseableNumber.bind() else validNumber.bind() + val dash2 = if (invalidCharFlags[3]) unparseableDash.bind() else "-" + val day = if (invalidCharFlags[4]) unparseableNumber.bind() else validNumber.bind() + + "$year$dash1$month$dash2$day" + } + } + } +} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/LocalDateSerializerUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/LocalDateSerializerUnitTest.kt index 6d94b9ca477..6bc0c45d3bc 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/LocalDateSerializerUnitTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/LocalDateSerializerUnitTest.kt @@ -15,6 +15,12 @@ */ @file:OptIn(ExperimentalKotest::class) +//////////////////////////////////////////////////////////////////////////////// +// THIS FILE WAS COPIED TO JavaTimeLocalDateSerializerUnitTest.kt and +// KotlinxDatetimeLocalDateSerializerUnitTest.kt AND ADAPTED TO TEST THE +// CORRESPONDING IMPLEMENTATIONS OF LocalDate. ANY CHANGES MADE TO THIS FILE +// MUST ALSO BE PORTED TO THOSE OTHER FILES, IF APPROPRIATE. +//////////////////////////////////////////////////////////////////////////////// package com.google.firebase.dataconnect.serializers import com.google.firebase.dataconnect.LocalDate @@ -56,6 +62,12 @@ import kotlinx.coroutines.test.runTest import kotlinx.serialization.encoding.Decoder import org.junit.Test +//////////////////////////////////////////////////////////////////////////////// +// THIS FILE WAS COPIED TO JavaTimeLocalDateSerializerUnitTest.kt and +// KotlinxDatetimeLocalDateSerializerUnitTest.kt AND ADAPTED TO TEST THE +// CORRESPONDING IMPLEMENTATIONS OF LocalDate. ANY CHANGES MADE TO THIS FILE +// MUST ALSO BE PORTED TO THOSE OTHER FILES, IF APPROPRIATE. +//////////////////////////////////////////////////////////////////////////////// class LocalDateSerializerUnitTest { @Test diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/JavaTime.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/JavaTime.kt index 7e1fc14c8fc..0e9837ee37b 100644 --- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/JavaTime.kt +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/JavaTime.kt @@ -16,11 +16,22 @@ package com.google.firebase.dataconnect.testutil -import com.google.firebase.Timestamp -import com.google.firebase.dataconnect.LocalDate -import org.threeten.bp.Instant +import android.annotation.SuppressLint -fun Instant.toTimestamp(): Timestamp = Timestamp(epochSecond, nano) +fun org.threeten.bp.Instant.toTimestamp(): com.google.firebase.Timestamp = + com.google.firebase.Timestamp(epochSecond, nano) -fun LocalDate.toTheeTenAbpJavaLocalDate(): org.threeten.bp.LocalDate = - org.threeten.bp.LocalDate.of(year, month, day) +fun com.google.firebase.dataconnect.LocalDate.toTheeTenAbpJavaLocalDate(): + org.threeten.bp.LocalDate = org.threeten.bp.LocalDate.of(year, month, day) + +@SuppressLint("NewApi") +fun java.time.LocalDate.toTheeTenAbpJavaLocalDate(): org.threeten.bp.LocalDate { + val threeTenBpMonth = org.threeten.bp.Month.of(monthValue) + return org.threeten.bp.LocalDate.of(year, threeTenBpMonth, dayOfMonth) +} + +@SuppressLint("NewApi") +fun kotlinx.datetime.LocalDate.toTheeTenAbpJavaLocalDate(): org.threeten.bp.LocalDate { + val threeTenBpMonth = org.threeten.bp.Month.of(monthNumber) + return org.threeten.bp.LocalDate.of(year, threeTenBpMonth, dayOfMonth) +} diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/dates.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/dates.kt index 1535b12a167..662062e4069 100644 --- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/dates.kt +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/dates.kt @@ -23,6 +23,7 @@ import com.google.firebase.dataconnect.testutil.NullableReference import com.google.firebase.dataconnect.testutil.dayRangeInYear import com.google.firebase.dataconnect.testutil.property.arbitrary.DateEdgeCases.MAX_YEAR import com.google.firebase.dataconnect.testutil.property.arbitrary.DateEdgeCases.MIN_YEAR +import com.google.firebase.dataconnect.toDataConnectLocalDate import io.kotest.property.Arb import io.kotest.property.RandomSource import io.kotest.property.Sample @@ -99,7 +100,17 @@ private class DateTestDataArb : Arb<DateTestData>() { data class DateTestData( val date: LocalDate, val string: String, -) +) { + constructor( + date: java.time.LocalDate, + string: String + ) : this(date.toDataConnectLocalDate(), string) + + constructor( + date: kotlinx.datetime.LocalDate, + string: String + ) : this(date.toDataConnectLocalDate(), string) +} @Suppress("MemberVisibilityCanBePrivate") object DateEdgeCases { @@ -149,11 +160,24 @@ data class ThreeDateTestDatas( fun idsMatchingSelected(getter: (ItemNumber) -> UUID): List<UUID> = idsMatching(selected?.date, getter) - fun idsMatching(localDate: LocalDate?, getter: (ItemNumber) -> UUID): List<UUID> { + fun idsMatching( + localDate: LocalDate?, + getter: (ItemNumber) -> UUID, + ): List<UUID> { val ids = listOf(getter(ItemNumber.ONE), getter(ItemNumber.TWO), getter(ItemNumber.THREE)) return ids.filterIndexed { index, _ -> all[index]?.date == localDate } } + fun idsMatching( + localDate: java.time.LocalDate?, + getter: (ItemNumber) -> UUID, + ): List<UUID> = idsMatching(localDate?.toDataConnectLocalDate(), getter) + + fun idsMatching( + localDate: kotlinx.datetime.LocalDate?, + getter: (ItemNumber) -> UUID, + ): List<UUID> = idsMatching(localDate?.toDataConnectLocalDate(), getter) + enum class ItemNumber { ONE, TWO, From 2eb3f3b80636b21fd55d643ea289a8b8ff03cd4d Mon Sep 17 00:00:00 2001 From: Denver Coneybeare <dconeybe@google.com> Date: Thu, 14 Nov 2024 06:20:31 +0000 Subject: [PATCH 12/13] Generate code for java.time and kotlinx.datetime LocalDate tests --- .../firebase-dataconnect.gradle.kts | 298 ++++++++++++++++++ .../JavaTimeLocalDateIntegrationTest.kt | 16 +- ...KotlinxDatetimeLocalDateIntegrationTest.kt | 16 +- .../dataconnect/LocalDateIntegrationTest.kt | 18 +- .../JavaTimeLocalDateSerializerUnitTest.kt | 16 +- ...linxDatetimeLocalDateSerializerUnitTest.kt | 16 +- .../LocalDateSerializerUnitTest.kt | 28 +- 7 files changed, 359 insertions(+), 49 deletions(-) diff --git a/firebase-dataconnect/firebase-dataconnect.gradle.kts b/firebase-dataconnect/firebase-dataconnect.gradle.kts index 87ffe572916..9ee826c6f6a 100644 --- a/firebase-dataconnect/firebase-dataconnect.gradle.kts +++ b/firebase-dataconnect/firebase-dataconnect.gradle.kts @@ -163,3 +163,301 @@ tasks.withType<KotlinCompile>().all { } } } + +/** + * Performs various transformations on a mutable list of strings. This class is _not_ thread safe; + * any concurrent use must be synchronized externally or else the behavior is undefined. + * @property lines the lines to mutate; this list is modified in-place. + */ +class TextLinesTransformer(val lines: MutableList<String>) { + constructor(lines: Iterable<String>) : this(lines.toMutableList()) + + fun indexOf(predicateDescription: String, predicate: (String) -> Boolean): Int { + val index = lines.indexOfFirst(predicate) + if (index < 0) { + throw TextLinesTransformerException("unable to find a line that $predicateDescription") + } + return index + } + + fun atLineThatStartsWith(prefix: String): IndexBasedOperations { + val index = indexOf("starts with \"$prefix\"") { it.startsWith(prefix) } + return IndexBasedOperations(index) + } + + fun removeLine(line: String) { + lines.removeAll { it.trim() == line } + } + + fun replaceLine(line: String, replacementLine: String) { + lines.replaceAll { originalLine -> originalLine.takeIf { it != line } ?: replacementLine } + } + + fun replaceWord( + original: String, + replacement: String, + predicate: (line: String) -> Boolean = { true } + ) { + val regex = Regex("""(\W|^)${Regex.escape(original)}(\W|$)""") + lines.replaceAll { line -> + if (!predicate(line)) { + line + } else { + regex.replace(line) { matchResult -> + val prefix = matchResult.groupValues[1] + val suffix = matchResult.groupValues[2] + "$prefix${Regex.escapeReplacement(replacement)}$suffix" + } + } + } + } + + fun replaceText(original: String, replacement: String) { + lines.replaceAll { it.replace(original, replacement) } + } + + fun replaceRegex(pattern: String, replacement: String) { + val regex = Regex(pattern) + lines.replaceAll { regex.replace(it, replacement) } + } + + fun applyReplacements(linesByReplacementId: Map<String, List<String>>) { + for (index in lines.indices.reversed()) { + val line = lines[index] + val matchResult = replacementsRegex.matchEntire(line.trim()) ?: continue + val lineDeleteCount = matchResult.groupValues[1].toInt() + 1 + val replacementId = matchResult.groupValues[2] + + val replacementLines = + linesByReplacementId[replacementId] + ?: throw Exception( + "Replacement ID \"$replacementId\" is not known; " + + "there are ${linesByReplacementId.size} known replacementIds: " + + linesByReplacementId.keys.sorted().joinToString(", ") + + " (error code zgcc257b23)" + ) + + repeat(lineDeleteCount) { lines.removeAt(index) } + lines.addAll(index, replacementLines) + } + } + + inner class IndexBasedOperations(private var index: Int) { + fun deleteLinesAboveThatStartWith(prefix: String): IndexBasedOperations = apply { + while (lines[index - 1].startsWith(prefix)) { + lines.removeAt(index - 1) + index-- + } + } + + fun insertAbove(line: String): IndexBasedOperations = apply { lines.add(index, line) } + + fun insertAbove(lines: Collection<String>): IndexBasedOperations = apply { + this@TextLinesTransformer.lines.addAll(index, lines) + } + } + + private class TextLinesTransformerException(message: String) : Exception(message) + + companion object { + private val replacementsRegex: Regex = run { + fun StringBuilder.appendRegexEscaped(s: String) = append(Regex.escape(s)) + val pattern = buildString { + appendRegexEscaped("//") + append("""\s*""") + appendRegexEscaped("""ReplaceLinesBelow(numLines=""") + append("""\s*(\d+)\s*,\s*""") + appendRegexEscaped("""replacementId=""") + append("""(\w+)""") + appendRegexEscaped(""")""") + } + Regex(pattern) + } + + fun getGeneratedFileWarningLines(srcFile: File) = + listOf( + "/".repeat(80), + "// WARNING: THIS FILE IS GENERATED FROM ${srcFile.name}", + "// DO NOT MODIFY THIS FILE BY HAND BECAUSE MANUAL CHANGES WILL GET OVERWRITTEN", + "// THE NEXT TIME THAT THIS FILE IS REGENERATED. TO REGENERATE THIS FILE, RUN:", + "// ./gradlew :firebase-dataconnect:generateDataConnectTestingSources", + "/".repeat(80), + ) + } +} + +fun generateLocalDateSerializerUnitTest( + srcFile: File, + classNameUnderTest: String, + localDateFullyQualifiedClassName: String, + localDateFactoryCall: String, + logger: Logger, +) { + logger.info("Reading {}", srcFile.absolutePath) + val transformer = TextLinesTransformer(srcFile.readLines(Charsets.UTF_8)) + + val linesByReplacementId = + mapOf( + "CoerceDayOfMonthIntoValidRangeFor" to + listOf( + "fun Int.coerceDayOfMonthIntoValidRangeFor(month: Int, year: Int): Int {", + " val monthObject = org.threeten.bp.Month.of(month)", + " val yearObject = org.threeten.bp.Year.of(year)", + " val dayRange = monthObject.dayRangeInYear(yearObject)", + " return coerceIn(dayRange)", + "}", + ), + "LocalDateSample" to + listOf( + "val coercedDayInt = dayInt.coerceDayOfMonthIntoValidRangeFor(month=monthInt, year=yearInt)", + "$localDateFullyQualifiedClassName$localDateFactoryCall(yearInt, monthInt, coercedDayInt)", + ), + ) + + val generatedFileWarningLines = TextLinesTransformer.getGeneratedFileWarningLines(srcFile) + + transformer.run { + atLineThatStartsWith("import ") + .insertAbove("import com.google.firebase.dataconnect.testutil.dayRangeInYear") + removeLine("import com.google.firebase.dataconnect.LocalDate") + replaceWord("LocalDate", localDateFullyQualifiedClassName) { !it.contains('`') } + replaceText("LocalDateSerializer", classNameUnderTest) + replaceText( + "val monthString = month.toZeroPaddedString(monthPadding)", + "val monthString = month.value.toZeroPaddedString(monthPadding)" + ) + replaceText( + "val dayString = day.toZeroPaddedString(dayPadding)", + "val dayString = dayOfMonth.toZeroPaddedString(dayPadding)" + ) + replaceText( + "year: Arb<Int> = intWithEvenNumDigitsDistribution()", + "year: Arb<Int> = intWithEvenNumDigitsDistribution(java.time.Year.MIN_VALUE..java.time.Year.MAX_VALUE)" + ) + replaceText( + "month: Arb<Int> = intWithEvenNumDigitsDistribution()", + "month: Arb<Int> = intWithEvenNumDigitsDistribution(1..12)" + ) + replaceText( + "day: Arb<Int> = intWithEvenNumDigitsDistribution()", + "day: Arb<Int> = intWithEvenNumDigitsDistribution(1..31)" + ) + applyReplacements(linesByReplacementId) + + atLineThatStartsWith("package ") + .deleteLinesAboveThatStartWith("//") + .insertAbove(generatedFileWarningLines) + + atLineThatStartsWith("class ") + .deleteLinesAboveThatStartWith("//") + .insertAbove(generatedFileWarningLines) + } + + val destFile = File(srcFile.parentFile, "${classNameUnderTest}UnitTest.kt") + logger.info("Writing {}", destFile.absolutePath) + destFile.writeText(transformer.lines.joinToString("\n")) +} + +fun generateLocalDateSerializerIntegrationTest( + srcFile: File, + destClassName: String, + localDateFullyQualifiedClassName: String, + localDateFactoryCall: String, + convertFromDataConnectLocalDateFunctionName: String, + serializerClassName: String, + logger: Logger, +) { + logger.info("Reading {}", srcFile.absolutePath) + val transformer = TextLinesTransformer(srcFile.readLines(Charsets.UTF_8)) + + val generatedFileWarningLines = TextLinesTransformer.getGeneratedFileWarningLines(srcFile) + + transformer.run { + removeLine("import com.google.firebase.dataconnect.LocalDate") + replaceLine( + "@file:UseSerializers(UUIDSerializer::class)", + "@file:UseSerializers(UUIDSerializer::class, $serializerClassName::class)" + ) + atLineThatStartsWith("import ") + .insertAbove("import com.google.firebase.dataconnect.serializers.$serializerClassName") + .insertAbove("import io.kotest.property.arbitrary.map") + replaceWord("LocalDate", localDateFullyQualifiedClassName) { !it.contains('`') } + replaceWord("LocalDateIntegrationTest", destClassName) + replaceWord( + "Arb.dataConnect.localDate()", + "Arb.dataConnect.localDate().map{it.$convertFromDataConnectLocalDateFunctionName()}" + ) + replaceRegex("""\?\.date(\W|$)""", "?.date?.$convertFromDataConnectLocalDateFunctionName()$1") + replaceRegex( + """([^?])\.date(\W|${'$'})""", + "$1.date.$convertFromDataConnectLocalDateFunctionName()$2" + ) + replaceText( + "$localDateFullyQualifiedClassName(", + "$localDateFullyQualifiedClassName$localDateFactoryCall(" + ) + + atLineThatStartsWith("package ") + .deleteLinesAboveThatStartWith("//") + .insertAbove(generatedFileWarningLines) + + atLineThatStartsWith("class ") + .deleteLinesAboveThatStartWith("//") + .insertAbove(generatedFileWarningLines) + } + + val destFile = File(srcFile.parentFile, "$destClassName.kt") + logger.info("Writing {}", destFile.absolutePath) + destFile.writeText(transformer.lines.joinToString("\n")) +} + +tasks.register("generateDataConnectUnitTestingSources") { + val dir = file("src/test/kotlin/com/google/firebase/dataconnect/serializers") + val srcFile = File(dir, "LocalDateSerializerUnitTest.kt") + doLast { + generateLocalDateSerializerUnitTest( + srcFile = srcFile, + classNameUnderTest = "JavaTimeLocalDateSerializer", + localDateFullyQualifiedClassName = "java.time.LocalDate", + localDateFactoryCall = ".of", + logger = logger, + ) + generateLocalDateSerializerUnitTest( + srcFile = srcFile, + classNameUnderTest = "KotlinxDatetimeLocalDateSerializer", + localDateFullyQualifiedClassName = "kotlinx.datetime.LocalDate", + localDateFactoryCall = "", + logger = logger, + ) + } +} + +tasks.register("generateDataConnectIntegrationTestingSources") { + val dir = file("src/androidTest/kotlin/com/google/firebase/dataconnect") + val srcFile = File(dir, "LocalDateIntegrationTest.kt") + doLast { + generateLocalDateSerializerIntegrationTest( + srcFile = srcFile, + destClassName = "JavaTimeLocalDateIntegrationTest", + localDateFullyQualifiedClassName = "java.time.LocalDate", + localDateFactoryCall = ".of", + convertFromDataConnectLocalDateFunctionName = "toJavaLocalDate", + serializerClassName = "JavaTimeLocalDateSerializer", + logger = logger, + ) + generateLocalDateSerializerIntegrationTest( + srcFile = srcFile, + destClassName = "KotlinxDatetimeLocalDateIntegrationTest", + localDateFullyQualifiedClassName = "kotlinx.datetime.LocalDate", + localDateFactoryCall = "", + convertFromDataConnectLocalDateFunctionName = "toKotlinxLocalDate", + serializerClassName = "KotlinxDatetimeLocalDateSerializer", + logger = logger, + ) + } +} + +tasks.register("generateDataConnectTestingSources") { + dependsOn("generateDataConnectUnitTestingSources") + dependsOn("generateDataConnectIntegrationTestingSources") +} diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/JavaTimeLocalDateIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/JavaTimeLocalDateIntegrationTest.kt index e99238cf00e..41622330084 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/JavaTimeLocalDateIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/JavaTimeLocalDateIntegrationTest.kt @@ -18,10 +18,10 @@ @file:UseSerializers(UUIDSerializer::class, JavaTimeLocalDateSerializer::class) //////////////////////////////////////////////////////////////////////////////// -// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateIntegrationTest.kt -// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO -// LocalDateIntegrationTest.kt AND PORTED TO LocalDateIntegrationTest.kt, -// if appropriate. +// WARNING: THIS FILE IS GENERATED FROM LocalDateIntegrationTest.kt +// DO NOT MODIFY THIS FILE BY HAND BECAUSE MANUAL CHANGES WILL GET OVERWRITTEN +// THE NEXT TIME THAT THIS FILE IS REGENERATED. TO REGENERATE THIS FILE, RUN: +// ./gradlew :firebase-dataconnect:generateDataConnectTestingSources //////////////////////////////////////////////////////////////////////////////// package com.google.firebase.dataconnect @@ -69,10 +69,10 @@ import kotlinx.serialization.serializer import org.junit.Test //////////////////////////////////////////////////////////////////////////////// -// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateIntegrationTest.kt -// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO -// LocalDateIntegrationTest.kt AND PORTED TO LocalDateIntegrationTest.kt, -// if appropriate. +// WARNING: THIS FILE IS GENERATED FROM LocalDateIntegrationTest.kt +// DO NOT MODIFY THIS FILE BY HAND BECAUSE MANUAL CHANGES WILL GET OVERWRITTEN +// THE NEXT TIME THAT THIS FILE IS REGENERATED. TO REGENERATE THIS FILE, RUN: +// ./gradlew :firebase-dataconnect:generateDataConnectTestingSources //////////////////////////////////////////////////////////////////////////////// class JavaTimeLocalDateIntegrationTest : DataConnectIntegrationTestBase() { diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/KotlinxDatetimeLocalDateIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/KotlinxDatetimeLocalDateIntegrationTest.kt index a4ea009a7c7..0b5bd5e7ec0 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/KotlinxDatetimeLocalDateIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/KotlinxDatetimeLocalDateIntegrationTest.kt @@ -18,10 +18,10 @@ @file:UseSerializers(UUIDSerializer::class, KotlinxDatetimeLocalDateSerializer::class) //////////////////////////////////////////////////////////////////////////////// -// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateIntegrationTest.kt -// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO -// LocalDateIntegrationTest.kt AND PORTED TO JavaTimeLocalDateIntegrationTest.kt, -// if appropriate. +// WARNING: THIS FILE IS GENERATED FROM LocalDateIntegrationTest.kt +// DO NOT MODIFY THIS FILE BY HAND BECAUSE MANUAL CHANGES WILL GET OVERWRITTEN +// THE NEXT TIME THAT THIS FILE IS REGENERATED. TO REGENERATE THIS FILE, RUN: +// ./gradlew :firebase-dataconnect:generateDataConnectTestingSources //////////////////////////////////////////////////////////////////////////////// package com.google.firebase.dataconnect @@ -69,10 +69,10 @@ import kotlinx.serialization.serializer import org.junit.Test //////////////////////////////////////////////////////////////////////////////// -// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateIntegrationTest.kt -// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO -// LocalDateIntegrationTest.kt AND PORTED TO JavaTimeLocalDateIntegrationTest.kt, -// if appropriate. +// WARNING: THIS FILE IS GENERATED FROM LocalDateIntegrationTest.kt +// DO NOT MODIFY THIS FILE BY HAND BECAUSE MANUAL CHANGES WILL GET OVERWRITTEN +// THE NEXT TIME THAT THIS FILE IS REGENERATED. TO REGENERATE THIS FILE, RUN: +// ./gradlew :firebase-dataconnect:generateDataConnectTestingSources //////////////////////////////////////////////////////////////////////////////// class KotlinxDatetimeLocalDateIntegrationTest : DataConnectIntegrationTestBase() { diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/LocalDateIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/LocalDateIntegrationTest.kt index 490fea04961..10631084900 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/LocalDateIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/LocalDateIntegrationTest.kt @@ -18,10 +18,11 @@ @file:UseSerializers(UUIDSerializer::class) //////////////////////////////////////////////////////////////////////////////// -// THIS FILE WAS COPIED TO JavaTimeLocalDateIntegrationTest.kt and -// KotlinxDatetimeLocalDateIntegrationTest.kt AND ADAPTED TO TEST THE -// CORRESPONDING IMPLEMENTATIONS OF LocalDate. ANY CHANGES MADE TO THIS FILE -// MUST ALSO BE PORTED TO THOSE OTHER FILES, IF APPROPRIATE. +// NOTE: THIS FILE IS USED AS A TEMPLATE TO GENERATE +// JavaTimeLocalDateIntegrationTest.kt and +// KotlinxDatetimeLocalDateIntegrationTest.kt. IF ANY CHANGES MADE TO THIS FILE +// THEN THOSE TWO FILES SHOULD ALSO BE REGENERATED BY RUNNING +// ./gradlew :firebase-dataconnect:generateDataConnectTestingSources //////////////////////////////////////////////////////////////////////////////// package com.google.firebase.dataconnect @@ -67,10 +68,11 @@ import kotlinx.serialization.serializer import org.junit.Test //////////////////////////////////////////////////////////////////////////////// -// THIS FILE WAS COPIED TO JavaTimeLocalDateIntegrationTest.kt and -// KotlinxDatetimeLocalDateIntegrationTest.kt AND ADAPTED TO TEST THE -// CORRESPONDING IMPLEMENTATIONS OF LocalDate. ANY CHANGES MADE TO THIS FILE -// MUST ALSO BE PORTED TO THOSE OTHER FILES, IF APPROPRIATE. +// NOTE: THIS FILE IS USED AS A TEMPLATE TO GENERATE +// JavaTimeLocalDateIntegrationTest.kt and +// KotlinxDatetimeLocalDateIntegrationTest.kt. IF ANY CHANGES MADE TO THIS FILE +// THEN THOSE TWO FILES SHOULD ALSO BE REGENERATED BY RUNNING +// ./gradlew :firebase-dataconnect:generateDataConnectTestingSources //////////////////////////////////////////////////////////////////////////////// class LocalDateIntegrationTest : DataConnectIntegrationTestBase() { diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/JavaTimeLocalDateSerializerUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/JavaTimeLocalDateSerializerUnitTest.kt index 4b46994985f..9bd34f3a3f8 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/JavaTimeLocalDateSerializerUnitTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/JavaTimeLocalDateSerializerUnitTest.kt @@ -16,10 +16,10 @@ @file:OptIn(ExperimentalKotest::class) //////////////////////////////////////////////////////////////////////////////// -// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateSerializerUnitTest.kt -// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO -// LocalDateIntegrationTest.kt AND PORTED TO KotlinxDatetimeLocalDateSerializerUnitTest.kt, -// if appropriate. +// WARNING: THIS FILE IS GENERATED FROM LocalDateSerializerUnitTest.kt +// DO NOT MODIFY THIS FILE BY HAND BECAUSE MANUAL CHANGES WILL GET OVERWRITTEN +// THE NEXT TIME THAT THIS FILE IS REGENERATED. TO REGENERATE THIS FILE, RUN: +// ./gradlew :firebase-dataconnect:generateDataConnectTestingSources //////////////////////////////////////////////////////////////////////////////// package com.google.firebase.dataconnect.serializers @@ -63,10 +63,10 @@ import kotlinx.serialization.encoding.Decoder import org.junit.Test //////////////////////////////////////////////////////////////////////////////// -// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateSerializerUnitTest.kt -// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO -// LocalDateIntegrationTest.kt AND PORTED TO KotlinxDatetimeLocalDateSerializerUnitTest.kt, -// if appropriate. +// WARNING: THIS FILE IS GENERATED FROM LocalDateSerializerUnitTest.kt +// DO NOT MODIFY THIS FILE BY HAND BECAUSE MANUAL CHANGES WILL GET OVERWRITTEN +// THE NEXT TIME THAT THIS FILE IS REGENERATED. TO REGENERATE THIS FILE, RUN: +// ./gradlew :firebase-dataconnect:generateDataConnectTestingSources //////////////////////////////////////////////////////////////////////////////// class JavaTimeLocalDateSerializerUnitTest { diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/KotlinxDatetimeLocalDateSerializerUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/KotlinxDatetimeLocalDateSerializerUnitTest.kt index 3ab96a648a9..d3d775e7c9f 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/KotlinxDatetimeLocalDateSerializerUnitTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/KotlinxDatetimeLocalDateSerializerUnitTest.kt @@ -16,10 +16,10 @@ @file:OptIn(ExperimentalKotest::class) //////////////////////////////////////////////////////////////////////////////// -// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateSerializerUnitTest.kt -// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO -// LocalDateIntegrationTest.kt AND PORTED TO JavaTimeLocalDateSerializerUnitTest.kt, -// if appropriate. +// WARNING: THIS FILE IS GENERATED FROM LocalDateSerializerUnitTest.kt +// DO NOT MODIFY THIS FILE BY HAND BECAUSE MANUAL CHANGES WILL GET OVERWRITTEN +// THE NEXT TIME THAT THIS FILE IS REGENERATED. TO REGENERATE THIS FILE, RUN: +// ./gradlew :firebase-dataconnect:generateDataConnectTestingSources //////////////////////////////////////////////////////////////////////////////// package com.google.firebase.dataconnect.serializers @@ -63,10 +63,10 @@ import kotlinx.serialization.encoding.Decoder import org.junit.Test //////////////////////////////////////////////////////////////////////////////// -// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateSerializerUnitTest.kt -// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO -// LocalDateIntegrationTest.kt AND PORTED TO JavaTimeLocalDateSerializerUnitTest.kt, -// if appropriate. +// WARNING: THIS FILE IS GENERATED FROM LocalDateSerializerUnitTest.kt +// DO NOT MODIFY THIS FILE BY HAND BECAUSE MANUAL CHANGES WILL GET OVERWRITTEN +// THE NEXT TIME THAT THIS FILE IS REGENERATED. TO REGENERATE THIS FILE, RUN: +// ./gradlew :firebase-dataconnect:generateDataConnectTestingSources //////////////////////////////////////////////////////////////////////////////// class KotlinxDatetimeLocalDateSerializerUnitTest { diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/LocalDateSerializerUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/LocalDateSerializerUnitTest.kt index 6bc0c45d3bc..1b256800ee2 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/LocalDateSerializerUnitTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/LocalDateSerializerUnitTest.kt @@ -16,10 +16,11 @@ @file:OptIn(ExperimentalKotest::class) //////////////////////////////////////////////////////////////////////////////// -// THIS FILE WAS COPIED TO JavaTimeLocalDateSerializerUnitTest.kt and -// KotlinxDatetimeLocalDateSerializerUnitTest.kt AND ADAPTED TO TEST THE -// CORRESPONDING IMPLEMENTATIONS OF LocalDate. ANY CHANGES MADE TO THIS FILE -// MUST ALSO BE PORTED TO THOSE OTHER FILES, IF APPROPRIATE. +// NOTE: THIS FILE IS USED AS A TEMPLATE TO GENERATE +// JavaTimeLocalDateSerializerUnitTest.kt and +// KotlinxDatetimeLocalDateSerializerUnitTest.kt. IF ANY CHANGES MADE TO THIS +// FILE THEN THOSE TWO FILES SHOULD ALSO BE REGENERATED BY RUNNING +// ./gradlew :firebase-dataconnect:generateDataConnectTestingSources //////////////////////////////////////////////////////////////////////////////// package com.google.firebase.dataconnect.serializers @@ -63,10 +64,11 @@ import kotlinx.serialization.encoding.Decoder import org.junit.Test //////////////////////////////////////////////////////////////////////////////// -// THIS FILE WAS COPIED TO JavaTimeLocalDateSerializerUnitTest.kt and -// KotlinxDatetimeLocalDateSerializerUnitTest.kt AND ADAPTED TO TEST THE -// CORRESPONDING IMPLEMENTATIONS OF LocalDate. ANY CHANGES MADE TO THIS FILE -// MUST ALSO BE PORTED TO THOSE OTHER FILES, IF APPROPRIATE. +// NOTE: THIS FILE IS USED AS A TEMPLATE TO GENERATE +// JavaTimeLocalDateSerializerUnitTest.kt and +// KotlinxDatetimeLocalDateSerializerUnitTest.kt. IF ANY CHANGES MADE TO THIS +// FILE THEN THOSE TWO FILES SHOULD ALSO BE REGENERATED BY RUNNING +// ./gradlew :firebase-dataconnect:generateDataConnectTestingSources //////////////////////////////////////////////////////////////////////////////// class LocalDateSerializerUnitTest { @@ -151,14 +153,22 @@ class LocalDateSerializerUnitTest { month: Arb<Int> = intWithEvenNumDigitsDistribution(), day: Arb<Int> = intWithEvenNumDigitsDistribution(), ): Arb<LocalDate> { + // ReplaceLinesBelow(numLines=0, replacementId=CoerceDayOfMonthIntoValidRangeFor) return arbitrary( edgecaseFn = { rs -> val yearInt = if (rs.random.nextBoolean()) year.next(rs) else year.edgecase(rs)!! val monthInt = if (rs.random.nextBoolean()) month.next(rs) else month.edgecase(rs)!! val dayInt = if (rs.random.nextBoolean()) day.next(rs) else day.edgecase(rs)!! + // ReplaceLinesBelow(numLines=1, replacementId=LocalDateSample) LocalDate(year = yearInt, month = monthInt, day = dayInt) }, - sampleFn = { LocalDate(year = year.bind(), month = month.bind(), day = day.bind()) } + sampleFn = { + val yearInt = year.bind() + val monthInt = month.bind() + val dayInt = day.bind() + // ReplaceLinesBelow(numLines=1, replacementId=LocalDateSample) + LocalDate(year = yearInt, month = monthInt, day = dayInt) + } ) } From 02d17557702a1a0cea3a35b52a562df625272d8b Mon Sep 17 00:00:00 2001 From: Denver Coneybeare <dconeybe@google.com> Date: Thu, 12 Dec 2024 22:12:26 +0000 Subject: [PATCH 13/13] emulator.sh updated to use firebase-tools --- firebase-dataconnect/emulator/emulator.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/firebase-dataconnect/emulator/emulator.sh b/firebase-dataconnect/emulator/emulator.sh index 68ccf3331a7..f27b6f2f6fe 100755 --- a/firebase-dataconnect/emulator/emulator.sh +++ b/firebase-dataconnect/emulator/emulator.sh @@ -24,8 +24,10 @@ readonly FIREBASE_ARGS=( firebase --debug emulators:start - --only auth,dataconnect ) -echo "[$0] Running command: ${FIREBASE_ARGS[*]}" +set -x + +export FIREBASE_DATACONNECT_POSTGRESQL_STRING=postgresql://postgres:postgres@localhost:5432?sslmode=disable +export DATACONNECT_EMULATOR_BINARY_PATH="${SELF_DIR}/cli" exec "${FIREBASE_ARGS[@]}"