Skip to content

Commit

Permalink
add fuzz test for write-only stack using the invariant_storeRoundTrip…
Browse files Browse the repository at this point in the history
…_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
  • Loading branch information
galganif authored and arcs-c3po committed Mar 18, 2021
1 parent 93bd41e commit 4237862
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 9 deletions.
16 changes: 11 additions & 5 deletions java/arcs/core/data/testutil/Generators.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,11 @@ import arcs.core.testutil.FuzzingRandom
import arcs.core.testutil.Generator
import arcs.core.testutil.Transformer
import arcs.core.testutil.Value
import arcs.core.testutil.midSizedAlphaNumericString
import arcs.core.testutil.midSizedUnicodeString
import arcs.core.type.Type
import arcs.core.util.ArcsDuration
import arcs.core.util.ArcsInstant
import arcs.core.util.BigInt

/**
Expand Down Expand Up @@ -106,7 +109,7 @@ class CreatableStorageKeyGenerator(
/**
* Pairs a [ParticleRegistration] with a [Plan.Particle]. These two objects need to
* have similar information in order for a plan to successfully start:
* - the location in Plan.Particle has to match the ParticelRegistration's ParticleIdentifier
* - the location in Plan.Particle has to match the ParticleRegistration's ParticleIdentifier
* - the Particle instance returned by the ParticleRegistration's ParticleConstructor needs
* - to have a handle field with a HandleHolder that recognizes any handleConnections listed in
* - the Plan.Particle as valid.
Expand Down Expand Up @@ -334,7 +337,8 @@ class FieldTypeGenerator(
* with appropriate data.
*/
class ReferencablePrimitiveFromPrimitiveType(
val s: FuzzingRandom
val s: FuzzingRandom,
val unicode: Boolean = true
) : Transformer<PrimitiveType, ReferencablePrimitive<*>>() {
override fun invoke(i: PrimitiveType): ReferencablePrimitive<*> {
return when (i) {
Expand All @@ -343,14 +347,16 @@ class ReferencablePrimitiveFromPrimitiveType(
PrimitiveType.Byte -> s.nextByte().toReferencable()
PrimitiveType.Char -> s.nextChar().toReferencable()
PrimitiveType.Double -> s.nextDouble().toReferencable()
PrimitiveType.Duration -> s.nextLong().toReferencable()
PrimitiveType.Duration -> ArcsDuration.valueOf(s.nextLong()).toReferencable()
PrimitiveType.Float -> s.nextFloat().toReferencable()
PrimitiveType.Instant -> s.nextLong().toReferencable()
PrimitiveType.Instant -> ArcsInstant.ofEpochMilli(s.nextLong()).toReferencable()
PrimitiveType.Int -> s.nextInt().toReferencable()
PrimitiveType.Long -> s.nextLong().toReferencable()
PrimitiveType.Number -> s.nextDouble().toReferencable()
PrimitiveType.Short -> s.nextShort().toReferencable()
PrimitiveType.Text -> midSizedUnicodeString(s)().toReferencable()
PrimitiveType.Text ->
(if (unicode) midSizedUnicodeString(s) else midSizedAlphaNumericString(s))()
.toReferencable()
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions java/arcs/core/entity/testutil/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,20 @@ arcs_kt_jvm_library(
deps = [
":fixture_arcs_gen",
"//java/arcs/core/common",
"//java/arcs/core/crdt/testutil",
"//java/arcs/core/crdt/testutil:generators",
"//java/arcs/core/data:annotations",
"//java/arcs/core/data:data-kt",
"//java/arcs/core/data:rawentity",
"//java/arcs/core/data:schema_fields",
"//java/arcs/core/data/testutil:generators",
"//java/arcs/core/data/util:data-util",
"//java/arcs/core/entity",
"//java/arcs/core/storage:reference",
"//java/arcs/core/storage:storage_key",
"//java/arcs/core/storage/testutil",
"//java/arcs/core/testutil",
"//java/arcs/core/testutil:generator_util",
"//java/arcs/core/util",
"//java/arcs/core/util:utils-platform-dependencies",
],
Expand Down
31 changes: 31 additions & 0 deletions java/arcs/core/entity/testutil/FixtureEntities.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
package arcs.core.entity.testutil

import arcs.core.common.ReferenceId
import arcs.core.crdt.testutil.RawEntityFromSchema
import arcs.core.data.FieldType
import arcs.core.data.RawEntity
import arcs.core.data.Schema
import arcs.core.data.SchemaRegistry
import arcs.core.data.testutil.SchemaWithReferencedSchemas
import arcs.core.data.util.ReferencableList
import arcs.core.entity.Reference
import arcs.core.storage.RawReference
import arcs.core.storage.StorageKey
import arcs.core.storage.testutil.DummyStorageKey
import arcs.core.testutil.FuzzingRandom
import arcs.core.testutil.IntInRange
import arcs.core.testutil.RandomPositiveLong
import arcs.core.testutil.midSizedAlphaNumericString
import arcs.core.testutil.referencableFieldValueFromFieldTypeDbCompatible
import arcs.core.util.ArcsDuration
import arcs.core.util.ArcsInstant
import arcs.core.util.BigInt
Expand Down Expand Up @@ -233,6 +240,30 @@ class FixtureEntities {
SchemaRegistry.register(MoreNested.SCHEMA)
SchemaRegistry.register(EmptyEntity.SCHEMA)
}

/**
* [SchemaWithReferencedSchemas] instance useful in fuzz testing.
*/
private val SCHEMA_WITH_REFERENCED = SchemaWithReferencedSchemas(
FixtureEntity.SCHEMA,
mapOf(
FixtureEntity.SCHEMA.hash to FixtureEntity.SCHEMA,
InnerEntity.SCHEMA.hash to InnerEntity.SCHEMA,
MoreNested.SCHEMA.hash to MoreNested.SCHEMA,
EmptyEntity.SCHEMA.hash to EmptyEntity.SCHEMA
)
)

/** Generates a random raw entity of type FixtureEntity, to be used for fuzz testing. */
fun randomRawEntity(s: FuzzingRandom): RawEntity {
return RawEntityFromSchema(
midSizedAlphaNumericString(s),
referencableFieldValueFromFieldTypeDbCompatible(s),
IntInRange(s, 1, 5),
RandomPositiveLong(s),
RandomPositiveLong(s)
)(SCHEMA_WITH_REFERENCED)
}
}
}

Expand Down
15 changes: 13 additions & 2 deletions java/arcs/core/testutil/Fuzzing.kt
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ interface Generator<T> {
*
* ```kotlin
* invariant_withdrawals_lessThan_initialBalance_willApply(
* initalBalance: Int,
* initialBalance: Int,
* withdrawal: Withdrawal
* ) {
* assertThat(withdrawal.amount).isLessThan(initialBalance)
Expand Down Expand Up @@ -231,6 +231,7 @@ interface FuzzingRandom {
fun nextInRange(min: Int, max: Int): Int
fun nextInt(): Int
fun nextLong(): Long
fun nextPositiveLong(): Long
fun nextBoolean(): Boolean
fun nextByte(): Byte
fun nextShort(): Short
Expand All @@ -249,6 +250,7 @@ open class SeededRandom(val seed: Long) : FuzzingRandom {
override fun nextInRange(min: Int, max: Int): Int = random.nextInt(min, max + 1)
override fun nextInt(): Int = random.nextInt()
override fun nextLong(): Long = random.nextLong()
override fun nextPositiveLong(): Long = random.nextLong(Long.MAX_VALUE)
override fun nextBoolean(): Boolean = random.nextBoolean()
override fun nextByte(): Byte = random.nextBytes(1)[0]
override fun nextShort(): Short = random.nextInt(Short.MAX_VALUE.toInt()).toShort()
Expand Down Expand Up @@ -293,6 +295,15 @@ class RandomLong(
}
}

/** A [Generator] that produces a positive long. */
class RandomPositiveLong(
val s: FuzzingRandom
) : Generator<Long> {
override fun invoke(): Long {
return s.nextPositiveLong()
}
}

/** A [Generator] that produces strings with a-zA-Z0-9 characters only. */
class AlphaNumericString(
val s: FuzzingRandom,
Expand Down Expand Up @@ -408,7 +419,7 @@ class MapOf<T, U>(
* Taking the following invariant:
* ```kotlin
* invariant_withdrawals_lessThan_initialBalance_willApply(
* initalBalance: Generator<Int>,
* initialBalance: Generator<Int>,
* withdrawal: Transformer<Int, Withdrawal>
* )
* ```
Expand Down
28 changes: 28 additions & 0 deletions java/arcs/core/testutil/GeneratorUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,34 @@ fun freeReferencableFromFieldType(
}
}

/**
* Returns a [Transformer] that produces [Referencable]s matching a given [FieldType]. The
* [Referencable]s are intended to be used as field values in entities (e.g. this will generate
* inline entities, not top level ones), and are compatible with their database representation,
* ie they can be used to test database roundtrips.
*/
fun referencableFieldValueFromFieldTypeDbCompatible(
s: FuzzingRandom
): Transformer<FieldTypeWithReferencedSchemas, Referencable> {
// Empty collection in inline entities are not stored in the database.
val oneOrMore = IntInRange(s, 1, 5)
return transformerWithRecursion {
ReferencableFromFieldType(
// Turn off unicode for text field, due to b/182713034.
ReferencablePrimitiveFromPrimitiveType(s, unicode = false),
oneOrMore,
RawEntityFromSchema(
midSizedAlphaNumericString(s),
it,
oneOrMore,
Value(-1),
Value(-1)
),
dummyReference(s)
)
}
}

/**
* Returns a [Generator] of reasonably-sized [CrdtEntity] instances with as few constraints as
* feasible.
Expand Down
17 changes: 17 additions & 0 deletions javatests/arcs/android/storage/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ arcs_kt_android_test_suite(
manifest = "//java/arcs/android/common:AndroidManifest.xml",
package = "arcs.android.storage",
deps = [
":generators",
":invariants",
"//java/arcs/android/crdt",
"//java/arcs/android/storage", # buildcleaner: keep
Expand All @@ -31,6 +32,7 @@ arcs_kt_android_test_suite(
"//java/arcs/core/storage/keys",
"//java/arcs/core/storage/referencemode",
"//java/arcs/core/storage/testutil",
"//java/arcs/core/testutil",
"//java/arcs/core/util",
"//java/arcs/flags/testing",
"//java/arcs/jvm/util",
Expand Down Expand Up @@ -60,3 +62,18 @@ arcs_kt_android_library(
"//third_party/kotlin/kotlinx_coroutines",
],
)

arcs_kt_android_library(
name = "generators",
testonly = 1,
srcs = [
"FixtureEntitiesOperationsGenerator.kt",
],
deps = [
"//java/arcs/core/crdt",
"//java/arcs/core/data:rawentity",
"//java/arcs/core/entity/testutil",
"//java/arcs/core/testutil",
"//java/arcs/core/testutil:generator_util",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package arcs.android.storage

import arcs.core.crdt.CrdtSet
import arcs.core.crdt.VersionMap
import arcs.core.data.RawEntity
import arcs.core.entity.testutil.FixtureEntities
import arcs.core.testutil.FuzzingRandom
import arcs.core.testutil.Generator

/**
* Generates a sequence of [CrdtSet.Operation<RawEntity>], using the FixtureEntity schema. The ops
* can be applied in sequence to a CrdtSet and should all be valid.
*/
class FixtureEntitiesOperationsGenerator(
val s: FuzzingRandom,
val sizeGenerator: Generator<Int>
) : Generator<List<CrdtSet.Operation<RawEntity>>> {
override fun invoke(): List<CrdtSet.Operation<RawEntity>> {
val ops = mutableListOf<CrdtSet.Operation<RawEntity>>()
// Keep track of the entities in the set, to generate valid remove ops.
val entities = mutableSetOf<RawEntity>()
repeat(sizeGenerator()) {
when (s.nextLessThan(3)) {
0 -> {
val e = FixtureEntities.randomRawEntity(s)
entities.add(e)
ops.add(CrdtSet.Operation.Add("", VersionMap(), e))
}
1 -> {
entities.randomOrNull()?.also {
ops.add(CrdtSet.Operation.Remove("", VersionMap(), it.id))
entities.remove(it)
}
}
else -> {
ops.add(CrdtSet.Operation.Clear("", VersionMap()))
entities.clear()
}
}
}
return ops
}
}
23 changes: 23 additions & 0 deletions javatests/arcs/android/storage/StoreInvariants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,29 @@ suspend fun invariant_storeRoundTrip_sameAsCrdtModel(
assertThat(model.values).isEqualTo(set.values)
}

/**
* Given a list of [ops], applies them to [stack1] (write through one store, read back through the
* other). Then compares it to sending an equivalent CrdtModel (to which the same [ops] are applied)
* through the second stack.
*/
suspend fun invariant_storeRoundTrip_sameAsCrdtModelReadBack(
stack1: StoresStack,
stack2: StoresStack,
ops: List<CrdtSet.Operation<RawEntity>>
) {
stack1.writeStore.onProxyMessage(ProxyMessage.Operations(ops, null))
val model1 = getModelFromStore(stack1.readStore)
stack2.writeStore.onProxyMessage(ProxyMessage.ModelUpdate(applyOpsToSet(ops).data, null))
val model2 = getModelFromStore(stack2.readStore)

assertThat(model1).isEqualTo(model2)
}

data class StoresStack(
val writeStore: UntypedActiveStore,
val readStore: UntypedActiveStore
)

private suspend fun getModelFromStore(store: UntypedActiveStore): CrdtSet.Data<RawEntity> {
val modelReceived = CompletableDeferred<CrdtSet.Data<RawEntity>>()
val callbackToken = store.on {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import arcs.core.storage.FixedDriverFactory
import arcs.core.storage.ProxyMessage
import arcs.core.storage.RawReference
import arcs.core.storage.ReferenceModeStore
import arcs.core.storage.StorageKey
import arcs.core.storage.StorageKeyManager
import arcs.core.storage.StoreOptions
import arcs.core.storage.UntypedActiveStore
Expand All @@ -45,6 +46,8 @@ import arcs.core.storage.keys.DATABASE_NAME_DEFAULT
import arcs.core.storage.keys.DatabaseStorageKey
import arcs.core.storage.referencemode.ReferenceModeStorageKey
import arcs.core.storage.testutil.testWriteBackProvider
import arcs.core.testutil.IntInRange
import arcs.core.testutil.runFuzzTest
import arcs.flags.BuildFlags
import arcs.jvm.util.JvmTime
import com.google.common.truth.Truth.assertThat
Expand Down Expand Up @@ -103,6 +106,23 @@ class WriteOnlyStoreDatabaseImplIntegrationTest {
invariant_storeRoundTrip_sameAsCrdtModel(writeStore, readStore, ops)
}

@Test
fun writeOnlyStore_sequenceOfOps_readByReferenceModeStore_FuzzTest() = runFuzzTest {
// Write in write-only-mode, read with ref-mode-store.
val writeOnlyStack = StoresStack(
createStore(true, TEST_KEY),
createStore(false, TEST_KEY)
)
// Write and read with ref-mode-store.
val refModeStack = StoresStack(
createStore(false, TEST_KEY_2),
createStore(false, TEST_KEY_2)
)
val ops = FixtureEntitiesOperationsGenerator(it, IntInRange(it, 1, 20))

invariant_storeRoundTrip_sameAsCrdtModelReadBack(writeOnlyStack, refModeStack, ops())
}

@Test
fun writeOnlyStore_propagatesToDatabase() = runBlockingTest {
val (writeStore, _) = createStores()
Expand Down Expand Up @@ -137,10 +157,10 @@ class WriteOnlyStoreDatabaseImplIntegrationTest {
return writeStore to readStore
}

private suspend fun createStore(writeOnly: Boolean) =
private suspend fun createStore(writeOnly: Boolean, storageKey: StorageKey = TEST_KEY) =
ActiveStore<CrdtData, CrdtOperation, Any?>(
StoreOptions(
TEST_KEY,
storageKey,
CollectionType(EntityType(FixtureEntity.SCHEMA)),
writeOnly = writeOnly
),
Expand Down Expand Up @@ -174,5 +194,9 @@ class WriteOnlyStoreDatabaseImplIntegrationTest {
DatabaseStorageKey.Persistent("entities", HASH),
DatabaseStorageKey.Persistent("set", HASH)
)
private val TEST_KEY_2 = ReferenceModeStorageKey(
DatabaseStorageKey.Persistent("entities2", HASH),
DatabaseStorageKey.Persistent("set2", HASH)
)
}
}

0 comments on commit 4237862

Please sign in to comment.