Skip to content

Commit df21d8b

Browse files
authored
dataconnect: proto Arb improvements for testing (#7589)
1 parent f9495bc commit df21d8b

File tree

8 files changed

+1072
-93
lines changed

8 files changed

+1072
-93
lines changed

firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcClientUnitTest.kt

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import io.kotest.property.arbitrary.alphanumeric
6868
import io.kotest.property.arbitrary.egyptianHieroglyphs
6969
import io.kotest.property.arbitrary.enum
7070
import io.kotest.property.arbitrary.int
71+
import io.kotest.property.arbitrary.map
7172
import io.kotest.property.arbitrary.merge
7273
import io.kotest.property.arbitrary.next
7374
import io.kotest.property.arbitrary.string
@@ -104,7 +105,7 @@ class DataConnectGrpcClientUnitTest {
104105
private val connectorConfig = Arb.dataConnect.connectorConfig().next(rs)
105106
private val requestId = Arb.dataConnect.requestId().next(rs)
106107
private val operationName = Arb.dataConnect.operationName().next(rs)
107-
private val variables = Arb.proto.struct().next(rs)
108+
private val variables = Arb.proto.struct().next(rs).struct
108109
private val callerSdkType = Arb.enum<FirebaseDataConnect.CallerSdkType>().next(rs)
109110

110111
private val mockDataConnectAuth: DataConnectAuth =
@@ -193,7 +194,7 @@ class DataConnectGrpcClientUnitTest {
193194

194195
@Test
195196
fun `executeQuery() should return data and errors`() = runTest {
196-
val responseData = Arb.proto.struct().next(rs)
197+
val responseData = Arb.proto.struct().next(rs).struct
197198
val responseErrors = List(3) { GraphqlErrorInfo.random(RandomSource.default()) }
198199
coEvery { mockDataConnectGrpcRPCs.executeQuery(any(), any(), any()) } returns
199200
ExecuteQueryResponse.newBuilder()
@@ -210,7 +211,7 @@ class DataConnectGrpcClientUnitTest {
210211

211212
@Test
212213
fun `executeMutation() should return data and errors`() = runTest {
213-
val responseData = Arb.proto.struct().next(rs)
214+
val responseData = Arb.proto.struct().next(rs).struct
214215
val responseErrors = List(3) { GraphqlErrorInfo.random(RandomSource.default()) }
215216
coEvery { mockDataConnectGrpcRPCs.executeMutation(any(), any(), any()) } returns
216217
ExecuteMutationResponse.newBuilder()
@@ -253,7 +254,7 @@ class DataConnectGrpcClientUnitTest {
253254

254255
@Test
255256
fun `executeQuery() should retry with a fresh auth token on UNAUTHENTICATED`() = runTest {
256-
val responseData = Arb.proto.struct().next(rs)
257+
val responseData = Arb.proto.struct().next(rs).struct
257258
val forceRefresh = AtomicBoolean(false)
258259
coEvery { mockDataConnectAuth.forceRefresh() } answers { forceRefresh.set(true) }
259260
coEvery { mockDataConnectGrpcRPCs.executeQuery(any(), any(), any()) } answers
@@ -284,7 +285,7 @@ class DataConnectGrpcClientUnitTest {
284285

285286
@Test
286287
fun `executeMutation() should retry with a fresh auth token on UNAUTHENTICATED`() = runTest {
287-
val responseData = Arb.proto.struct().next(rs)
288+
val responseData = Arb.proto.struct().next(rs).struct
288289
val forceRefresh = AtomicBoolean(false)
289290
coEvery { mockDataConnectAuth.forceRefresh() } answers { forceRefresh.set(true) }
290291
coEvery { mockDataConnectGrpcRPCs.executeMutation(any(), any(), any()) } answers
@@ -315,7 +316,7 @@ class DataConnectGrpcClientUnitTest {
315316

316317
@Test
317318
fun `executeQuery() should retry with a fresh AppCheck token on UNAUTHENTICATED`() = runTest {
318-
val responseData = Arb.proto.struct().next(rs)
319+
val responseData = Arb.proto.struct().next(rs).struct
319320
val forceRefresh = AtomicBoolean(false)
320321
coEvery { mockDataConnectAppCheck.forceRefresh() } answers { forceRefresh.set(true) }
321322
coEvery { mockDataConnectGrpcRPCs.executeQuery(any(), any(), any()) } answers
@@ -346,7 +347,7 @@ class DataConnectGrpcClientUnitTest {
346347

347348
@Test
348349
fun `executeMutation() should retry with a fresh AppCheck token on UNAUTHENTICATED`() = runTest {
349-
val responseData = Arb.proto.struct().next(rs)
350+
val responseData = Arb.proto.struct().next(rs).struct
350351
val forceRefresh = AtomicBoolean(false)
351352
coEvery { mockDataConnectAppCheck.forceRefresh() } answers { forceRefresh.set(true) }
352353
coEvery { mockDataConnectGrpcRPCs.executeMutation(any(), any(), any()) } answers
@@ -593,9 +594,9 @@ class DataConnectGrpcClientOperationResultUnitTest {
593594
fun `deserialize() with non-null data should treat DataConnectUntypedData specially`() = runTest {
594595
checkAll(propTestConfig, Arb.proto.struct(), Arb.dataConnect.operationErrors()) { data, errors
595596
->
596-
val operationResult = OperationResult(data, errors)
597+
val operationResult = OperationResult(data.struct, errors)
597598
val result = operationResult.deserialize(DataConnectUntypedData, serializersModule = null)
598-
result.shouldHaveDataAndErrors(data, errors)
599+
result.shouldHaveDataAndErrors(data.struct, errors)
599600
}
600601
}
601602

@@ -635,7 +636,7 @@ class DataConnectGrpcClientOperationResultUnitTest {
635636
runTest {
636637
checkAll(
637638
propTestConfig,
638-
Arb.proto.struct(),
639+
Arb.proto.struct().map { it.struct },
639640
Arb.dataConnect.operationErrors(range = 1..10)
640641
) { dataStruct, errors ->
641642
val operationResult = OperationResult(dataStruct, errors)
@@ -697,7 +698,7 @@ class DataConnectGrpcClientOperationResultUnitTest {
697698

698699
@Test
699700
fun `deserialize() should throw if decoding fails and error list is empty`() = runTest {
700-
checkAll(propTestConfig, Arb.proto.struct()) { dataStruct ->
701+
checkAll(propTestConfig, Arb.proto.struct().map { it.struct }) { dataStruct ->
701702
assume(!dataStruct.containsFields("foo"))
702703
val operationResult = OperationResult(dataStruct, emptyList())
703704
val exception: DataConnectOperationException =

firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ internal fun DataConnectArb.operationErrorInfo(
9090
Arb.bind(message, path) { message0, path0 -> ErrorInfoImpl(message0, path0) }
9191

9292
internal fun DataConnectArb.operationRawData(): Arb<Map<String, Any?>?> =
93-
Arb.proto.struct().map { it.toMap() }.orNull(nullProbability = 0.33)
93+
Arb.proto.struct().map { it.struct.toMap() }.orNull(nullProbability = 0.33)
9494

9595
internal data class SampleOperationData(val value: String)
9696

@@ -112,7 +112,7 @@ internal fun DataConnectArb.operationFailureResponseImpl(
112112
}
113113

114114
internal fun DataConnectArb.operationResult(
115-
data: Arb<Struct?> = Arb.proto.struct().orNull(nullProbability = 0.2),
115+
data: Arb<Struct?> = Arb.proto.struct().map { it.struct }.orNull(nullProbability = 0.2),
116116
errors: Arb<List<ErrorInfoImpl>> = operationErrors(),
117117
) =
118118
Arb.bind(data, errors) { data0, errors0 -> DataConnectGrpcClient.OperationResult(data0, errors0) }
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2024 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.dataconnect.testutil
18+
19+
import com.google.protobuf.ListValue
20+
import com.google.protobuf.Struct
21+
import com.google.protobuf.Value
22+
23+
fun Boolean.toValueProto(): Value = Value.newBuilder().setBoolValue(this).build()
24+
25+
fun String.toValueProto(): Value = Value.newBuilder().setStringValue(this).build()
26+
27+
fun Double.toValueProto(): Value = Value.newBuilder().setNumberValue(this).build()
28+
29+
fun Struct.toValueProto(): Value = Value.newBuilder().setStructValue(this).build()
30+
31+
fun ListValue.toValueProto(): Value = Value.newBuilder().setListValue(this).build()
32+
33+
val Value.isStructValue: Boolean
34+
get() = kindCase == Value.KindCase.STRUCT_VALUE
35+
36+
val Value.isListValue: Boolean
37+
get() = kindCase == Value.KindCase.LIST_VALUE
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2024 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.firebase.dataconnect.testutil
17+
18+
import com.google.protobuf.Value
19+
import java.util.Objects
20+
21+
sealed interface ProtoValuePathComponent {
22+
23+
class StructKey(val key: String) : ProtoValuePathComponent {
24+
override fun equals(other: Any?) = other is StructKey && other.key == key
25+
override fun hashCode() = Objects.hash(StructKey::class.java, key)
26+
override fun toString() = "StructKey(\"$key\")"
27+
}
28+
29+
class ListIndex(val index: Int) : ProtoValuePathComponent {
30+
override fun equals(other: Any?) = other is ListIndex && other.index == index
31+
override fun hashCode() = Objects.hash(ListIndex::class.java, index)
32+
override fun toString() = "ListIndex($index)"
33+
}
34+
}
35+
36+
typealias ProtoValuePath = List<ProtoValuePathComponent>
37+
38+
data class ProtoValuePathPair(val path: ProtoValuePath, val value: Value)
39+
40+
fun ProtoValuePath.withAppendedListIndex(index: Int): ProtoValuePath =
41+
withAppendedComponent(ProtoValuePathComponent.ListIndex(index))
42+
43+
fun ProtoValuePath.withAppendedStructKey(key: String): ProtoValuePath =
44+
withAppendedComponent(ProtoValuePathComponent.StructKey(key))
45+
46+
fun ProtoValuePath.withAppendedComponent(component: ProtoValuePathComponent): ProtoValuePath =
47+
buildList {
48+
addAll(this@withAppendedComponent)
49+
add(component)
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2024 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.dataconnect.testutil.property.arbitrary
18+
19+
import io.kotest.common.ExperimentalKotest
20+
import io.kotest.property.PropTestConfig
21+
22+
fun PropTestConfig.withIterations(iterations: Int): PropTestConfig {
23+
@OptIn(ExperimentalKotest::class) return copy(iterations = iterations)
24+
}

firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import com.google.firebase.dataconnect.ConnectorConfig
2222
import com.google.firebase.dataconnect.DataConnectPathSegment
2323
import com.google.firebase.dataconnect.DataConnectSettings
2424
import io.kotest.property.Arb
25+
import io.kotest.property.RandomSource
2526
import io.kotest.property.arbitrary.Codepoint
2627
import io.kotest.property.arbitrary.alphanumeric
2728
import io.kotest.property.arbitrary.arabic
@@ -161,3 +162,14 @@ val Arb.Companion.dataConnect: DataConnectArb
161162
get() = DataConnectArb
162163

163164
inline fun <reified T : Any> Arb.Companion.mock(): Arb<T> = arbitrary { mockk<T>(relaxed = true) }
165+
166+
fun <T> Arb<T>.next(rs: RandomSource, edgeCaseProbability: Float): T {
167+
require(edgeCaseProbability in 0.0f..1.0f) {
168+
"invalid edgeCaseProbability: $edgeCaseProbability (must be between 0.0 and 1.0, inclusive)"
169+
}
170+
return if (rs.random.nextFloat() < edgeCaseProbability) {
171+
edgecase(rs)!!
172+
} else {
173+
sample(rs).value
174+
}
175+
}

0 commit comments

Comments
 (0)