diff --git a/java/arcs/core/data/testutil/Generators.kt b/java/arcs/core/data/testutil/Generators.kt index c21601b6661..cfc945db2c1 100644 --- a/java/arcs/core/data/testutil/Generators.kt +++ b/java/arcs/core/data/testutil/Generators.kt @@ -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 /** @@ -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. @@ -334,7 +337,8 @@ class FieldTypeGenerator( * with appropriate data. */ class ReferencablePrimitiveFromPrimitiveType( - val s: FuzzingRandom + val s: FuzzingRandom, + val unicode: Boolean = true ) : Transformer>() { override fun invoke(i: PrimitiveType): ReferencablePrimitive<*> { return when (i) { @@ -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() } } } diff --git a/java/arcs/core/entity/testutil/BUILD b/java/arcs/core/entity/testutil/BUILD index 40097b9c91d..3e0f69c57ec 100644 --- a/java/arcs/core/entity/testutil/BUILD +++ b/java/arcs/core/entity/testutil/BUILD @@ -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", ], diff --git a/java/arcs/core/entity/testutil/FixtureEntities.kt b/java/arcs/core/entity/testutil/FixtureEntities.kt index 25207d1a829..e90506f4515 100644 --- a/java/arcs/core/entity/testutil/FixtureEntities.kt +++ b/java/arcs/core/entity/testutil/FixtureEntities.kt @@ -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 @@ -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) + } } } diff --git a/java/arcs/core/testutil/Fuzzing.kt b/java/arcs/core/testutil/Fuzzing.kt index adf108db9a5..90b07e114ba 100644 --- a/java/arcs/core/testutil/Fuzzing.kt +++ b/java/arcs/core/testutil/Fuzzing.kt @@ -181,7 +181,7 @@ interface Generator { * * ```kotlin * invariant_withdrawals_lessThan_initialBalance_willApply( - * initalBalance: Int, + * initialBalance: Int, * withdrawal: Withdrawal * ) { * assertThat(withdrawal.amount).isLessThan(initialBalance) @@ -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 @@ -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() @@ -293,6 +295,15 @@ class RandomLong( } } +/** A [Generator] that produces a positive long. */ +class RandomPositiveLong( + val s: FuzzingRandom +) : Generator { + override fun invoke(): Long { + return s.nextPositiveLong() + } +} + /** A [Generator] that produces strings with a-zA-Z0-9 characters only. */ class AlphaNumericString( val s: FuzzingRandom, @@ -408,7 +419,7 @@ class MapOf( * Taking the following invariant: * ```kotlin * invariant_withdrawals_lessThan_initialBalance_willApply( - * initalBalance: Generator, + * initialBalance: Generator, * withdrawal: Transformer * ) * ``` diff --git a/java/arcs/core/testutil/GeneratorUtil.kt b/java/arcs/core/testutil/GeneratorUtil.kt index 37369134957..bc1aff47f77 100644 --- a/java/arcs/core/testutil/GeneratorUtil.kt +++ b/java/arcs/core/testutil/GeneratorUtil.kt @@ -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 { + // 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. diff --git a/javatests/arcs/android/storage/BUILD b/javatests/arcs/android/storage/BUILD index 7af071f4b03..8ecd6f16cab 100644 --- a/javatests/arcs/android/storage/BUILD +++ b/javatests/arcs/android/storage/BUILD @@ -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 @@ -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", @@ -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", + ], +) diff --git a/javatests/arcs/android/storage/FixtureEntitiesOperationsGenerator.kt b/javatests/arcs/android/storage/FixtureEntitiesOperationsGenerator.kt new file mode 100644 index 00000000000..84c9dda4355 --- /dev/null +++ b/javatests/arcs/android/storage/FixtureEntitiesOperationsGenerator.kt @@ -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], 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 +) : Generator>> { + override fun invoke(): List> { + val ops = mutableListOf>() + // Keep track of the entities in the set, to generate valid remove ops. + val entities = mutableSetOf() + 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 + } +} diff --git a/javatests/arcs/android/storage/StoreInvariants.kt b/javatests/arcs/android/storage/StoreInvariants.kt index 200551ac012..09ec5afba18 100644 --- a/javatests/arcs/android/storage/StoreInvariants.kt +++ b/javatests/arcs/android/storage/StoreInvariants.kt @@ -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> +) { + 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 { val modelReceived = CompletableDeferred>() val callbackToken = store.on { diff --git a/javatests/arcs/android/storage/WriteOnlyStoreDatabaseImplIntegrationTest.kt b/javatests/arcs/android/storage/WriteOnlyStoreDatabaseImplIntegrationTest.kt index 1b2274c4a6d..7eed45e90c9 100644 --- a/javatests/arcs/android/storage/WriteOnlyStoreDatabaseImplIntegrationTest.kt +++ b/javatests/arcs/android/storage/WriteOnlyStoreDatabaseImplIntegrationTest.kt @@ -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 @@ -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 @@ -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() @@ -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( StoreOptions( - TEST_KEY, + storageKey, CollectionType(EntityType(FixtureEntity.SCHEMA)), writeOnly = writeOnly ), @@ -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) + ) } }