From 1a1eced580383a553adce6273b033f606d399db7 Mon Sep 17 00:00:00 2001 From: Zach Klippenstein Date: Mon, 29 Jun 2020 11:16:39 -0700 Subject: [PATCH] Introduce unsnapshottableIdentifier() to allow transient WorkflowIdentifiers. This fixes #79 and unblocks the ability to implement GUWT Workers as Workflows. `WorkflowIdentifier` can now indicate that it is not serializable by returning null from `toByteString()`, in which case neither it nor the snapshot of the workflow it identifies will be serialized. Because these identifiers do not need to be serialized, they can contain arbitrary values such as `KType`s, which is how Workers distinguish themselves for example. Workers also do not need to be snapshotted, so this works out. --- workflow-core/api/workflow-core.api | 5 +- .../squareup/workflow/WorkflowIdentifier.kt | 103 ++++++++++++------ .../workflow/WorkflowIdentifierTest.kt | 94 ++++++++++++---- .../com/squareup/workflow/TreeSnapshot.kt | 26 ++++- .../workflow/internal/WorkflowNodeId.kt | 33 +++--- .../com/squareup/workflow/TreeSnapshotTest.kt | 68 ++++++++++-- 6 files changed, 247 insertions(+), 82 deletions(-) diff --git a/workflow-core/api/workflow-core.api b/workflow-core/api/workflow-core.api index ce684944e0..c65493d42a 100644 --- a/workflow-core/api/workflow-core.api +++ b/workflow-core/api/workflow-core.api @@ -172,12 +172,12 @@ public final class com/squareup/workflow/WorkflowIdentifier { public static final field Companion Lcom/squareup/workflow/WorkflowIdentifier$Companion; public fun equals (Ljava/lang/Object;)Z public fun hashCode ()I + public final fun toByteStringOrNull ()Lokio/ByteString; public fun toString ()Ljava/lang/String; - public final fun write (Lokio/BufferedSink;)V } public final class com/squareup/workflow/WorkflowIdentifier$Companion { - public final fun read (Lokio/BufferedSource;)Lcom/squareup/workflow/WorkflowIdentifier; + public final fun parse (Lokio/ByteString;)Lcom/squareup/workflow/WorkflowIdentifier; } public final class com/squareup/workflow/WorkflowOutput { @@ -225,6 +225,7 @@ public final class com/squareup/workflow/Workflows { public static synthetic fun stateful$default (Lcom/squareup/workflow/Workflow$Companion;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lcom/squareup/workflow/StatefulWorkflow; public static final fun stateless (Lcom/squareup/workflow/Workflow$Companion;Lkotlin/jvm/functions/Function2;)Lcom/squareup/workflow/Workflow; public static final fun transform (Lcom/squareup/workflow/Worker;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow/Worker; + public static final fun unsnapshottableIdentifier (Lkotlin/reflect/KType;)Lcom/squareup/workflow/WorkflowIdentifier; public static final fun workflowAction (Lcom/squareup/workflow/StatefulWorkflow;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow/WorkflowAction; public static final fun workflowAction (Lcom/squareup/workflow/StatefulWorkflow;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow/WorkflowAction; public static synthetic fun workflowAction$default (Lcom/squareup/workflow/StatefulWorkflow;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/squareup/workflow/WorkflowAction; diff --git a/workflow-core/src/main/java/com/squareup/workflow/WorkflowIdentifier.kt b/workflow-core/src/main/java/com/squareup/workflow/WorkflowIdentifier.kt index 9933c39d87..7544f3787f 100644 --- a/workflow-core/src/main/java/com/squareup/workflow/WorkflowIdentifier.kt +++ b/workflow-core/src/main/java/com/squareup/workflow/WorkflowIdentifier.kt @@ -18,11 +18,13 @@ package com.squareup.workflow -import okio.BufferedSink -import okio.BufferedSource +import okio.Buffer +import okio.ByteString import okio.EOFException import kotlin.LazyThreadSafetyMode.PUBLICATION +import kotlin.reflect.KAnnotatedElement import kotlin.reflect.KClass +import kotlin.reflect.KType /** * Represents a [Workflow]'s "identity" and is used by the runtime to determine whether a workflow @@ -31,61 +33,82 @@ import kotlin.reflect.KClass * * A workflow's identity consists primarily of its concrete type (i.e. the class that implements * the [Workflow] interface). Two workflows of the same concrete type are considered identical. - * * However, if a workflow class implements [ImpostorWorkflow], the identifier will also include * that workflow's [ImpostorWorkflow.realIdentifier]. * + * Instances of this class are [equatable][equals] and [hashable][hashCode]. + * + * ## Identifiers and snapshots + * + * Since workflows can be [serialized][StatefulWorkflow.snapshotState], workflows' identifiers must + * also be serializable in order to match workflows back up with their snapshots when restoring. + * However, some [WorkflowIdentifier]s may represent workflows that cannot be snapshotted. When an + * identifier is not snapshottable, [toByteStringOrNull] will return null, and any identifiers that + * reference [ImpostorWorkflow]s whose [ImpostorWorkflow.realIdentifier] is not snapshottable will + * also not be snapshottable. Such identifiers are created with [unsnapshottableIdentifier], but + * should not be used to wrap arbitrary workflows since those workflows may expect to be + * snapshotted. + * * @constructor - * @param type The [KClass] of the [Workflow] this identifier identifies. + * @param type The [KClass] of the [Workflow] this identifier identifies, or the [KType] of an + * [unsnapshottableIdentifier]. * @param proxiedIdentifier An optional identifier from [ImpostorWorkflow.realIdentifier] that will * be used to further narrow the scope of this identifier. */ @ExperimentalWorkflowApi class WorkflowIdentifier internal constructor( - private val type: KClass>, - private val proxiedIdentifier: WorkflowIdentifier? + private val type: KAnnotatedElement, + private val proxiedIdentifier: WorkflowIdentifier? = null ) { /** - * The fully-qualified name of [type]. Computed lazily. + * The fully-qualified name of the type of workflow this identifier identifies. Computed lazily + * and cached. */ - private val typeString: String by lazy(PUBLICATION) { type.java.name } + private val typeName: String by lazy(PUBLICATION) { + if (type is KClass<*>) type.java.name else type.toString() + } /** - * Returns a description of this identifier including the name of its workflow type and any - * [proxiedIdentifier]s. Computes [typeString] if it has not already been computed. + * If this identifier is snapshottable, returns the serialized form of the identifier. + * If it is not snapshottable, returns null. */ - override fun toString(): String = - generateSequence(this) { it.proxiedIdentifier } - .joinToString { it.typeString } - .let { "WorkflowIdentifier($it)" } + fun toByteStringOrNull(): ByteString? { + if (type !is KClass<*>) return null - /** - * Serializes this identifier to the sink. It can be read back with [WorkflowIdentifier.read]. - */ - fun write(sink: BufferedSink) { - sink.writeUtf8WithLength(typeString) - if (proxiedIdentifier != null) { - sink.writeByte(PROXY_IDENTIFIER_TAG.toInt()) - proxiedIdentifier.write(sink) - } else { - sink.writeByte(NO_PROXY_IDENTIFIER_TAG.toInt()) + val proxiedBytes = proxiedIdentifier?.let { + // If we have a proxied identifier but it's not serializable, then we can't be serializable + // either. + it.toByteStringOrNull() ?: return null + } + + return Buffer().let { sink -> + sink.writeUtf8WithLength(typeName) + if (proxiedBytes != null) { + sink.writeByte(PROXY_IDENTIFIER_TAG.toInt()) + sink.write(proxiedBytes) + } else { + sink.writeByte(NO_PROXY_IDENTIFIER_TAG.toInt()) + } + sink.readByteString() } } /** - * Determines equality to another [WorkflowIdentifier] by comparing their [type]s and their - * [proxiedIdentifier]s. + * Returns a description of this identifier including the name of its workflow type and any + * [ImpostorWorkflow.realIdentifier]s. */ + override fun toString(): String = + generateSequence(this) { it.proxiedIdentifier } + .joinToString { it.typeName } + .let { "WorkflowIdentifier($it)" } + override fun equals(other: Any?): Boolean = when { this === other -> true other !is WorkflowIdentifier -> false else -> type == other.type && proxiedIdentifier == other.proxiedIdentifier } - /** - * Derives a hashcode from [type] and [proxiedIdentifier]. - */ override fun hashCode(): Int { var result = type.hashCode() result = 31 * result + (proxiedIdentifier?.hashCode() ?: 0) @@ -97,18 +120,20 @@ class WorkflowIdentifier internal constructor( private const val PROXY_IDENTIFIER_TAG = 1.toByte() /** - * Reads a [WorkflowIdentifier] from [source]. + * Reads a [WorkflowIdentifier] from a [ByteString] as written by [toByteStringOrNull]. * * @throws IllegalArgumentException if the source does not contain a valid [WorkflowIdentifier] * @throws ClassNotFoundException if one of the workflow types can't be found in the class * loader */ - fun read(source: BufferedSource): WorkflowIdentifier? { + fun parse(bytes: ByteString): WorkflowIdentifier? = Buffer().let { source -> + source.write(bytes) + try { val typeString = source.readUtf8WithLength() val proxiedIdentifier = when (source.readByte()) { NO_PROXY_IDENTIFIER_TAG -> null - PROXY_IDENTIFIER_TAG -> read(source) + PROXY_IDENTIFIER_TAG -> parse(source.readByteString()) else -> throw IllegalArgumentException("Invalid WorkflowIdentifier") } @@ -122,6 +147,20 @@ class WorkflowIdentifier internal constructor( } } +/** + * Creates a [WorkflowIdentifier] that is not capable of being snapshotted and will cause any + * [ImpostorWorkflow] workflow identified by it to also not be snapshotted. + * + * **This function should not be used for [ImpostorWorkflow]s that wrap arbitrary workflows**, since + * those workflows may expect to be on snapshotted. Using such identifiers _anywhere in the + * [ImpostorWorkflow.realIdentifier] chain_ will disable snapshotting for that workflow. **This + * function should only be used for [ImpostorWorkflow]s that wrap a closed set of known workflow + * types.** + */ +@ExperimentalWorkflowApi +@Suppress("unused") +fun unsnapshottableIdentifier(type: KType): WorkflowIdentifier = WorkflowIdentifier(type) + /** * The [WorkflowIdentifier] that identifies this [Workflow]. */ diff --git a/workflow-core/src/test/java/com/squareup/workflow/WorkflowIdentifierTest.kt b/workflow-core/src/test/java/com/squareup/workflow/WorkflowIdentifierTest.kt index 2bcad39d40..8cdf81dd40 100644 --- a/workflow-core/src/test/java/com/squareup/workflow/WorkflowIdentifierTest.kt +++ b/workflow-core/src/test/java/com/squareup/workflow/WorkflowIdentifierTest.kt @@ -16,12 +16,16 @@ package com.squareup.workflow import okio.Buffer +import okio.ByteString +import kotlin.reflect.KType +import kotlin.reflect.typeOf import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertNotEquals +import kotlin.test.assertNull -@OptIn(ExperimentalWorkflowApi::class) +@OptIn(ExperimentalWorkflowApi::class, ExperimentalStdlibApi::class) class WorkflowIdentifierTest { @Test fun `flat identifier toString`() { @@ -43,8 +47,8 @@ class WorkflowIdentifierTest { @Test fun `restored identifier toString`() { val id = TestWorkflow1.identifier - val serializedId = Buffer().also(id::write) - val restoredId = WorkflowIdentifier.read(serializedId) + val serializedId = id.toByteStringOrNull()!! + val restoredId = WorkflowIdentifier.parse(serializedId) assertEquals(id.toString(), restoredId.toString()) } @@ -82,8 +86,8 @@ class WorkflowIdentifierTest { @Test fun `identifier restored from source is equal to itself`() { val id = TestWorkflow1.identifier - val serializedId = Buffer().also(id::write) - val restoredId = WorkflowIdentifier.read(serializedId) + val serializedId = id.toByteStringOrNull()!! + val restoredId = WorkflowIdentifier.parse(serializedId) assertEquals(id, restoredId) assertEquals(id.hashCode(), restoredId.hashCode()) } @@ -91,15 +95,15 @@ class WorkflowIdentifierTest { @Test fun `identifier restored from source is not equal to different identifier`() { val id1 = TestWorkflow1.identifier val id2 = TestWorkflow2.identifier - val serializedId = Buffer().also(id1::write) - val restoredId = WorkflowIdentifier.read(serializedId) + val serializedId = id1.toByteStringOrNull()!! + val restoredId = WorkflowIdentifier.parse(serializedId) assertNotEquals(id2, restoredId) } @Test fun `impostor identifier restored from source is equal to itself`() { val id = TestImpostor1(TestWorkflow1).identifier - val serializedId = Buffer().also(id::write) - val restoredId = WorkflowIdentifier.read(serializedId) + val serializedId = id.toByteStringOrNull()!! + val restoredId = WorkflowIdentifier.parse(serializedId) assertEquals(id, restoredId) assertEquals(id.hashCode(), restoredId.hashCode()) } @@ -108,8 +112,8 @@ class WorkflowIdentifierTest { fun `impostor identifier restored from source is not equal to impostor with different proxied class`() { val id1 = TestImpostor1(TestWorkflow1).identifier val id2 = TestImpostor1(TestWorkflow2).identifier - val serializedId = Buffer().also(id1::write) - val restoredId = WorkflowIdentifier.read(serializedId) + val serializedId = id1.toByteStringOrNull()!! + val restoredId = WorkflowIdentifier.parse(serializedId) assertNotEquals(id2, restoredId) } @@ -117,39 +121,83 @@ class WorkflowIdentifierTest { fun `impostor identifier restored from source is not equal to different impostor with same proxied class`() { val id1 = TestImpostor1(TestWorkflow1).identifier val id2 = TestImpostor2(TestWorkflow1).identifier - val serializedId = Buffer().also(id1::write) - val restoredId = WorkflowIdentifier.read(serializedId) + val serializedId = id1.toByteStringOrNull()!! + val restoredId = WorkflowIdentifier.parse(serializedId) assertNotEquals(id2, restoredId) } @Test fun `read from empty source throws`() { - val source = Buffer() assertFailsWith { - WorkflowIdentifier.read(source) + WorkflowIdentifier.parse(ByteString.EMPTY) } } @Test fun `read from invalid source throws`() { val source = Buffer().apply { writeUtf8("invalid data") } + .readByteString() assertFailsWith { - WorkflowIdentifier.read(source) + WorkflowIdentifier.parse(source) } } @Test fun `read from corrupted source throws`() { - val source = Buffer().also(TestWorkflow1.identifier::write) - .readByteArray() + val source = TestWorkflow1.identifier.toByteStringOrNull()!! + .toByteArray() source.indices.reversed() .take(10) .forEach { i -> source[i] = 0 } val corruptedSource = Buffer().apply { write(source) } + .readByteString() assertFailsWith { - WorkflowIdentifier.read(corruptedSource) + WorkflowIdentifier.parse(corruptedSource) } } + @Test fun `unsnapshottable identifier returns null ByteString`() { + val id = unsnapshottableIdentifier(typeOf()) + assertNull(id.toByteStringOrNull()) + } + + @Test fun `unsnapshottable identifier toString()`() { + val id = unsnapshottableIdentifier(typeOf()) + assertEquals( + "WorkflowIdentifier(${String::class.java.name} (Kotlin reflection is not available))", + id.toString() + ) + } + + @Test fun `unsnapshottable identifiers for same class are equal`() { + val id1 = unsnapshottableIdentifier(typeOf()) + val id2 = unsnapshottableIdentifier(typeOf()) + assertEquals(id1, id2) + } + + @Test fun `unsnapshottable identifiers for different class are not equal`() { + val id1 = unsnapshottableIdentifier(typeOf()) + val id2 = unsnapshottableIdentifier(typeOf()) + assertNotEquals(id1, id2) + } + + @Test fun `unsnapshottable impostor identifier returns null ByteString`() { + val id = TestUnsnapshottableImpostor(typeOf()).identifier + assertNull(id.toByteStringOrNull()) + } + + @Test fun `impostor of unsnapshottable impostor identifier returns null ByteString`() { + val id = TestImpostor1(TestUnsnapshottableImpostor(typeOf())).identifier + assertNull(id.toByteStringOrNull()) + } + + @Test fun `unsnapshottable impostor identifier toString()`() { + val id = TestUnsnapshottableImpostor(typeOf()).identifier + assertEquals( + "WorkflowIdentifier(${TestUnsnapshottableImpostor::class.java.name}, " + + "${String::class.java.name} (Kotlin reflection is not available))", id.toString() + ) + } + private object TestWorkflow1 : Workflow { override fun asStatefulWorkflow(): StatefulWorkflow = throw NotImplementedError() @@ -175,4 +223,12 @@ class WorkflowIdentifierTest { override fun asStatefulWorkflow(): StatefulWorkflow = throw NotImplementedError() } + + private class TestUnsnapshottableImpostor( + type: KType + ) : Workflow, ImpostorWorkflow { + override val realIdentifier: WorkflowIdentifier = unsnapshottableIdentifier(type) + override fun asStatefulWorkflow(): StatefulWorkflow = + throw NotImplementedError() + } } diff --git a/workflow-runtime/src/main/java/com/squareup/workflow/TreeSnapshot.kt b/workflow-runtime/src/main/java/com/squareup/workflow/TreeSnapshot.kt index 96227f55d6..7d3f2dc76d 100644 --- a/workflow-runtime/src/main/java/com/squareup/workflow/TreeSnapshot.kt +++ b/workflow-runtime/src/main/java/com/squareup/workflow/TreeSnapshot.kt @@ -16,6 +16,7 @@ package com.squareup.workflow import com.squareup.workflow.TreeSnapshot.Companion.forRootOnly +import com.squareup.workflow.TreeSnapshot.Companion.parse import com.squareup.workflow.internal.WorkflowNodeId import okio.Buffer import okio.ByteString @@ -54,13 +55,24 @@ class TreeSnapshot internal constructor( /** * Writes this [Snapshot] and all its children into a [ByteString]. The snapshot can be restored * with [parse]. + * + * Any children snapshots for workflows whose [WorkflowIdentifier]s are + * [unsnapshottable][unsnapshottableIdentifier] will not be serialized. */ fun toByteString(): ByteString = Buffer().let { sink -> sink.writeByteStringWithLength(workflowSnapshot?.bytes ?: ByteString.EMPTY) - sink.writeInt(childTreeSnapshots.size) - childTreeSnapshots.forEach { (childId, childSnapshot) -> - childId.writeTo(sink) - sink.writeByteStringWithLength(childSnapshot.toByteString()) + val childBytes: List> = + childTreeSnapshots.mapNotNull { (childId, childSnapshot) -> + val childIdBytes = childId.toByteStringOrNull() ?: return@mapNotNull null + val childSnapshotBytes = childSnapshot.toByteString() + .takeUnless { it.size == 0 } + ?: return@mapNotNull null + return@mapNotNull Pair(childIdBytes, childSnapshotBytes) + } + sink.writeInt(childBytes.size) + childBytes.forEach { (childIdBytes, childSnapshotBytes) -> + sink.writeByteStringWithLength(childIdBytes) + sink.writeByteStringWithLength(childSnapshotBytes) } sink.readByteString() } @@ -68,7 +80,8 @@ class TreeSnapshot internal constructor( override fun equals(other: Any?): Boolean = when { other === this -> true other !is TreeSnapshot -> false - else -> other.workflowSnapshot == workflowSnapshot && other.childTreeSnapshots == childTreeSnapshots + else -> other.workflowSnapshot == workflowSnapshot && + other.childTreeSnapshots == childTreeSnapshots } override fun hashCode(): Int { @@ -107,7 +120,8 @@ class TreeSnapshot internal constructor( val childSnapshotCount = source.readInt() buildMap(childSnapshotCount) { for (i in 0 until childSnapshotCount) { - val id = WorkflowNodeId.readFrom(source) + val idBytes = source.readByteStringWithLength() + val id = WorkflowNodeId.parse(idBytes) val childSnapshot = source.readByteStringWithLength() this[id] = parse(childSnapshot) } diff --git a/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowNodeId.kt b/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowNodeId.kt index 3c58ba61c1..deb305553f 100644 --- a/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowNodeId.kt +++ b/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowNodeId.kt @@ -19,11 +19,12 @@ import com.squareup.workflow.ExperimentalWorkflowApi import com.squareup.workflow.Workflow import com.squareup.workflow.WorkflowIdentifier import com.squareup.workflow.identifier +import com.squareup.workflow.readByteStringWithLength import com.squareup.workflow.readUtf8WithLength +import com.squareup.workflow.writeByteStringWithLength import com.squareup.workflow.writeUtf8WithLength -import okio.BufferedSink -import okio.BufferedSource -import kotlin.LazyThreadSafetyMode.NONE +import okio.Buffer +import okio.ByteString /** * Value type that can be used to distinguish between different workflows of different types or @@ -44,22 +45,22 @@ internal data class WorkflowNodeId( otherName: String ): Boolean = identifier == otherWorkflow.identifier && name == otherName - /** - * String representation of this workflow's type (i.e. [WorkflowIdentifier]), suitable for use in - * diagnostic output (see - * [WorkflowHierarchyDebugSnapshot][com.squareup.workflow.diagnostic.WorkflowHierarchyDebugSnapshot] - * ). - */ - val typeDebugString: String by lazy(NONE) { identifier.toString() } - - internal fun writeTo(sink: BufferedSink) { - identifier.write(sink) - sink.writeUtf8WithLength(name) + internal fun toByteStringOrNull(): ByteString? { + // If identifier is not snapshottable, neither are we. + val identifierBytes = identifier.toByteStringOrNull() ?: return null + return Buffer().let { sink -> + sink.writeByteStringWithLength(identifierBytes) + sink.writeUtf8WithLength(name) + sink.readByteString() + } } internal companion object { - internal fun readFrom(source: BufferedSource): WorkflowNodeId { - val identifier = WorkflowIdentifier.read(source) + fun parse(bytes: ByteString): WorkflowNodeId = Buffer().let { source -> + source.write(bytes) + + val identifierBytes = source.readByteStringWithLength() + val identifier = WorkflowIdentifier.parse(identifierBytes) ?: throw ClassCastException("Invalid WorkflowIdentifier in ByteString") val name = source.readUtf8WithLength() return WorkflowNodeId(identifier, name) diff --git a/workflow-runtime/src/test/java/com/squareup/workflow/TreeSnapshotTest.kt b/workflow-runtime/src/test/java/com/squareup/workflow/TreeSnapshotTest.kt index 2d89b6a237..871254c685 100644 --- a/workflow-runtime/src/test/java/com/squareup/workflow/TreeSnapshotTest.kt +++ b/workflow-runtime/src/test/java/com/squareup/workflow/TreeSnapshotTest.kt @@ -18,11 +18,13 @@ package com.squareup.workflow import com.squareup.workflow.internal.WorkflowNodeId import com.squareup.workflow.internal.id import org.junit.Test +import kotlin.reflect.typeOf import kotlin.test.assertEquals import kotlin.test.assertNull import kotlin.test.assertTrue import kotlin.test.fail +@OptIn(ExperimentalStdlibApi::class, ExperimentalWorkflowApi::class) class TreeSnapshotTest { @Test fun `overrides equals`() { @@ -71,18 +73,70 @@ class TreeSnapshotTest { ) } - @Test fun `empty root is null`() { + @Test fun `serialize handles single unsnapshottable identifier`() { + val rootSnapshot = Snapshot.of("roo") + val id = WorkflowNodeId(UnsnapshottableWorkflow1) + val childSnapshots = mapOf(id to TreeSnapshot.forRootOnly(Snapshot.of("one"))) + + val bytes = TreeSnapshot(rootSnapshot) { childSnapshots }.toByteString() + val treeSnapshot = TreeSnapshot.parse(bytes) + + assertEquals(rootSnapshot.bytes, treeSnapshot.workflowSnapshot?.bytes) + assertTrue(treeSnapshot.childTreeSnapshots.isEmpty()) + } + + @Test fun `serialize drops unsnapshottable identifiers`() { + val rootSnapshot = Snapshot.of("roo") + val id1 = WorkflowNodeId(Workflow1) + val id2 = WorkflowNodeId(UnsnapshottableWorkflow1) + val id3 = WorkflowNodeId(Workflow2, name = "b") + val id4 = WorkflowNodeId(UnsnapshottableWorkflow2, name = "c") + val childSnapshots = mapOf( + id1 to TreeSnapshot.forRootOnly(Snapshot.of("one")), + id2 to TreeSnapshot.forRootOnly(Snapshot.of("two")), + id3 to TreeSnapshot.forRootOnly(Snapshot.of("three")), + id4 to TreeSnapshot.forRootOnly(Snapshot.of("four")) + ) + + val bytes = TreeSnapshot(rootSnapshot) { childSnapshots }.toByteString() + val treeSnapshot = TreeSnapshot.parse(bytes) + + assertEquals(rootSnapshot.bytes, treeSnapshot.workflowSnapshot?.bytes) + assertTrue(id1 in treeSnapshot.childTreeSnapshots) + assertTrue(id2 !in treeSnapshot.childTreeSnapshots) + assertTrue(id3 in treeSnapshot.childTreeSnapshots) + assertTrue(id4 !in treeSnapshot.childTreeSnapshots) + + assertEquals( + "one", treeSnapshot.childTreeSnapshots.getValue(id1).workflowSnapshot!!.bytes.utf8() + ) + assertEquals( + "three", treeSnapshot.childTreeSnapshots.getValue(id3).workflowSnapshot!!.bytes.utf8() + ) + } + + @Test fun `empty root is converted to null`() { val rootSnapshot = Snapshot.EMPTY val treeSnapshot = TreeSnapshot(rootSnapshot, ::emptyMap) assertNull(treeSnapshot.workflowSnapshot) } -} -private object Workflow1 : Workflow { - override fun asStatefulWorkflow(): StatefulWorkflow = fail() -} + private object Workflow1 : Workflow { + override fun asStatefulWorkflow(): StatefulWorkflow = fail() + } -private object Workflow2 : Workflow { - override fun asStatefulWorkflow(): StatefulWorkflow = fail() + private object Workflow2 : Workflow { + override fun asStatefulWorkflow(): StatefulWorkflow = fail() + } + + private object UnsnapshottableWorkflow1 : Workflow, ImpostorWorkflow { + override val realIdentifier = unsnapshottableIdentifier(typeOf()) + override fun asStatefulWorkflow(): StatefulWorkflow = fail() + } + + private object UnsnapshottableWorkflow2 : Workflow, ImpostorWorkflow { + override val realIdentifier = unsnapshottableIdentifier(typeOf()) + override fun asStatefulWorkflow(): StatefulWorkflow = fail() + } }