diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/ProtoUtil.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/ProtoUtil.kt index 4cfe1ffd288..0ba6a34b34a 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/ProtoUtil.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/ProtoUtil.kt @@ -248,11 +248,13 @@ internal object ProtoUtil { fun Value.toAny(): Any? = valueToAnyMutualRecursion.anyValueFromValue(this) - fun List.toValueProto(): Value { + fun List.toValueProto(): Value { val key = "y8czq9rh75" return mapOf(key to this).toStructProto().getFieldsOrThrow(key) } + fun List.toListValueProto(): ListValue = toValueProto().listValue + fun Map.toValueProto(): Value = Value.newBuilder().setStructValue(toStructProto()).build() diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderUnitTest.kt index fabfca07d56..1d760cc99dc 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderUnitTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderUnitTest.kt @@ -13,505 +13,458 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -@file:OptIn(ExperimentalSerializationApi::class, ExperimentalSerializationApi::class) - package com.google.firebase.dataconnect -import com.google.common.truth.Truth.assertThat +import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingTextIgnoringCase import com.google.firebase.dataconnect.util.ProtoUtil.buildStructProto import com.google.firebase.dataconnect.util.ProtoUtil.decodeFromStruct import com.google.firebase.dataconnect.util.ProtoUtil.encodeToStruct import com.google.protobuf.Struct import com.google.protobuf.Value import com.google.protobuf.Value.KindCase -import java.util.regex.Pattern -import kotlin.reflect.KClass -import kotlinx.serialization.ExperimentalSerializationApi +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeSameInstanceAs +import io.kotest.property.Arb +import io.kotest.property.arbitrary.arbitrary +import io.kotest.property.arbitrary.boolean +import io.kotest.property.arbitrary.constant +import io.kotest.property.arbitrary.double +import io.kotest.property.arbitrary.filter +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.list +import io.kotest.property.arbitrary.orNull +import io.kotest.property.arbitrary.string +import io.kotest.property.checkAll +import kotlinx.coroutines.test.runTest import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationException -import org.junit.Assert.assertThrows +import org.junit.Ignore import org.junit.Test class ProtoStructDecoderUnitTest { @Test - fun `decodeFromStruct() can encode and decode a complex object A`() { - val obj = - SerializationTestData.AllTheTypes.newInstance(seed = "TheQuickBrown").withEmptyUnitLists() - val struct = encodeToStruct(obj) - val decodedObj = decodeFromStruct(struct) - assertThat(decodedObj).isEqualTo(obj) - } - - @Test - fun `decodeFromStruct() can encode and decode a complex object B`() { - val obj = - SerializationTestData.AllTheTypes.newInstance(seed = "FoxJumpsOver").withEmptyUnitLists() - val struct = encodeToStruct(obj) - val decodedObj = decodeFromStruct(struct) - assertThat(decodedObj).isEqualTo(obj) - } - - @Test - fun `decodeFromStruct() can encode and decode a complex object C`() { - val obj = - SerializationTestData.AllTheTypes.newInstance(seed = "TheLazyDog").withEmptyUnitLists() - val struct = encodeToStruct(obj) - val decodedObj = decodeFromStruct(struct) - assertThat(decodedObj).isEqualTo(obj) + fun `decodeFromStruct() can encode and decode complex objects`() = runTest { + val seeds = Arb.string().filter { it.hashCode() != 0 } + checkAll(iterations = 20, seeds) { seed -> + val obj = SerializationTestData.AllTheTypes.newInstance(seed).withEmptyUnitLists() + val struct = encodeToStruct(obj) + val decodedObj = decodeFromStruct(struct) + decodedObj shouldBe obj + } } @Test - fun `decodeFromStruct() can encode and decode a list of nullable Unit ending in null`() { - @Serializable data class TestData(val list: List) - val struct = encodeToStruct(TestData(listOf(null, Unit, null))) - val decodedObj = decodeFromStruct(struct) - assertThat(decodedObj).isEqualTo(TestData(listOf(null, Unit, null))) + @Ignore("A List gets decoded as an empty list; if anyone cares, fix it.") + fun `decodeFromStruct() can encode and decode a list of non-nullable Unit`() = runTest { + @Serializable data class TestData(val list: List) + checkAll(Arb.list(Arb.constant(Unit))) { list -> + val struct = encodeToStruct(TestData(list)) + val decodedObj = decodeFromStruct(struct) + decodedObj shouldBe TestData(list) + } } @Test - fun `decodeFromStruct() can encode and decode a list of nullable Unit ending in Unit`() { + fun `decodeFromStruct() can encode and decode a list of nullable Unit`() = runTest { @Serializable data class TestData(val list: List) - val struct = encodeToStruct(TestData(listOf(null, Unit, null, Unit))) - val decodedObj = decodeFromStruct(struct) - assertThat(decodedObj).isEqualTo(TestData(listOf(null, Unit, null, Unit))) + checkAll(Arb.list(Arb.constant(Unit).orNull())) { list -> + val struct = encodeToStruct(TestData(list)) + val decodedObj = decodeFromStruct(struct) + decodedObj shouldBe TestData(list) + } } @Test fun `decodeFromStruct() can decode a Struct to Unit`() { val decodedTestData = decodeFromStruct(Struct.getDefaultInstance()) - assertThat(decodedTestData).isSameInstanceAs(Unit) + decodedTestData shouldBeSameInstanceAs Unit } @Test - fun `decodeFromStruct() can decode a Struct with String values`() { + fun `decodeFromStruct() can decode a Struct with String values`() = runTest { @Serializable data class TestData(val value1: String, val value2: String) - val struct = encodeToStruct(TestData(value1 = "foo", value2 = "bar")) - - val decodedTestData = decodeFromStruct(struct) - - assertThat(decodedTestData).isEqualTo(TestData(value1 = "foo", value2 = "bar")) + val strings = Arb.string() + checkAll(strings, strings) { value1, value2 -> + val struct = encodeToStruct(TestData(value1 = value1, value2 = value2)) + val decodedTestData = decodeFromStruct(struct) + decodedTestData shouldBe TestData(value1 = value1, value2 = value2) + } } @Test - fun `decodeFromStruct() can decode a Struct with _nullable_ String values`() { - @Serializable data class TestData(val isNull: String?, val isNotNull: String?) - val struct = encodeToStruct(TestData(isNull = null, isNotNull = "NotNull")) - - val decodedTestData = decodeFromStruct(struct) - - assertThat(decodedTestData).isEqualTo(TestData(isNull = null, isNotNull = "NotNull")) + fun `decodeFromStruct() can decode a Struct with _nullable_ String values`() = runTest { + @Serializable data class TestData(val value1: String?, val value2: String?) + val nullableStrings = Arb.string().orNull() + checkAll(nullableStrings, nullableStrings) { value1, value2 -> + val struct = encodeToStruct(TestData(value1 = value1, value2 = value2)) + val decodedTestData = decodeFromStruct(struct) + decodedTestData shouldBe TestData(value1 = value1, value2 = value2) + } } @Test - fun `decodeFromStruct() can decode a Struct with Boolean values`() { + fun `decodeFromStruct() can decode a Struct with Boolean values`() = runTest { @Serializable data class TestData(val value1: Boolean, val value2: Boolean) - val struct = encodeToStruct(TestData(value1 = true, value2 = false)) - - val decodedTestData = decodeFromStruct(struct) - - assertThat(decodedTestData).isEqualTo(TestData(value1 = true, value2 = false)) + val booleans = Arb.boolean() + checkAll(booleans, booleans) { value1, value2 -> + val struct = encodeToStruct(TestData(value1 = value1, value2 = value2)) + val decodedTestData = decodeFromStruct(struct) + decodedTestData shouldBe TestData(value1 = value1, value2 = value2) + } } @Test - fun `decodeFromStruct() can decode a Struct with _nullable_ Boolean values`() { - @Serializable data class TestData(val isNull: Boolean?, val isNotNull: Boolean?) - val struct = encodeToStruct(TestData(isNull = null, isNotNull = true)) - - val decodedTestData = decodeFromStruct(struct) - - assertThat(decodedTestData).isEqualTo(TestData(isNull = null, isNotNull = true)) + fun `decodeFromStruct() can decode a Struct with _nullable_ Boolean values`() = runTest { + @Serializable data class TestData(val value1: Boolean?, val value2: Boolean?) + val nullableBooleans = Arb.boolean().orNull() + checkAll(nullableBooleans, nullableBooleans) { value1, value2 -> + val struct = encodeToStruct(TestData(value1 = value1, value2 = value2)) + val decodedTestData = decodeFromStruct(struct) + decodedTestData shouldBe TestData(value1 = value1, value2 = value2) + } } @Test - fun `decodeFromStruct() can decode a Struct with Int values`() { + fun `decodeFromStruct() can decode a Struct with Int values`() = runTest { @Serializable data class TestData(val value1: Int, val value2: Int) - val struct = encodeToStruct(TestData(value1 = 123, value2 = -456)) - - val decodedTestData = decodeFromStruct(struct) - - assertThat(decodedTestData).isEqualTo(TestData(value1 = 123, value2 = -456)) - } - - @Test - fun `decodeFromStruct() can decode a Struct with _nullable_ Int values`() { - @Serializable data class TestData(val isNull: Int?, val isNotNull: Int?) - val struct = encodeToStruct(TestData(isNull = null, isNotNull = 42)) - - val decodedTestData = decodeFromStruct(struct) - - assertThat(decodedTestData).isEqualTo(TestData(isNull = null, isNotNull = 42)) + val ints = Arb.int() + checkAll(ints, ints) { value1, value2 -> + val struct = encodeToStruct(TestData(value1 = value1, value2 = value2)) + val decodedTestData = decodeFromStruct(struct) + decodedTestData shouldBe TestData(value1 = value1, value2 = value2) + } } @Test - fun `decodeFromStruct() can decode a Struct with extreme Int values`() { - @Serializable data class TestData(val max: Int, val min: Int) - val struct = encodeToStruct(TestData(max = Int.MAX_VALUE, min = Int.MIN_VALUE)) - - val decodedTestData = decodeFromStruct(struct) - - assertThat(decodedTestData).isEqualTo(TestData(max = Int.MAX_VALUE, min = Int.MIN_VALUE)) + fun `decodeFromStruct() can decode a Struct with _nullable_ Int values`() = runTest { + @Serializable data class TestData(val value1: Int?, val value2: Int?) + val nullableInts = Arb.int().orNull() + checkAll(nullableInts, nullableInts) { value1, value2 -> + val struct = encodeToStruct(TestData(value1 = value1, value2 = value2)) + val decodedTestData = decodeFromStruct(struct) + decodedTestData shouldBe TestData(value1 = value1, value2 = value2) + } } @Test - fun `decodeFromStruct() can decode a Struct with Double values`() { + fun `decodeFromStruct() can decode a Struct with Double values`() = runTest { @Serializable data class TestData(val value1: Double, val value2: Double) - val struct = encodeToStruct(TestData(value1 = 123.45, value2 = -456.78)) - - val decodedTestData = decodeFromStruct(struct) - - assertThat(decodedTestData).isEqualTo(TestData(value1 = 123.45, value2 = -456.78)) - } - - @Test - fun `decodeFromStruct() can decode a Struct with _nullable_ Double values`() { - @Serializable data class TestData(val isNull: Double?, val isNotNull: Double?) - val struct = encodeToStruct(TestData(isNull = null, isNotNull = 987.654)) - - val decodedTestData = decodeFromStruct(struct) - - assertThat(decodedTestData).isEqualTo(TestData(isNull = null, isNotNull = 987.654)) + val doubles = Arb.double() + checkAll(doubles, doubles) { value1, value2 -> + val struct = encodeToStruct(TestData(value1 = value1, value2 = value2)) + val decodedTestData = decodeFromStruct(struct) + decodedTestData shouldBe TestData(value1 = value1, value2 = value2) + } } @Test - fun `decodeFromStruct() can decode a Struct with extreme Double values`() { - @Serializable - data class TestData( - val min: Double, - val max: Double, - val positiveInfinity: Double, - val negativeInfinity: Double, - val nan: Double - ) - val struct = - encodeToStruct( - TestData( - min = Double.MIN_VALUE, - max = Double.MAX_VALUE, - positiveInfinity = Double.POSITIVE_INFINITY, - negativeInfinity = Double.NEGATIVE_INFINITY, - nan = Double.NaN - ) - ) - - val decodedTestData = decodeFromStruct(struct) - - assertThat(decodedTestData) - .isEqualTo( - TestData( - min = Double.MIN_VALUE, - max = Double.MAX_VALUE, - positiveInfinity = Double.POSITIVE_INFINITY, - negativeInfinity = Double.NEGATIVE_INFINITY, - nan = Double.NaN - ) - ) + fun `decodeFromStruct() can decode a Struct with _nullable_ Double values`() = runTest { + @Serializable data class TestData(val value1: Double?, val value2: Double?) + val nullableDoubles = Arb.double().orNull() + checkAll(nullableDoubles, nullableDoubles) { value1, value2 -> + val struct = encodeToStruct(TestData(value1 = value1, value2 = value2)) + val decodedTestData = decodeFromStruct(struct) + decodedTestData shouldBe TestData(value1 = value1, value2 = value2) + } } @Test - fun `decodeFromStruct() can decode a Struct with nested Struct values`() { + fun `decodeFromStruct() can decode a Struct with nested Struct values`() = runTest { @Serializable data class TestDataA(val base: String) @Serializable data class TestDataB(val dataA: TestDataA) @Serializable data class TestDataC(val dataB: TestDataB) @Serializable data class TestDataD(val dataC: TestDataC) + val arb: Arb = arbitrary { + TestDataD(TestDataC(TestDataB(TestDataA(Arb.string().bind())))) + } - val struct = encodeToStruct(TestDataD(TestDataC(TestDataB(TestDataA("hello"))))) - - val decodedTestData = decodeFromStruct(struct) - - assertThat(decodedTestData).isEqualTo(TestDataD(TestDataC(TestDataB(TestDataA("hello"))))) + checkAll(arb) { value -> + val struct = encodeToStruct(value) + val decodedTestData = decodeFromStruct(struct) + decodedTestData shouldBe value + } } @Test - fun `decodeFromStruct() can decode a Struct with nested _nullable_ Struct values`() { + fun `decodeFromStruct() can decode a Struct with nested _nullable_ Struct values`() = runTest { @Serializable data class TestDataA(val base: String) - @Serializable data class TestDataB(val dataANull: TestDataA?, val dataANotNull: TestDataA?) - @Serializable data class TestDataC(val dataBNull: TestDataB?, val dataBNotNull: TestDataB?) - @Serializable data class TestDataD(val dataCNull: TestDataC?, val dataCNotNull: TestDataC?) - - val struct = - encodeToStruct(TestDataD(null, TestDataC(null, TestDataB(null, TestDataA("hello"))))) - - val decodedTestData = decodeFromStruct(struct) - - assertThat(decodedTestData) - .isEqualTo(TestDataD(null, TestDataC(null, TestDataB(null, TestDataA("hello"))))) + @Serializable data class TestDataB(val child1: TestDataA?, val child2: TestDataA?) + @Serializable data class TestDataC(val child1: TestDataB?, val child2: TestDataB?) + @Serializable data class TestDataD(val child1: TestDataC?, val child2: TestDataC?) + val arbA: Arb = arbitrary { TestDataA(Arb.string().bind()) } + val arbB: Arb = arbitrary { arbA.orNull(0.2).run { TestDataB(bind(), bind()) } } + val arbC: Arb = arbitrary { arbB.orNull(0.2).run { TestDataC(bind(), bind()) } } + val arbD: Arb = arbitrary { arbC.orNull(0.2).run { TestDataD(bind(), bind()) } } + + checkAll(arbD) { value -> + val struct = encodeToStruct(value) + val decodedTestData = decodeFromStruct(struct) + decodedTestData shouldBe value + } } @Test - fun `decodeFromStruct() can decode a Struct with nullable ListValue values`() { - @Serializable data class TestData(val nullList: List?, val nonNullList: List?) - val struct = encodeToStruct(TestData(nullList = null, nonNullList = listOf("a", "b"))) - - val decodedTestData = decodeFromStruct(struct) + fun `decodeFromStruct() can decode a Struct with nullable ListValue values`() = runTest { + @Serializable data class TestData(val value1: List?, val value2: List?) + val arb: Arb = arbitrary { + Arb.list(Arb.string()).orNull(0.33).run { TestData(bind(), bind()) } + } - assertThat(decodedTestData).isEqualTo(TestData(nullList = null, nonNullList = listOf("a", "b"))) + checkAll(iterations = 20, arb) { value -> + val struct = encodeToStruct(value) + val decodedTestData = decodeFromStruct(struct) + decodedTestData shouldBe value + } } @Test - fun `decodeFromStruct() can decode a ListValue of String`() { - @Serializable data class TestData(val list: List) - val struct = encodeToStruct(TestData(listOf("elem1", "elem2"))) - - val decodedTestData = decodeFromStruct(struct) + fun `decodeFromStruct() can decode a ListValue of String`() = runTest { + @Serializable data class TestData(val value1: List, val value2: List) + val arb: Arb = arbitrary { Arb.list(Arb.string()).run { TestData(bind(), bind()) } } - assertThat(decodedTestData).isEqualTo(TestData(listOf("elem1", "elem2"))) + checkAll(iterations = 20, arb) { value -> + val struct = encodeToStruct(value) + val decodedTestData = decodeFromStruct(struct) + decodedTestData shouldBe value + } } @Test - fun `decodeFromStruct() can decode a ListValue of _nullable_ String`() { - @Serializable data class TestData(val list: List) - val struct = encodeToStruct(TestData(listOf(null, "aaa", null, "bbb"))) - - val decodedTestData = decodeFromStruct(struct) + fun `decodeFromStruct() can decode a ListValue of _nullable_ String`() = runTest { + @Serializable data class TestData(val value1: List, val value2: List) + val arb: Arb = arbitrary { + Arb.list(Arb.string().orNull(0.33)).run { TestData(bind(), bind()) } + } - assertThat(decodedTestData).isEqualTo(TestData(listOf(null, "aaa", null, "bbb"))) + checkAll(iterations = 20, arb) { value -> + val struct = encodeToStruct(value) + val decodedTestData = decodeFromStruct(struct) + decodedTestData shouldBe value + } } @Test - fun `decodeFromStruct() can decode a ListValue of Boolean`() { - @Serializable data class TestData(val list: List) - val struct = encodeToStruct(TestData(listOf(true, false, true, false))) - - val decodedTestData = decodeFromStruct(struct) + fun `decodeFromStruct() can decode a ListValue of Boolean`() = runTest { + @Serializable data class TestData(val value1: List, val value2: List) + val arb: Arb = arbitrary { Arb.list(Arb.boolean()).run { TestData(bind(), bind()) } } - assertThat(decodedTestData).isEqualTo(TestData(listOf(true, false, true, false))) + checkAll(arb) { value -> + val struct = encodeToStruct(value) + val decodedTestData = decodeFromStruct(struct) + decodedTestData shouldBe value + } } @Test - fun `decodeFromStruct() can decode a ListValue of _nullable_ Boolean`() { - @Serializable data class TestData(val list: List) - val struct = encodeToStruct(TestData(listOf(null, true, false, null, true, false))) - - val decodedTestData = decodeFromStruct(struct) + fun `decodeFromStruct() can decode a ListValue of _nullable_ Boolean`() = runTest { + @Serializable data class TestData(val value1: List, val value2: List) + val arb: Arb = arbitrary { + Arb.list(Arb.boolean().orNull(0.33)).run { TestData(bind(), bind()) } + } - assertThat(decodedTestData).isEqualTo(TestData(listOf(null, true, false, null, true, false))) + checkAll(arb) { value -> + val struct = encodeToStruct(value) + val decodedTestData = decodeFromStruct(struct) + decodedTestData shouldBe value + } } @Test - fun `decodeFromStruct() can decode a ListValue of Int`() { - @Serializable data class TestData(val list: List) - val struct = encodeToStruct(TestData(listOf(1, 0, -1, Int.MAX_VALUE, Int.MIN_VALUE))) + fun `decodeFromStruct() can decode a ListValue of Int`() = runTest { + @Serializable data class TestData(val value1: List, val value2: List) + val arb: Arb = arbitrary { Arb.list(Arb.int()).run { TestData(bind(), bind()) } } - val decodedTestData = decodeFromStruct(struct) - - assertThat(decodedTestData).isEqualTo(TestData(listOf(1, 0, -1, Int.MAX_VALUE, Int.MIN_VALUE))) + checkAll(arb) { value -> + val struct = encodeToStruct(value) + val decodedTestData = decodeFromStruct(struct) + decodedTestData shouldBe value + } } @Test - fun `decodeFromStruct() can decode a ListValue of _nullable_ Int`() { - @Serializable data class TestData(val list: List) - val struct = encodeToStruct(TestData(listOf(1, 0, -1, Int.MAX_VALUE, Int.MIN_VALUE, null))) - - val decodedTestData = decodeFromStruct(struct) + fun `decodeFromStruct() can decode a ListValue of _nullable_ Int`() = runTest { + @Serializable data class TestData(val value1: List, val value2: List) + val arb: Arb = arbitrary { + Arb.list(Arb.int().orNull(0.33)).run { TestData(bind(), bind()) } + } - assertThat(decodedTestData) - .isEqualTo(TestData(listOf(1, 0, -1, Int.MAX_VALUE, Int.MIN_VALUE, null))) + checkAll(arb) { value -> + val struct = encodeToStruct(value) + val decodedTestData = decodeFromStruct(struct) + decodedTestData shouldBe value + } } @Test - fun `decodeFromStruct() can decode a ListValue of Double`() { - @Serializable data class TestData(val list: List) - val struct = - encodeToStruct( - TestData( - listOf( - 1.0, - 0.0, - -0.0, - -1.0, - Double.MAX_VALUE, - Double.MIN_VALUE, - Double.NaN, - Double.POSITIVE_INFINITY, - Double.NEGATIVE_INFINITY - ) - ) - ) + fun `decodeFromStruct() can decode a ListValue of Double`() = runTest { + @Serializable data class TestData(val value1: List, val value2: List) + val arb: Arb = arbitrary { Arb.list(Arb.double()).run { TestData(bind(), bind()) } } - val decodedTestData = decodeFromStruct(struct) - - assertThat(decodedTestData) - .isEqualTo( - TestData( - listOf( - 1.0, - 0.0, - -0.0, - -1.0, - Double.MAX_VALUE, - Double.MIN_VALUE, - Double.NaN, - Double.POSITIVE_INFINITY, - Double.NEGATIVE_INFINITY - ) - ) - ) + checkAll(arb) { value -> + val struct = encodeToStruct(value) + val decodedTestData = decodeFromStruct(struct) + decodedTestData shouldBe value + } } @Test - fun `decodeFromStruct() can decode a ListValue of _nullable_ Double`() { - @Serializable data class TestData(val list: List) - val struct = - encodeToStruct( - TestData( - listOf( - 1.0, - 0.0, - -0.0, - -1.0, - Double.MAX_VALUE, - Double.MIN_VALUE, - Double.NaN, - Double.POSITIVE_INFINITY, - Double.NEGATIVE_INFINITY, - null - ) - ) - ) + fun `decodeFromStruct() can decode a ListValue of _nullable_ Double`() = runTest { + @Serializable data class TestData(val value1: List, val value2: List) + val arb: Arb = arbitrary { + Arb.list(Arb.double().orNull(0.33)).run { TestData(bind(), bind()) } + } - val decodedTestData = decodeFromStruct(struct) - - assertThat(decodedTestData) - .isEqualTo( - TestData( - listOf( - 1.0, - 0.0, - -0.0, - -1.0, - Double.MAX_VALUE, - Double.MIN_VALUE, - Double.NaN, - Double.POSITIVE_INFINITY, - Double.NEGATIVE_INFINITY, - null - ) - ) - ) + checkAll(arb) { value -> + val struct = encodeToStruct(value) + val decodedTestData = decodeFromStruct(struct) + decodedTestData shouldBe value + } } @Test - fun `decodeFromStruct() can decode a ListValue of Struct`() { + fun `decodeFromStruct() can decode a ListValue of Struct`() = runTest { @Serializable data class TestDataA(val s1: String, val s2: String?) - @Serializable data class TestData(val list: List) - val struct = encodeToStruct(TestData(listOf(TestDataA("aa", null), TestDataA("bb", null)))) - - val decodedTestData = decodeFromStruct(struct) + @Serializable data class TestDataB(val value1: List, val value2: List) + val arbA: Arb = arbitrary { + val value1 = Arb.string().bind() + val value2 = Arb.string().orNull(0.33).bind() + TestDataA(value1, value2) + } + val arb: Arb = arbitrary { Arb.list(arbA).run { TestDataB(bind(), bind()) } } - assertThat(decodedTestData) - .isEqualTo(TestData(listOf(TestDataA("aa", null), TestDataA("bb", null)))) + checkAll(iterations = 20, arb) { value -> + val struct = encodeToStruct(value) + val decodedTestData = decodeFromStruct(struct) + decodedTestData shouldBe value + } } @Test - fun `decodeFromStruct() can decode a ListValue of _nullable_ Struct`() { + fun `decodeFromStruct() can decode a ListValue of _nullable_ Struct`() = runTest { @Serializable data class TestDataA(val s1: String, val s2: String?) - @Serializable data class TestData(val list: List) - val struct = - encodeToStruct(TestData(listOf(null, TestDataA("aa", null), TestDataA("bb", null), null))) - - val decodedTestData = decodeFromStruct(struct) + @Serializable data class TestDataB(val value1: List, val value2: List) + val arbA: Arb = arbitrary { + val value1 = Arb.string().bind() + val value2 = Arb.string().orNull(0.33).bind() + TestDataA(value1, value2) + } + val arb: Arb = arbitrary { + Arb.list(arbA.orNull(0.33)).run { TestDataB(bind(), bind()) } + } - assertThat(decodedTestData) - .isEqualTo(TestData(listOf(null, TestDataA("aa", null), TestDataA("bb", null), null))) + checkAll(iterations = 20, arb) { value -> + val struct = encodeToStruct(value) + val decodedTestData = decodeFromStruct(struct) + decodedTestData shouldBe value + } } @Test - fun `decodeFromStruct() can decode a ListValue of ListValue`() { - @Serializable data class TestData(val list: List>) - val struct = encodeToStruct(TestData(listOf(listOf(1, 2, 3), listOf(4, 5, 6)))) - - val decodedTestData = decodeFromStruct(struct) + fun `decodeFromStruct() can decode a ListValue of ListValue`() = runTest { + @Serializable data class TestData(val value1: List>, val value2: List>) + val arb: Arb = arbitrary { + Arb.list(Arb.list(Arb.int())).run { TestData(bind(), bind()) } + } - assertThat(decodedTestData).isEqualTo(TestData(listOf(listOf(1, 2, 3), listOf(4, 5, 6)))) + checkAll(iterations = 20, arb) { value -> + val struct = encodeToStruct(value) + val decodedTestData = decodeFromStruct(struct) + decodedTestData shouldBe value + } } @Test - fun `decodeFromStruct() can decode a ListValue of _nullable_ ListValue`() { - @Serializable data class TestData(val list: List?>) - val struct = encodeToStruct(TestData(listOf(listOf(1, 2, 3), listOf(4, 5, 6), null))) - - val decodedTestData = decodeFromStruct(struct) + fun `decodeFromStruct() can decode a ListValue of _nullable_ ListValue`() = runTest { + @Serializable data class TestData(val value1: List>, val value2: List?>) + val arb: Arb = arbitrary { + val value1 = Arb.list(Arb.list(Arb.int().orNull(0.33))).bind() + val value2 = Arb.list(Arb.list(Arb.int()).orNull(0.33)).bind() + TestData(value1, value2) + } - assertThat(decodedTestData).isEqualTo(TestData(listOf(listOf(1, 2, 3), listOf(4, 5, 6), null))) + checkAll(iterations = 20, arb) { value -> + val struct = encodeToStruct(value) + val decodedTestData = decodeFromStruct(struct) + decodedTestData shouldBe value + } } @Test - fun `decodeFromStruct() can decode a Struct with Inline values`() { + fun `decodeFromStruct() can decode a Struct with Inline values`() = runTest { @Serializable data class TestData(val s: TestStringValueClass, val i: TestIntValueClass) - val struct = encodeToStruct(TestData(TestStringValueClass("TestString"), TestIntValueClass(42))) - - val decodedTestData = decodeFromStruct(struct) + val arb: Arb = arbitrary { + TestData(Arb.testStringValueClass().bind(), Arb.testIntValueClass().bind()) + } - assertThat(decodedTestData) - .isEqualTo(TestData(TestStringValueClass("TestString"), TestIntValueClass(42))) + checkAll(arb) { value -> + val struct = encodeToStruct(value) + val decodedTestData = decodeFromStruct(struct) + decodedTestData shouldBe value + } } @Test - fun `decodeFromStruct() can decode a Struct with _nullable_ Inline values`() { + fun `decodeFromStruct() can decode a Struct with _nullable_ Inline values`() = runTest { @Serializable data class TestData( - val s: TestStringValueClass?, - val snull: TestStringValueClass?, - val i: TestIntValueClass?, - val inull: TestIntValueClass? + val string: TestStringValueClass?, + val nullString: TestStringValueClass?, + val int: TestIntValueClass?, + val nullInt: TestIntValueClass? ) - val struct = - encodeToStruct( - TestData(TestStringValueClass("TestString"), null, TestIntValueClass(42), null) + val arb: Arb = arbitrary { + TestData( + Arb.testStringValueClass().bind(), + Arb.testStringValueClass().orNull(0.33).bind(), + Arb.testIntValueClass().bind(), + Arb.testIntValueClass().orNull(0.33).bind(), ) + } - val decodedTestData = decodeFromStruct(struct) - - assertThat(decodedTestData) - .isEqualTo(TestData(TestStringValueClass("TestString"), null, TestIntValueClass(42), null)) + checkAll(arb) { value -> + val struct = encodeToStruct(value) + val decodedTestData = decodeFromStruct(struct) + decodedTestData shouldBe value + } } @Test - fun `decodeFromStruct() can decode a ListValue with Inline values`() { + fun `decodeFromStruct() can decode a ListValue with Inline values`() = runTest { @Serializable data class TestData(val s: List, val i: List) - val struct = - encodeToStruct( - TestData( - listOf(TestStringValueClass("TestString1"), TestStringValueClass("TestString2")), - listOf(TestIntValueClass(42), TestIntValueClass(43)) - ) + val arb: Arb = arbitrary { + TestData( + Arb.list(Arb.testStringValueClass()).bind(), + Arb.list(Arb.testIntValueClass()).bind(), ) + } - val decodedTestData = decodeFromStruct(struct) - - assertThat(decodedTestData) - .isEqualTo( - TestData( - listOf(TestStringValueClass("TestString1"), TestStringValueClass("TestString2")), - listOf(TestIntValueClass(42), TestIntValueClass(43)) - ) - ) + checkAll(iterations = 20, arb) { value -> + val struct = encodeToStruct(value) + val decodedTestData = decodeFromStruct(struct) + decodedTestData shouldBe value + } } @Test - fun `decodeFromStruct() can decode a ListValue with _nullable_ Inline values`() { + fun `decodeFromStruct() can decode a ListValue with _nullable_ Inline values`() = runTest { @Serializable data class TestData(val s: List, val i: List) - val struct = - encodeToStruct( - TestData( - listOf(TestStringValueClass("TestString1"), null, TestStringValueClass("TestString2")), - listOf(TestIntValueClass(42), null, TestIntValueClass(43)) - ) + val arb: Arb = arbitrary { + TestData( + Arb.list(Arb.testStringValueClass().orNull(0.33)).bind(), + Arb.list(Arb.testIntValueClass().orNull(0.33)).bind(), ) + } - val decodedTestData = decodeFromStruct(struct) - - assertThat(decodedTestData) - .isEqualTo( - TestData( - listOf(TestStringValueClass("TestString1"), null, TestStringValueClass("TestString2")), - listOf(TestIntValueClass(42), null, TestIntValueClass(43)) - ) - ) + checkAll(iterations = 20, arb) { value -> + val struct = encodeToStruct(value) + val decodedTestData = decodeFromStruct(struct) + decodedTestData shouldBe value + } } @Test @@ -667,18 +620,18 @@ class ProtoStructDecoderUnitTest { ) } - private enum class TestEnum { - A, - B, - C, - D - } - @Serializable @JvmInline private value class TestStringValueClass(val a: String) @Serializable @JvmInline private value class TestIntValueClass(val a: Int) - @Serializable @JvmInline private value class TestByteValueClass(val a: Byte) + private companion object { + fun Arb.Companion.testStringValueClass(): Arb = arbitrary { + TestStringValueClass(Arb.string().bind()) + } + fun Arb.Companion.testIntValueClass(): Arb = arbitrary { + TestIntValueClass(Arb.int().bind()) + } + } // TODO: Add tests for decoding to objects with unsupported field types (e.g. Byte, Char) and // list elements of unsupported field types (e.g. Byte, Char). @@ -701,38 +654,15 @@ private inline fun assertDecodeFromStructThrowsIncorrectKindCase( struct: Struct = Struct.getDefaultInstance(), path: String? = null ) { - val exception = assertThrows(SerializationException::class.java) { decodeFromStruct(struct) } + val exception = shouldThrow { decodeFromStruct(struct) } // The error message is expected to look something like this: // "expected NUMBER_VALUE, but got STRUCT_VALUE" - assertThat(exception).hasMessageThat().ignoringCase().contains("expected $expectedKind") - assertThat(exception).hasMessageThat().ignoringCase().contains("got $actualKind") - assertThat(exception).hasMessageThat().ignoringCase().contains("($actualValue)") - if (path !== null) { - assertThat(exception).hasMessageThat().ignoringCase().contains("decoding \"$path\"") + assertSoftly { + exception.message shouldContainWithNonAbuttingTextIgnoringCase "expected $expectedKind" + exception.message shouldContainWithNonAbuttingTextIgnoringCase "got $actualKind" + exception.message shouldContainWithNonAbuttingTextIgnoringCase "($actualValue)" + if (path !== null) { + exception.message shouldContainWithNonAbuttingTextIgnoringCase "decoding \"$path\"" + } } } - -/** - * Asserts that `decodeFromStruct` throws [SerializationException], with a message that indicates - * that the type `T` being decoded is not supported. - * - * @param expectedTypeInMessage The type that the exception's message should indicate is not - * supported; if not specified, use `T`. Note that the only case where this argument's value should - * be anything _other_ than `T` is for _value classes_ that are mapped to a primitive type. - */ -private inline fun assertThrowsNotSupported( - expectedTypeInMessage: KClass<*> = T::class -) { - val exception = - assertThrows(SerializationException::class.java) { - decodeFromStruct(Struct.getDefaultInstance()) - } - assertThat(exception) - .hasMessageThat() - .containsMatch( - Pattern.compile( - "decoding.*${Pattern.quote(expectedTypeInMessage.qualifiedName!!)}.*not supported", - Pattern.CASE_INSENSITIVE - ) - ) -} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructEncoderUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructEncoderUnitTest.kt index c73a31aa732..dea606aa7d7 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructEncoderUnitTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructEncoderUnitTest.kt @@ -13,179 +13,146 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -@file:OptIn(ExperimentalSerializationApi::class) - package com.google.firebase.dataconnect -import com.google.common.truth.Truth.assertThat -import com.google.common.truth.extensions.proto.LiteProtoTruth.assertThat +import com.google.firebase.dataconnect.testutil.shouldBe +import com.google.firebase.dataconnect.testutil.shouldBeDefaultInstance +import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingText import com.google.firebase.dataconnect.util.ProtoUtil.buildStructProto import com.google.firebase.dataconnect.util.ProtoUtil.encodeToStruct +import com.google.firebase.dataconnect.util.ProtoUtil.toListValueProto import com.google.protobuf.Struct -import java.util.concurrent.atomic.AtomicLong -import kotlinx.serialization.ExperimentalSerializationApi +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.property.Arb +import io.kotest.property.arbitrary.arbitrary +import io.kotest.property.arbitrary.boolean +import io.kotest.property.arbitrary.double +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.list +import io.kotest.property.arbitrary.next +import io.kotest.property.arbitrary.orNull +import io.kotest.property.arbitrary.string +import io.kotest.property.checkAll +import kotlinx.coroutines.test.runTest import kotlinx.serialization.Serializable -import kotlinx.serialization.SerializationStrategy -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.CompositeEncoder -import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.modules.EmptySerializersModule -import kotlinx.serialization.serializer -import org.junit.Assert.assertThrows import org.junit.Test class ProtoStructEncoderUnitTest { @Test fun `encodeToStruct() should throw if a NUMBER_VALUE is produced`() { - val exception = assertThrows(IllegalArgumentException::class.java) { encodeToStruct(42) } - assertThat(exception).hasMessageThat().contains("NUMBER_VALUE") + val intValue: Int = Arb.int().next() + val exception = shouldThrow { encodeToStruct(intValue) } + exception.message shouldContainWithNonAbuttingText "NUMBER_VALUE" } @Test fun `encodeToStruct() should throw if a BOOL_VALUE is produced`() { - val exception = assertThrows(IllegalArgumentException::class.java) { encodeToStruct(true) } - assertThat(exception).hasMessageThat().contains("BOOL_VALUE") + val booleanValue: Boolean = Arb.boolean().next() + val exception = shouldThrow { encodeToStruct(booleanValue) } + exception.message shouldContainWithNonAbuttingText "BOOL_VALUE" } @Test fun `encodeToStruct() should throw if a STRING_VALUE is produced`() { - val exception = - assertThrows(IllegalArgumentException::class.java) { - encodeToStruct("arbitrary string value") - } - assertThat(exception).hasMessageThat().contains("STRING_VALUE") + val stringValue: String = Arb.string().next() + val exception = shouldThrow { encodeToStruct(stringValue) } + exception.message shouldContainWithNonAbuttingText "STRING_VALUE" } @Test fun `encodeToStruct() should throw if a LIST_VALUE is produced`() { - val exception = - assertThrows(IllegalArgumentException::class.java) { - encodeToStruct(listOf("element1", "element2")) - } - assertThat(exception).hasMessageThat().contains("LIST_VALUE") + val listValue: List = Arb.list(Arb.string(5..10), 1..10).next() + val exception = shouldThrow { encodeToStruct(listValue) } + exception.message shouldContainWithNonAbuttingText "LIST_VALUE" } @Test fun `encodeToStruct() should return an empty struct if an empty map is given`() { val encodedStruct = encodeToStruct(emptyMap()) - assertThat(encodedStruct).isEqualToDefaultInstance() + encodedStruct.shouldBeDefaultInstance() } @Test fun `encodeToStruct() should encode Unit as an empty struct`() { val encodedStruct = encodeToStruct(Unit) - assertThat(encodedStruct).isEqualToDefaultInstance() + encodedStruct.shouldBeDefaultInstance() } @Test - fun `encodeToStruct() should encode an class with all primitive types`() { + fun `encodeToStruct() should encode an class with all primitive types`() = runTest { @Serializable data class TestData( val iv: Int, val dv: Double, - val bvt: Boolean, - val bvf: Boolean, + val bv: Boolean, val sv: String, - val nsvn: String?, - val nsvnn: String? + val svn: String?, ) - val encodedStruct = - encodeToStruct( - TestData( - iv = 42, - dv = 1234.5, - bvt = true, - bvf = false, - sv = "blah blah", - nsvn = null, - nsvnn = "I'm not null" - ) - ) - - assertThat(encodedStruct) - .isEqualTo( - buildStructProto { - put("iv", 42.0) - put("dv", 1234.5) - put("bvt", true) - put("bvf", false) - put("sv", "blah blah") - putNull("nsvn") - put("nsvnn", "I'm not null") + val arb: Arb> = arbitrary { + val iv = Arb.int().bind() + val dv = Arb.double().bind() + val bv = Arb.boolean().bind() + val sv = Arb.string().bind() + val svn = Arb.string().orNull(0.33).bind() + + val testData = TestData(iv = iv, dv = dv, bv = bv, sv = sv, svn = svn) + + val struct = buildStructProto { + put("iv", iv) + put("dv", dv) + put("bv", bv) + put("sv", sv) + if (svn === null) { + putNull("svn") + } else { + put("svn", svn) } - ) + } + + Pair(testData, struct) + } + + checkAll(arb) { (testData, expectedStruct) -> + val encodedStruct = encodeToStruct(testData) + encodedStruct shouldBe expectedStruct + } } @Test - fun `encodeToStruct() should encode lists with all primitive types`() { + fun `encodeToStruct() should encode lists with all primitive types`() = runTest { @Serializable data class TestData( val iv: List, val dv: List, val bv: List, val sv: List, - val nsv: List + val svn: List ) - val encodedStruct = - encodeToStruct( - TestData( - iv = listOf(42, 43), - dv = listOf(1234.5, 5678.9), - bv = listOf(true, false, false, true), - sv = listOf("abcde", "fghij"), - nsv = listOf("klmno", null, "pqrst", null) - ) - ) - - assertThat(encodedStruct) - .isEqualTo( - buildStructProto { - putList("iv") { - add(42.0) - add(43.0) - } - putList("dv") { - add(1234.5) - add(5678.9) - } - putList("bv") { - add(true) - add(false) - add(false) - add(true) - } - putList("sv") { - add("abcde") - add("fghij") - } - putList("nsv") { - add("klmno") - addNull() - add("pqrst") - addNull() - } - } - ) - } + val arb: Arb> = arbitrary { + val iv = Arb.list(Arb.int()).bind() + val dv = Arb.list(Arb.double()).bind() + val bv = Arb.list(Arb.boolean()).bind() + val sv = Arb.list(Arb.string()).bind() + val svn = Arb.list(Arb.string().orNull(0.33)).bind() + + val testData = TestData(iv = iv, dv = dv, bv = bv, sv = sv, svn = svn) + + val struct = buildStructProto { + put("iv", iv.map { it.toDouble() }.toListValueProto()) + put("dv", dv.toListValueProto()) + put("bv", bv.toListValueProto()) + put("sv", sv.toListValueProto()) + put("svn", svn.toListValueProto()) + } - @Test - fun `encodeToStruct() should support nested composite types`() { - @Serializable data class TestData3(val s: String) - @Serializable data class TestData2(val data3: TestData3, val data3N: TestData3?) - @Serializable data class TestData1(val data2: TestData2) - val encodedStruct = encodeToStruct(TestData1(TestData2(TestData3("zzzz"), null))) + Pair(testData, struct) + } - assertThat(encodedStruct) - .isEqualTo( - buildStructProto { - putStruct("data2") { - putNull("data3N") - putStruct("data3") { put("s", "zzzz") } - } - } - ) + checkAll(arb) { (testData, expectedStruct) -> + val encodedStruct = encodeToStruct(testData) + encodedStruct shouldBe expectedStruct + } } @Test @@ -194,7 +161,7 @@ class ProtoStructEncoderUnitTest { val encodedStruct = encodeToStruct(TestData(OptionalVariable.Undefined)) - assertThat(encodedStruct).isEqualTo(Struct.getDefaultInstance()) + encodedStruct shouldBe Struct.getDefaultInstance() } @Test @@ -203,196 +170,49 @@ class ProtoStructEncoderUnitTest { val encodedStruct = encodeToStruct(TestData(OptionalVariable.Undefined)) - assertThat(encodedStruct).isEqualTo(Struct.getDefaultInstance()) + encodedStruct shouldBe Struct.getDefaultInstance() } @Test - fun `encodeToStruct() should support OptionalVariable Value when T is not nullable`() { - @Serializable data class TestData(val s: OptionalVariable) - - val encodedStruct = encodeToStruct(TestData(OptionalVariable.Value("Hello"))) - - assertThat(encodedStruct).isEqualTo(buildStructProto { put("s", "Hello") }) - } - - @Test - fun `encodeToStruct() should support OptionalVariable Value when T is nullable but not null`() { - @Serializable data class TestData(val s: OptionalVariable) - - val encodedStruct = encodeToStruct(TestData(OptionalVariable.Value("World"))) + fun `encodeToStruct() should support OptionalVariable Value when T is not nullable`() = runTest { + @Serializable + data class TestData(val s: OptionalVariable, val i: OptionalVariable) + val arb = arbitrary { + val s = OptionalVariable.Value(Arb.string().bind()) + val i = OptionalVariable.Value(Arb.int().bind()) + TestData(s, i) + } - assertThat(encodedStruct).isEqualTo(buildStructProto { put("s", "World") }) + checkAll(arb) { testData -> + val encodedStruct = encodeToStruct(testData) + val expected = buildStructProto { + put("s", testData.s.valueOrThrow()) + put("i", testData.i.valueOrThrow()) + } + encodedStruct shouldBe expected + } } @Test - fun `encodeToStruct() should support OptionalVariable Value when T is nullable and null`() { - @Serializable data class TestData(val s: OptionalVariable) - - val encodedStruct = encodeToStruct(TestData(OptionalVariable.Value(null))) - - assertThat(encodedStruct).isEqualTo(buildStructProto { putNull("s") }) - } -} - -/** - * An encoder that can be useful during testing to simply print the method invocations in order to - * discover how an encoder should be implemented. - */ -@Suppress("unused") -private class LoggingEncoder( - private val idBySerialDescriptor: MutableMap = mutableMapOf() -) : Encoder, CompositeEncoder { - val id = nextEncoderId.incrementAndGet() - - override val serializersModule = EmptySerializersModule() - - private fun log(message: String) { - println("zzyzx LoggingEncoder[$id] $message") - } - - private fun idFor(descriptor: SerialDescriptor) = - idBySerialDescriptor[descriptor] - ?: nextSerialDescriptorId.incrementAndGet().also { idBySerialDescriptor[descriptor] = it } - - override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { - log( - "beginStructure() descriptorId=${idFor(descriptor)} kind=${descriptor.kind} " + - "elementsCount=${descriptor.elementsCount}" - ) - return LoggingEncoder(idBySerialDescriptor) - } - - override fun endStructure(descriptor: SerialDescriptor) { - log("endStructure() descriptorId=${idFor(descriptor)} kind=${descriptor.kind}") - } - - override fun encodeBoolean(value: Boolean) { - log("encodeBoolean($value)") - } - - override fun encodeByte(value: Byte) { - log("encodeByte($value)") - } - - override fun encodeChar(value: Char) { - log("encodeChar($value)") - } - - override fun encodeDouble(value: Double) { - log("encodeDouble($value)") - } - - override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) { - log("encodeEnum($index)") - } - - override fun encodeFloat(value: Float) { - log("encodeFloat($value)") - } - - override fun encodeInline(descriptor: SerialDescriptor): Encoder { - log("encodeInline() kind=${descriptor.kind} serialName=${descriptor.serialName}") - return LoggingEncoder(idBySerialDescriptor) - } - - override fun encodeInt(value: Int) { - log("encodeInt($value)") - } - - override fun encodeLong(value: Long) { - log("encodeLong($value)") - } - - @ExperimentalSerializationApi - override fun encodeNull() { - log("encodeNull()") - } - - override fun encodeShort(value: Short) { - log("encodeShort($value)") - } - - override fun encodeString(value: String) { - log("encodeString($value)") - } - - override fun encodeBooleanElement(descriptor: SerialDescriptor, index: Int, value: Boolean) { - log("encodeBooleanElement($value) index=$index elementName=${descriptor.getElementName(index)}") - } - - override fun encodeByteElement(descriptor: SerialDescriptor, index: Int, value: Byte) { - log("encodeByteElement($value) index=$index elementName=${descriptor.getElementName(index)}") - } - - override fun encodeCharElement(descriptor: SerialDescriptor, index: Int, value: Char) { - log("encodeCharElement($value) index=$index elementName=${descriptor.getElementName(index)}") - } - - override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) { - log("encodeDoubleElement($value) index=$index elementName=${descriptor.getElementName(index)}") - } - - override fun encodeFloatElement(descriptor: SerialDescriptor, index: Int, value: Float) { - log("encodeFloatElement($value) index=$index elementName=${descriptor.getElementName(index)}") - } - - override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder { - log("encodeInlineElement() index=$index elementName=${descriptor.getElementName(index)}") - return LoggingEncoder(idBySerialDescriptor) - } - - override fun encodeIntElement(descriptor: SerialDescriptor, index: Int, value: Int) { - log("encodeIntElement($value) index=$index elementName=${descriptor.getElementName(index)}") - } - - override fun encodeLongElement(descriptor: SerialDescriptor, index: Int, value: Long) { - log("encodeLongElement($value) index=$index elementName=${descriptor.getElementName(index)}") - } - - override fun encodeShortElement(descriptor: SerialDescriptor, index: Int, value: Short) { - log("encodeShortElement($value) index=$index elementName=${descriptor.getElementName(index)}") - } - - override fun encodeStringElement(descriptor: SerialDescriptor, index: Int, value: String) { - log("encodeStringElement($value) index=$index elementName=${descriptor.getElementName(index)}") - } - - @ExperimentalSerializationApi - override fun encodeNullableSerializableElement( - descriptor: SerialDescriptor, - index: Int, - serializer: SerializationStrategy, - value: T? - ) { - log( - "encodeNullableSerializableElement($value) index=$index elementName=${descriptor.getElementName(index)}" + fun `encodeToStruct() should support OptionalVariable Value when T is nullable`() = runTest { + @Serializable + data class TestData( + val s: OptionalVariable, + val i: OptionalVariable, ) - if (value != null) { - encodeSerializableValue(serializer, value) + val arb = arbitrary { + val s = OptionalVariable.Value(Arb.string().orNull(0.33).bind()) + val i = OptionalVariable.Value(Arb.int().orNull(0.33).bind()) + TestData(s, i) } - } - - override fun encodeSerializableElement( - descriptor: SerialDescriptor, - index: Int, - serializer: SerializationStrategy, - value: T - ) { - log( - "encodeSerializableElement($value) index=$index elementName=${descriptor.getElementName(index)}" - ) - encodeSerializableValue(serializer, value) - } - - companion object { - fun encode(serializer: SerializationStrategy, value: T) { - LoggingEncoder().encodeSerializableValue(serializer, value) + checkAll(arb) { testData -> + val encodedStruct = encodeToStruct(testData) + val expected = buildStructProto { + put("s", testData.s.valueOrThrow()) + put("i", testData.i.valueOrThrow()) + } + encodedStruct shouldBe expected } - - inline fun encode(value: T) = encode(serializer(), value) - - private val nextEncoderId = AtomicLong(0) - private val nextSerialDescriptorId = AtomicLong(998800000L) } } diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/TimestampSerializerUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/TimestampSerializerUnitTest.kt index 8939279bbb0..d68b2e73a5c 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/TimestampSerializerUnitTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/TimestampSerializerUnitTest.kt @@ -16,12 +16,14 @@ package com.google.firebase.dataconnect.serializers -import com.google.common.truth.Truth.assertThat import com.google.firebase.Timestamp -import com.google.firebase.dataconnect.testutil.assertThrows import com.google.firebase.dataconnect.util.ProtoUtil.buildStructProto import com.google.firebase.dataconnect.util.ProtoUtil.decodeFromStruct import com.google.firebase.dataconnect.util.ProtoUtil.encodeToStruct +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.assertions.withClue +import io.kotest.matchers.shouldBe import kotlinx.serialization.Serializable import org.junit.Test @@ -54,150 +56,143 @@ class TimestampSerializerUnitTest { @Test fun `decoding should succeed when 'time-secfrac' is omitted`() { - assertThat(decodeTimestamp("2006-01-02T15:04:05Z")).isEqualTo(Timestamp(1136214245, 0)) + decodeTimestamp("2006-01-02T15:04:05Z") shouldBe Timestamp(1136214245, 0) } @Test fun `decoding should succeed when 'time-secfrac' has millisecond precision`() { - assertThat(decodeTimestamp("2006-01-02T15:04:05.123Z")) - .isEqualTo(Timestamp(1136214245, 123_000_000)) + decodeTimestamp("2006-01-02T15:04:05.123Z") shouldBe Timestamp(1136214245, 123_000_000) } @Test fun `decoding should succeed when 'time-secfrac' has microsecond precision`() { - assertThat(decodeTimestamp("2006-01-02T15:04:05.123456Z")) - .isEqualTo(Timestamp(1136214245, 123_456_000)) + decodeTimestamp("2006-01-02T15:04:05.123456Z") shouldBe Timestamp(1136214245, 123_456_000) } @Test fun `decoding should succeed when 'time-secfrac' has nanosecond precision`() { - assertThat(decodeTimestamp("2006-01-02T15:04:05.123456789Z")) - .isEqualTo(Timestamp(1136214245, 123_456_789)) + decodeTimestamp("2006-01-02T15:04:05.123456789Z") shouldBe Timestamp(1136214245, 123_456_789) } @Test fun `decoding should succeed when time offset is 0`() { - assertThat(decodeTimestamp("2006-01-02T15:04:05-00:00")) - .isEqualTo(decodeTimestamp("2006-01-02T15:04:05Z")) - - assertThat(decodeTimestamp("2006-01-02T15:04:05+00:00")) - .isEqualTo(decodeTimestamp("2006-01-02T15:04:05Z")) + assertSoftly { + decodeTimestamp("2006-01-02T15:04:05-00:00") shouldBe decodeTimestamp("2006-01-02T15:04:05Z") + decodeTimestamp("2006-01-02T15:04:05+00:00") shouldBe decodeTimestamp("2006-01-02T15:04:05Z") + } } @Test fun `decoding should succeed when time offset is positive`() { - assertThat(decodeTimestamp("2006-01-02T15:04:05+11:01")) - .isEqualTo(decodeTimestamp("2006-01-02T04:03:05Z")) + decodeTimestamp("2006-01-02T15:04:05+11:01") shouldBe decodeTimestamp("2006-01-02T04:03:05Z") } @Test fun `decoding should succeed when time offset is negative`() { - assertThat(decodeTimestamp("2006-01-02T15:04:05-05:10")) - .isEqualTo(decodeTimestamp("2006-01-02T20:14:05Z")) + decodeTimestamp("2006-01-02T15:04:05-05:10") shouldBe decodeTimestamp("2006-01-02T20:14:05Z") } @Test fun `decoding should succeed when there are both time-secfrac and - time offset`() { - assertThat(decodeTimestamp("2023-05-21T11:04:05.462-11:07")) - .isEqualTo(decodeTimestamp("2023-05-21T22:11:05.462Z")) + assertSoftly { + decodeTimestamp("2023-05-21T11:04:05.462-11:07") shouldBe + decodeTimestamp("2023-05-21T22:11:05.462Z") - assertThat(decodeTimestamp("2053-11-02T15:04:05.743393-05:10")) - .isEqualTo(decodeTimestamp("2053-11-02T20:14:05.743393Z")) + decodeTimestamp("2053-11-02T15:04:05.743393-05:10") shouldBe + decodeTimestamp("2053-11-02T20:14:05.743393Z") - assertThat(decodeTimestamp("1538-03-05T15:04:05.653498752-03:01")) - .isEqualTo(decodeTimestamp("1538-03-05T18:05:05.653498752Z")) + decodeTimestamp("1538-03-05T15:04:05.653498752-03:01") shouldBe + decodeTimestamp("1538-03-05T18:05:05.653498752Z") + } } @Test fun `decoding should succeed when there are both time-secfrac and + time offset`() { - assertThat(decodeTimestamp("2023-05-21T11:04:05.662+11:01")) - .isEqualTo(decodeTimestamp("2023-05-21T00:03:05.662Z")) + assertSoftly { + decodeTimestamp("2023-05-21T11:04:05.662+11:01") shouldBe + decodeTimestamp("2023-05-21T00:03:05.662Z") - assertThat(decodeTimestamp("2144-01-02T15:04:05.753493+01:00")) - .isEqualTo(decodeTimestamp("2144-01-02T14:04:05.753493Z")) + decodeTimestamp("2144-01-02T15:04:05.753493+01:00") shouldBe + decodeTimestamp("2144-01-02T14:04:05.753493Z") - assertThat(decodeTimestamp("1358-03-05T15:04:05.527094582+11:03")) - .isEqualTo(decodeTimestamp("1358-03-05T04:01:05.527094582Z")) + decodeTimestamp("1358-03-05T15:04:05.527094582+11:03") shouldBe + decodeTimestamp("1358-03-05T04:01:05.527094582Z") + } } @Test fun `decoding should be case-insensitive`() { // According to https://www.rfc-editor.org/rfc/rfc3339#section-5.6 the "t" and "z" are // case-insensitive. - assertThat(decodeTimestamp("2006-01-02t15:04:05.123456789z")) - .isEqualTo(decodeTimestamp("2006-01-02T15:04:05.123456789Z")) + decodeTimestamp("2006-01-02t15:04:05.123456789z") shouldBe + decodeTimestamp("2006-01-02T15:04:05.123456789Z") } @Test fun `decoding should parse the minimum value officially supported by Data Connect`() { - assertThat(decodeTimestamp("1583-01-01T00:00:00.000000Z")).isEqualTo(Timestamp(-12212553600, 0)) + decodeTimestamp("1583-01-01T00:00:00.000000Z") shouldBe Timestamp(-12212553600, 0) } @Test fun `decoding should parse the maximum value officially supported by Data Connect`() { - assertThat(decodeTimestamp("9999-12-31T23:59:59.999999999Z")) - .isEqualTo(Timestamp(253402300799, 999999999)) + decodeTimestamp("9999-12-31T23:59:59.999999999Z") shouldBe Timestamp(253402300799, 999999999) } @Test fun `decoding should fail for an empty string`() { - assertThrows(IllegalArgumentException::class) { decodeTimestamp("") } + shouldThrow { decodeTimestamp("") } } @Test fun `decoding should fail if 'time-offset' is omitted`() { - assertThrows(IllegalArgumentException::class) { - decodeTimestamp("2006-01-02T15:04:05.123456789") - } + shouldThrow { decodeTimestamp("2006-01-02T15:04:05.123456789") } } @Test fun `decoding should fail if 'time-offset' when 'time-secfrac' and time offset are both omitted`() { - assertThrows(IllegalArgumentException::class) { decodeTimestamp("2006-01-02T15:04:05") } + shouldThrow { decodeTimestamp("2006-01-02T15:04:05") } } @Test fun `decoding should fail if the date portion cannot be parsed`() { - assertThrows(IllegalArgumentException::class) { - decodeTimestamp("200X-01-02T15:04:05.123456789Z") - } + shouldThrow { decodeTimestamp("200X-01-02T15:04:05.123456789Z") } } @Test fun `decoding should fail if some character other than period delimits the 'time-secfrac'`() { - assertThrows(IllegalArgumentException::class) { - decodeTimestamp("2006-01-02T15:04:05 123456789Z") - } + shouldThrow { decodeTimestamp("2006-01-02T15:04:05 123456789Z") } } @Test fun `decoding should fail if 'time-secfrac' contains an invalid character`() { - assertThrows(IllegalArgumentException::class) { - decodeTimestamp("2006-01-02T15:04:05.123456X89Z") - } + shouldThrow { decodeTimestamp("2006-01-02T15:04:05.123456X89Z") } } @Test fun `decoding should fail if time offset has no + or - sign`() { - assertThrows(IllegalArgumentException::class) { decodeTimestamp("1985-04-12T23:20:5007:00") } + shouldThrow { decodeTimestamp("1985-04-12T23:20:5007:00") } } @Test fun `decoding should fail if time string has mix format`() { - assertThrows(IllegalArgumentException::class) { + shouldThrow { decodeTimestamp("2006-01-02T15:04:05-07:00.123456X89Z") } } @Test fun `decoding should fail if time offset is not in the correct format`() { - assertThrows(IllegalArgumentException::class) { decodeTimestamp("1985-04-12T23:20:50+7:00") } + shouldThrow { decodeTimestamp("1985-04-12T23:20:50+7:00") } } @Test fun `decoding should throw an exception if the timestamp is invalid`() { - invalidTimestampStrs.forEach { - assertThrows(IllegalArgumentException::class) { decodeTimestamp(it) } + assertSoftly { + for (invalidTimestampStr in invalidTimestampStrs) { + withClue(invalidTimestampStr) { + shouldThrow { decodeTimestamp(invalidTimestampStr) } + } + } } } @@ -211,7 +206,7 @@ class TimestampSerializerUnitTest { fun verifyEncodeDecodeRoundTrip(timestamp: Timestamp) { val encoded = encodeToStruct(TimestampWrapper(timestamp)) val decoded = decodeFromStruct(encoded) - assertThat(decoded.timestamp).isEqualTo(timestamp) + decoded.timestamp shouldBe timestamp } fun decodeTimestamp(text: String): Timestamp { diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/ProtoTestUtils.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/ProtoTestUtils.kt new file mode 100644 index 00000000000..fc5ee4ceb39 --- /dev/null +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/ProtoTestUtils.kt @@ -0,0 +1,172 @@ +/* + * 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.testutil + +import com.google.protobuf.MessageLite +import com.google.protobuf.Struct +import com.google.protobuf.Value +import io.kotest.assertions.print.print +import io.kotest.matchers.Matcher +import io.kotest.matchers.MatcherResult +import io.kotest.matchers.neverNullMatcher +import io.kotest.matchers.should +import java.util.regex.Pattern + +/** Asserts that a proto message is equal to the given proto message. */ +infix fun MessageLite?.shouldBe(other: MessageLite?): MessageLite? { + this should beEqualTo(other) + return this +} + +/** Asserts that a proto Struct is equal to the given proto Struct. */ +infix fun Struct?.shouldBe(other: Struct?): Struct? { + this should beEqualTo(other) + return this +} + +/** Asserts that a proto Struct is equal to the given proto Struct. */ +infix fun Value?.shouldBe(other: Value?): Value? { + this should beEqualTo(other) + return this +} + +/** Asserts that a proto message is equal to the default instance of its type. */ +fun MessageLite?.shouldBeDefaultInstance(): MessageLite? { + this should beEqualToDefaultInstance() + return this +} + +/** + * Creates and returns a [Matcher] that can be used with kotest assertions for verifying that a + * proto message is equal to the default instance of that type. + */ +fun beEqualToDefaultInstance(): Matcher = neverNullMatcher { value -> + val valueStr = value.toTrimmedStringForTesting() + val defaultInstance = value.defaultInstanceForType + val defaultStr = defaultInstance.toTrimmedStringForTesting() + MatcherResult( + valueStr == defaultStr, + { + "${value::class.qualifiedName} ${value.print().value} should be equal to " + + "the default instance: ${defaultInstance.print().value}" + }, + { + "${value::class.qualifiedName} ${value.print().value} should not be equal to : " + + "the default instance: ${defaultInstance.print().value}" + } + ) +} + +/** + * Creates and returns a [Matcher] that can be used with kotest assertions for verifying that a + * proto message is equal to the given proto message. + */ +fun beEqualTo(other: MessageLite?): Matcher = neverNullMatcher { value -> + if (other === null) { + MatcherResult( + false, + { "${value::class.qualifiedName} ${value.print().value} should be null" }, + { TODO("should not get here (error code: r2kap3te33)") } + ) + } else { + val valueClass = value::class + val otherClass = other::class + val valueStr = value.toTrimmedStringForTesting() + val otherStr = other.toTrimmedStringForTesting() + MatcherResult( + valueClass == otherClass && valueStr == otherStr, + { + "${valueClass.qualifiedName} ${value.print().value} should be equal to " + + "${otherClass.qualifiedName}: ${other.print().value}" + }, + { + "${valueClass.qualifiedName} ${value.print().value} should not be equal to " + + "${otherClass.qualifiedName}: ${other.print().value}" + } + ) + } +} + +/** + * Creates and returns a [Matcher] that can be used with kotest assertions for verifying that a + * proto Struct is equal to the given proto Struct. + */ +fun beEqualTo(other: Struct?): Matcher = neverNullMatcher { value -> + if (other === null) { + MatcherResult( + false, + { "${value.print().value} should be null" }, + { TODO("should not get here (error code: r2kap3te33)") } + ) + } else { + MatcherResult( + value == other, + { "${value.print().value} should be equal to ${other.print().value}" }, + { "${value.print().value} should not be equal to ${other.print().value}" } + ) + } +} + +/** + * Creates and returns a [Matcher] that can be used with kotest assertions for verifying that a + * proto Value is equal to the given proto Value. + */ +fun beEqualTo(other: Value?): Matcher = neverNullMatcher { value -> + if (other === null) { + MatcherResult( + false, + { "${value.print().value} should be null" }, + { TODO("should not get here (error code: r2kap3te33)") } + ) + } else { + MatcherResult( + value == other, + { "${value.print().value} should be equal to ${other.print().value}" }, + { "${value.print().value} should not be equal to ${other.print().value}" } + ) + } +} + +// It is wrong to compare protos using their string representations. The MessageLite runtime +// deliberately prefixes debug strings with their Object.toString() to discourage string +// comparison. However, this reads poorly in tests, and makes it harder to identify differences +// from the strings alone. So, we manually strip this prefix. +// This code was adapted from +// https://github.com/google/truth/blob/f1f4d1450d/extensions/liteproto/src/main/java/com/google/common/truth/extensions/proto/LiteProtoSubject.java#L73-L93 +private fun MessageLite?.toTrimmedStringForTesting(): String { + if (this === null) { + return "" + } + + val subjectString = + toString().let { subjectString -> + subjectString.trim().let { trimmedSubjectString -> + if (!trimmedSubjectString.startsWith("# ")) { + subjectString + } else { + val objectToString = "# ${this::class.qualifiedName}@${Integer.toHexString(hashCode())}" + if (trimmedSubjectString.startsWith(objectToString)) { + trimmedSubjectString.replaceFirst(Pattern.quote(objectToString), "").trim() + } else { + subjectString + } + } + } + } + + return subjectString.ifEmpty { "[empty proto]" } +} diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/TestUtils.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/TestUtils.kt index a30090c37d1..2d74680ed46 100644 --- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/TestUtils.kt +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/TestUtils.kt @@ -50,19 +50,26 @@ import org.junit.Assert * string contains the given string with non-abutting text. See [shouldContainWithNonAbuttingText] * for full details. */ -fun containWithNonAbuttingText(s: String): Matcher = neverNullMatcher { value -> - val fullPattern = "(^|\\W)${Pattern.quote(s)}($|\\W)" - val expr = Pattern.compile(fullPattern) - MatcherResult( - expr.matcher(value).find(), - { - "${value.print().value} should contain the substring ${s.print().value} with non-abutting text" - }, - { - "${value.print().value} should not contain the substring ${s.print().value} with non-abutting text" - } - ) -} +fun containWithNonAbuttingText(s: String, ignoreCase: Boolean = false): Matcher = + neverNullMatcher { value -> + val fullPattern = "(^|\\W)${Pattern.quote(s)}($|\\W)" + val expr = + if (ignoreCase) { + Pattern.compile(fullPattern) + } else { + Pattern.compile(fullPattern, Pattern.CASE_INSENSITIVE) + } + + MatcherResult( + expr.matcher(value).find(), + { + "${value.print().value} should contain the substring ${s.print().value} with non-abutting text" + }, + { + "${value.print().value} should not contain the substring ${s.print().value} with non-abutting text" + } + ) + } /** * Asserts that a string contains another string, verifying that the character immediately preceding @@ -72,7 +79,13 @@ fun containWithNonAbuttingText(s: String): Matcher = neverNullMatcher { * messages and forgetting to leave a space between words. */ infix fun String?.shouldContainWithNonAbuttingText(s: String): String? { - this should containWithNonAbuttingText(s) + this should containWithNonAbuttingText(s, ignoreCase = false) + return this +} + +/** Same as [shouldContainWithNonAbuttingText] but ignoring case. */ +infix fun String?.shouldContainWithNonAbuttingTextIgnoringCase(s: String): String? { + this should containWithNonAbuttingText(s, ignoreCase = false) return this } diff --git a/firebase-dataconnect/testutil/testutil.gradle.kts b/firebase-dataconnect/testutil/testutil.gradle.kts index 64d2595bcc8..7912b82f6dd 100644 --- a/firebase-dataconnect/testutil/testutil.gradle.kts +++ b/firebase-dataconnect/testutil/testutil.gradle.kts @@ -60,6 +60,7 @@ dependencies { implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.serialization.core) implementation(libs.mockk) + implementation(libs.protobuf.java.lite) implementation(libs.robolectric) implementation(libs.truth) }