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() + } }