Skip to content

Commit 4237862

Browse files
galganifarcs-c3po
authored andcommitted
add fuzz test for write-only stack using the invariant_storeRoundTrip_sameAsCrdtModel invariant. for this, we use a generators of FixtureEntities. includes some tweaks to some existing generators. had to turn off unicode for strings as it is not preserved well by database roundtrips (b/182713034).
PiperOrigin-RevId: 363578372
1 parent 93bd41e commit 4237862

File tree

9 files changed

+197
-9
lines changed

9 files changed

+197
-9
lines changed

java/arcs/core/data/testutil/Generators.kt

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@ import arcs.core.testutil.FuzzingRandom
2525
import arcs.core.testutil.Generator
2626
import arcs.core.testutil.Transformer
2727
import arcs.core.testutil.Value
28+
import arcs.core.testutil.midSizedAlphaNumericString
2829
import arcs.core.testutil.midSizedUnicodeString
2930
import arcs.core.type.Type
31+
import arcs.core.util.ArcsDuration
32+
import arcs.core.util.ArcsInstant
3033
import arcs.core.util.BigInt
3134

3235
/**
@@ -106,7 +109,7 @@ class CreatableStorageKeyGenerator(
106109
/**
107110
* Pairs a [ParticleRegistration] with a [Plan.Particle]. These two objects need to
108111
* have similar information in order for a plan to successfully start:
109-
* - the location in Plan.Particle has to match the ParticelRegistration's ParticleIdentifier
112+
* - the location in Plan.Particle has to match the ParticleRegistration's ParticleIdentifier
110113
* - the Particle instance returned by the ParticleRegistration's ParticleConstructor needs
111114
* - to have a handle field with a HandleHolder that recognizes any handleConnections listed in
112115
* - the Plan.Particle as valid.
@@ -334,7 +337,8 @@ class FieldTypeGenerator(
334337
* with appropriate data.
335338
*/
336339
class ReferencablePrimitiveFromPrimitiveType(
337-
val s: FuzzingRandom
340+
val s: FuzzingRandom,
341+
val unicode: Boolean = true
338342
) : Transformer<PrimitiveType, ReferencablePrimitive<*>>() {
339343
override fun invoke(i: PrimitiveType): ReferencablePrimitive<*> {
340344
return when (i) {
@@ -343,14 +347,16 @@ class ReferencablePrimitiveFromPrimitiveType(
343347
PrimitiveType.Byte -> s.nextByte().toReferencable()
344348
PrimitiveType.Char -> s.nextChar().toReferencable()
345349
PrimitiveType.Double -> s.nextDouble().toReferencable()
346-
PrimitiveType.Duration -> s.nextLong().toReferencable()
350+
PrimitiveType.Duration -> ArcsDuration.valueOf(s.nextLong()).toReferencable()
347351
PrimitiveType.Float -> s.nextFloat().toReferencable()
348-
PrimitiveType.Instant -> s.nextLong().toReferencable()
352+
PrimitiveType.Instant -> ArcsInstant.ofEpochMilli(s.nextLong()).toReferencable()
349353
PrimitiveType.Int -> s.nextInt().toReferencable()
350354
PrimitiveType.Long -> s.nextLong().toReferencable()
351355
PrimitiveType.Number -> s.nextDouble().toReferencable()
352356
PrimitiveType.Short -> s.nextShort().toReferencable()
353-
PrimitiveType.Text -> midSizedUnicodeString(s)().toReferencable()
357+
PrimitiveType.Text ->
358+
(if (unicode) midSizedUnicodeString(s) else midSizedAlphaNumericString(s))()
359+
.toReferencable()
354360
}
355361
}
356362
}

java/arcs/core/entity/testutil/BUILD

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,20 @@ arcs_kt_jvm_library(
1919
deps = [
2020
":fixture_arcs_gen",
2121
"//java/arcs/core/common",
22+
"//java/arcs/core/crdt/testutil",
23+
"//java/arcs/core/crdt/testutil:generators",
2224
"//java/arcs/core/data:annotations",
2325
"//java/arcs/core/data:data-kt",
2426
"//java/arcs/core/data:rawentity",
2527
"//java/arcs/core/data:schema_fields",
28+
"//java/arcs/core/data/testutil:generators",
2629
"//java/arcs/core/data/util:data-util",
2730
"//java/arcs/core/entity",
2831
"//java/arcs/core/storage:reference",
2932
"//java/arcs/core/storage:storage_key",
3033
"//java/arcs/core/storage/testutil",
34+
"//java/arcs/core/testutil",
35+
"//java/arcs/core/testutil:generator_util",
3136
"//java/arcs/core/util",
3237
"//java/arcs/core/util:utils-platform-dependencies",
3338
],

java/arcs/core/entity/testutil/FixtureEntities.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
package arcs.core.entity.testutil
22

33
import arcs.core.common.ReferenceId
4+
import arcs.core.crdt.testutil.RawEntityFromSchema
45
import arcs.core.data.FieldType
56
import arcs.core.data.RawEntity
67
import arcs.core.data.Schema
78
import arcs.core.data.SchemaRegistry
9+
import arcs.core.data.testutil.SchemaWithReferencedSchemas
810
import arcs.core.data.util.ReferencableList
911
import arcs.core.entity.Reference
1012
import arcs.core.storage.RawReference
1113
import arcs.core.storage.StorageKey
1214
import arcs.core.storage.testutil.DummyStorageKey
15+
import arcs.core.testutil.FuzzingRandom
16+
import arcs.core.testutil.IntInRange
17+
import arcs.core.testutil.RandomPositiveLong
18+
import arcs.core.testutil.midSizedAlphaNumericString
19+
import arcs.core.testutil.referencableFieldValueFromFieldTypeDbCompatible
1320
import arcs.core.util.ArcsDuration
1421
import arcs.core.util.ArcsInstant
1522
import arcs.core.util.BigInt
@@ -233,6 +240,30 @@ class FixtureEntities {
233240
SchemaRegistry.register(MoreNested.SCHEMA)
234241
SchemaRegistry.register(EmptyEntity.SCHEMA)
235242
}
243+
244+
/**
245+
* [SchemaWithReferencedSchemas] instance useful in fuzz testing.
246+
*/
247+
private val SCHEMA_WITH_REFERENCED = SchemaWithReferencedSchemas(
248+
FixtureEntity.SCHEMA,
249+
mapOf(
250+
FixtureEntity.SCHEMA.hash to FixtureEntity.SCHEMA,
251+
InnerEntity.SCHEMA.hash to InnerEntity.SCHEMA,
252+
MoreNested.SCHEMA.hash to MoreNested.SCHEMA,
253+
EmptyEntity.SCHEMA.hash to EmptyEntity.SCHEMA
254+
)
255+
)
256+
257+
/** Generates a random raw entity of type FixtureEntity, to be used for fuzz testing. */
258+
fun randomRawEntity(s: FuzzingRandom): RawEntity {
259+
return RawEntityFromSchema(
260+
midSizedAlphaNumericString(s),
261+
referencableFieldValueFromFieldTypeDbCompatible(s),
262+
IntInRange(s, 1, 5),
263+
RandomPositiveLong(s),
264+
RandomPositiveLong(s)
265+
)(SCHEMA_WITH_REFERENCED)
266+
}
236267
}
237268
}
238269

