diff --git a/java/arcs/android/crdt/ParcelableRawEntity.kt b/java/arcs/android/crdt/ParcelableRawEntity.kt index a1d53113c18..95cef76369b 100644 --- a/java/arcs/android/crdt/ParcelableRawEntity.kt +++ b/java/arcs/android/crdt/ParcelableRawEntity.kt @@ -39,6 +39,7 @@ data class ParcelableRawEntity( parcel.writeTypedObject(ParcelableReferencable(it), flags) } } + parcel.writeLong(actual.creationTimestamp) parcel.writeLong(actual.expirationTimestamp) } @@ -67,10 +68,19 @@ data class ParcelableRawEntity( collections[key] = set } + @Suppress("GoodTime") // use Instant + val creationTimestamp = requireNotNull(parcel.readLong()) + @Suppress("GoodTime") // use Instant val expirationTimestamp = requireNotNull(parcel.readLong()) - val rawEntity = RawEntity(id, singletons, collections, expirationTimestamp) + val rawEntity = RawEntity( + id, + singletons, + collections, + creationTimestamp, + expirationTimestamp + ) return ParcelableRawEntity(rawEntity) } diff --git a/java/arcs/core/common/Referencable.kt b/java/arcs/core/common/Referencable.kt index 6fc259fc629..edbcba2c1e0 100644 --- a/java/arcs/core/common/Referencable.kt +++ b/java/arcs/core/common/Referencable.kt @@ -19,6 +19,11 @@ interface Referencable { /** Unique identifier of the Referencable object. */ val id: ReferenceId + /** Creation timestamp (in millis) on the Referencable object. */ + var creationTimestamp: Long + get() = TODO("not implemented") + set(@Suppress("UNUSED_PARAMETER") value) = TODO("not implemented") + /** Expiration timestamp (in millis) on the Referencable object. */ var expirationTimestamp: Long get() = TODO("not implemented") diff --git a/java/arcs/core/data/RawEntity.kt b/java/arcs/core/data/RawEntity.kt index 41b333e6d2c..f81020c24f7 100644 --- a/java/arcs/core/data/RawEntity.kt +++ b/java/arcs/core/data/RawEntity.kt @@ -29,21 +29,34 @@ data class RawEntity( override fun unwrap(): Referencable { val entity = RawEntity( id = id, + creationTimestamp = creationTimestamp, expirationTimestamp = expirationTimestamp, singletons = singletons.mapValues { it.value?.unwrap() }, collections = collections.mapValues { it.value.map { item -> item.unwrap() }.toSet() } ) + entity.creationTimestamp = creationTimestamp entity.expirationTimestamp = expirationTimestamp return entity } + /** Entity creation time (in millis). */ + @Suppress("GoodTime") // use Instant + override var creationTimestamp: Long = UNINITIALIZED_TIMESTAMP + set(value) { + require(this.creationTimestamp == UNINITIALIZED_TIMESTAMP) { + "cannot override creationTimestamp $value" + } + @Suppress("GoodTime") // use Instant + field = value + } + /** Entity expiration time (in millis). */ @Suppress("GoodTime") // use Instant - override var expirationTimestamp: Long = NO_EXPIRATION + override var expirationTimestamp: Long = UNINITIALIZED_TIMESTAMP set(value) { - require(this.expirationTimestamp == NO_EXPIRATION) { + require(this.expirationTimestamp == UNINITIALIZED_TIMESTAMP) { "cannot override expirationTimestamp $value" } @Suppress("GoodTime") // use Instant @@ -62,7 +75,8 @@ data class RawEntity( id: ReferenceId = NO_REFERENCE_ID, singletonFields: Set = emptySet(), collectionFields: Set = emptySet(), - expirationTimestamp: Long = NO_EXPIRATION + creationTimestamp: Long = UNINITIALIZED_TIMESTAMP, + expirationTimestamp: Long = UNINITIALIZED_TIMESTAMP ) : this( id, singletonFields.associateWith { null }, @@ -73,7 +87,7 @@ data class RawEntity( companion object { const val NO_REFERENCE_ID = "NO REFERENCE ID" - const val NO_EXPIRATION: Long = -1 + const val UNINITIALIZED_TIMESTAMP: Long = -1 } } @@ -81,9 +95,13 @@ fun RawEntity( id: String, singletons: Map, collections: Map>, + creationTimestamp: Long, expirationTimestamp: Long ) = RawEntity( id, singletons, collections -).also { it.expirationTimestamp = expirationTimestamp } +).also { + it.creationTimestamp = creationTimestamp + it.expirationTimestamp = expirationTimestamp +} diff --git a/java/arcs/core/data/Ttl.kt b/java/arcs/core/data/Ttl.kt index 6a0d2ee2290..dd8c9734681 100644 --- a/java/arcs/core/data/Ttl.kt +++ b/java/arcs/core/data/Ttl.kt @@ -34,7 +34,7 @@ sealed class Ttl(count: Int, val isInfinite: Boolean = false) { override fun hashCode(): Int = minutes.hashCode() fun calculateExpiration(time: Time): Long = - if (isInfinite) RawEntity.NO_EXPIRATION + if (isInfinite) RawEntity.UNINITIALIZED_TIMESTAMP else requireNotNull(time).currentTimeMillis + (minutes * 60 * 1000) data class Minutes(val count: Int) : Ttl(count) diff --git a/java/arcs/core/data/proto/TypeProtoDecoders.kt b/java/arcs/core/data/proto/TypeProtoDecoders.kt index dbef7c92b02..01bec00dc43 100644 --- a/java/arcs/core/data/proto/TypeProtoDecoders.kt +++ b/java/arcs/core/data/proto/TypeProtoDecoders.kt @@ -11,10 +11,11 @@ package arcs.core.data.proto +import arcs.core.data.FieldType import arcs.core.data.PrimitiveType /** - * Converts a [PrimitiveTypeProto] protobuf instance into a native kotlin [PrimitiveType] instance. + * Converts a [PrimitiveTypeProto] protobuf instance into a Kotlin [PrimitiveType] instance. */ fun PrimitiveTypeProto.decode(): PrimitiveType = when (this) { @@ -24,3 +25,25 @@ fun PrimitiveTypeProto.decode(): PrimitiveType = PrimitiveTypeProto.UNRECOGNIZED -> throw IllegalArgumentException("Unknown PrimitiveTypeProto value.") } + +/** + * Converts a [PrimitiveTypeProto] protobuf instance into a Kotlin [FieldType] instance. + */ +fun PrimitiveTypeProto.decodeAsFieldType(): FieldType.Primitive = FieldType.Primitive(decode()) + +/** + * Converts a [TypeProto] protobuf instance into a Kotlin [FieldType] instance. + * + * @throws [IllegalArgumentexception] if the type cannot be converted to [FieldType]. + */ +fun TypeProto.decodeAsFieldType(): FieldType = + when (getDataCase()) { + TypeProto.DataCase.PRIMITIVE -> getPrimitive().decodeAsFieldType() + // TODO: Handle FieldType.EntityRef. It is not clear how it is + // represented in the proto. + TypeProto.DataCase.DATA_NOT_SET -> + throw IllegalArgumentException("Unknown data field in TypeProto.") + else -> + throw IllegalArgumentException( + "Cannot decode a ${getDataCase().name} type to a [FieldType].") + } diff --git a/java/arcs/core/storage/handle/CollectionImpl.kt b/java/arcs/core/storage/handle/CollectionImpl.kt index f0c0507f9c9..07fa4970808 100644 --- a/java/arcs/core/storage/handle/CollectionImpl.kt +++ b/java/arcs/core/storage/handle/CollectionImpl.kt @@ -88,6 +88,8 @@ class CollectionImpl( */ suspend fun store(entity: T): Boolean { log.debug { "Storing: $entity" } + @Suppress("GoodTime") // use Instant + entity.creationTimestamp = requireNotNull(time).currentTimeMillis if (!Ttl.Infinite.equals(ttl)) { @Suppress("GoodTime") // use Instant entity.expirationTimestamp = ttl.calculateExpiration(time) diff --git a/java/arcs/core/storage/handle/SingletonImpl.kt b/java/arcs/core/storage/handle/SingletonImpl.kt index cfd8d699104..a2be4e00cd4 100644 --- a/java/arcs/core/storage/handle/SingletonImpl.kt +++ b/java/arcs/core/storage/handle/SingletonImpl.kt @@ -64,6 +64,8 @@ class SingletonImpl( * did not apply fully. Fetch the latest value and retry. * */ suspend fun store(entity: T): Boolean { + @Suppress("GoodTime") // use Instant + entity.creationTimestamp = requireNotNull(time).currentTimeMillis if (!Ttl.Infinite.equals(ttl)) { @Suppress("GoodTime") // use Instant entity.expirationTimestamp = ttl.calculateExpiration(time) diff --git a/java/arcs/sdk/android/dev/api/CollectionProxy.java b/java/arcs/sdk/android/dev/api/CollectionProxy.java index 2ccd99f7816..2673bc5ae4a 100644 --- a/java/arcs/sdk/android/dev/api/CollectionProxy.java +++ b/java/arcs/sdk/android/dev/api/CollectionProxy.java @@ -286,6 +286,15 @@ public Referencable unwrap() { return this; } + @Override + public long getCreationTimestamp() { + throw new AssertionError("ModelEntry::getCreationTimestamp not implemented"); + } + + @Override + public void setCreationTimestamp(long creationTimestamp) { + throw new AssertionError("ModelEntry::setCreationTimestamp not implemented"); + } @Override public long getExpirationTimestamp() { diff --git a/java/arcs/sdk/android/storage/ServiceStore.kt b/java/arcs/sdk/android/storage/ServiceStore.kt index 567b28e5682..4048547f1a3 100644 --- a/java/arcs/sdk/android/storage/ServiceStore.kt +++ b/java/arcs/sdk/android/storage/ServiceStore.kt @@ -142,12 +142,15 @@ class ServiceStore( val service = connection.connectAsync().await() val messageChannel = ParcelableProxyMessageChannel(coroutineContext) - serviceCallbackToken = withContext(coroutineContext) { - service.registerCallback(messageChannel) - } + // Open subscription before attaching callback to make sure that we capture all messages + val subscription = messageChannel.openSubscription() scope.launch { - messageChannel.openSubscription().consumeEach { handleMessageAndResultFromService(it) } + subscription.consumeEach { handleMessageAndResultFromService(it) } + } + + serviceCallbackToken = withContext(coroutineContext) { + service.registerCallback(messageChannel) } this.serviceConnection = connection diff --git a/javatests/arcs/android/host/AndroidAllocatorTest.kt b/javatests/arcs/android/host/AndroidAllocatorTest.kt index 6c8b8861454..30c2d4599cf 100644 --- a/javatests/arcs/android/host/AndroidAllocatorTest.kt +++ b/javatests/arcs/android/host/AndroidAllocatorTest.kt @@ -13,7 +13,6 @@ package arcs.android.host import android.content.Context import androidx.lifecycle.Lifecycle -import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.work.testing.WorkManagerTestInitHelper @@ -26,12 +25,9 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.TestCoroutineScope -import kotlinx.coroutines.test.runBlockingTest import org.junit.Before import org.junit.runner.RunWith import org.robolectric.Robolectric -import kotlin.coroutines.CoroutineContext /** * These tests are the same as [AllocatorTestBase] but run with Android Services, @@ -82,24 +78,4 @@ open class AndroidAllocatorTest : AllocatorTestBase() { writingService = Robolectric.setupService(TestWritingExternalHostService::class.java) super.setUp() } - - override fun runAllocatorTest( - coroutineContext: CoroutineContext, - testBody: suspend TestCoroutineScope.() -> Unit - ) = runBlockingTest(coroutineContext) { - val scenario = ActivityScenario.launch(TestActivity::class.java) - - scenario.moveToState(Lifecycle.State.STARTED) - - val activityJob = launch { - scenario.onActivity { activity -> - runBlocking { - this@runBlockingTest.testBody() - } - scenario.close() - } - } - - activityJob.join() - } } diff --git a/javatests/arcs/android/host/AndroidEntityHandleManagerTest.kt b/javatests/arcs/android/host/AndroidEntityHandleManagerTest.kt index 48ab6ae88a8..8288912ab28 100644 --- a/javatests/arcs/android/host/AndroidEntityHandleManagerTest.kt +++ b/javatests/arcs/android/host/AndroidEntityHandleManagerTest.kt @@ -2,7 +2,8 @@ package arcs.android.host import android.app.Application import androidx.lifecycle.Lifecycle -import androidx.test.core.app.ActivityScenario +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.work.testing.WorkManagerTestInitHelper @@ -27,8 +28,6 @@ import arcs.sdk.android.storage.service.testutil.TestBindingDelegate import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.TestCoroutineScope -import kotlinx.coroutines.test.runBlockingTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -38,12 +37,15 @@ typealias Person = TestParticleInternal1 @Suppress("EXPERIMENTAL_API_USAGE") @RunWith(AndroidJUnit4::class) -class AndroidEntityHandleManagerTest { +class AndroidEntityHandleManagerTest : LifecycleOwner { private lateinit var app: Application + private lateinit var lifecycle: LifecycleRegistry + override fun getLifecycle() = lifecycle val entity1 = Person("Jason", 21.0, false) val entity2 = Person("Jason", 22.0, true) lateinit var handleHolder: TestParticleHandles + private lateinit var handleManager: EntityHandleManager private val schema = Schema( listOf(SchemaName("Person")), @@ -69,37 +71,24 @@ class AndroidEntityHandleManagerTest { @Before fun setUp() { app = ApplicationProvider.getApplicationContext() - app.setTheme(R.style.Theme_AppCompat); + lifecycle = LifecycleRegistry(this).apply { + setCurrentState(Lifecycle.State.CREATED) + setCurrentState(Lifecycle.State.STARTED) + setCurrentState(Lifecycle.State.RESUMED) + } // Initialize WorkManager for instrumentation tests. WorkManagerTestInitHelper.initializeTestWorkManager(app) handleHolder = TestParticleHandles() - } - fun handleManagerTest( - block: suspend TestCoroutineScope.(EntityHandleManager) -> Unit - ) = runBlockingTest { - val scenario = ActivityScenario.launch(TestActivity::class.java) - - scenario.moveToState(Lifecycle.State.STARTED) - - val activityJob = launch { - scenario.onActivity { activity -> - val hf = AndroidHandleManager( - lifecycle = activity.lifecycle, context = activity, - connectionFactory = DefaultConnectionFactory( - activity, TestBindingDelegate(app), coroutineContext - ), - coroutineContext = coroutineContext - ) - runBlocking { - this@runBlockingTest.block(EntityHandleManager(hf)) - } - scenario.close() - } - } - activityJob.join() + handleManager = EntityHandleManager( + AndroidHandleManager( + lifecycle = lifecycle, + context = app, + connectionFactory = DefaultConnectionFactory(app, TestBindingDelegate(app)) + ) + ) } private fun expectHandleException(handleName: String, block: () -> Unit) { @@ -110,7 +99,7 @@ class AndroidEntityHandleManagerTest { } @Test - fun handle_uninitializedThrowsException() = handleManagerTest { + fun handle_uninitializedThrowsException() = runBlocking { expectHandleException("writeHandle") { handleHolder.writeHandle } @@ -161,7 +150,7 @@ class AndroidEntityHandleManagerTest { ) @Test - fun singletonHandle_writeFollowedByReadWithOnUpdate() = handleManagerTest { handleManager -> + fun singletonHandle_writeFollowedByReadWithOnUpdate() = runBlocking { val writeHandle = createSingletonHandle( handleManager, "writeHandle", @@ -208,7 +197,7 @@ class AndroidEntityHandleManagerTest { } @Test - fun setHandle_writeFollowedByReadWithOnUpdate() = handleManagerTest { handleManager -> + fun setHandle_writeFollowedByReadWithOnUpdate() = runBlocking { val writeSetHandle = createSetHandle( handleManager, "writeSetHandle", diff --git a/javatests/arcs/android/host/AndroidManifest.xml b/javatests/arcs/android/host/AndroidManifest.xml index 5f39c2a208b..51645f1e827 100644 --- a/javatests/arcs/android/host/AndroidManifest.xml +++ b/javatests/arcs/android/host/AndroidManifest.xml @@ -15,12 +15,6 @@ - - - - - - diff --git a/javatests/arcs/android/host/BUILD b/javatests/arcs/android/host/BUILD index ba4ab0d17f9..6e30595bc52 100644 --- a/javatests/arcs/android/host/BUILD +++ b/javatests/arcs/android/host/BUILD @@ -9,16 +9,6 @@ licenses(["notice"]) package(default_visibility = ["//visibility:public"]) -kt_android_library( - name = "test_app", - testonly = 1, - srcs = ["TestActivity.kt"], - manifest = ":AndroidManifest.xml", - deps = [ - "//third_party/java/androidx/appcompat", - ], -) - arcs_kt_android_test_suite( name = "host", srcs = glob(["*Test.kt"]), @@ -27,28 +17,18 @@ arcs_kt_android_test_suite( deps = [ ":schemas", ":services", - ":test_app", - "//java/arcs/android/crdt", "//java/arcs/android/host", "//java/arcs/android/sdk/host", - "//java/arcs/android/storage", "//java/arcs/android/storage/database", "//java/arcs/android/storage/handle", - "//java/arcs/android/storage/service", - "//java/arcs/android/storage/service:aidl", - "//java/arcs/core/allocator", "//java/arcs/core/common", - "//java/arcs/core/crdt", "//java/arcs/core/data", "//java/arcs/core/host", "//java/arcs/core/storage", - "//java/arcs/core/storage:storage_key", "//java/arcs/core/storage/api", "//java/arcs/core/storage/driver", - "//java/arcs/core/storage/handle", "//java/arcs/core/storage/referencemode", "//java/arcs/core/testutil", - "//java/arcs/core/type", "//java/arcs/core/util", "//java/arcs/sdk/android/storage", "//java/arcs/sdk/android/storage/service", @@ -66,7 +46,6 @@ arcs_kt_android_test_suite( "//third_party/kotlin/kotlin:kotlin_reflect", "//third_party/kotlin/kotlinx_coroutines", "//third_party/kotlin/kotlinx_coroutines:kotlinx_coroutines_test", - "//third_party/kotlin/mockito_kotlin:mockito_kotlin-android", ], ) diff --git a/javatests/arcs/android/host/TestActivity.kt b/javatests/arcs/android/host/TestActivity.kt deleted file mode 100644 index 8399ec3552b..00000000000 --- a/javatests/arcs/android/host/TestActivity.kt +++ /dev/null @@ -1,5 +0,0 @@ -package arcs.android.host - -import androidx.appcompat.app.AppCompatActivity - -class TestActivity : AppCompatActivity() diff --git a/javatests/arcs/android/storage/database/BUILD b/javatests/arcs/android/storage/database/BUILD index fd42f3f5462..b792e6fba8d 100644 --- a/javatests/arcs/android/storage/database/BUILD +++ b/javatests/arcs/android/storage/database/BUILD @@ -9,6 +9,7 @@ package(default_visibility = ["//visibility:public"]) arcs_kt_android_test_suite( name = "database", + size = "medium", manifest = "//java/arcs/android/common:AndroidManifest.xml", package = "arcs.android.storage.database", deps = [ diff --git a/javatests/arcs/android/storage/handle/AndroidHandleManagerTest.kt b/javatests/arcs/android/storage/handle/AndroidHandleManagerTest.kt index bfcc4eb2ee7..923b5a5f602 100644 --- a/javatests/arcs/android/storage/handle/AndroidHandleManagerTest.kt +++ b/javatests/arcs/android/storage/handle/AndroidHandleManagerTest.kt @@ -4,7 +4,6 @@ import android.app.Application import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry -import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.work.testing.WorkManagerTestInitHelper diff --git a/javatests/arcs/core/allocator/AllocatorTestBase.kt b/javatests/arcs/core/allocator/AllocatorTestBase.kt index 239845aa2e4..115830fc948 100644 --- a/javatests/arcs/core/allocator/AllocatorTestBase.kt +++ b/javatests/arcs/core/allocator/AllocatorTestBase.kt @@ -26,6 +26,7 @@ import arcs.core.testutil.assertSuspendingThrows import arcs.core.type.Type import arcs.jvm.host.ExplicitHostRegistry import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestCoroutineScope @@ -73,8 +74,8 @@ open class AllocatorTestBase { open val storageCapability = Capabilities.TiedToRuntime open fun runAllocatorTest( coroutineContext: CoroutineContext = EmptyCoroutineContext, - testBody: suspend TestCoroutineScope.() -> Unit - ) = runBlockingTest(coroutineContext, testBody) + testBody: suspend CoroutineScope.() -> Unit + ) = runBlocking(coroutineContext) { testBody() } open suspend fun hostRegistry(): HostRegistry { val registry = ExplicitHostRegistry() diff --git a/javatests/arcs/core/data/proto/TypeProtoDecodersTest.kt b/javatests/arcs/core/data/proto/TypeProtoDecodersTest.kt index 85a3d63acf5..8eb0ba34c10 100644 --- a/javatests/arcs/core/data/proto/TypeProtoDecodersTest.kt +++ b/javatests/arcs/core/data/proto/TypeProtoDecodersTest.kt @@ -1,7 +1,8 @@ package arcs.core.data.proto -import arcs.core.data.PrimitiveType +import arcs.core.data.* import arcs.core.testutil.assertThrows +import arcs.core.testutil.fail import arcs.repoutils.runfilesDir import com.google.common.truth.Truth.assertThat import com.google.protobuf.Message.Builder @@ -12,6 +13,15 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 +/** + * Parses a given proto text as [TypeProto]. + */ +fun parseTypeProtoText(protoText: String): TypeProto { + val builder = TypeProto.newBuilder() + TextFormat.getParser().merge(protoText, builder) + return builder.build() +} + @RunWith(JUnit4::class) class TypeProtoDecodersTest { @Test @@ -23,4 +33,33 @@ class TypeProtoDecodersTest { PrimitiveTypeProto.UNRECOGNIZED.decode() } } + + @Test + fun decodesPrimitiveTypeAsFieldType() { + val textField = PrimitiveTypeProto.TEXT.decodeAsFieldType() + assertThat(textField.primitiveType).isEqualTo(PrimitiveType.Text) + val numberField = PrimitiveTypeProto.NUMBER.decodeAsFieldType() + assertThat(numberField.primitiveType).isEqualTo(PrimitiveType.Number) + val booleanField = PrimitiveTypeProto.BOOLEAN.decodeAsFieldType() + assertThat(booleanField.primitiveType).isEqualTo(PrimitiveType.Boolean) + } + + @Test + fun decodesTypeProtoAsFieldType() { + fun checkPrimitive(textProto: String, expected: PrimitiveType) { + val primitiveTypeProto = parseTypeProtoText(textProto) + val field = primitiveTypeProto.decodeAsFieldType() + when (field) { + is FieldType.Primitive -> + assertThat(field.primitiveType).isEqualTo(expected) + else -> fail("TypeProto should have been decoded to [FieldType.Primitive].") + } + } + checkPrimitive("primitive: TEXT", PrimitiveType.Text) + checkPrimitive("primitive: BOOLEAN", PrimitiveType.Boolean) + checkPrimitive("primitive: NUMBER", PrimitiveType.Number) + assertThrows(IllegalArgumentException::class) { + checkPrimitive("""variable: { name: "Blah"}""", PrimitiveType.Text) + } + } } diff --git a/javatests/arcs/core/storage/handle/CollectionIntegrationTest.kt b/javatests/arcs/core/storage/handle/CollectionIntegrationTest.kt index 877276839e8..a6110f4e71a 100644 --- a/javatests/arcs/core/storage/handle/CollectionIntegrationTest.kt +++ b/javatests/arcs/core/storage/handle/CollectionIntegrationTest.kt @@ -170,20 +170,25 @@ class CollectionIntegrationTest { fun addElementsWithTtls() = runBlockingTest { val person = Person("John", 29, false) collectionA.store(person.toRawEntity()) + val creationTimestampA = collectionA.fetchAll().first().creationTimestamp + assertThat(creationTimestampA).isNotEqualTo(RawEntity.UNINITIALIZED_TIMESTAMP) assertThat(collectionA.fetchAll().first().expirationTimestamp) - .isEqualTo(RawEntity.NO_EXPIRATION) + .isEqualTo(RawEntity.UNINITIALIZED_TIMESTAMP) val collectionC = CollectionImpl("collectionC", storageProxy, null, null, Ttl.Days(2), TimeImpl()) storageProxy.registerHandle(collectionC) assertThat(collectionC.store(person.toRawEntity())).isTrue() val entityC = collectionC.fetchAll().first() - assertThat(entityC.expirationTimestamp).isGreaterThan(RawEntity.NO_EXPIRATION) + assertThat(entityC.creationTimestamp).isGreaterThan(creationTimestampA) + assertThat(entityC.expirationTimestamp).isGreaterThan(RawEntity.UNINITIALIZED_TIMESTAMP) val collectionD = CollectionImpl("collectionD", storageProxy, null, null, Ttl.Minutes(1), TimeImpl()) storageProxy.registerHandle(collectionD) assertThat(collectionD.store(person.toRawEntity())).isTrue() val entityD = collectionD.fetchAll().first() - assertThat(entityD.expirationTimestamp).isGreaterThan(RawEntity.NO_EXPIRATION) + assertThat(entityD.creationTimestamp).isGreaterThan(creationTimestampA) + assertThat(entityD.creationTimestamp).isGreaterThan(entityC.creationTimestamp) + assertThat(entityD.expirationTimestamp).isGreaterThan(RawEntity.UNINITIALIZED_TIMESTAMP) assertThat(entityC.expirationTimestamp).isGreaterThan(entityD.expirationTimestamp) } diff --git a/javatests/arcs/core/storage/handle/SingletonIntegrationTest.kt b/javatests/arcs/core/storage/handle/SingletonIntegrationTest.kt index c07c7181369..d3083b5fdc4 100644 --- a/javatests/arcs/core/storage/handle/SingletonIntegrationTest.kt +++ b/javatests/arcs/core/storage/handle/SingletonIntegrationTest.kt @@ -138,20 +138,25 @@ class SingletonIntegrationTest { fun addEntityWithTtl() = runBlockingTest { val person = Person("Jane", 29, false) assertThat(singletonA.store(person.toRawEntity())).isTrue() + val creationTimestampA = requireNotNull(singletonA.fetch()).creationTimestamp; + assertThat(creationTimestampA).isNotEqualTo(RawEntity.UNINITIALIZED_TIMESTAMP) assertThat(requireNotNull(singletonA.fetch()).expirationTimestamp) - .isEqualTo(RawEntity.NO_EXPIRATION) + .isEqualTo(RawEntity.UNINITIALIZED_TIMESTAMP) val singletonC = SingletonImpl("singletonC", storageProxy, null, Ttl.Days(2), TimeImpl()) storageProxy.registerHandle(singletonC) assertThat(singletonC.store(person.toRawEntity())).isTrue() val entityC = requireNotNull(singletonC.fetch()) - assertThat(entityC.expirationTimestamp).isGreaterThan(RawEntity.NO_EXPIRATION) + assertThat(entityC.creationTimestamp).isGreaterThan(creationTimestampA) + assertThat(entityC.expirationTimestamp).isGreaterThan(RawEntity.UNINITIALIZED_TIMESTAMP) val singletonD = SingletonImpl("singletonD", storageProxy, null, Ttl.Minutes(1), TimeImpl()) storageProxy.registerHandle(singletonD) assertThat(singletonD.store(person.toRawEntity())).isTrue() val entityD = requireNotNull(singletonD.fetch()) - assertThat(entityD.expirationTimestamp).isGreaterThan(RawEntity.NO_EXPIRATION) + assertThat(entityD.creationTimestamp).isGreaterThan(creationTimestampA) + assertThat(entityD.creationTimestamp).isGreaterThan(entityC.creationTimestamp) + assertThat(entityD.expirationTimestamp).isGreaterThan(RawEntity.UNINITIALIZED_TIMESTAMP) assertThat(entityC.expirationTimestamp).isGreaterThan(entityD.expirationTimestamp) } diff --git a/src/runtime/capabilities.ts b/src/runtime/capabilities.ts index 46abeab4026..ad610736f37 100644 --- a/src/runtime/capabilities.ts +++ b/src/runtime/capabilities.ts @@ -18,6 +18,7 @@ export class Capabilities { } get isPersistent() { return this.capabilities.has('persistent'); } + get isQueryable() { return this.capabilities.has('queryable'); } get isTiedToRuntime() { return this.capabilities.has('tied-to-runtime'); } get isTiedToArc() { return this.capabilities.has('tied-to-arc'); } @@ -44,4 +45,6 @@ export class Capabilities { static readonly tiedToArc = new Capabilities(['tied-to-arc']); static readonly tiedToRuntime = new Capabilities(['tied-to-runtime']); static readonly persistent = new Capabilities(['persistent']); + static readonly queryable = new Capabilities(['queryable']); + static readonly persistentQueryable = new Capabilities(['persistent', 'queryable']); } diff --git a/src/runtime/manifest-ast-nodes.ts b/src/runtime/manifest-ast-nodes.ts index b684efeb131..0247547ef78 100644 --- a/src/runtime/manifest-ast-nodes.ts +++ b/src/runtime/manifest-ast-nodes.ts @@ -402,7 +402,7 @@ export interface ParticleConnectionTargetComponents extends BaseNode { export type RecipeHandleFate = string; -export type RecipeHandleCapability = 'persistent' | 'tied-to-runtime' | 'tied-to-arc'; +export type RecipeHandleCapability = 'persistent' | 'queryable' | 'tied-to-runtime' | 'tied-to-arc'; export interface RecipeHandle extends BaseNode { kind: 'handle'; diff --git a/src/runtime/manifest-parser.pegjs b/src/runtime/manifest-parser.pegjs index dc05292608d..da31979c846 100644 --- a/src/runtime/manifest-parser.pegjs +++ b/src/runtime/manifest-parser.pegjs @@ -1064,6 +1064,7 @@ RecipeHandleFate RecipeHandleCapability = 'persistent' + / 'queryable' / 'tied-to-runtime' / 'tied-to-arc' diff --git a/src/runtime/manifest.ts b/src/runtime/manifest.ts index 865ff437d13..0679cb9aa36 100644 --- a/src/runtime/manifest.ts +++ b/src/runtime/manifest.ts @@ -175,7 +175,7 @@ export class Manifest { } get allHandles() { // TODO(#4820) Update `reduce` to use flatMap - return [...new Set(this._findAll(manifest => manifest._recipes.reduce((acc, x) => acc.concat(x.handles), [])))]; + return this.allRecipes.reduce((acc, x) => acc.concat(x.handles), []); } get activeRecipe() { return this._recipes.find(recipe => recipe.annotation === 'active'); @@ -312,7 +312,7 @@ export class Manifest { return tags.filter(tag => !manifest.storeTags.get(store).includes(tag)).length === 0; } const stores = [...this._findAll(manifest => - manifest._stores.filter(store => this._typePredicate(store, type, subtype) && tagPredicate(manifest, store)))]; + manifest._stores.filter(store => this.typesMatch(store, type, subtype) && tagPredicate(manifest, store)))]; // Quick check that a new handle can fulfill the type contract. // Rewrite of this method tracked by https://github.com/PolymerLabs/arcs/issues/1636. @@ -323,16 +323,16 @@ export class Manifest { const tags = options.tags || []; const subtype = options.subtype || false; const fates = options.fates || []; - function tagPredicate(handle: Handle) { - return tags.filter(tag => !handle.tags.includes(tag)).length === 0; + function hasAllTags(handle: Handle) { + return tags.every(tag => handle.tags.includes(tag)); } - function fatePredicate(handle: Handle) { + function matchesFate(handle: Handle) { return fates === [] || fates.includes(handle.fate); } // TODO(#4820) Update `reduce` to use flatMap - return [...this._findAll(manifest => manifest._recipes + return [...this.allRecipes .reduce((acc, r) => acc.concat(r.handles), []) - .filter(h => this._typePredicate(h, type, subtype) && tagPredicate(h) && fatePredicate(h)))]; + .filter(h => this.typesMatch(h, type, subtype) && hasAllTags(h) && matchesFate(h))]; } findHandlesById(id: string): Handle[] { return this.allHandles.filter(h => h.id === id); @@ -343,7 +343,7 @@ export class Manifest { findRecipesByVerb(verb: string) { return [...this._findAll(manifest => manifest._recipes.filter(recipe => recipe.verbs.includes(verb)))]; } - _typePredicate(candidate: {type: Type}, type: Type, checkSubtype: boolean) { + private typesMatch(candidate: {type: Type}, type: Type, checkSubtype: boolean) { const resolvedType = type.resolvedType(); if (!resolvedType.isResolved()) { return (type instanceof CollectionType) === (candidate.type instanceof CollectionType) && diff --git a/src/runtime/refiner.ts b/src/runtime/refiner.ts index 16f0596f48f..ed2e2699f07 100644 --- a/src/runtime/refiner.ts +++ b/src/runtime/refiner.ts @@ -1172,13 +1172,16 @@ export class SQLExtracter { } } +// A constant is represented by an empty Term object, where there is no indeterminate. +const CONSTANT = '{}'; + export class Fraction { - num: Polynomial; - den: Polynomial; + num: Multinomial; + den: Multinomial; - constructor(n?: Polynomial, d?: Polynomial) { - this.num = n ? Polynomial.copyOf(n) : new Polynomial(); - this.den = d ? Polynomial.copyOf(d) : new Polynomial([1]); + constructor(n?: Multinomial, d?: Multinomial) { + this.num = n ? Multinomial.copyOf(n) : new Multinomial(); + this.den = d ? Multinomial.copyOf(d) : new Multinomial({[CONSTANT]: 1}); if (this.den.isZero()) { throw new Error('Division by zero.'); } @@ -1186,13 +1189,13 @@ export class Fraction { } static add(a: Fraction, b: Fraction): Fraction { - const den = Polynomial.multiply(a.den, b.den); - const num = Polynomial.add(Polynomial.multiply(a.num, b.den), Polynomial.multiply(b.num, a.den)); + const den = Multinomial.multiply(a.den, b.den); + const num = Multinomial.add(Multinomial.multiply(a.num, b.den), Multinomial.multiply(b.num, a.den)); return new Fraction(num, den); } static negate(a: Fraction): Fraction { - return new Fraction(Polynomial.negate(a.num), a.den); + return new Fraction(Multinomial.negate(a.num), a.den); } static subtract(a: Fraction, b: Fraction): Fraction { @@ -1201,7 +1204,7 @@ export class Fraction { } static multiply(a: Fraction, b: Fraction): Fraction { - return new Fraction(Polynomial.multiply(a.num, b.num), Polynomial.multiply(a.den, b.den)); + return new Fraction(Multinomial.multiply(a.num, b.num), Multinomial.multiply(a.den, b.den)); } static divide(a: Fraction, b: Fraction): Fraction { @@ -1211,12 +1214,21 @@ export class Fraction { reduce() { if (this.num.isZero()) { - this.den = new Polynomial([1]); + this.den = new Multinomial({[CONSTANT]: 1}); return; } - if (this.num.isConstant() && this.den.isConstant()) { - this.num = new Polynomial([this.num.coeffs[0]/this.den.coeffs[0]]); - this.den = new Polynomial([1]); + if (this.num.isConstant() && + this.den.isConstant() && + Number.isInteger(this.num.terms[CONSTANT]) && + Number.isInteger(this.den.terms[CONSTANT]) + ) { + const gcd = (a: number, b: number) => { + a = Math.abs(a); b = Math.abs(b); + return b === 0 ? a : gcd(b, a%b); + }; + const g = gcd(this.num.terms[CONSTANT], this.den.terms[CONSTANT]); + this.num = new Multinomial({[CONSTANT]: this.num.terms[CONSTANT]/g}); + this.den = new Multinomial({[CONSTANT]: this.den.terms[CONSTANT]/g}); return; } // TODO(ragdev): Fractions can be reduced further by factoring out the gcd of @@ -1234,9 +1246,10 @@ export class Fraction { const fn = Fraction.fromExpression(expr.expr); return Fraction.updateGivenOp(expr.operator.op, [fn]); } else if (expr instanceof FieldNamePrimitive && expr.evalType === Primitive.NUMBER) { - return new Fraction(new Polynomial([0, 1], expr.value)); + const term = new Term({[expr.value]: 1}); + return new Fraction(new Multinomial({[term.toKey()]: 1})); } else if (expr instanceof NumberPrimitive) { - return new Fraction(new Polynomial([expr.value])); + return new Fraction(new Multinomial({[CONSTANT]: expr.value})); } throw new Error(`Cannot resolve expression: ${expr.toString()}`); } @@ -1254,143 +1267,219 @@ export class Fraction { } } -export class Polynomial { - private _coeffs: number[]; - indeterminate?: string; +export class Term { + private _indeterminates: Dictionary; - constructor(coeffs: number[] = [0], variable?: string) { - this.coeffs = coeffs; - this.indeterminate = variable; + constructor(indeterminates: Dictionary = {}) { + this.indeterminates = indeterminates; } - static copyOf(pn: Polynomial): Polynomial { - return new Polynomial(pn.coeffs, pn.indeterminate); + static copyOf(tm: Term): Term { + return new Term(tm.indeterminates); } - get coeffs(): number[] { - while (this._coeffs.length >= 1 && this._coeffs[this._coeffs.length - 1] === 0) { - this._coeffs.pop(); + get indeterminates(): Dictionary { + for (const [indeterminate, power] of Object.entries(this._indeterminates)) { + if (power === 0) { + delete this._indeterminates[indeterminate]; + } } - if (this._coeffs.length === 0) { - this._coeffs.push(0); + return this._indeterminates; + } + + set indeterminates(indtrms: Dictionary) { + this._indeterminates = {...indtrms}; + } + + toKey(): string { + // sort the indeterminates + const ordered = {}; + const unordered = this.indeterminates; + Object.keys(unordered).sort().forEach((key) => { + ordered[key] = unordered[key]; + }); + return JSON.stringify(ordered); + } + + static fromKey(key: string): Term { + return new Term(JSON.parse(key)); + } + + static indeterminateToExpression(fn: string, pow: number): RefinementExpression { + if (pow <= 0) { + throw new Error('Must have positive power.'); } - return this._coeffs; + if (pow === 1) { + return new FieldNamePrimitive(fn, Primitive.NUMBER); + } + return new BinaryExpression( + Term.indeterminateToExpression(fn, 1), + Term.indeterminateToExpression(fn, pow - 1), + new RefinementOperator(Op.MUL)); } - set coeffs(cfs: number[]) { - this._coeffs = [...cfs]; + // assumes that term is not a constant i.e. not {} + toExpression(): RefinementExpression { + if (Object.keys(this.indeterminates).length === 0) { + throw new Error('Cannot convert an empty term to expression'); + } + let expr = null; + for (const [indeterminate, power] of Object.entries(this.indeterminates)) { + const indtrExpr = Term.indeterminateToExpression(indeterminate, power); + expr = expr ? new BinaryExpression(expr, indtrExpr, new RefinementOperator(Op.MUL)) : indtrExpr; + } + return expr; } +} - degree(): number { - return this.coeffs.length - 1; +export class Multinomial { + private _terms: Dictionary; + + constructor(terms: Dictionary = {}) { + this.terms = terms; + } + + static copyOf(mn: Multinomial): Multinomial { + return new Multinomial(mn.terms); } - static assertCompatibility(a: Polynomial, b: Polynomial) { - if (a.indeterminate && b.indeterminate && a.indeterminate !== b.indeterminate) { - throw new Error('Incompatible polynomials'); + get terms(): Dictionary { + for (const [term, coeff] of Object.entries(this._terms)) { + if (coeff === 0) { + delete this._terms[term]; + } } + const ordered = {}; + const unordered = this._terms; + Object.keys(unordered).sort().forEach((key) => { + ordered[key] = unordered[key]; + }); + this._terms = ordered; + return this._terms; + } + + set terms(tms: Dictionary) { + this._terms = {...tms}; } - static add(a: Polynomial, b: Polynomial): Polynomial { - Polynomial.assertCompatibility(a, b); - const sum = a.degree() > b.degree() ? Polynomial.copyOf(a) : Polynomial.copyOf(b); - const other = a.degree() > b.degree() ? b : a; - for (const [i, coeff] of other.coeffs.entries()) { - sum.coeffs[i] += coeff; + static add(a: Multinomial, b: Multinomial): Multinomial { + const sum = Multinomial.copyOf(a); + for (const [term, coeff] of Object.entries(b.terms)) { + const val = (sum.terms[term] || 0) + coeff; + sum.terms[term] = val; } return sum; } - static subtract(a: Polynomial, b: Polynomial): Polynomial { - Polynomial.assertCompatibility(a, b); - return Polynomial.add(a, Polynomial.negate(b)); + static subtract(a: Multinomial, b: Multinomial): Multinomial { + return Multinomial.add(a, Multinomial.negate(b)); } - static negate(a: Polynomial): Polynomial { - return new Polynomial(a.coeffs.map(co => -co), a.indeterminate); + static negate(a: Multinomial): Multinomial { + const neg = Multinomial.copyOf(a); + for (const [term, coeff] of Object.entries(neg.terms)) { + neg.terms[term] = -coeff; + } + return neg; } - static multiply(a: Polynomial, b: Polynomial): Polynomial { - Polynomial.assertCompatibility(a, b); - const deg = a.degree() + b.degree(); - const coeffs = new Array(deg+1).fill(0); - for (let i = 0; i < coeffs.length; i += 1) { - for (let j = 0; j <= i; j += 1) { - if (j <= a.degree() && (i-j) <= b.degree()) { - coeffs[i] += a.coeffs[j]*b.coeffs[i-j]; + static multiply(a: Multinomial, b: Multinomial): Multinomial { + const prod = new Multinomial(); + for (const [aKey, acoeff] of Object.entries(a.terms)) { + for (const [bKey, bcoeff] of Object.entries(b.terms)) { + const tprod = Term.fromKey(aKey); + const bterm = Term.fromKey(bKey); + for (const [indeterminate, power] of Object.entries(bterm.indeterminates)) { + const val = power + (tprod.indeterminates[indeterminate] || 0); + tprod.indeterminates[indeterminate] = val; } + const val = acoeff*bcoeff + (prod.terms[tprod.toKey()] || 0); + prod.terms[tprod.toKey()] = val; } } - return new Polynomial(coeffs, a.indeterminate || b.indeterminate); + return prod; } isZero(): boolean { - return this.isConstant() && this.coeffs[0] === 0; + return Object.keys(this.terms).length === 0; } isConstant(): boolean { - return this.degree() === 0; + return this.isZero() || (Object.keys(this.terms).length === 1 && this.terms.hasOwnProperty(CONSTANT)); } - static inderminateToExpression(pow: number, fn: string): RefinementExpression { - if (pow < 0) { - throw new Error('Must have non-negative power.'); - } - if (pow === 0) { - return new NumberPrimitive(1); + getIndeterminates(): Set { + const indeterminates = new Set(); + for (const tKey of Object.keys(this.terms)) { + const term = Term.fromKey(tKey); + for (const indeterminate of Object.keys(term.indeterminates)) { + indeterminates.add(indeterminate); + } } - if (pow === 1) { - return new FieldNamePrimitive(fn, Primitive.NUMBER); + return indeterminates; + } + + isUnivariate(): boolean { + return this.getIndeterminates().size === 1; + } + + degree(): number { + let degree = 0; + for (const tKey of Object.keys(this.terms)) { + const term = Term.fromKey(tKey); + let sum = 0; + for (const power of Object.values(term.indeterminates)) { + sum += power; + } + degree = sum > degree ? sum : degree; } - return new BinaryExpression( - Polynomial.inderminateToExpression(1, fn), - Polynomial.inderminateToExpression(pow-1, fn), - new RefinementOperator(Op.MUL)); + return degree; } - // returns ax^n + bx^n-1 + ... c 0 + // returns CONSTANT toExpression(op: Op): RefinementExpression { - if (this.degree() === 0) { + if (this.isConstant()) { return new BinaryExpression( - new NumberPrimitive(this.coeffs[0]), + new NumberPrimitive(this.isZero() ? 0 : this.terms[CONSTANT]), new NumberPrimitive(0), new RefinementOperator(op)); } - if (!this.indeterminate) { - throw new Error('FieldName not found!'); - } - if (this.degree() === 1) { - // ax+b 0 => x -b/a + if (this.isUnivariate() && this.degree() === 1) { const operator = new RefinementOperator(op); - if (this.coeffs[1] < 0) { + const indeterminate = this.getIndeterminates().values().next().value; + // TODO(ragdev): Implement a neater way to get the leading coefficient + const leadingCoeff = this.terms[`{"${indeterminate}":1}`]; + const cnst = this.terms[CONSTANT] || 0; + if (leadingCoeff < 0) { operator.flip(); } return new BinaryExpression( - new FieldNamePrimitive(this.indeterminate, Primitive.NUMBER), - new NumberPrimitive(-this.coeffs[0]/this.coeffs[1]), + new FieldNamePrimitive(indeterminate, Primitive.NUMBER), + new NumberPrimitive(-cnst/leadingCoeff), operator); } - let expr = null; - for (let i = this.coeffs.length - 1; i >= 0; i -= 1) { - if (this.coeffs[i] === 0) { - continue; + const termToExpression = (tKey: string, tCoeff: number) => { + const termExpr = Term.fromKey(tKey).toExpression(); + if (tCoeff === 1) { + return termExpr; } - let termExpr = null; - if (i > 0) { - termExpr = Polynomial.inderminateToExpression(i, this.indeterminate); - if (Math.abs(this.coeffs[i]) !== 1) { - termExpr = new BinaryExpression( - new NumberPrimitive(this.coeffs[i]), - termExpr, - new RefinementOperator(Op.MUL) - ); - } + return new BinaryExpression( + new NumberPrimitive(tCoeff), + termExpr, + new RefinementOperator(Op.MUL) + ); + }; + let expr = null; + let cnst = 0; + for (const [tKey, tCoeff] of Object.entries(this.terms)) { + if (tKey === CONSTANT) { + cnst = tCoeff; } else { - termExpr = new NumberPrimitive(this.coeffs[0]); + const termExpr = termToExpression(tKey, tCoeff); + expr = expr ? new BinaryExpression(expr, termExpr, new RefinementOperator(Op.ADD)) : termExpr; } - expr = expr ? new BinaryExpression(expr, termExpr, new RefinementOperator(Op.ADD)) : termExpr; } - return new BinaryExpression(expr, new NumberPrimitive(0), new RefinementOperator(op)); + return new BinaryExpression(expr, new NumberPrimitive(-cnst), new RefinementOperator(op)); } } diff --git a/src/runtime/storageNG/testing/mock-firebase.ts b/src/runtime/storageNG/testing/mock-firebase.ts index 6dfc5feccc7..3c7d65de2e5 100644 --- a/src/runtime/storageNG/testing/mock-firebase.ts +++ b/src/runtime/storageNG/testing/mock-firebase.ts @@ -355,7 +355,7 @@ export class MockFirebaseStorageDriverProvider extends FirebaseStorageDriverProv const {projectId, domain, apiKey} = mockFirebaseStorageKeyOptions; CapabilitiesResolver.registerKeyCreator( 'firebase', - Capabilities.persistent, + Capabilities.persistentQueryable, (options: StorageKeyOptions) => new FirebaseStorageKey(projectId, domain, apiKey, options.location())); } diff --git a/src/runtime/tests/capabilities-resolver-test.ts b/src/runtime/tests/capabilities-resolver-test.ts index bc70c859db6..8b62df3434b 100644 --- a/src/runtime/tests/capabilities-resolver-test.ts +++ b/src/runtime/tests/capabilities-resolver-test.ts @@ -117,5 +117,7 @@ describe('Capabilities Resolver', () => { assert.sameMembers([...resolver2.findStorageKeyProtocols(Capabilities.tiedToArc)], ['volatile']); assert.sameMembers([...resolver2.findStorageKeyProtocols(Capabilities.tiedToRuntime)], ['ramdisk']); assert.sameMembers([...resolver2.findStorageKeyProtocols(Capabilities.persistent)], ['firebase']); + assert.sameMembers([...resolver2.findStorageKeyProtocols(Capabilities.queryable)], ['firebase']); + assert.sameMembers([...resolver2.findStorageKeyProtocols(Capabilities.persistentQueryable)], ['firebase']); }); }); diff --git a/src/runtime/tests/capabilities-test.ts b/src/runtime/tests/capabilities-test.ts index c9ca492355c..9d1eeebeaf4 100644 --- a/src/runtime/tests/capabilities-test.ts +++ b/src/runtime/tests/capabilities-test.ts @@ -13,15 +13,22 @@ import {Capabilities} from '../capabilities.js'; describe('Capabilities', () => { it('verifies same capabilities', () => { assert.isTrue(Capabilities.persistent.isSame(Capabilities.persistent)); + assert.isTrue(Capabilities.queryable.isSame(Capabilities.queryable)); + assert.isTrue(Capabilities.persistentQueryable.isSame( + Capabilities.persistentQueryable)); assert.isTrue(Capabilities.tiedToRuntime.isSame(Capabilities.tiedToRuntime)); assert.isTrue(Capabilities.tiedToArc.isSame(Capabilities.tiedToArc)); assert.isFalse(Capabilities.persistent.isSame(Capabilities.tiedToRuntime)); assert.isFalse(Capabilities.tiedToRuntime.isSame(Capabilities.tiedToArc)); assert.isFalse(Capabilities.tiedToArc.isSame(Capabilities.persistent)); + assert.isFalse(Capabilities.queryable.isSame(Capabilities.persistentQueryable)); + assert.isTrue(new Capabilities(['persistent', 'tied-to-arc']).isSame( new Capabilities(['persistent', 'tied-to-arc']))); + assert.isTrue(new Capabilities(['persistent', 'queryable']).isSame( + Capabilities.persistentQueryable)); assert.isFalse(new Capabilities(['persistent', 'tied-to-arc']).isSame(Capabilities.persistent)); assert.isFalse(Capabilities.persistent.isSame( new Capabilities(['persistent', 'tied-to-arc']))); @@ -29,12 +36,19 @@ describe('Capabilities', () => { it('verifies contained capabilities', () => { assert.isTrue(Capabilities.persistent.contains(Capabilities.persistent)); + assert.isTrue(Capabilities.queryable.contains(Capabilities.queryable)); + assert.isTrue(Capabilities.persistentQueryable.contains(Capabilities.persistentQueryable)); + assert.isTrue(Capabilities.persistentQueryable.contains(Capabilities.persistent)); + assert.isTrue(Capabilities.persistentQueryable.contains(Capabilities.queryable)); assert.isTrue(Capabilities.tiedToRuntime.contains(Capabilities.tiedToRuntime)); assert.isTrue(Capabilities.tiedToArc.contains(Capabilities.tiedToArc)); assert.isFalse(Capabilities.persistent.contains(Capabilities.tiedToRuntime)); assert.isFalse(Capabilities.tiedToRuntime.contains(Capabilities.tiedToArc)); assert.isFalse(Capabilities.tiedToArc.contains(Capabilities.persistent)); + assert.isFalse(Capabilities.persistent.contains(Capabilities.persistentQueryable)); + assert.isFalse(Capabilities.queryable.contains(Capabilities.persistentQueryable)); + assert.isFalse(Capabilities.queryable.contains(Capabilities.persistent)); assert.isTrue(new Capabilities(['persistent', 'tied-to-arc']).contains( new Capabilities(['persistent', 'tied-to-arc']))); diff --git a/src/runtime/tests/refiner-test.ts b/src/runtime/tests/refiner-test.ts index 0fd1a6643b1..740fb75cb1b 100644 --- a/src/runtime/tests/refiner-test.ts +++ b/src/runtime/tests/refiner-test.ts @@ -8,7 +8,7 @@ * http://polymer.github.io/PATENTS.txt */ -import {Range, Segment, Refinement, BinaryExpression, UnaryExpression, SQLExtracter, Polynomial, Fraction} from '../refiner.js'; +import {Range, Segment, Refinement, BinaryExpression, Multinomial, SQLExtracter, Fraction, Term} from '../refiner.js'; import {parse} from '../../gen/runtime/manifest-parser.js'; import {assert} from '../../platform/chai-web.js'; import {Manifest} from '../manifest.js'; @@ -364,14 +364,14 @@ describe('normalisation', () => { it(`tests if expressions are rearranged 3`, Flags.withFieldRefinementsAllowed(async () => { const manifestAst1 = parse(` particle Foo - input: reads Something {num: Number [ (num+2)*(num+1)+3 > 4 ] } + input: reads Something {num: Number [ (num+2)*(num+1)+3 > 6 ] } `); const typeData = {'num': 'Number'}; const ref1 = Refinement.fromAst(manifestAst1[0].args[0].type.fields[0].type.refinement, typeData); ref1.normalize(); const manifestAst2 = parse(` particle Foo - input: reads Something {num: Number [ num*num + num*3 + 1 > 0 ] } + input: reads Something {num: Number [ num*3 + num*num > 1 ] } `); const ref2 = Refinement.fromAst(manifestAst2[0].args[0].type.fields[0].type.refinement, typeData); // normalized version of ref1 should be the same as ref2 @@ -393,6 +393,40 @@ describe('normalisation', () => { // normalized version of ref1 should be the same as ref2 assert.strictEqual(JSON.stringify(ref1), JSON.stringify(ref2)); })); + it(`tests if expressions are rearranged 5`, () => { + const manifestAst1 = parse(` + particle Foo + input: reads Something {a: Number, b: Number } [3*(a+b) > a+2*b] + `); + const typeData = {'a': 'Number', 'b': 'Number'}; + const ref1 = Refinement.fromAst(manifestAst1[0].args[0].type.refinement, typeData); + ref1.normalize(); + const manifestAst2 = parse(` + particle Foo + input: reads Something {a: Number, b: Number } [b + a*2 > 0] + `); + const ref2 = Refinement.fromAst(manifestAst2[0].args[0].type.refinement, typeData); + // normalized version of ref1 should be the same as ref2 + assert.strictEqual(JSON.stringify(ref1), JSON.stringify(ref2)); + }); + it(`tests if expressions are rearranged 6`, () => { + const manifestAst1 = parse(` + particle Foo + input: reads Something {a: Number, b: Number } [2*(a+b) > a+2*b] + `); + const typeData = {'a': 'Number', 'b': 'Number'}; + const ref1 = Refinement.fromAst(manifestAst1[0].args[0].type.refinement, typeData); + ref1.normalize(); + const manifestAst2 = parse(` + particle Foo + input: reads Something {a: Number, b: Number } [a > 0] + `); + const ref2 = Refinement.fromAst(manifestAst2[0].args[0].type.refinement, typeData); + console.log(ref1.toString()); + console.log(`${ref2}`); + // normalized version of ref1 should be the same as ref2 + assert.strictEqual(JSON.stringify(ref1), JSON.stringify(ref2)); + }); }); describe('Range', () => { @@ -487,7 +521,7 @@ describe('SQLExtracter', () => { `); const schema = manifest.particles[0].handleConnectionMap.get('input').type.getEntitySchema(); const query: string = SQLExtracter.fromSchema(schema, 'table'); - assert.strictEqual(query, 'SELECT * FROM table WHERE ((a + (b / 3)) > 100) AND ((a > 3) AND (NOT (a = 100))) AND ((b > 20) AND (b < 100));'); + assert.strictEqual(query, 'SELECT * FROM table WHERE ((b + (a * 3)) > 300) AND ((a > 3) AND (NOT (a = 100))) AND ((b > 20) AND (b < 100));'); })); it('tests can create queries from refinement expressions involving boolean expressions', Flags.withFieldRefinementsAllowed(async () => { const manifest = await Manifest.parse(` @@ -527,137 +561,285 @@ describe('SQLExtracter', () => { }); }); -describe('Polynomial', () => { - it('tests coeffs getters, setters and degree works', () => { - let pn = new Polynomial([0, 1, 2]); // 2a^2 + a - assert.deepEqual(pn.coeffs, [0, 1, 2]); - assert.strictEqual(pn.degree(), 2); - pn = new Polynomial([0, 1, 2, 0, 0, 0]); // 2a^2 + a - assert.strictEqual(pn.degree(), 2); - pn = new Polynomial([0, 0, 0, 0]); // 0 - assert.deepEqual(pn.coeffs, [0]); - assert.strictEqual(pn.degree(), 0); - pn = new Polynomial([]); // 0 - assert.deepEqual(pn.coeffs, [0]); - assert.strictEqual(pn.degree(), 0); - pn = new Polynomial(); // 0 - assert.deepEqual(pn.coeffs, [0]); - assert.strictEqual(pn.degree(), 0); +describe('Fractions', () => { + it('tests fraction addition works', () => { + const a = new Term({'a': 1}); // a + const a2 = new Term({'a': 2}); // a^2 + const cnst = new Term({}); + let num1 = new Multinomial({ + [a2.toKey()]: 1, // a^2 + [a.toKey()]: 1, // a + [cnst.toKey()]: 9 // 9 + }); // a^2 + a + 9 + const den1 = new Multinomial({ + [a.toKey()]: 2, // 2a + }); // 2a + let frac1 = new Fraction(num1, den1); // (a^2+a+9)/2a + let num2 = new Multinomial({ + [a.toKey()]: 1, // a + [cnst.toKey()]: 5 // 5 + }); // a+5 + let den2 = new Multinomial({ + [cnst.toKey()]: 3 // 3 + }); // 3 + let frac2 = new Fraction(num2, den2); // (a+5)/3 + let sum = Fraction.add(frac1, frac2); // (5a^2+13a+27)/6a + assert.deepEqual(sum.num.terms, { + [a2.toKey()]: 5, // a^2 + [a.toKey()]: 13, // a + [cnst.toKey()]: 27 // 9 + }); + assert.deepEqual(sum.den.terms, { + [a.toKey()]: 6 // a + }); + num1 = new Multinomial({ + [a.toKey()]: 1, // a + }); // a + frac1 = new Fraction(num1); // a/1 + num2 = new Multinomial({ + [cnst.toKey()]: 5 // 5 + }); // 5 + den2 = new Multinomial({ + [cnst.toKey()]: 9 // 9 + }); // 9 + frac2 = new Fraction(num2, den2); // 5/9 + sum = Fraction.add(frac1, frac2); // (9a+5)/9 + assert.deepEqual(sum.num.terms, { + [a.toKey()]: 9, // 9a + [cnst.toKey()]: 5 // 5 + }); + assert.deepEqual(sum.den.terms, { + [cnst.toKey()]: 9 // 9 + }); }); - it('tests polynomial addition works', () => { - let pn1 = new Polynomial([0, 1, 2]); // 2a^2 + a - let pn2 = new Polynomial(); // 0 - let sum = Polynomial.add(pn1, pn2); // 2a^2 + a - assert.deepEqual(sum.coeffs, [0, 1, 2]); - assert.strictEqual(sum.degree(), 2); - pn1 = new Polynomial([0, 1, 2]); // 2a^2 + a - pn2 = new Polynomial([3, 2, 1]); // a^2 + 2a + 3 - sum = Polynomial.add(pn1, pn2); // 3a^2 + 3a + 3 - assert.deepEqual(sum.coeffs, [3, 3, 3]); - assert.strictEqual(sum.degree(), 2); - pn1 = new Polynomial([0, 1, 2]); // 2a^2 + a - pn2 = new Polynomial([3, 0, 0, 0, 0]); // 3 - sum = Polynomial.add(pn1, pn2); // 2a^2 + a + 3 - assert.deepEqual(sum.coeffs, [3, 1, 2]); - assert.strictEqual(sum.degree(), 2); + it('tests fraction subtraction works', () => { + const a = new Term({'a': 1}); // a + const a2 = new Term({'a': 2}); // a^2 + const cnst = new Term({}); + const num1 = new Multinomial({ + [a2.toKey()]: 1, // a^2 + [a.toKey()]: 1, // a + [cnst.toKey()]: 9 // 9 + }); // a^2 + a + 9 + const den1 = new Multinomial({ + [a.toKey()]: 2, // 2a + }); // 2a + const frac1 = new Fraction(num1, den1); // (a^2+a+9)/2a + const num2 = new Multinomial({ + [a.toKey()]: 1, // a + [cnst.toKey()]: 5 // 5 + }); // a+5 + const den2 = new Multinomial({ + [cnst.toKey()]: 3 // 3 + }); // 3 + const frac2 = new Fraction(num2, den2); // (a+5)/3 + const sum = Fraction.subtract(frac1, frac2); // (a^2-7a+27)/6a + assert.deepEqual(sum.num.terms, { + [a2.toKey()]: 1, // a^2 + [a.toKey()]: -7, // -7a + [cnst.toKey()]: 27 // 27 + }); + assert.deepEqual(sum.den.terms, { + [a.toKey()]: 6, // 6a + }); }); - it('tests polynomial negation works', () => { - const pn1 = new Polynomial([0, 1, 2]); // 2a^2 + a - const neg = Polynomial.negate(pn1); // -2a^2 -a - assert.deepEqual(neg.coeffs, [-0, -1, -2]); - assert.strictEqual(neg.degree(), 2); + it('tests fraction negation works', () => { + const a = new Term({'a': 1}); // a + const a2 = new Term({'a': 2}); // a^2 + const cnst = new Term({}); + const num1 = new Multinomial({ + [a2.toKey()]: 1, // a^2 + [a.toKey()]: 1, // a + [cnst.toKey()]: 9 // 9 + }); // a^2 + a + 9 + const den1 = new Multinomial({ + [a.toKey()]: 2, // 2a + }); // 2a + const frac1 = new Fraction(num1, den1); // (a^2+a+9)/2a + const neg = Fraction.negate(frac1); // (-a^2-a+-9)/2a + assert.deepEqual(neg.num.terms, { + [a2.toKey()]: -1, // a^2 + [a.toKey()]: -1, // a + [cnst.toKey()]: -9 // 9 + }); + assert.deepEqual(neg.den.terms, { + [a.toKey()]: 2, // 2a + }); }); - it('tests polynomial subtraction works', () => { - const pn1 = new Polynomial([0, 1]); // a - const pn2 = new Polynomial([2, 2, 2]); // 2a^2 + 2a + 2 - const sub = Polynomial.subtract(pn1, pn2); // -2a^2 -a -2 - assert.deepEqual(sub.coeffs, [-2, -1, -2]); - assert.strictEqual(sub.degree(), 2); + it('tests fraction multiplication works', () => { + const a = new Term({'a': 1}); // a + const a2 = new Term({'a': 2}); // a^2 + const cnst = new Term({}); + let num1 = new Multinomial({ + [a.toKey()]: 1, // a + [cnst.toKey()]: 9 // 9 + }); // a + 9 + const den1 = new Multinomial({ + [a.toKey()]: 2, // 2a + }); // 2a + let frac1 = new Fraction(num1, den1); // (a+9)/2a + const num2 = new Multinomial({ + [a.toKey()]: 1, // a + [cnst.toKey()]: 5 // 5 + }); // a + 5 + const den2 = new Multinomial({ + [cnst.toKey()]: 3 // 9 + }); // a + 9 + let frac2 = new Fraction(num2, den2); // (a+5)/3 + let sum = Fraction.multiply(frac1, frac2); // (a^2+14a+45)/6a + assert.deepEqual(sum.num.terms, { + [a2.toKey()]: 1, // a^2 + [a.toKey()]: 14, // 14a + [cnst.toKey()]: 45 // 45 + }); // a^2 + 14a + 45 + assert.deepEqual(sum.den.terms, { + [a.toKey()]: 6, // 14a + }); + num1 = new Multinomial({ + [a2.toKey()]: 1, // a^2 + [a.toKey()]: 1, // a + }); + frac1 = new Fraction(num1); // (a^2+a)/1 + frac2 = new Fraction(); // 0 / 1 + sum = Fraction.multiply(frac1, frac2); // 0/1 + assert.deepEqual(sum.num.terms, {}); + assert.deepEqual(sum.den.terms, { + [cnst.toKey()]: 1 // 1 + }); }); - it('tests polynomial multiplication works', () => { - let pn1 = new Polynomial([0, 1, 2]); // 2a^2 + a - let pn2 = new Polynomial(); // 0 - let prod = Polynomial.multiply(pn1, pn2); // 0 - assert.deepEqual(prod.coeffs, [0]); - assert.strictEqual(prod.degree(), 0); - pn1 = new Polynomial([9, 2]); // 2a + 9 - pn2 = new Polynomial([3, -2]); // -2a + 3 - prod = Polynomial.multiply(pn1, pn2); // -4a^2 -12a + 27 - assert.deepEqual(prod.coeffs, [27, -12, -4]); - assert.strictEqual(prod.degree(), 2); - pn1 = new Polynomial([3, -2]); // -2a + 3 - pn2 = new Polynomial([9, 2, 7, 11]); // 11a^3 + 7a^2 + 2a + 9 - prod = Polynomial.multiply(pn1, pn2); // -22a^4 + 19a^3 + 17a^2 - 12a + 27 - assert.deepEqual(prod.coeffs, [27, -12, 17, 19, -22]); - assert.strictEqual(prod.degree(), 4); + it('tests fraction division works', () => { + const a = new Term({'a': 1}); // a + const a2 = new Term({'a': 2}); // a^2 + const cnst = new Term({}); + const num1 = new Multinomial({ + [a.toKey()]: 1, // a + [cnst.toKey()]: 9 // 9 + }); // a + 9 + const den1 = new Multinomial({ + [a.toKey()]: 2, // 2a + }); // 2a + const frac1 = new Fraction(num1, den1); // (a+9)/2a + const num2 = new Multinomial({ + [a.toKey()]: 1, // a + [cnst.toKey()]: 5 // 5 + }); // a + 5 + const den2 = new Multinomial({ + [cnst.toKey()]: 3 // 9 + }); // a + 9 + const frac2 = new Fraction(num2, den2); // (a+5)/3 + const sum = Fraction.divide(frac1, frac2); // (3a+27)/(2a^2+10a) + assert.deepEqual(sum.num.terms, { + [a.toKey()]: 3, // 3a + [cnst.toKey()]: 27 // 27 + }); + assert.deepEqual(sum.den.terms, { + [a2.toKey()]: 2, // 2a^2 + [a.toKey()]: 10, // 10a + }); }); }); -describe('Fractions', () => { - it('tests fraction addition works', () => { - let num1 = new Polynomial([9, 1, 1]); - const den1 = new Polynomial([0, 2]); - let frac1 = new Fraction(num1, den1); // (a^2+a+9)/2a - let num2 = new Polynomial([5, 1]); - let den2 = new Polynomial([3]); - let frac2 = new Fraction(num2, den2); // (a+5)/3 - let sum = Fraction.add(frac1, frac2); // (5a^2+13a+27)/6a - assert.deepEqual(sum.num.coeffs, [27, 13, 5]); - assert.deepEqual(sum.den.coeffs, [0, 6]); - num1 = new Polynomial([0, 1]); - frac1 = new Fraction(num1); // a/1 - num2 = new Polynomial([5]); - den2 = new Polynomial([9]); - frac2 = new Fraction(num2, den2); // 0.55/1 - sum = Fraction.add(frac1, frac2); // (a+0.55)/1 - assert.deepEqual(sum.num.coeffs, [5/9, 1]); - assert.deepEqual(sum.den.coeffs, [1]); +describe('Terms', () => { + it('tests to and from key', () => { + const term1 = new Term({'b': 1, 'a': 1}); + const term2 = new Term({'a': 1, 'b': 1}); + assert.strictEqual(term1.toKey(), term2.toKey()); + assert.deepEqual(Term.fromKey(term1.toKey()), Term.fromKey(term2.toKey())); }); - it('tests fraction subtraction works', () => { - const num1 = new Polynomial([9, 1, 1]); - const den1 = new Polynomial([0, 2]); - const frac1 = new Fraction(num1, den1); // (a^2+a+9)/2a - const num2 = new Polynomial([5, 1]); - const den2 = new Polynomial([3]); - const frac2 = new Fraction(num2, den2); // (a+5)/3 - const sum = Fraction.subtract(frac1, frac2); // (a^2-7a+27)/6a - assert.deepEqual(sum.num.coeffs, [27, -7, 1]); - assert.deepEqual(sum.den.coeffs, [0, 6]); +}); + +describe('Multinomials', () => { + it('tests multinomial setters and getters work', () => { + const aIb = new Term({'b': 1, 'a': 1}); // ab + const aIb2 = new Term({'b': 2, 'a': 1}); // ab^2 + const cnst = new Term({}); + const num = new Multinomial({ + [aIb.toKey()]: 2, // 2ab + [aIb2.toKey()]: 1, // ab^2 + [cnst.toKey()]: 5 // 5 + }); // 2ab + ab^2 + 5 + assert.deepEqual(num.terms, { + [aIb2.toKey()]: 1, // ab^2 + [cnst.toKey()]: 5, // 5 + [aIb.toKey()]: 2, // 2ab + }); }); - it('tests fraction negation works', () => { - const num1 = new Polynomial([9, 1, 1]); - const den1 = new Polynomial([0, 2]); - const frac1 = new Fraction(num1, den1); // (a^2+a+9)/2a - const sum = Fraction.negate(frac1); // (-a^2-a+-9)/2a - assert.deepEqual(sum.num.coeffs, [-9, -1, -1]); - assert.deepEqual(sum.den.coeffs, [0, 2]); + it('tests multinomial addition works', () => { + const aIb = new Term({'b': 1, 'a': 1}); // ab + const aIb2 = new Term({'b': 2, 'a': 1}); // ab^2 + const cnst = new Term({}); + const num1 = new Multinomial({ + [aIb2.toKey()]: 2, // 2ab^2 + [cnst.toKey()]: 1 // 5 + }); // 2ab^2 + 1 + const num2 = new Multinomial({ + [aIb.toKey()]: 2, // 2ab + [aIb2.toKey()]: 1, // ab^2 + [cnst.toKey()]: 5 // 5 + }); // 2ab + ab^2 + 5 + const sum = Multinomial.add(num1, num2); // 2ab + 3ab^2 + 5 + assert.deepEqual(sum.terms, { + [aIb.toKey()]: 2, // 2ab + [aIb2.toKey()]: 3, // 3ab^2 + [cnst.toKey()]: 6 // 6 + }); }); - it('tests fraction multiplication works', () => { - let num1 = new Polynomial([9, 1]); - const den1 = new Polynomial([0, 2]); - let frac1 = new Fraction(num1, den1); // (a+9)/2a - const num2 = new Polynomial([5, 1]); - const den2 = new Polynomial([3]); - let frac2 = new Fraction(num2, den2); // (a+5)/3 - let sum = Fraction.multiply(frac1, frac2); // (a^2+14a+45)/6a - assert.deepEqual(sum.num.coeffs, [45, 14, 1]); - assert.deepEqual(sum.den.coeffs, [0, 6]); - num1 = new Polynomial([0, 1, 1]); - frac1 = new Fraction(num1); // (a^2+a)/1 - frac2 = new Fraction(); // 0 / 1 - sum = Fraction.multiply(frac1, frac2); // 0/1 - assert.deepEqual(sum.num.coeffs, [0]); - assert.deepEqual(sum.den.coeffs, [1]); + it('tests multinomial negation works', () => { + const aIb2 = new Term({'b': 2, 'a': 1}); // ab^2 + const cnst = new Term({}); + const num1 = new Multinomial({ + [aIb2.toKey()]: 2, // 2ab^2 + [cnst.toKey()]: 1 // 5 + }); // 2ab^2 + 1 + const sum = Multinomial.negate(num1); // -2ab^2 - 1 + assert.deepEqual(sum.terms, { + [aIb2.toKey()]: -2, // -2ab^2 + [cnst.toKey()]: -1 // -1 + }); }); - it('tests fraction division works', () => { - const num1 = new Polynomial([9, 1]); - const den1 = new Polynomial([0, 2]); - const frac1 = new Fraction(num1, den1); // (a+9)/2a - const num2 = new Polynomial([5, 1]); - const den2 = new Polynomial([3]); - const frac2 = new Fraction(num2, den2); // (a+5)/3 - const sum = Fraction.divide(frac1, frac2); // (3a+9)/(2a^2+10a) - assert.deepEqual(sum.num.coeffs, [27, 3]); - assert.deepEqual(sum.den.coeffs, [0, 10, 2]); + it('tests multinomial subtraction works', () => { + const aIb = new Term({'b': 1, 'a': 1}); // ab + const aIb2 = new Term({'b': 2, 'a': 1}); // ab^2 + const cnst = new Term({}); + const num1 = new Multinomial({ + [aIb2.toKey()]: 2, // 2ab^2 + [cnst.toKey()]: 1 // 5 + }); // 2ab^2 + 1 + const num2 = new Multinomial({ + [aIb.toKey()]: 2, // 2ab + [aIb2.toKey()]: 2, // 2ab^2 + [cnst.toKey()]: 5 // 5 + }); // 2ab + ab^2 + 5 + const sum = Multinomial.subtract(num1, num2); // -2ab - 4 + assert.deepEqual(sum.terms, { + [aIb.toKey()]: -2, // 2ab + [cnst.toKey()]: -4 // -4 + }); + }); + it('tests multinomial multiplication works', () => { + const aIb = new Term({'b': 1, 'a': 1}); // ab + const aIb2 = new Term({'b': 2, 'a': 1}); // ab^2 + const cnst = new Term({}); + const a2Ib3 = new Term({'a': 2, 'b': 3}); + const a2Ib4 = new Term({'a': 2, 'b': 4}); + const num1 = new Multinomial({ + [aIb2.toKey()]: 2, // 2ab^2 + [cnst.toKey()]: 1 // 1 + }); // 2ab^2 + 1 + const num2 = new Multinomial({ + [aIb.toKey()]: 2, // 2ab + [aIb2.toKey()]: 1, // ab^2 + [cnst.toKey()]: 5 // 5 + }); // 2ab + ab^2 + 5 + const sum = Multinomial.multiply(num1, num2); // 4a^2b^3 + 2a^2b^4 + 11ab^2 + 2ab + 5 + assert.deepEqual(sum.terms, { + [aIb.toKey()]: 2, // 2ab + [aIb2.toKey()]: 11, // 11ab^2 + [cnst.toKey()]: 5, // 5 + [a2Ib3.toKey()]: 4, // 4a^2b^3 + [a2Ib4.toKey()]: 2, // 2a^2b^4 + }); }); }); + + diff --git a/src/tools/recipe2plan.ts b/src/tools/recipe2plan.ts index 62a7df044f1..b03977723df 100644 --- a/src/tools/recipe2plan.ts +++ b/src/tools/recipe2plan.ts @@ -21,7 +21,7 @@ import {StorageKeyRecipeResolver} from './storage-key-recipe-resolver.js'; export async function recipe2plan(path: string): Promise { const manifest = await Runtime.parseFile(path); - const recipes = new StorageKeyRecipeResolver(manifest).resolve(); + const recipes = await (new StorageKeyRecipeResolver(manifest)).resolve(); const plans = await generatePlans(recipes); @@ -35,9 +35,8 @@ export async function recipe2plan(path: string): Promise { * @param resolutions A series of resolved recipes. * @return List of generated Kotlin plans */ -async function generatePlans(resolutions: AsyncGenerator): Promise { +async function generatePlans(resolutions: Recipe[]): Promise { // TODO Implement return ['']; } - diff --git a/src/tools/storage-key-recipe-resolver.ts b/src/tools/storage-key-recipe-resolver.ts index fe3372a012f..ef95ca795c6 100644 --- a/src/tools/storage-key-recipe-resolver.ts +++ b/src/tools/storage-key-recipe-resolver.ts @@ -35,22 +35,23 @@ export class StorageKeyRecipeResolver { * @throws Error if recipe fails to resolve on first or second pass. * @yields Resolved recipes with storage keys */ - async* resolve(): AsyncGenerator { + async resolve(): Promise { + const recipes = []; for (const recipe of this.runtime.context.allRecipes) { const arc = this.runtime.newArc(this.getArcId(recipe), ramDiskStorageKeyPrefixForTest()); const opts = {errors: new Map()}; - const resolved = await this.resolveOrNormalize(recipe, arc, opts); + const resolved = await this.tryResolve(recipe, arc, opts); if (!resolved) { throw Error(`Recipe ${recipe.name} failed to resolve:\n${[...opts.errors.values()].join('\n')}`); } this.createStoresForCreateHandles(resolved, arc); - resolved.normalize(); if (!resolved.isResolved()) { throw Error(`Recipe ${resolved.name} did not properly resolve!\n${resolved.toString({showUnresolved: true})}`); } this.matchKeysToHandles(recipe); - yield resolved; + recipes.push(resolved) } + return recipes; } /** @@ -60,7 +61,7 @@ export class StorageKeyRecipeResolver { * @param arc Arc is associated with input recipe * @param opts contains `errors` map for reporting. */ - async resolveOrNormalize(recipe: Recipe, arc: Arc, opts?: IsValidOptions): Promise { + async tryResolve(recipe: Recipe, arc: Arc, opts?: IsValidOptions): Promise { const normalized = recipe.clone(); const successful = normalized.normalize(opts); if (!successful) return null; diff --git a/src/tools/tests/storage-key-recipe-resolver-test.ts b/src/tools/tests/storage-key-recipe-resolver-test.ts index c4eebe98a80..ee93f88892c 100644 --- a/src/tools/tests/storage-key-recipe-resolver-test.ts +++ b/src/tools/tests/storage-key-recipe-resolver-test.ts @@ -14,8 +14,9 @@ import {StorageKeyRecipeResolver} from '../storage-key-recipe-resolver.js'; import {assertThrowsAsync} from '../../testing/test-util.js'; describe('recipe2plan', () => { - it('Long + Long: If ReadingRecipe is long running, it is a valid use case', async () => { - const manifest = await Manifest.parse(`\ + describe('storage-key-recipe-resolver', () => { + it('Resolves mapping a handle from a long running arc into another long running arc', async () => { + const manifest = await Manifest.parse(`\ particle Reader data: reads Thing {name: Text} @@ -42,10 +43,10 @@ describe('recipe2plan', () => { Reader data: reads data`); - const resolver = new StorageKeyRecipeResolver(manifest); - for await (const it of resolver.resolve()) { - assert.isTrue(it.isResolved()); - } + const resolver = new StorageKeyRecipeResolver(manifest); + for (const it of (await resolver.resolve())) { + assert.isTrue(it.isResolved()); + } }); it('Short + Short: If WritingRecipe is short lived, it is not valid', async () => { const manifest = await Manifest.parse(`\ @@ -68,7 +69,7 @@ describe('recipe2plan', () => { const resolver = new StorageKeyRecipeResolver(manifest); await assertThrowsAsync(async () => { - for await (const it of resolver.resolve()) { + for (const it of await resolver.resolve()) { continue; } }, Error, 'Handle data mapped to ephemeral handle thing.'); @@ -96,7 +97,7 @@ describe('recipe2plan', () => { const resolver = new StorageKeyRecipeResolver(manifest); await assertThrowsAsync(async () => { - for await (const it of resolver.resolve()) { + for (const it of await resolver.resolve()) { continue; } }, Error, 'Handle data mapped to ephemeral handle thing.'); @@ -123,7 +124,7 @@ describe('recipe2plan', () => { data: reads data`); const resolver = new StorageKeyRecipeResolver(manifest); - for await (const it of resolver.resolve()) { + for (const it of await resolver.resolve()) { assert.isTrue(it.isResolved()); } }); @@ -154,7 +155,7 @@ describe('recipe2plan', () => { const resolver = new StorageKeyRecipeResolver(manifest); // TODO: specify the correct error to be thrown await assertThrowsAsync(async () => { - for await (const it of resolver.resolve()) { + for (const it of await resolver.resolve()) { continue; } }, /Recipe ReadingRecipe failed to resolve:/);