diff --git a/java/arcs/core/data/proto/HandleProtoDecoder.kt b/java/arcs/core/data/proto/HandleProtoDecoder.kt index 6a4920f5d5e..2c3733bb934 100644 --- a/java/arcs/core/data/proto/HandleProtoDecoder.kt +++ b/java/arcs/core/data/proto/HandleProtoDecoder.kt @@ -27,6 +27,15 @@ fun HandleProto.Fate.decode() = when (this) { throw IllegalArgumentException("Invalid HandleProto.Fate value.") } +/** Converts [Handle.Fate] into [HandleProto.Fate] enum. */ +fun Handle.Fate.encode(): HandleProto.Fate = when (this) { + Handle.Fate.CREATE -> HandleProto.Fate.CREATE + Handle.Fate.USE -> HandleProto.Fate.USE + Handle.Fate.MAP -> HandleProto.Fate.MAP + Handle.Fate.COPY -> HandleProto.Fate.COPY + Handle.Fate.JOIN -> HandleProto.Fate.JOIN +} + /** * Converts [HandleProto] into [Handle]. * @@ -43,3 +52,19 @@ fun HandleProto.decode(knownHandles: Map = emptyMap()) = Handle( annotations = annotationsList.map { it.decode() }, associatedHandles = associatedHandlesList.map { requireNotNull(knownHandles[it]) } ) + +/** Converts a [Handle] to a [HandleProto]. */ +fun Handle.encode(): HandleProto { + val builder = HandleProto.newBuilder() + .setName(name) + .setId(id) + .setFate(fate.encode()) + .addAllTags(tags) + .setType(type.encode()) + .addAllAnnotations(annotations.map { it.encode() }) + .addAllAssociatedHandles(associatedHandles.map { it.name }) + + storageKey?.let { builder.setStorageKey(it) } + + return builder.build() +} diff --git a/java/arcs/core/data/proto/ManifestProtoDecoder.kt b/java/arcs/core/data/proto/ManifestProtoDecoder.kt index 952225667cb..6a3bf32319f 100644 --- a/java/arcs/core/data/proto/ManifestProtoDecoder.kt +++ b/java/arcs/core/data/proto/ManifestProtoDecoder.kt @@ -21,3 +21,12 @@ fun ManifestProto.decodeRecipes(): List { /** Extracts [ParticleSpec]s from the [ManifestProto]. */ fun ManifestProto.decodeParticleSpecs() = particleSpecsList.map { it.decode() } + +/** Convert a series of [Recipe]s into a [ManifestProto]. */ +fun Collection.encodeManifest(): ManifestProto = ManifestProto.newBuilder() + .addAllParticleSpecs( + this.flatMap { it.particles.map { particle -> particle.spec } } + .map { it.encode() } + ) + .addAllRecipes(this.map { it.encode() }) + .build() diff --git a/java/arcs/core/data/proto/ParticleProtoDecoder.kt b/java/arcs/core/data/proto/ParticleProtoDecoder.kt index 18386c0d3d2..1667933de5a 100644 --- a/java/arcs/core/data/proto/ParticleProtoDecoder.kt +++ b/java/arcs/core/data/proto/ParticleProtoDecoder.kt @@ -27,7 +27,7 @@ data class DecodingContext( var recipeHandles: Map ) -/** Converts a [HandleConnectionProto] into a [Recipe.Particle.HandleConnection]. */ +/** Converts a [HandleConnectionProto] into a [HandleConnection]. */ fun HandleConnectionProto.decode( particleSpec: ParticleSpec, context: DecodingContext @@ -41,7 +41,15 @@ fun HandleConnectionProto.decode( return HandleConnection(handleSpec, recipeHandle, type.decode()) } -/** Converts a [ParticleProto] into a [Recipe.Particle]. */ +/** Converts a [HandleConnection] into a [HandleConnectionProto]. */ +fun HandleConnection.encode(): HandleConnectionProto = + HandleConnectionProto.newBuilder() + .setName(spec.name) + .setHandle(handle.name) + .setType(type.encode()) + .build() + +/** Converts a [ParticleProto] into a [Particle]. */ fun ParticleProto.decode(context: DecodingContext): Particle { val particleSpec = requireNotNull(context.particleSpecs[specName]) { "ParticleSpec '$specName' not found in decoding context." @@ -49,3 +57,9 @@ fun ParticleProto.decode(context: DecodingContext): Particle { val handleConnections = connectionsList.map { it.decode(particleSpec, context) } return Particle(particleSpec, handleConnections) } + +/** Converts a [Particle] to a [ParticleProto]. */ +fun Particle.encode(): ParticleProto = ParticleProto.newBuilder() + .setSpecName(spec.name) + .addAllConnections(handleConnections.map { it.encode() }) + .build() diff --git a/java/arcs/core/data/proto/ParticleSpecProtoDecoder.kt b/java/arcs/core/data/proto/ParticleSpecProtoDecoder.kt index b70cfc1a703..42c7f24c020 100644 --- a/java/arcs/core/data/proto/ParticleSpecProtoDecoder.kt +++ b/java/arcs/core/data/proto/ParticleSpecProtoDecoder.kt @@ -16,6 +16,7 @@ import arcs.core.data.HandleConnectionSpec import arcs.core.data.HandleMode import arcs.core.data.ParticleSpec import arcs.core.data.expression.PaxelParser +import arcs.core.data.expression.stringify typealias DirectionProto = HandleConnectionSpecProto.Direction @@ -26,11 +27,26 @@ fun DirectionProto.decode() = throw IllegalArgumentException("Direction not set in [HandleConnectionSpec]") DirectionProto.READS -> HandleMode.Read DirectionProto.WRITES -> HandleMode.Write + DirectionProto.QUERY -> HandleMode.Query DirectionProto.READS_WRITES -> HandleMode.ReadWrite + DirectionProto.READS_QUERY -> HandleMode.ReadQuery + DirectionProto.WRITES_QUERY -> HandleMode.WriteQuery + DirectionProto.READS_WRITES_QUERY -> HandleMode.ReadWriteQuery DirectionProto.UNRECOGNIZED -> throw IllegalArgumentException("Invalid direction when decoding [HandleConnectionSpec]") } +/** Converts a [HandleMode] to a [HandleConnectionSpecProto.Direction]. */ +fun HandleMode.encode(): DirectionProto = when (this) { + HandleMode.Read -> DirectionProto.READS + HandleMode.Write -> DirectionProto.WRITES + HandleMode.Query -> DirectionProto.QUERY + HandleMode.ReadWrite -> DirectionProto.READS_WRITES + HandleMode.ReadQuery -> DirectionProto.READS_QUERY + HandleMode.WriteQuery -> DirectionProto.WRITES_QUERY + HandleMode.ReadWriteQuery -> DirectionProto.READS_WRITES_QUERY +} + /** Converts a [HandleConnnectionSpecProto] to the corresponding [HandleConnectionSpec] instance. */ fun HandleConnectionSpecProto.decode() = HandleConnectionSpec( name = name, @@ -43,6 +59,18 @@ fun HandleConnectionSpecProto.decode() = HandleConnectionSpec( } ) +/** Converts a [HandleConnectionSpec] into a [HandeConnectionSpecProto]. */ +fun HandleConnectionSpec.encode(): HandleConnectionSpecProto { + val builder = HandleConnectionSpecProto.newBuilder() + .setName(name) + .setDirection(direction.encode()) + .setType(type.encode()) + + expression?.let { builder.setExpression(it.stringify()) } + + return builder.build() +} + /** Converts a [ParticleSpecProto] to the corresponding [ParticleSpec] instance. */ fun ParticleSpecProto.decode(): ParticleSpec { val connections = mutableMapOf() @@ -59,3 +87,13 @@ fun ParticleSpecProto.decode(): ParticleSpec { val annotations = annotationsList.map { it.decode() } return ParticleSpec(name, connections, location, claims, checks, annotations) } + +/** Converts a [ParticleSpec] into a [ParticleSpecProto]. */ +fun ParticleSpec.encode(): ParticleSpecProto = ParticleSpecProto.newBuilder() + .setName(name) + .addAllConnections(connections.values.map { it.encode() }) + .setLocation(location) + .addAllClaims(claims.map { it.encode() }) + .addAllChecks(checks.map { it.encode() }) + .addAllAnnotations(annotations.map { it.encode() }) + .build() diff --git a/java/arcs/core/data/proto/RecipeProtoDecoder.kt b/java/arcs/core/data/proto/RecipeProtoDecoder.kt index 8a2a5c7ff7f..7f676277232 100644 --- a/java/arcs/core/data/proto/RecipeProtoDecoder.kt +++ b/java/arcs/core/data/proto/RecipeProtoDecoder.kt @@ -37,3 +37,15 @@ fun RecipeProto.decode(particleSpecs: Map): Recipe { val annotations = annotationsList.map { it.decode() } return Recipe(name.ifBlank { null }, recipeHandles, particles, annotations) } + +/** Converts a [Recipe] into [RecipeProto]. */ +fun Recipe.encode(): RecipeProto { + val builder = RecipeProto.newBuilder() + .addAllHandles(handles.values.map { it.encode() }) + .addAllParticles(particles.map { it.encode() }) + .addAllAnnotations(annotations.map { it.encode() }) + + name?.let { builder.setName(it) } + + return builder.build() +} diff --git a/java/arcs/core/data/proto/manifest.proto b/java/arcs/core/data/proto/manifest.proto index fdd34d8ba49..f35aa7562b4 100644 --- a/java/arcs/core/data/proto/manifest.proto +++ b/java/arcs/core/data/proto/manifest.proto @@ -89,6 +89,10 @@ message HandleConnectionSpecProto { READS = 1; WRITES = 2; READS_WRITES = 3; + QUERY = 4; + READS_QUERY = 5; + WRITES_QUERY = 6; + READS_WRITES_QUERY = 7; } // Identifies a connection in a particle spec. string name = 1; diff --git a/javatests/arcs/core/data/proto/HandleProtoDecoderTest.kt b/javatests/arcs/core/data/proto/HandleProtoDecoderTest.kt index 01ae09607d1..13c22fab798 100644 --- a/javatests/arcs/core/data/proto/HandleProtoDecoderTest.kt +++ b/javatests/arcs/core/data/proto/HandleProtoDecoderTest.kt @@ -1,3 +1,13 @@ +/* + * Copyright 2020 Google LLC. + * + * This code may only be used under the BSD style license found at + * http://polymer.github.io/LICENSE.txt + * + * Code distributed by Google as part of this project is also subject to an additional IP rights + * grant found at + * http://polymer.github.io/PATENTS.txt + */ package arcs.core.data.proto import arcs.core.data.Annotation @@ -17,6 +27,11 @@ fun parseHandleProtoText(protoText: String): HandleProto { return builder.build() } +/** Assert that a [HandleProto] is equal to itself after a round trip of decoding and encoding. */ +fun assertRoundTrip(proto: HandleProto, handles: Map) { + assertThat(proto.decode(handles).encode()).isEqualTo(proto) +} + @RunWith(JUnit4::class) class HandleProtoDecoderTest { @Test @@ -34,6 +49,15 @@ class HandleProtoDecoderTest { } } + @Test + fun encodesHandleProtoFate() { + assertThat(Handle.Fate.CREATE.encode()).isEqualTo(HandleProto.Fate.CREATE) + assertThat(Handle.Fate.USE.encode()).isEqualTo(HandleProto.Fate.USE) + assertThat(Handle.Fate.MAP.encode()).isEqualTo(HandleProto.Fate.MAP) + assertThat(Handle.Fate.COPY.encode()).isEqualTo(HandleProto.Fate.COPY) + assertThat(Handle.Fate.JOIN.encode()).isEqualTo(HandleProto.Fate.JOIN) + } + @Test fun decodesHandleProtoWithNoType() { val storageKey = "ramdisk://a" @@ -58,20 +82,47 @@ class HandleProtoDecoderTest { } } + @Test + fun roundTripsHandleProtoWithNoType_createsTypeVariable() { + val storageKey = "ramdisk://a" + val handleText = buildHandleProtoText( + "notype_thing", "CREATE", "", storageKey, "handle_c", "[{name: \"tiedToArc\"}]" + ) + val handles = mapOf( + "handle_c" to Handle("handle_c", Handle.Fate.MAP, TypeVariable("handle_c")), + "handle1" to Handle("handle1", Handle.Fate.MAP, TypeVariable("handle1")) + ) + val handleProto = parseHandleProtoText(handleText) + val roundTrip = handleProto.decode(handles).encode() + + with(roundTrip) { + assertThat(name).isEqualTo(handleProto.name) + assertThat(id).isEqualTo(handleProto.id) + assertThat(fate).isEqualTo(handleProto.fate) + assertThat(storageKey).isEqualTo(handleProto.storageKey) + assertThat(associatedHandlesList).isEqualTo(handleProto.associatedHandlesList) + assertThat(tagsList).isEqualTo(handleProto.tagsList) + assertThat(annotationsList).isEqualTo(handleProto.annotationsList) + + // Round-trip should infer a type variable. + assertThat(type).isEqualTo(TypeVariable("notype_thing").encode()) + } + } + @Test fun decodesHandleProtoWithType() { val entityTypeProto = """ - entity { - schema { - names: "Thing" - fields { - key: "name" - value: { primitive: TEXT } - } - } - } - """.trimIndent() + entity { + schema { + names: "Thing" + fields { + key: "name" + value: { primitive: TEXT } + } + } + } + """.trimIndent() val storageKey = "ramdisk://b" val entityType = parseTypeProtoText(entityTypeProto).decode() val handleText = buildHandleProtoText( @@ -107,20 +158,55 @@ class HandleProtoDecoderTest { } } + @Test + fun roundTripsHandleProtoWithType() { + val entityTypeProto = + """ + entity { + schema { + names: "Thing" + fields { + key: "name" + value: { primitive: TEXT } + } + refinement: "true" + query: "true" + } + } + """.trimIndent() + val storageKey = "ramdisk://b" + val handleText = buildHandleProtoText( + name = "thing", + fate = "JOIN", + type = "type { $entityTypeProto }", + storageKey = storageKey, + associatedHandle = "handle_join", + annotations = "[{name: \"persistent\"}, {name: \"queryable\"}]" + ) + val handleProto = parseHandleProtoText(handleText) + + val handles = mapOf( + "handle1" to Handle("handle1", Handle.Fate.MAP, TypeVariable("handle1")), + "handle_join" to Handle("handle_join", Handle.Fate.JOIN, TypeVariable("handle_join")) + ) + + assertRoundTrip(handleProto, handles) + } + @Test fun decodesHandleProtoWithTypeAndTags() { val entityTypeProto = """ - entity { - schema { - names: "Thing" - fields { - key: "name" - value: { primitive: TEXT } - } - } - } - """.trimIndent() + entity { + schema { + names: "Thing" + fields { + key: "name" + value: { primitive: TEXT } + } + } + } + """.trimIndent() val storageKey = "ramdisk://b" val entityType = parseTypeProtoText(entityTypeProto).decode() val handleText = buildHandleProtoText( @@ -167,6 +253,51 @@ class HandleProtoDecoderTest { } } + @Test + fun roundTrip_handleProtoWithTypeAndTags() { + val entityTypeProto = + """ + entity { + schema { + names: "Thing" + fields { + key: "name" + value: { primitive: TEXT } + } + refinement: "true" + query: "true" + } + } + """.trimIndent() + val storageKey = "ramdisk://b" + val handleText = buildHandleProtoText( + name = "thing", + fate = "JOIN", + type = "type { $entityTypeProto }", + storageKey = storageKey, + associatedHandle = "handle_join", + annotations = "[{name: \"persistent\"}, {name: \"queryable\"}]", + tags = listOf("foo", "bar", "baz") + ) + val handleProto = parseHandleProtoText(handleText) + + val handles = mapOf( + "handle1" to Handle( + "handle1", + Handle.Fate.MAP, + TypeVariable("handle1"), + tags = listOf("foo", "bar", "baz") + ), + "handle_join" to Handle( + "handle_join", + Handle.Fate.JOIN, + TypeVariable("handle_join"), + tags = listOf("foo", "bar", "baz") + ) + ) + assertRoundTrip(handleProto, handles) + } + @Test fun decodesHandleProtoWithId() { val storageKey = "ramdisk://a" @@ -206,12 +337,86 @@ class HandleProtoDecoderTest { } } + @Test + fun roundTrip_handleProtoWithId() { + val storageKey = "ramdisk://a" + val handleText = buildHandleProtoText( + name = "notype_thing", + fate = "CREATE", + type = """ + type { + variable { + name: "notype_thing" + constraint { + } + } + } + """.trimIndent(), + storageKey = storageKey, + associatedHandle = "handle_c", + annotations = "{name: \"tiedToArc\"}", + tags = emptyList(), + id = "veryofficialid_2342" + ) + val handles = mapOf( + "handle_c" to Handle( + "handle_c", + Handle.Fate.MAP, + TypeVariable("handle_c") + ), + "handle1" to Handle( + "handle1", + Handle.Fate.MAP, + TypeVariable("handle1") + ) + ) + val handleProto = parseHandleProtoText(handleText) + assertRoundTrip(handleProto, handles) + } + + @Test + fun roundTrip_handleProtoWithNoStorageKey() { + val storageKey = null + val handleText = buildHandleProtoText( + name = "notype_thing", + fate = "CREATE", + type = """ + type { + variable { + name: "notype_thing" + constraint { + } + } + } + """.trimIndent(), + storageKey = storageKey, + associatedHandle = "handle_c", + annotations = "{name: \"tiedToArc\"}", + tags = emptyList(), + id = "veryofficialid_2342" + ) + val handles = mapOf( + "handle_c" to Handle( + "handle_c", + Handle.Fate.MAP, + TypeVariable("handle_c") + ), + "handle1" to Handle( + "handle1", + Handle.Fate.MAP, + TypeVariable("handle1") + ) + ) + val handleProto = parseHandleProtoText(handleText) + assertRoundTrip(handleProto, handles) + } + /** A helper function to build a handle proto in text format. */ fun buildHandleProtoText( name: String, fate: String, type: String, - storageKey: String, + storageKey: String?, associatedHandle: String, annotations: String, tags: List = emptyList(), @@ -221,7 +426,7 @@ class HandleProtoDecoderTest { name: "$name" id: "$id" fate: $fate - storage_key: "$storageKey" + ${storageKey?.let { """storage_key: "$storageKey"""" } ?: ""} associated_handles: "handle1" associated_handles: "$associatedHandle" $type diff --git a/javatests/arcs/core/data/proto/ManifestProtoDecoderTest.kt b/javatests/arcs/core/data/proto/ManifestProtoDecoderTest.kt index 2d20b99dfb1..b4a9bc8f34c 100644 --- a/javatests/arcs/core/data/proto/ManifestProtoDecoderTest.kt +++ b/javatests/arcs/core/data/proto/ManifestProtoDecoderTest.kt @@ -44,6 +44,11 @@ class ManifestProtoDecoderTest { ) } + @Test + fun recipeManifestRoundTrip() { + assertThat(manifestProto.decodeRecipes().encodeManifest()).isEqualTo(manifestProto) + } + @Test fun decodesParticleSpecs() { assertThat(manifestProto.decodeParticleSpecs().map { it.name }).containsExactly( diff --git a/javatests/arcs/core/data/proto/ParticleProtoDecoderTest.kt b/javatests/arcs/core/data/proto/ParticleProtoDecoderTest.kt index 3c7ef282ced..e391805af9b 100644 --- a/javatests/arcs/core/data/proto/ParticleProtoDecoderTest.kt +++ b/javatests/arcs/core/data/proto/ParticleProtoDecoderTest.kt @@ -29,16 +29,18 @@ class ParticleProtoDecoderTest { ) val thingTypeProto = parseTypeProtoText( """ - entity { - schema { - names: "Thing" - fields { - key: "name" - value: { primitive: TEXT } - } - } - } - """.trimIndent() + entity { + schema { + names: "Thing" + fields { + key: "name" + value: { primitive: TEXT } + } + refinement: "true" + query: "true" + } + } + """.trimIndent() ) val thingType = EntityType( Schema( @@ -50,10 +52,10 @@ class ParticleProtoDecoderTest { hash = "" ) ) - val readConnectionSpec = HandleConnectionSpec("data", HandleMode.Read, TypeVariable("data")) - val readerSpec = ParticleSpec("Reader", mapOf("data" to readConnectionSpec), "ReaderLocation") - val writeConnectionSpec = HandleConnectionSpec("data", HandleMode.Write, TypeVariable("data")) - val writerSpec = ParticleSpec("Writer", mapOf("data" to writeConnectionSpec), "WriterLocation") + val readConnectionSpec = HandleConnectionSpec("read", HandleMode.Read, TypeVariable("data")) + val readerSpec = ParticleSpec("Reader", mapOf("read" to readConnectionSpec), "ReaderLocation") + val writeConnectionSpec = HandleConnectionSpec("write", HandleMode.Write, TypeVariable("data")) + val writerSpec = ParticleSpec("Writer", mapOf("write" to writeConnectionSpec), "WriterLocation") val readerWriterSpec = ParticleSpec( "ReaderWriter", mapOf( @@ -72,10 +74,10 @@ class ParticleProtoDecoderTest { ) @Test - fun decodesHandleConnnection() { + fun decodesHandleConnection() { var handleConnectionProto = HandleConnectionProto .newBuilder() - .setName("data") + .setName("read") .setHandle("thing") .setType(thingTypeProto) .build() @@ -86,6 +88,18 @@ class ParticleProtoDecoderTest { assertThat(readConnection.type).isEqualTo(thingType) } + @Test + fun roundTripHandleConnection() { + var handleConnectionProto = HandleConnectionProto + .newBuilder() + .setName("read") + .setHandle("thing") + .setType(thingTypeProto) + .build() + // When decoded as part of readerSpec, the mode is [HandleMode.Read]. + assertRoundTrip(handleConnectionProto, context, readerSpec) + } + @Test fun decodeHandleConnectionDetectsMissingConnectionSpec() { var proto = HandleConnectionProto @@ -106,7 +120,7 @@ class ParticleProtoDecoderTest { fun decodeHandleConnectionDetectsMissingHandle() { val proto = HandleConnectionProto .newBuilder() - .setName("data") + .setName("write") .setHandle("unknown") .setType(thingTypeProto) .build() @@ -122,7 +136,7 @@ class ParticleProtoDecoderTest { fun decodeHandleConnectionDetectsMissingType() { val proto = HandleConnectionProto .newBuilder() - .setName("data") + .setName("read") .setHandle("thing") .build() @@ -138,7 +152,7 @@ class ParticleProtoDecoderTest { fun decodesParticleProto() { val readConnectionProto = HandleConnectionProto .newBuilder() - .setName("data") + .setName("read") .setHandle("thing") .setType(thingTypeProto) .build() @@ -156,6 +170,23 @@ class ParticleProtoDecoderTest { } } + @Test + fun roundTripParticleProto() { + val readConnectionProto = HandleConnectionProto + .newBuilder() + .setName("read") + .setHandle("thing") + .setType(thingTypeProto) + .build() + val particleProto = ParticleProto + .newBuilder() + .setSpecName("Reader") + .addConnections(readConnectionProto) + .build() + assertRoundTrip(readConnectionProto, context, readerSpec) + assertRoundTrip(particleProto, context) + } + @Test fun decodesParticleProtoWithMultipleConnections() { val readConnectionProto = HandleConnectionProto @@ -187,6 +218,32 @@ class ParticleProtoDecoderTest { } } + @Test + fun roundTripParticleProtoWithMultipleConnections() { + val readConnectionProto = HandleConnectionProto + .newBuilder() + .setName("read") + .setHandle("thing") + .setType(thingTypeProto) + .build() + val writeConnectionProto = HandleConnectionProto + .newBuilder() + .setName("write") + .setHandle("thing") + .setType(thingTypeProto) + .build() + val particleProto = ParticleProto + .newBuilder() + .setSpecName("ReaderWriter") + .addConnections(readConnectionProto) + .addConnections(writeConnectionProto) + .build() + + assertRoundTrip(readConnectionProto, context, readerWriterSpec) + assertRoundTrip(writeConnectionProto, context, readerWriterSpec) + assertRoundTrip(particleProto, context) + } + @Test fun decodesParticleProtoWithNoConnections() { val emptyParticleProto = ParticleProto @@ -199,6 +256,15 @@ class ParticleProtoDecoderTest { } } + @Test + fun roundTripParticleProtoWithNoConnections() { + val emptyParticleProto = ParticleProto + .newBuilder() + .setSpecName("Reader") + .build() + assertRoundTrip(emptyParticleProto, context) + } + @Test fun decodeParticleDetectsMissingSpecs() { val particleProto = ParticleProto @@ -210,4 +276,19 @@ class ParticleProtoDecoderTest { } assertThat(exception).hasMessageThat().contains("ParticleSpec 'NonExistent' not found") } + + companion object { + + private fun assertRoundTrip(proto: ParticleProto, context: DecodingContext) { + assertThat(proto.decode(context).encode()).isEqualTo(proto) + } + + private fun assertRoundTrip( + proto: HandleConnectionProto, + context: DecodingContext, + spec: ParticleSpec + ) { + assertThat(proto.decode(spec, context).encode()).isEqualTo(proto) + } + } } diff --git a/javatests/arcs/core/data/proto/ParticleSpecProtoDecoderTest.kt b/javatests/arcs/core/data/proto/ParticleSpecProtoDecoderTest.kt index 61de53123c5..e46a191e944 100644 --- a/javatests/arcs/core/data/proto/ParticleSpecProtoDecoderTest.kt +++ b/javatests/arcs/core/data/proto/ParticleSpecProtoDecoderTest.kt @@ -28,17 +28,25 @@ typealias DirectionProto = HandleConnectionSpecProto.Direction /** * Decodes the given [HandleConnectionSpecProto] text as [HandleConnectionSpec]. */ -fun decodeHandleConnectionSpecProto(protoText: String): HandleConnectionSpec { +fun toHandleConnectionSpecProto(protoText: String): HandleConnectionSpecProto { val builder = HandleConnectionSpecProto.newBuilder() TextFormat.getParser().merge(protoText, builder) - return builder.build().decode() + return builder.build() } /** Decodes the given [ParticleSpecProto] text as [ParticleSpec]. */ -fun decodeParticleSpecProto(protoText: String): ParticleSpec { +fun toParticleSpecProto(protoText: String): ParticleSpecProto { val builder = ParticleSpecProto.newBuilder() TextFormat.getParser().merge(protoText, builder) - return builder.build().decode() + return builder.build() +} + +fun assertRoundTrip(proto: HandleConnectionSpecProto) { + assertThat(proto.decode().encode()).isEqualTo(proto) +} + +fun assertRoundTrip(proto: ParticleSpecProto) { + assertThat(proto.decode().encode()).isEqualTo(proto) } @RunWith(JUnit4::class) @@ -51,11 +59,28 @@ class ParticleSpecProtoDecoderTest { assertThat(DirectionProto.READS.decode()).isEqualTo(HandleMode.Read) assertThat(DirectionProto.WRITES.decode()).isEqualTo(HandleMode.Write) assertThat(DirectionProto.READS_WRITES.decode()).isEqualTo(HandleMode.ReadWrite) + assertThat(DirectionProto.READS_WRITES_QUERY.decode()).isEqualTo(HandleMode.ReadWriteQuery) + assertThat(DirectionProto.QUERY.decode()).isEqualTo(HandleMode.Query) + assertThat(DirectionProto.READS_QUERY.decode()).isEqualTo(HandleMode.ReadQuery) + assertThat(DirectionProto.WRITES_QUERY.decode()).isEqualTo(HandleMode.WriteQuery) + assertThat(DirectionProto.READS_WRITES_QUERY.decode()).isEqualTo(HandleMode.ReadWriteQuery) assertFailsWith { DirectionProto.UNRECOGNIZED.decode() } } + @Test + fun roundTripsDirectionProto() { + assertThat(DirectionProto.READS.decode().encode()).isEqualTo(DirectionProto.READS) + assertThat(DirectionProto.WRITES.decode().encode()).isEqualTo(DirectionProto.WRITES) + assertThat(DirectionProto.READS_WRITES.decode().encode()).isEqualTo(DirectionProto.READS_WRITES) + assertThat(DirectionProto.QUERY.decode().encode()).isEqualTo(DirectionProto.QUERY) + assertThat(DirectionProto.READS_QUERY.decode().encode()).isEqualTo(DirectionProto.READS_QUERY) + assertThat(DirectionProto.WRITES_QUERY.decode().encode()).isEqualTo(DirectionProto.WRITES_QUERY) + assertThat(DirectionProto.READS_WRITES_QUERY.decode().encode()) + .isEqualTo(DirectionProto.READS_WRITES_QUERY) + } + private val schema = Schema( names = setOf(SchemaName("Thing")), fields = SchemaFields( @@ -66,7 +91,7 @@ class ParticleSpecProtoDecoderTest { hash = "" ) - private fun getHandleConnectionSpecProto( + private fun getHandleConnectionSpecProtoText( name: String, direction: String, schemaName: String, @@ -83,6 +108,8 @@ class ParticleSpecProtoDecoderTest { key: "name" value: { primitive: TEXT } } + refinement: "true" + query: "true" } } } @@ -92,30 +119,54 @@ class ParticleSpecProtoDecoderTest { @Test fun decodesHandleConnectionSpecProto() { - val handleConnectionSpecProto = getHandleConnectionSpecProto("data", "READS", "Thing") - val connectionSpec = decodeHandleConnectionSpecProto(handleConnectionSpecProto) + val handleConnectionSpecProto = getHandleConnectionSpecProtoText("data", "READS", "Thing") + val connectionSpec = toHandleConnectionSpecProto(handleConnectionSpecProto).decode() assertThat(connectionSpec.name).isEqualTo("data") assertThat(connectionSpec.direction).isEqualTo(HandleMode.Read) assertThat(connectionSpec.type).isEqualTo(EntityType(schema)) assertThat(connectionSpec.expression).isNull() } + @Test + fun roundTripsHandleConnectionSpecProto() { + val connectionSpec = toHandleConnectionSpecProto( + getHandleConnectionSpecProtoText("data", "READS", "Thing") + ) + assertRoundTrip(connectionSpec) + } + @Test fun decodesHandleConnectionSpecProto_withExpression() { - val handleConnectionSpecProto = getHandleConnectionSpecProto( + val handleConnectionSpecProto = getHandleConnectionSpecProtoText( "data", "READS", "Thing", "new Thing {x: foo.y}" ) - val connectionSpec = decodeHandleConnectionSpecProto(handleConnectionSpecProto) + val connectionSpec = toHandleConnectionSpecProto(handleConnectionSpecProto).decode() assertThat(connectionSpec.name).isEqualTo("data") assertThat(connectionSpec.direction).isEqualTo(HandleMode.Read) assertThat(connectionSpec.type).isEqualTo(EntityType(schema)) assertThat(connectionSpec.expression).isInstanceOf(Expression.NewExpression::class.java) } + @Test + fun roundTripsHandleConnectionSpecProto_withExpression() { + val connectionSpec = toHandleConnectionSpecProto( + getHandleConnectionSpecProtoText( + "data", "READS", "Thing", "new Thing {x: foo.y}" + ) + ) + with(connectionSpec.decode().encode()) { + assertThat(name).isEqualTo(connectionSpec.name) + assertThat(direction).isEqualTo(connectionSpec.direction) + assertThat(type).isEqualTo(connectionSpec.type) + assertThat(expression.replace("\\s+".toRegex(), "")) + .isEqualTo(connectionSpec.expression.replace("\\s+".toRegex(), "")) + } + } + @Test fun decodesParticleSpecProto() { - val readConnectionSpecProto = getHandleConnectionSpecProto("read", "READS", "Thing") - val writeConnectionSpecProto = getHandleConnectionSpecProto("write", "WRITES", "Thing") + val readConnectionSpecProto = getHandleConnectionSpecProtoText("read", "READS", "Thing") + val writeConnectionSpecProto = getHandleConnectionSpecProtoText("write", "WRITES", "Thing") val readerSpecProto = """ name: "Reader" connections { $readConnectionSpecProto } @@ -124,8 +175,8 @@ class ParticleSpecProtoDecoderTest { name: "isolated" } """.trimIndent() - val readerSpec = decodeParticleSpecProto(readerSpecProto) - val readConnectionSpec = decodeHandleConnectionSpecProto(readConnectionSpecProto) + val readerSpec = toParticleSpecProto(readerSpecProto).decode() + val readConnectionSpec = toHandleConnectionSpecProto(readConnectionSpecProto).decode() assertThat(readerSpec).isEqualTo( ParticleSpec( name = "Reader", @@ -148,8 +199,8 @@ class ParticleSpecProtoDecoderTest { } } """.trimIndent() - val readerWriterSpec = decodeParticleSpecProto(readerWriterSpecProto) - val writeConnectionSpec = decodeHandleConnectionSpecProto(writeConnectionSpecProto) + val readerWriterSpec = toParticleSpecProto(readerWriterSpecProto).decode() + val writeConnectionSpec = toHandleConnectionSpecProto(writeConnectionSpecProto).decode() assertThat(readerWriterSpec).isEqualTo( ParticleSpec( name = "ReaderWriter", @@ -160,10 +211,46 @@ class ParticleSpecProtoDecoderTest { ) } + @Test + fun roundTripsParticleSpecProto() { + val readConnectionSpecProto = getHandleConnectionSpecProtoText("read", "READS", "Thing") + val writeConnectionSpecProto = getHandleConnectionSpecProtoText("write", "WRITES", "Thing") + val readerSpecProto = """ + name: "Reader" + connections { $readConnectionSpecProto } + location: "Everywhere" + annotations { + name: "isolated" + } + """.trimIndent() + val readerSpec = toParticleSpecProto(readerSpecProto) + val readConnectionSpec = toHandleConnectionSpecProto(readConnectionSpecProto) + assertRoundTrip(readerSpec) + assertRoundTrip(readConnectionSpec) + + val readerWriterSpecProto = """ + name: "ReaderWriter" + connections { $readConnectionSpecProto } + connections { $writeConnectionSpecProto } + location: "Nowhere" + annotations { + name: "egress" + params { + name: "type" + str_value: "MyEgressType" + } + } + """.trimIndent() + val readerWriterSpec = toParticleSpecProto(readerWriterSpecProto) + val writeConnectionSpec = toHandleConnectionSpecProto(writeConnectionSpecProto) + assertRoundTrip(readerWriterSpec) + assertRoundTrip(writeConnectionSpec) + } + @Test fun decodesParticleSpecProtoWithClaims() { - val readConnectionSpecProto = getHandleConnectionSpecProto("read", "READS", "Thing") - val writeConnectionSpecProto = getHandleConnectionSpecProto("write", "WRITES", "Thing") + val readConnectionSpecProto = getHandleConnectionSpecProtoText("read", "READS", "Thing") + val writeConnectionSpecProto = getHandleConnectionSpecProtoText("write", "WRITES", "Thing") val readerWriterSpecProto = """ name: "ReaderWriter" connections { $readConnectionSpecProto } @@ -201,9 +288,9 @@ class ParticleSpecProtoDecoderTest { } } """.trimIndent() - val readerWriterSpec = decodeParticleSpecProto(readerWriterSpecProto) - val readConnectionSpec = decodeHandleConnectionSpecProto(readConnectionSpecProto) - val writeConnectionSpec = decodeHandleConnectionSpecProto(writeConnectionSpecProto) + val readerWriterSpec = toParticleSpecProto(readerWriterSpecProto).decode() + val readConnectionSpec = toHandleConnectionSpecProto(readConnectionSpecProto).decode() + val writeConnectionSpec = toHandleConnectionSpecProto(writeConnectionSpecProto).decode() assertThat(readerWriterSpec.claims).containsExactly( Claim.Assume( AccessPath("ReaderWriter", writeConnectionSpec), @@ -216,9 +303,58 @@ class ParticleSpecProtoDecoderTest { ) } + @Test + fun roundTripsParticleSpecProtoWithClaims() { + val readConnectionSpecProto = getHandleConnectionSpecProtoText("read", "READS", "Thing") + val writeConnectionSpecProto = getHandleConnectionSpecProtoText("write", "WRITES", "Thing") + val readerWriterSpecProto = """ + name: "ReaderWriter" + connections { $readConnectionSpecProto } + connections { $writeConnectionSpecProto } + location: "Nowhere" + claims { + assume { + access_path { + handle { + particle_spec: "ReaderWriter" + handle_connection: "write" + } + } + predicate { + label { + semantic_tag: "public" + } + } + } + } + claims { + derives_from { + target { + handle { + particle_spec: "ReaderWriter" + handle_connection: "write" + } + } + source { + handle { + particle_spec: "ReaderWriter" + handle_connection: "read" + } + } + } + } + """.trimIndent() + val readerWriterSpec = toParticleSpecProto(readerWriterSpecProto) + val readConnectionSpec = toHandleConnectionSpecProto(readConnectionSpecProto) + val writeConnectionSpec = toHandleConnectionSpecProto(writeConnectionSpecProto) + assertRoundTrip(readerWriterSpec) + assertRoundTrip(readConnectionSpec) + assertRoundTrip(writeConnectionSpec) + } + @Test fun decodesParticleSpecProtoWithChecks() { - val readConnectionSpecProto = getHandleConnectionSpecProto("read", "READS", "Thing") + val readConnectionSpecProto = getHandleConnectionSpecProtoText("read", "READS", "Thing") val readerWriterSpecProto = """ name: "ReaderWriter" connections { $readConnectionSpecProto } @@ -250,8 +386,8 @@ class ParticleSpecProtoDecoderTest { } } """.trimIndent() - val readerWriterSpec = decodeParticleSpecProto(readerWriterSpecProto) - val readConnectionSpec = decodeHandleConnectionSpecProto(readConnectionSpecProto) + val readerWriterSpec = toParticleSpecProto(readerWriterSpecProto).decode() + val readConnectionSpec = toHandleConnectionSpecProto(readConnectionSpecProto).decode() assertThat(readerWriterSpec.checks).containsExactly( Check( AccessPath("ReaderWriter", readConnectionSpec), @@ -264,9 +400,49 @@ class ParticleSpecProtoDecoderTest { ) } + @Test + fun roundTripsParticleSpecProtoWithChecks() { + val readConnectionSpecProto = getHandleConnectionSpecProtoText("read", "READS", "Thing") + val readerWriterSpecProto = """ + name: "ReaderWriter" + connections { $readConnectionSpecProto } + location: "Nowhere" + checks { + access_path { + handle { + particle_spec: "ReaderWriter" + handle_connection: "read" + } + } + predicate { + label { + semantic_tag: "public" + } + } + } + checks { + access_path { + handle { + particle_spec: "ReaderWriter" + handle_connection: "read" + } + } + predicate { + label { + semantic_tag: "invalid" + } + } + } + """.trimIndent() + val readerWriterSpec = toParticleSpecProto(readerWriterSpecProto) + val readConnectionSpec = toHandleConnectionSpecProto(readConnectionSpecProto) + assertRoundTrip(readerWriterSpec) + assertRoundTrip(readConnectionSpec) + } + @Test fun detectsDuplicateConnections() { - val readConnectionSpecProto = getHandleConnectionSpecProto("read", "READS", "Thing") + val readConnectionSpecProto = getHandleConnectionSpecProtoText("read", "READS", "Thing") val readerSpecProto = """ name: "Reader" connections { $readConnectionSpecProto } @@ -274,7 +450,7 @@ class ParticleSpecProtoDecoderTest { location: "Everywhere" """.trimIndent() val exception = assertFailsWith { - decodeParticleSpecProto(readerSpecProto) + toParticleSpecProto(readerSpecProto).decode() } assertThat(exception).hasMessageThat().contains("Duplicate connection 'read'") } diff --git a/javatests/arcs/core/data/proto/RecipeProtoDecoderTest.kt b/javatests/arcs/core/data/proto/RecipeProtoDecoderTest.kt index 91ed5afc5a5..e5532fe9bcc 100644 --- a/javatests/arcs/core/data/proto/RecipeProtoDecoderTest.kt +++ b/javatests/arcs/core/data/proto/RecipeProtoDecoderTest.kt @@ -27,25 +27,43 @@ fun arcIdAnnotationProto(id: String): AnnotationProto { .build() } +/** Asserts that a [RecipeProto] is equal to itself after decoding and encoding. */ +fun assertRoundTrip(proto: RecipeProto, particleSpecs: Map) { + assertThat(proto.decode(particleSpecs).encode()).isEqualTo(proto) +} + +private fun typeVariable(name: String): TypeProto { + return TypeProto.newBuilder() + .setVariable( + TypeVariableProto.newBuilder() + .setName(name) + .setConstraint(ConstraintInfo.newBuilder().build()) + ) + .build() +} + @RunWith(JUnit4::class) class RecipeProtoDecoderTest { // The test environment. val ramdiskStorageKey = "ramdisk://" val thingTypeProto = parseTypeProtoText( """ - entity { - schema { - names: "Thing" - fields { - key: "name" - value: { primitive: TEXT } - } - } - } + entity { + schema { + names: "Thing" + fields { + key: "name" + value: { primitive: TEXT } + } + refinement: "true" + query: "true" + } + } """.trimIndent() ) val thingHandleProto = HandleProto.newBuilder() .setName("thing") + .setType(typeVariable("thing")) .setFate(HandleProto.Fate.CREATE) .setStorageKey(ramdiskStorageKey + "thing") .build() @@ -64,6 +82,7 @@ class RecipeProtoDecoderTest { val thingEntity = EntityType(thingSchema) val thangHandleProto = HandleProto.newBuilder() .setName("thang") + .setType(typeVariable("thang")) .setFate(HandleProto.Fate.MAP) .setStorageKey(ramdiskStorageKey + "thang") .build() @@ -77,6 +96,7 @@ class RecipeProtoDecoderTest { val joinHandleProto = HandleProto.newBuilder() .setName("pairs") .setFate(HandleProto.Fate.JOIN) + .setType(typeVariable("pairs")) .addAssociatedHandles("thing") .addAssociatedHandles("thang") .setStorageKey(ramdiskStorageKey + "pairs") @@ -150,6 +170,11 @@ class RecipeProtoDecoderTest { } } + @Test + fun roundTripsRecipe() { + assertRoundTrip(recipeProto, context.particleSpecs) + } + @Test fun decodesEmptyRecipe() { val emptyRecipeProto = RecipeProto.newBuilder() @@ -162,6 +187,12 @@ class RecipeProtoDecoderTest { } } + @Test + fun roundTripsEmptyRecipe() { + val emptyRecipeProto = RecipeProto.newBuilder().build() + assertRoundTrip(emptyRecipeProto, context.particleSpecs) + } + @Test fun decodeRecipeDetectsDuplicateHandles() { val duplicateHandlesRecipeProto = RecipeProto.newBuilder() @@ -191,4 +222,9 @@ class RecipeProtoDecoderTest { assertThat(annotations).isEqualTo(listOf(Annotation.createArcId("arc-with-join"))) } } + + @Test + fun roundTripsRecipeWithJoins() { + assertRoundTrip(recipeWithJoin, context.particleSpecs) + } }