java/arcs/core/testutil/Fuzzing.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ interface Generator<T> {
181181
*
182182
* ```kotlin
183183
* invariant_withdrawals_lessThan_initialBalance_willApply(
184-
* initalBalance: Int,
184+
* initialBalance: Int,
185185
* withdrawal: Withdrawal
186186
* ) {
187187
* assertThat(withdrawal.amount).isLessThan(initialBalance)
@@ -231,6 +231,7 @@ interface FuzzingRandom {
231231
fun nextInRange(min: Int, max: Int): Int
232232
fun nextInt(): Int
233233
fun nextLong(): Long
234+
fun nextPositiveLong(): Long
234235
fun nextBoolean(): Boolean
235236
fun nextByte(): Byte
236237
fun nextShort(): Short
@@ -249,6 +250,7 @@ open class SeededRandom(val seed: Long) : FuzzingRandom {
249250
override fun nextInRange(min: Int, max: Int): Int = random.nextInt(min, max + 1)
250251
override fun nextInt(): Int = random.nextInt()
251252
override fun nextLong(): Long = random.nextLong()
253+
override fun nextPositiveLong(): Long = random.nextLong(Long.MAX_VALUE)
252254
override fun nextBoolean(): Boolean = random.nextBoolean()
253255
override fun nextByte(): Byte = random.nextBytes(1)[0]
254256
override fun nextShort(): Short = random.nextInt(Short.MAX_VALUE.toInt()).toShort()
@@ -293,6 +295,15 @@ class RandomLong(
293295
}
294296
}
295297

298+
/** A [Generator] that produces a positive long. */
299+
class RandomPositiveLong(
300+
val s: FuzzingRandom
301+
) : Generator<Long> {
302+
override fun invoke(): Long {
303+
return s.nextPositiveLong()
304+
}
305+
}
306+
296307
/** A [Generator] that produces strings with a-zA-Z0-9 characters only. */
297308
class AlphaNumericString(
298309
val s: FuzzingRandom,
@@ -408,7 +419,7 @@ class MapOf<T, U>(
408419
* Taking the following invariant:
409420
* ```kotlin
410421
* invariant_withdrawals_lessThan_initialBalance_willApply(
411-
* initalBalance: Generator<Int>,
422+
* initialBalance: Generator<Int>,
412423
* withdrawal: Transformer<Int, Withdrawal>
413424
* )
414425
* ```

java/arcs/core/testutil/GeneratorUtil.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,34 @@ fun freeReferencableFromFieldType(
6363
}
6464
}
6565

66+
/**
67+
* Returns a [Transformer] that produces [Referencable]s matching a given [FieldType]. The
68+
* [Referencable]s are intended to be used as field values in entities (e.g. this will generate
69+
* inline entities, not top level ones), and are compatible with their database representation,
70+
* ie they can be used to test database roundtrips.
71+
*/
72+
fun referencableFieldValueFromFieldTypeDbCompatible(
73+
s: FuzzingRandom
74+
): Transformer<FieldTypeWithReferencedSchemas, Referencable> {
75+
// Empty collection in inline entities are not stored in the database.
76+
val oneOrMore = IntInRange(s, 1, 5)
77+
return transformerWithRecursion {
78+
ReferencableFromFieldType(
79+
// Turn off unicode for text field, due to b/182713034.
80+
ReferencablePrimitiveFromPrimitiveType(s, unicode = false),
81+
oneOrMore,
82+
RawEntityFromSchema(
83+
midSizedAlphaNumericString(s),
84+
it,
85+
oneOrMore,
86+
Value(-1),
87+
Value(-1)
88+
),
89+
dummyReference(s)
90+
)
91+
}
92+
}
93+
6694
/**
6795
* Returns a [Generator] of reasonably-sized [CrdtEntity] instances with as few constraints as
6896
* feasible.

javatests/arcs/android/storage/BUILD

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ arcs_kt_android_test_suite(
1515
manifest = "//java/arcs/android/common:AndroidManifest.xml",
1616
package = "arcs.android.storage",
1717
deps = [
18+
":generators",
1819
":invariants",
1920
"//java/arcs/android/crdt",
2021
"//java/arcs/android/storage", # buildcleaner: keep
@@ -31,6 +32,7 @@ arcs_kt_android_test_suite(
3132
"//java/arcs/core/storage/keys",
3233
"//java/arcs/core/storage/referencemode",
3334
"//java/arcs/core/storage/testutil",
35+
"//java/arcs/core/testutil",
3436
"//java/arcs/core/util",
3537
"//java/arcs/flags/testing",
3638
"//java/arcs/jvm/util",
@@ -60,3 +62,18 @@ arcs_kt_android_library(
6062
"//third_party/kotlin/kotlinx_coroutines",
6163
],
6264
)
65+
66+
arcs_kt_android_library(
67+
name = "generators",
68+
testonly = 1,
69+
srcs = [
70+
"FixtureEntitiesOperationsGenerator.kt",
71+
],
72+
deps = [
73+
"//java/arcs/core/crdt",
74+
"//java/arcs/core/data:rawentity",
75+
"//java/arcs/core/entity/testutil",
76+
"//java/arcs/core/testutil",
77+
"//java/arcs/core/testutil:generator_util",
78+
],
79+
)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package arcs.android.storage
2+
3+
import arcs.core.crdt.CrdtSet
4+
import arcs.core.crdt.VersionMap
5+
import arcs.core.data.RawEntity
6+
import arcs.core.entity.testutil.FixtureEntities
7+
import arcs.core.testutil.FuzzingRandom
8+
import arcs.core.testutil.Generator
9+
10+
/**
11+
* Generates a sequence of [CrdtSet.Operation<RawEntity>], using the FixtureEntity schema. The ops
12+
* can be applied in sequence to a CrdtSet and should all be valid.
13+
*/
14+
class FixtureEntitiesOperationsGenerator(
15+
val s: FuzzingRandom,
16+
val sizeGenerator: Generator<Int>
17+
) : Generator<List<CrdtSet.Operation<RawEntity>>> {
18+
override fun invoke(): List<CrdtSet.Operation<RawEntity>> {
19+
val ops = mutableListOf<CrdtSet.Operation<RawEntity>>()
20+
// Keep track of the entities in the set, to generate valid remove ops.
21+
val entities = mutableSetOf<RawEntity>()
22+
repeat(sizeGenerator()) {
23+
when (s.nextLessThan(3)) {
24+
0 -> {
25+
val e = FixtureEntities.randomRawEntity(s)
26+
entities.add(e)
27+
ops.add(CrdtSet.Operation.Add("", VersionMap(), e))
28+
}
29+
1 -> {
30+
entities.randomOrNull()?.also {
31+
ops.add(CrdtSet.Operation.Remove("", VersionMap(), it.id))
32+
entities.remove(it)
33+
}
34+
}
35+
else -> {
36+
ops.add(CrdtSet.Operation.Clear("", VersionMap()))
37+
entities.clear()
38+
}
39+
}
40+
}
41+
return ops
42+
}
43+
}

javatests/arcs/android/storage/StoreInvariants.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,29 @@ suspend fun invariant_storeRoundTrip_sameAsCrdtModel(
3838
assertThat(model.values).isEqualTo(set.values)
3939
}
4040

41+
/**
42+
* Given a list of [ops], applies them to [stack1] (write through one store, read back through the
43+
* other). Then compares it to sending an equivalent CrdtModel (to which the same [ops] are applied)
44+
* through the second stack.
45+
*/
46+
suspend fun invariant_storeRoundTrip_sameAsCrdtModelReadBack(
47+
stack1: StoresStack,
48+
stack2: StoresStack,
49+
ops: List<CrdtSet.Operation<RawEntity>>
50+
) {
51+
stack1.writeStore.onProxyMessage(ProxyMessage.Operations(ops, null))
52+
val model1 = getModelFromStore(stack1.readStore)
53+
stack2.writeStore.onProxyMessage(ProxyMessage.ModelUpdate(applyOpsToSet(ops).data, null))
54+
val model2 = getModelFromStore(stack2.readStore)
55+
56+
assertThat(model1).isEqualTo(model2)
57+
}
58+
59+
data class StoresStack(
60+
val writeStore: UntypedActiveStore,
61+
val readStore: UntypedActiveStore
62+
)
63+
4164
private suspend fun getModelFromStore(store: UntypedActiveStore): CrdtSet.Data<RawEntity> {
4265
val modelReceived = CompletableDeferred<CrdtSet.Data<RawEntity>>()
4366
val callbackToken = store.on {

javatests/arcs/android/storage/WriteOnlyStoreDatabaseImplIntegrationTest.kt

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import arcs.core.storage.FixedDriverFactory
3131
import arcs.core.storage.ProxyMessage
3232
import arcs.core.storage.RawReference
3333
import arcs.core.storage.ReferenceModeStore
34+
import arcs.core.storage.StorageKey
3435
import arcs.core.storage.StorageKeyManager
3536
import arcs.core.storage.StoreOptions
3637
import arcs.core.storage.UntypedActiveStore
@@ -45,6 +46,8 @@ import arcs.core.storage.keys.DATABASE_NAME_DEFAULT
4546
import arcs.core.storage.keys.DatabaseStorageKey
4647
import arcs.core.storage.referencemode.ReferenceModeStorageKey
4748
import arcs.core.storage.testutil.testWriteBackProvider
49+
import arcs.core.testutil.IntInRange
50+
import arcs.core.testutil.runFuzzTest
4851
import arcs.flags.BuildFlags
4952
import arcs.jvm.util.JvmTime
5053
import com.google.common.truth.Truth.assertThat
@@ -103,6 +106,23 @@ class WriteOnlyStoreDatabaseImplIntegrationTest {
103106
invariant_storeRoundTrip_sameAsCrdtModel(writeStore, readStore, ops)
104107
}
105108

109+
@Test
110+
fun writeOnlyStore_sequenceOfOps_readByReferenceModeStore_FuzzTest() = runFuzzTest {
111+
// Write in write-only-mode, read with ref-mode-store.
112+
val writeOnlyStack = StoresStack(
113+
createStore(true, TEST_KEY),
114+
createStore(false, TEST_KEY)
115+
)
116+
// Write and read with ref-mode-store.
117+
val refModeStack = StoresStack(
118+
createStore(false, TEST_KEY_2),
119+
createStore(false, TEST_KEY_2)
120+
)
121+
val ops = FixtureEntitiesOperationsGenerator(it, IntInRange(it, 1, 20))
122+
123+
invariant_storeRoundTrip_sameAsCrdtModelReadBack(writeOnlyStack, refModeStack, ops())
124+
}
125+
106126
@Test
107127
fun writeOnlyStore_propagatesToDatabase() = runBlockingTest {
108128
val (writeStore, _) = createStores()
@@ -137,10 +157,10 @@ class WriteOnlyStoreDatabaseImplIntegrationTest {
137157
return writeStore to readStore
138158
}
139159

140-
private suspend fun createStore(writeOnly: Boolean) =
160+
private suspend fun createStore(writeOnly: Boolean, storageKey: StorageKey = TEST_KEY) =
141161
ActiveStore<CrdtData, CrdtOperation, Any?>(
142162
StoreOptions(
143-
TEST_KEY,
163+
storageKey,
144164
CollectionType(EntityType(FixtureEntity.SCHEMA)),
145165
writeOnly = writeOnly
146166
),
@@ -174,5 +194,9 @@ class WriteOnlyStoreDatabaseImplIntegrationTest {
174194
DatabaseStorageKey.Persistent("entities", HASH),
175195
DatabaseStorageKey.Persistent("set", HASH)
176196
)
197+
private val TEST_KEY_2 = ReferenceModeStorageKey(
198+
DatabaseStorageKey.Persistent("entities2", HASH),
199+
DatabaseStorageKey.Persistent("set2", HASH)
200+
)
177201
}
178202
}

0 commit comments

Comments
 (0)