diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4a15284bd3..c3cba9fc44 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,8 +50,11 @@ kotlin = "1.6.10" kotlinx-binary-compatibility = "0.6.0" kotlinx-coroutines = "1.5.1" +# The 1.5.1 test artifact is jvm-only. The commonTest module should use 1.6.1. +kotlinx-coroutines-test-common = "1.6.1" kotlinx-serialization-json = "1.3.2" kotlinx-benchmark = "0.4.2" +kotlinx-atomicfu = "0.17.2" ktlint = "10.3.0" material = "1.3.0" @@ -184,9 +187,11 @@ kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutine kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-rx2 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx2", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } +kotlinx-coroutines-test-common = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-test-common" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" } kotlinx-benchmark-gradle-plugin = { module = "org.jetbrains.kotlinx:kotlinx-benchmark-plugin", version.ref = "kotlinx-benchmark" } kotlinx-benchmark-runtime = { module = "org.jetbrains.kotlinx:kotlinx-benchmark-runtime", version.ref = "kotlinx-benchmark" } +kotlinx-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "kotlinx-atomicfu" } ktlint-gradle = { module = "org.jlleitschuh.gradle:ktlint-gradle", version.ref = "ktlint" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 5092500b96..2072070084 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -25,7 +25,7 @@ gradleEnterprise { @Suppress("UnstableApiUsage") dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositoriesMode.set(RepositoriesMode.PREFER_PROJECT) repositories { mavenCentral() google() diff --git a/workflow-core/api/workflow-core.api b/workflow-core/api/workflow-core.api index f540ed9da1..adf6f9a240 100644 --- a/workflow-core/api/workflow-core.api +++ b/workflow-core/api/workflow-core.api @@ -164,6 +164,9 @@ public final class com/squareup/workflow1/TypedWorker : com/squareup/workflow1/W public fun toString ()Ljava/lang/String; } +public final class com/squareup/workflow1/Void { +} + public abstract interface class com/squareup/workflow1/Worker { public static final field Companion Lcom/squareup/workflow1/Worker$Companion; public abstract fun doesSameWorkAs (Lcom/squareup/workflow1/Worker;)Z @@ -228,10 +231,10 @@ public final class com/squareup/workflow1/WorkflowAction$Updater { public final class com/squareup/workflow1/WorkflowIdentifier { public static final field Companion Lcom/squareup/workflow1/WorkflowIdentifier$Companion; - public fun (Lkotlin/reflect/KAnnotatedElement;Lcom/squareup/workflow1/WorkflowIdentifier;Lkotlin/jvm/functions/Function0;)V - public synthetic fun (Lkotlin/reflect/KAnnotatedElement;Lcom/squareup/workflow1/WorkflowIdentifier;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lcom/squareup/workflow1/WorkflowIdentifierType;Lcom/squareup/workflow1/WorkflowIdentifier;Lkotlin/jvm/functions/Function0;)V + public synthetic fun (Lcom/squareup/workflow1/WorkflowIdentifierType;Lcom/squareup/workflow1/WorkflowIdentifier;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun equals (Ljava/lang/Object;)Z - public final fun getRealIdentifierType ()Lkotlin/reflect/KAnnotatedElement; + public final fun getRealIdentifierType ()Lcom/squareup/workflow1/WorkflowIdentifierType; public fun hashCode ()I public final fun toByteStringOrNull ()Lokio/ByteString; public fun toString ()Ljava/lang/String; @@ -241,6 +244,41 @@ public final class com/squareup/workflow1/WorkflowIdentifier$Companion { public final fun parse (Lokio/ByteString;)Lcom/squareup/workflow1/WorkflowIdentifier; } +public final class com/squareup/workflow1/WorkflowIdentifierExKt { + public static final fun getWorkflowIdentifier (Lkotlin/reflect/KClass;)Lcom/squareup/workflow1/WorkflowIdentifier; +} + +public abstract class com/squareup/workflow1/WorkflowIdentifierType { + public abstract fun getTypeName ()Ljava/lang/String; +} + +public final class com/squareup/workflow1/WorkflowIdentifierType$Snapshottable : com/squareup/workflow1/WorkflowIdentifierType { + public fun (Ljava/lang/String;Lkotlin/reflect/KClass;)V + public synthetic fun (Ljava/lang/String;Lkotlin/reflect/KClass;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lkotlin/reflect/KClass;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Lkotlin/reflect/KClass; + public final fun copy (Ljava/lang/String;Lkotlin/reflect/KClass;)Lcom/squareup/workflow1/WorkflowIdentifierType$Snapshottable; + public static synthetic fun copy$default (Lcom/squareup/workflow1/WorkflowIdentifierType$Snapshottable;Ljava/lang/String;Lkotlin/reflect/KClass;ILjava/lang/Object;)Lcom/squareup/workflow1/WorkflowIdentifierType$Snapshottable; + public fun equals (Ljava/lang/Object;)Z + public final fun getKClass ()Lkotlin/reflect/KClass; + public fun getTypeName ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/workflow1/WorkflowIdentifierType$Unsnapshottable : com/squareup/workflow1/WorkflowIdentifierType { + public fun (Lkotlin/reflect/KType;)V + public final fun component1 ()Lkotlin/reflect/KType; + public final fun copy (Lkotlin/reflect/KType;)Lcom/squareup/workflow1/WorkflowIdentifierType$Unsnapshottable; + public static synthetic fun copy$default (Lcom/squareup/workflow1/WorkflowIdentifierType$Unsnapshottable;Lkotlin/reflect/KType;ILjava/lang/Object;)Lcom/squareup/workflow1/WorkflowIdentifierType$Unsnapshottable; + public fun equals (Ljava/lang/Object;)Z + public final fun getKType ()Lkotlin/reflect/KType; + public fun getTypeName ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class com/squareup/workflow1/WorkflowOutput { public fun (Ljava/lang/Object;)V public fun equals (Ljava/lang/Object;)Z @@ -266,7 +304,6 @@ public final class com/squareup/workflow1/Workflows { public static final fun collectToSink (Lkotlinx/coroutines/flow/Flow;Lcom/squareup/workflow1/Sink;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun contraMap (Lcom/squareup/workflow1/Sink;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/Sink; public static final fun getIdentifier (Lcom/squareup/workflow1/Workflow;)Lcom/squareup/workflow1/WorkflowIdentifier; - public static final fun getWorkflowIdentifier (Lkotlin/reflect/KClass;)Lcom/squareup/workflow1/WorkflowIdentifier; public static final fun mapRendering (Lcom/squareup/workflow1/Workflow;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/Workflow; public static final fun renderChild (Lcom/squareup/workflow1/BaseRenderContext;Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object; public static final fun renderChild (Lcom/squareup/workflow1/BaseRenderContext;Lcom/squareup/workflow1/Workflow;Ljava/lang/String;)Ljava/lang/Object; diff --git a/workflow-core/build.gradle.kts b/workflow-core/build.gradle.kts index ec5ebbc09b..6a29d177e6 100644 --- a/workflow-core/build.gradle.kts +++ b/workflow-core/build.gradle.kts @@ -7,21 +7,26 @@ apply(from = rootProject.file(".buildscript/configure-maven-publish.gradle")) kotlin { jvm { withJava() } + ios() sourceSets { - val jvmMain by getting { + all { + languageSettings.apply { + optIn("kotlin.RequiresOptIn") + } + } + val commonMain by getting { dependencies { - compileOnly(libs.jetbrains.annotations) - api(libs.kotlin.jdk6) api(libs.kotlinx.coroutines.core) // For Snapshot. api(libs.squareup.okio) } } - val jvmTest by getting { + val commonTest by getting { dependencies { - implementation(libs.kotlinx.coroutines.test) + implementation(libs.kotlinx.atomicfu) + implementation(libs.kotlinx.coroutines.test.common) implementation(libs.kotlin.test.jdk) } } diff --git a/workflow-core/src/jvmMain/baseline-prof.txt b/workflow-core/src/commonMain/baseline-prof.txt similarity index 100% rename from workflow-core/src/jvmMain/baseline-prof.txt rename to workflow-core/src/commonMain/baseline-prof.txt diff --git a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/BaseRenderContext.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/BaseRenderContext.kt similarity index 99% rename from workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/BaseRenderContext.kt rename to workflow-core/src/commonMain/kotlin/com/squareup/workflow1/BaseRenderContext.kt index 1f1d906ba7..c727556de0 100644 --- a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/BaseRenderContext.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/BaseRenderContext.kt @@ -6,6 +6,8 @@ package com.squareup.workflow1 import com.squareup.workflow1.WorkflowAction.Companion.noAction import kotlinx.coroutines.CoroutineScope +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName import kotlin.reflect.KType import kotlin.reflect.typeOf diff --git a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/ImpostorWorkflow.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/ImpostorWorkflow.kt similarity index 95% rename from workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/ImpostorWorkflow.kt rename to workflow-core/src/commonMain/kotlin/com/squareup/workflow1/ImpostorWorkflow.kt index 781367dc87..4e75c8f7dd 100644 --- a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/ImpostorWorkflow.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/ImpostorWorkflow.kt @@ -3,6 +3,9 @@ package com.squareup.workflow1 +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName + /** * Optional interface that [Workflow]s should implement if they need the runtime to consider their * identity to include a child workflow's identity. Two [ImpostorWorkflow]s with the same concrete diff --git a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/LifecycleWorker.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/LifecycleWorker.kt similarity index 97% rename from workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/LifecycleWorker.kt rename to workflow-core/src/commonMain/kotlin/com/squareup/workflow1/LifecycleWorker.kt index 7a993f6168..de812509ee 100644 --- a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/LifecycleWorker.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/LifecycleWorker.kt @@ -6,6 +6,8 @@ package com.squareup.workflow1 import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName /** * [Worker] that performs some action when the worker is started and/or stopped. diff --git a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/Sink.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Sink.kt similarity index 98% rename from workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/Sink.kt rename to workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Sink.kt index 8bfa70168f..9ad52680c2 100644 --- a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/Sink.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Sink.kt @@ -7,6 +7,8 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName /** * An object that receives values (commonly events or [WorkflowAction]). diff --git a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/Snapshot.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Snapshot.kt similarity index 95% rename from workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/Snapshot.kt rename to workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Snapshot.kt index 81f207ae3a..0aa2bbba77 100644 --- a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/Snapshot.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Snapshot.kt @@ -8,8 +8,8 @@ import okio.BufferedSink import okio.BufferedSource import okio.ByteString import okio.ByteString.Companion.encodeUtf8 -import java.lang.Float.floatToRawIntBits -import java.lang.Float.intBitsToFloat +import kotlin.jvm.JvmName +import kotlin.jvm.JvmStatic /** * A lazy wrapper of [ByteString]. Allows [Workflow]s to capture their state frequently, without @@ -93,9 +93,9 @@ public fun BufferedSink.writeBooleanAsInt(bool: Boolean): BufferedSink = public fun BufferedSource.readBooleanFromInt(): Boolean = readInt() == 1 -public fun BufferedSink.writeFloat(float: Float): BufferedSink = writeInt(floatToRawIntBits(float)) +public fun BufferedSink.writeFloat(float: Float): BufferedSink = writeInt(float.toRawBits()) -public fun BufferedSource.readFloat(): Float = intBitsToFloat(readInt()) +public fun BufferedSource.readFloat(): Float = Float.fromBits(readInt()) public fun BufferedSink.writeUtf8WithLength(str: String): BufferedSink { return writeByteStringWithLength(str.encodeUtf8()) @@ -130,7 +130,7 @@ public fun > BufferedSink.writeOptionalEnumByOrdinal(enumVal: T?): B } public inline fun > BufferedSource.readEnumByOrdinal(): T { - return T::class.java.enumConstants[readInt()] + return enumValues()[readInt()] } public fun > BufferedSink.writeEnumByOrdinal(enumVal: T): BufferedSink { diff --git a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/StatefulWorkflow.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/StatefulWorkflow.kt similarity index 99% rename from workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/StatefulWorkflow.kt rename to workflow-core/src/commonMain/kotlin/com/squareup/workflow1/StatefulWorkflow.kt index a90ffc1b97..031235ff4c 100644 --- a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/StatefulWorkflow.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/StatefulWorkflow.kt @@ -6,6 +6,8 @@ package com.squareup.workflow1 import com.squareup.workflow1.StatefulWorkflow.RenderContext import com.squareup.workflow1.WorkflowAction.Companion.toString +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName /** * A composable, stateful object that can [handle events][RenderContext.actionSink], diff --git a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/StatelessWorkflow.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/StatelessWorkflow.kt similarity index 98% rename from workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/StatelessWorkflow.kt rename to workflow-core/src/commonMain/kotlin/com/squareup/workflow1/StatelessWorkflow.kt index 47552a7fc2..f6545524e2 100644 --- a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/StatelessWorkflow.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/StatelessWorkflow.kt @@ -3,6 +3,9 @@ package com.squareup.workflow1 +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName + /** * Minimal implementation of [Workflow] that maintains no state of its own. * diff --git a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Void.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Void.kt new file mode 100644 index 0000000000..d5cddaf06a --- /dev/null +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Void.kt @@ -0,0 +1,7 @@ +package com.squareup.workflow1 + +/** + * [Nothing] cannot be used as a reified type so we have duplicated + * it here to avoid adding a dependency on kotlin.reflect. + */ +internal class Void private constructor() diff --git a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/Worker.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Worker.kt similarity index 99% rename from workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/Worker.kt rename to workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Worker.kt index 41ae6c4038..ade5f07b1b 100644 --- a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/Worker.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Worker.kt @@ -14,6 +14,8 @@ import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flow import kotlin.experimental.ExperimentalTypeInference +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName import kotlin.reflect.KType import kotlin.reflect.typeOf diff --git a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/WorkerWorkflow.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkerWorkflow.kt similarity index 98% rename from workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/WorkerWorkflow.kt rename to workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkerWorkflow.kt index 9f42428434..c5ec0bba90 100644 --- a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/WorkerWorkflow.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkerWorkflow.kt @@ -6,6 +6,8 @@ package com.squareup.workflow1 import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName import kotlin.reflect.KType /** diff --git a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/Workflow.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Workflow.kt similarity index 99% rename from workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/Workflow.kt rename to workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Workflow.kt index d64e0b858f..ce2aaf685d 100644 --- a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/Workflow.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Workflow.kt @@ -3,6 +3,9 @@ package com.squareup.workflow1 +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName + /** * A composable, optionally-stateful object that can [handle events][BaseRenderContext.actionSink], * [delegate to children][BaseRenderContext.renderChild], diff --git a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/WorkflowAction.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowAction.kt similarity index 98% rename from workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/WorkflowAction.kt rename to workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowAction.kt index 5c96593832..42e0a27918 100644 --- a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/WorkflowAction.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowAction.kt @@ -4,6 +4,8 @@ package com.squareup.workflow1 import com.squareup.workflow1.WorkflowAction.Companion.toString +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName /** * An atomic operation that updates the state of a [Workflow], and also optionally emits an output. diff --git a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/WorkflowIdentifier.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowIdentifier.kt similarity index 77% rename from workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/WorkflowIdentifier.kt rename to workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowIdentifier.kt index 10698ef083..a155834531 100644 --- a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/WorkflowIdentifier.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowIdentifier.kt @@ -3,12 +3,14 @@ package com.squareup.workflow1 +import com.squareup.workflow1.WorkflowIdentifierType.Snapshottable +import com.squareup.workflow1.WorkflowIdentifierType.Unsnapshottable import okio.Buffer import okio.ByteString import okio.EOFException -import org.jetbrains.annotations.TestOnly import kotlin.LazyThreadSafetyMode.PUBLICATION -import kotlin.reflect.KAnnotatedElement +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName import kotlin.reflect.KClass import kotlin.reflect.KType @@ -36,30 +38,23 @@ import kotlin.reflect.KType * snapshotted. * * @constructor - * @param type The [KClass] of the [Workflow] this identifier identifies, or the [KType] of an - * [unsnapshottableIdentifier]. + * @param type Wrapper around 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. * @param description Implementation of [ImpostorWorkflow.describeRealIdentifier]. */ public class WorkflowIdentifier internal constructor( - private val type: KAnnotatedElement, + private val type: WorkflowIdentifierType, private val proxiedIdentifier: WorkflowIdentifier? = null, private val description: (() -> String?)? = null ) { - init { - require( - type is KClass<*> || (type is KType && type.classifier is KClass<*>) - ) { "Expected type to be either a KClass or a KType with a KClass classifier, but was $type" } - } /** * The fully-qualified name of the type of workflow this identifier identifies. Computed lazily * and cached. */ - private val typeName: String by lazy(PUBLICATION) { - if (type is KClass<*>) type.java.name else type.toString() - } + private val typeName: String by lazy(PUBLICATION) { type.typeName } private val proxiedIdentifiers = generateSequence(this) { it.proxiedIdentifier } @@ -68,7 +63,7 @@ public class WorkflowIdentifier internal constructor( * If it is not snapshottable, returns null. */ public fun toByteStringOrNull(): ByteString? { - if (type !is KClass<*>) return null + if (type is Unsnapshottable) return null val proxiedBytes = proxiedIdentifier?.let { // If we have a proxied identifier but it's not serializable, then we can't be serializable @@ -92,8 +87,7 @@ public class WorkflowIdentifier internal constructor( * Returns either a [KClass] or [KType] representing the "real" type that this identifier * identifies – i.e. which is not an [ImpostorWorkflow]. */ - @TestOnly - public fun getRealIdentifierType(): KAnnotatedElement = proxiedIdentifiers.last().type + public fun getRealIdentifierType(): WorkflowIdentifierType = proxiedIdentifiers.last().type /** * If this identifier identifies an [ImpostorWorkflow], returns the result of that workflow's @@ -110,11 +104,11 @@ public class WorkflowIdentifier internal constructor( override fun equals(other: Any?): Boolean = when { this === other -> true other !is WorkflowIdentifier -> false - else -> type == other.type && proxiedIdentifier == other.proxiedIdentifier + else -> type.typeName == other.type.typeName && proxiedIdentifier == other.proxiedIdentifier } override fun hashCode(): Int { - var result = type.hashCode() + var result = type.typeName.hashCode() result = 31 * result + (proxiedIdentifier?.hashCode() ?: 0) return result } @@ -141,9 +135,7 @@ public class WorkflowIdentifier internal constructor( else -> throw IllegalArgumentException("Invalid WorkflowIdentifier") } - @Suppress("UNCHECKED_CAST") - val type = Class.forName(typeString) as Class> - return WorkflowIdentifier(type.kotlin, proxiedIdentifier) + return WorkflowIdentifier(Snapshottable(typeString), proxiedIdentifier) } catch (e: EOFException) { throw IllegalArgumentException("Invalid WorkflowIdentifier") } @@ -158,7 +150,7 @@ public val Workflow<*, *, *>.identifier: WorkflowIdentifier get() { val maybeImpostor = this as? ImpostorWorkflow return WorkflowIdentifier( - type = this::class, + type = Snapshottable(this::class), proxiedIdentifier = maybeImpostor?.realIdentifier, description = maybeImpostor?.let { it::describeRealIdentifier } ) @@ -174,22 +166,5 @@ public val Workflow<*, *, *>.identifier: WorkflowIdentifier * function should only be used for [ImpostorWorkflow]s that wrap a closed set of known workflow * types.** */ -public fun unsnapshottableIdentifier(type: KType): WorkflowIdentifier = WorkflowIdentifier(type) - -/** - * The [WorkflowIdentifier] that identifies the workflow this [KClass] represents. - * - * This workflow must not be an [ImpostorWorkflow], or this property will throw an - * [IllegalArgumentException]. - */ -@OptIn(ExperimentalStdlibApi::class) -@get:TestOnly -public val KClass>.workflowIdentifier: WorkflowIdentifier - get() { - val workflowClass = this@workflowIdentifier - require(!ImpostorWorkflow::class.java.isAssignableFrom(workflowClass.java)) { - "Cannot create WorkflowIdentifier from a KClass of ImpostorWorkflow: " + - workflowClass.qualifiedName.toString() - } - return WorkflowIdentifier(type = workflowClass) - } +public fun unsnapshottableIdentifier(type: KType): WorkflowIdentifier = + WorkflowIdentifier(Unsnapshottable(type)) diff --git a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowIdentifierType.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowIdentifierType.kt new file mode 100644 index 0000000000..462de85c28 --- /dev/null +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowIdentifierType.kt @@ -0,0 +1,46 @@ +package com.squareup.workflow1 + +import kotlin.reflect.KClass +import kotlin.reflect.KType + +/** + * Represents a subset of [KAnnotatedElement], namely [KClass] or [KType]. Used by the runtime to + * determine whether a [WorkflowIdentifier], and thus the [Workflow] it identifies, is serializable + * or not via the [Snapshot] mechanism. + */ +public sealed class WorkflowIdentifierType { + + public abstract val typeName: String + + /** + * A [WorkflowIdentifier] is snapshottable if its type is this [Snapshottable] class. + * + * @constructor + * @param typeName The qualified name of its corresponding [Workflow]. + * @param kClass The [KClass] of the [Workflow] this helps identify. + */ + public data class Snapshottable( + override val typeName: String, + val kClass: KClass<*>? = null, + ) : WorkflowIdentifierType() { + public constructor(kClass: KClass<*>) : this( + kClass.qualifiedName ?: kClass.toString(), kClass + ) + } + + /** + * A [WorkflowIdentifier] is unsnapshottable if its type is this [Unsnapshottable] class. + * + * @constructor + * @param kType The [KType] of the [Workflow] this helps identify. + */ + public data class Unsnapshottable(val kType: KType) : WorkflowIdentifierType() { + init { + require(kType.classifier is KClass<*>) { + "Expected a KType with a KClass classifier, but was ${kType.classifier}" + } + } + + override val typeName: String = kType.toString() + } +} diff --git a/workflow-core/src/jvmTest/kotlin/com/squareup/workflow1/SinkTest.kt b/workflow-core/src/commonTest/kotlin/com/squareup/workflow1/SinkTest.kt similarity index 69% rename from workflow-core/src/jvmTest/kotlin/com/squareup/workflow1/SinkTest.kt rename to workflow-core/src/commonTest/kotlin/com/squareup/workflow1/SinkTest.kt index 127b3d29a6..6a1639b6c8 100644 --- a/workflow-core/src/jvmTest/kotlin/com/squareup/workflow1/SinkTest.kt +++ b/workflow-core/src/commonTest/kotlin/com/squareup/workflow1/SinkTest.kt @@ -1,13 +1,17 @@ package com.squareup.workflow1 +import kotlinx.atomicfu.atomic import kotlinx.coroutines.CoroutineStart.UNDISPATCHED import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.launch -import kotlinx.coroutines.test.runBlockingTest -import java.util.concurrent.atomic.AtomicInteger +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -23,53 +27,51 @@ internal class SinkTest { private val sink = RecordingSink() - @Test fun `collectToSink sends action`() { - runBlockingTest { - val flow = MutableStateFlow(1) - val collector = launch { - flow.collectToSink(sink) { - action { - state = "$props $state $it" - setOutput("output: $it") - } + @Test fun `collectToSink sends action`() = runTest { + val flow = MutableStateFlow(1) + val collector = launch { + flow.collectToSink(sink) { + action { + state = "$props $state $it" + setOutput("output: $it") } } + } - advanceUntilIdle() - assertEquals(1, sink.actions.size) - sink.actions.removeFirst() - .let { action -> - val (newState, output) = action.applyTo("props", "state") - assertEquals("props state 1", newState) - assertEquals("output: 1", output?.value) - } - assertTrue(sink.actions.isEmpty()) - - flow.value = 2 - advanceUntilIdle() - assertEquals(1, sink.actions.size) - sink.actions.removeFirst() - .let { action -> - val (newState, output) = action.applyTo("props", "state") - assertEquals("props state 2", newState) - assertEquals("output: 2", output?.value) - } + advanceUntilIdle() + assertEquals(1, sink.actions.size) + sink.actions.removeFirst() + .let { action -> + val (newState, output) = action.applyTo("props", "state") + assertEquals("props state 1", newState) + assertEquals("output: 1", output?.value) + } + assertTrue(sink.actions.isEmpty()) + + flow.value = 2 + advanceUntilIdle() + assertEquals(1, sink.actions.size) + sink.actions.removeFirst() + .let { action -> + val (newState, output) = action.applyTo("props", "state") + assertEquals("props state 2", newState) + assertEquals("output: 2", output?.value) + } - collector.cancel() - } + collector.cancel() } @Test fun `collectToSink propagates backpressure`() { val channel = Channel() val flow = channel.consumeAsFlow() // Used to assert ordering. - val counter = AtomicInteger(0) + val counter = atomic(0) val sentActions = mutableListOf>() val sink = Sink> { sentActions += it } - runBlockingTest { + runTest(UnconfinedTestDispatcher()) { val collectJob = launch { flow.collectToSink(sink) { action { setOutput(it) } } } @@ -118,7 +120,7 @@ internal class SinkTest { setOutput("output") } - runBlockingTest { + runTest { launch { sink.sendAndAwaitApplication(action) } advanceUntilIdle() @@ -130,33 +132,32 @@ internal class SinkTest { } } - @Test fun `sendAndAwaitApplication suspends until after applied`() { - runBlockingTest { - var resumed = false - val action = action { - assertFalse(resumed) - } - launch { - sink.sendAndAwaitApplication(action) - resumed = true - } - advanceUntilIdle() + @Test fun `sendAndAwaitApplication suspends until after applied`() = runTest { + var resumed = false + val action = action { assertFalse(resumed) - assertEquals(1, sink.actions.size) + } + launch { + sink.sendAndAwaitApplication(action) + resumed = true + } + advanceUntilIdle() + assertFalse(resumed) + assertEquals(1, sink.actions.size) - val enqueuedAction = sink.actions.removeFirst() - pauseDispatcher() - enqueuedAction.applyTo("props", "state") + val enqueuedAction = sink.actions.removeFirst() + withContext(StandardTestDispatcher(testScheduler)) { + enqueuedAction.applyTo("props", "state") assertFalse(resumed) - resumeDispatcher() - advanceUntilIdle() - assertTrue(resumed) } + + advanceUntilIdle() + assertTrue(resumed) } - @Test fun `sendAndAwaitApplication doesn't apply action when cancelled while suspended`() { - runBlockingTest { + @Test fun `sendAndAwaitApplication doesn't apply action when cancelled while suspended`() = + runTest { var applied = false val action = action { applied = true @@ -176,7 +177,6 @@ internal class SinkTest { assertEquals("state", newState) assertNull(output) } - } private class RecordingSink : Sink> { val actions = mutableListOf>() diff --git a/workflow-core/src/jvmTest/kotlin/com/squareup/workflow1/WorkerTest.kt b/workflow-core/src/commonTest/kotlin/com/squareup/workflow1/WorkerTest.kt similarity index 100% rename from workflow-core/src/jvmTest/kotlin/com/squareup/workflow1/WorkerTest.kt rename to workflow-core/src/commonTest/kotlin/com/squareup/workflow1/WorkerTest.kt diff --git a/workflow-core/src/jvmTest/kotlin/com/squareup/workflow1/WorkerWorkflowTest.kt b/workflow-core/src/commonTest/kotlin/com/squareup/workflow1/WorkerWorkflowTest.kt similarity index 65% rename from workflow-core/src/jvmTest/kotlin/com/squareup/workflow1/WorkerWorkflowTest.kt rename to workflow-core/src/commonTest/kotlin/com/squareup/workflow1/WorkerWorkflowTest.kt index e88ff82660..90fbef4392 100644 --- a/workflow-core/src/jvmTest/kotlin/com/squareup/workflow1/WorkerWorkflowTest.kt +++ b/workflow-core/src/commonTest/kotlin/com/squareup/workflow1/WorkerWorkflowTest.kt @@ -7,30 +7,9 @@ import kotlinx.coroutines.runBlocking import kotlin.coroutines.coroutineContext import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertFailsWith class WorkerWorkflowTest { - /** - * This should be impossible, since the return type is non-nullable. However it is very easy to - * accidentally create a mock using libraries like Mockito in unit tests that return null Flows. - */ - @Test fun `runWorker throws when flow is null`() { - val nullFlowWorker = NullFlowWorker() - - val error = runBlocking { - assertFailsWith { - runWorker(nullFlowWorker, "", NoopSink) - } - } - - assertEquals( - "Worker NullFlowWorker.toString returned a null Flow. " + - "If this is a test mock, make sure you mock the run() method!", - error.message - ) - } - @Test fun `runWorker coroutine is named without key`() { val worker = CoroutineNameWorker() runBlocking { diff --git a/workflow-core/src/jvmTest/kotlin/com/squareup/workflow1/WorkflowActionTest.kt b/workflow-core/src/commonTest/kotlin/com/squareup/workflow1/WorkflowActionTest.kt similarity index 100% rename from workflow-core/src/jvmTest/kotlin/com/squareup/workflow1/WorkflowActionTest.kt rename to workflow-core/src/commonTest/kotlin/com/squareup/workflow1/WorkflowActionTest.kt diff --git a/workflow-core/src/jvmTest/kotlin/com/squareup/workflow1/WorkflowIdentifierTest.kt b/workflow-core/src/commonTest/kotlin/com/squareup/workflow1/WorkflowIdentifierTest.kt similarity index 75% rename from workflow-core/src/jvmTest/kotlin/com/squareup/workflow1/WorkflowIdentifierTest.kt rename to workflow-core/src/commonTest/kotlin/com/squareup/workflow1/WorkflowIdentifierTest.kt index ba25668e37..aaf5aef4ab 100644 --- a/workflow-core/src/jvmTest/kotlin/com/squareup/workflow1/WorkflowIdentifierTest.kt +++ b/workflow-core/src/commonTest/kotlin/com/squareup/workflow1/WorkflowIdentifierTest.kt @@ -1,5 +1,7 @@ package com.squareup.workflow1 +import com.squareup.workflow1.WorkflowIdentifierType.Snapshottable +import com.squareup.workflow1.WorkflowIdentifierType.Unsnapshottable import okio.Buffer import okio.ByteString import kotlin.reflect.KType @@ -16,7 +18,7 @@ internal class WorkflowIdentifierTest { @Test fun `flat identifier toString`() { val id = TestWorkflow1.identifier assertEquals( - "WorkflowIdentifier(com.squareup.workflow1.WorkflowIdentifierTest\$TestWorkflow1)", + "WorkflowIdentifier(com.squareup.workflow1.WorkflowIdentifierTest.TestWorkflow1)", id.toString() ) } @@ -47,15 +49,18 @@ internal class WorkflowIdentifierTest { val id = TestImpostor().identifier assertEquals( - "WorkflowIdentifier(${TestImpostor::class.java.name}, " + - "com.squareup.workflow1.WorkflowIdentifierTest\$TestWorkflow1)", + "WorkflowIdentifier(${TestImpostor::class}, " + + "com.squareup.workflow1.WorkflowIdentifierTest.TestWorkflow1)", id.toString() ) } @Test fun `impostor identifier description`() { val id = TestImpostor1(TestWorkflow1).identifier - assertEquals("TestImpostor1(TestWorkflow1)", id.toString()) + assertEquals( + "TestImpostor1(com.squareup.workflow1.WorkflowIdentifierTest.TestWorkflow1)", + id.toString() + ) } @Test fun `restored identifier toString`() { @@ -156,14 +161,11 @@ internal class WorkflowIdentifierTest { @Test fun `read from corrupted source throws`() { val source = TestWorkflow1.identifier.toByteStringOrNull()!! .toByteArray() - source.indices.reversed() - .take(10) - .forEach { i -> - source[i] = 0 - } - val corruptedSource = Buffer().apply { write(source) } + + val corruptedSource = Buffer().apply { write(source.dropLast(2).toByteArray()) } .readByteString() - assertFailsWith { + + assertFailsWith { WorkflowIdentifier.parse(corruptedSource) } } @@ -173,14 +175,6 @@ internal class WorkflowIdentifierTest { 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()) @@ -203,79 +197,46 @@ internal class WorkflowIdentifierTest { 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() - ) - } - - @Test fun `workflowIdentifier from Workflow class is equal to identifier from workflow`() { - val instanceId = TestWorkflow1.identifier - val classId = TestWorkflow1::class.workflowIdentifier - assertEquals(instanceId, classId) - } - - @Test - fun `workflowIdentifier from Workflow class is not equal to identifier from different class`() { - val id1 = TestWorkflow1::class.workflowIdentifier - val id2 = TestWorkflow2::class.workflowIdentifier - assertNotEquals(id1, id2) - } - - @Test fun `workflowIdentifier from ImpostorWorkflow class throws`() { - val error = assertFailsWith { - TestImpostor1::class.workflowIdentifier - } - assertEquals( - "Cannot create WorkflowIdentifier from a KClass of ImpostorWorkflow: " + - TestImpostor1::class.qualifiedName, - error.message - ) - } - - @Test fun `getRealIdentifierType() returns self for non-impostor workflow`() { + @Test fun `getRealIdentifierType returns self for non-impostor workflow`() { val id = TestWorkflow1.identifier - assertEquals(TestWorkflow1::class, id.getRealIdentifierType()) + assertEquals(Snapshottable(TestWorkflow1::class), id.getRealIdentifierType()) } - @Test fun `getRealIdentifierType() returns real identifier for impostor workflow`() { + @Test fun `getRealIdentifierType returns real identifier for impostor workflow`() { val id = TestImpostor1(TestWorkflow1).identifier - assertEquals(TestWorkflow1::class, id.getRealIdentifierType()) + assertEquals(Snapshottable(TestWorkflow1::class), id.getRealIdentifierType()) } - @Test fun `getRealIdentifierType() returns leaf real identifier for impostor workflow chain`() { + @Test fun `getRealIdentifierType returns leaf real identifier for impostor workflow chain`() { val id = TestImpostor2(TestImpostor1(TestWorkflow1)).identifier - assertEquals(TestWorkflow1::class, id.getRealIdentifierType()) + assertEquals(Snapshottable(TestWorkflow1::class), id.getRealIdentifierType()) } - @Test fun `getRealIdentifierType() returns KType of unsnapshottable identifier`() { + @Test fun `getRealIdentifierType returns KType of unsnapshottable identifier`() { val id = TestUnsnapshottableImpostor(typeOf>()).identifier - assertEquals(typeOf>(), id.getRealIdentifierType()) + assertEquals(Unsnapshottable(typeOf>()), id.getRealIdentifierType()) } - private object TestWorkflow1 : Workflow { + public object TestWorkflow1 : Workflow { override fun asStatefulWorkflow(): StatefulWorkflow = throw NotImplementedError() } - private object TestWorkflow2 : Workflow { + public object TestWorkflow2 : Workflow { override fun asStatefulWorkflow(): StatefulWorkflow = throw NotImplementedError() } - private class TestImpostor1( + public class TestImpostor1( private val proxied: Workflow<*, *, *> ) : Workflow, ImpostorWorkflow { override val realIdentifier: WorkflowIdentifier = proxied.identifier - override fun describeRealIdentifier(): String = "TestImpostor1(${proxied::class.simpleName})" + override fun describeRealIdentifier(): String = "TestImpostor1(${proxied::class.qualifiedName})" override fun asStatefulWorkflow(): StatefulWorkflow = throw NotImplementedError() } - private class TestImpostor2( + public class TestImpostor2( proxied: Workflow<*, *, *> ) : Workflow, ImpostorWorkflow { override val realIdentifier: WorkflowIdentifier = proxied.identifier @@ -283,7 +244,7 @@ internal class WorkflowIdentifierTest { throw NotImplementedError() } - private class TestUnsnapshottableImpostor( + public class TestUnsnapshottableImpostor( type: KType ) : Workflow, ImpostorWorkflow { override val realIdentifier: WorkflowIdentifier = unsnapshottableIdentifier(type) diff --git a/workflow-core/src/iosTest/kotlin/com/squareup/workflow1/NativeWorkflowIdentifierTest.kt b/workflow-core/src/iosTest/kotlin/com/squareup/workflow1/NativeWorkflowIdentifierTest.kt new file mode 100644 index 0000000000..e2a28e123d --- /dev/null +++ b/workflow-core/src/iosTest/kotlin/com/squareup/workflow1/NativeWorkflowIdentifierTest.kt @@ -0,0 +1,26 @@ +package com.squareup.workflow1 + +import com.squareup.workflow1.WorkflowIdentifierTest.TestUnsnapshottableImpostor +import kotlin.reflect.typeOf +import kotlin.test.Test +import kotlin.test.assertEquals + +class NativeWorkflowIdentifierTest { + + @Test fun `unsnapshottable identifier toString`() { + val id = unsnapshottableIdentifier(typeOf()) + assertEquals( + "WorkflowIdentifier(${String::class.qualifiedName})", + id.toString() + ) + } + + @Test fun `unsnapshottable impostor identifier toString`() { + val id = TestUnsnapshottableImpostor(typeOf()).identifier + assertEquals( + "WorkflowIdentifier(${TestUnsnapshottableImpostor::class.qualifiedName}, " + + "${String::class.qualifiedName})", + id.toString() + ) + } +} diff --git a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/WorkflowIdentifierEx.kt b/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/WorkflowIdentifierEx.kt new file mode 100644 index 0000000000..c20fc5a85c --- /dev/null +++ b/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/WorkflowIdentifierEx.kt @@ -0,0 +1,23 @@ +package com.squareup.workflow1 + +import com.squareup.workflow1.WorkflowIdentifierType.Snapshottable +import org.jetbrains.annotations.TestOnly +import kotlin.reflect.KClass + +/** + * The [WorkflowIdentifier] that identifies the workflow this [KClass] represents. + * + * This workflow must not be an [ImpostorWorkflow], or this property will throw an + * [IllegalArgumentException]. + */ +@OptIn(ExperimentalStdlibApi::class) +@get:TestOnly +public val KClass>.workflowIdentifier: WorkflowIdentifier + get() { + val workflowClass = this@workflowIdentifier + require(!ImpostorWorkflow::class.java.isAssignableFrom(workflowClass.java)) { + "Cannot create WorkflowIdentifier from a KClass of ImpostorWorkflow: " + + workflowClass.qualifiedName.toString() + } + return WorkflowIdentifier(type = Snapshottable(workflowClass)) + } diff --git a/workflow-core/src/jvmTest/kotlin/com/squareup/workflow1/JvmWorkerWorkflowTest.kt b/workflow-core/src/jvmTest/kotlin/com/squareup/workflow1/JvmWorkerWorkflowTest.kt new file mode 100644 index 0000000000..2c95110ef5 --- /dev/null +++ b/workflow-core/src/jvmTest/kotlin/com/squareup/workflow1/JvmWorkerWorkflowTest.kt @@ -0,0 +1,35 @@ +package com.squareup.workflow1 + +import kotlinx.coroutines.runBlocking +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class JvmWorkerWorkflowTest { + + /** + * This should be impossible, since the return type is non-nullable. However it is very easy to + * accidentally create a mock using libraries like Mockito in unit tests that return null Flows. + */ + @Test fun `runWorker throws when flow is null`() { + val nullFlowWorker = NullFlowWorker() + + val error = runBlocking { + assertFailsWith { + runWorker(nullFlowWorker, "", NoopSink) + } + } + + assertEquals( + "Worker NullFlowWorker.toString returned a null Flow. " + + "If this is a test mock, make sure you mock the run() method!", + error.message + ) + } + + private object NoopSink : Sink { + override fun send(value: Any?) { + // Noop + } + } +} diff --git a/workflow-core/src/jvmTest/kotlin/com/squareup/workflow1/JvmWorkflowIdentifierTest.kt b/workflow-core/src/jvmTest/kotlin/com/squareup/workflow1/JvmWorkflowIdentifierTest.kt new file mode 100644 index 0000000000..372f23a665 --- /dev/null +++ b/workflow-core/src/jvmTest/kotlin/com/squareup/workflow1/JvmWorkflowIdentifierTest.kt @@ -0,0 +1,55 @@ +package com.squareup.workflow1 + +import com.squareup.workflow1.WorkflowIdentifierTest.TestImpostor1 +import com.squareup.workflow1.WorkflowIdentifierTest.TestUnsnapshottableImpostor +import com.squareup.workflow1.WorkflowIdentifierTest.TestWorkflow1 +import com.squareup.workflow1.WorkflowIdentifierTest.TestWorkflow2 +import org.junit.Test +import kotlin.reflect.typeOf +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNotEquals + +class JvmWorkflowIdentifierTest { + + @Test fun `workflowIdentifier from Workflow class is equal to identifier from workflow`() { + val instanceId = TestWorkflow1.identifier + val classId = TestWorkflow1::class.workflowIdentifier + assertEquals(instanceId, classId) + } + + @Test + fun `workflowIdentifier from Workflow class is not equal to identifier from different class`() { + val id1 = TestWorkflow1::class.workflowIdentifier + val id2 = TestWorkflow2::class.workflowIdentifier + assertNotEquals(id1, id2) + } + + @Test fun `workflowIdentifier from ImpostorWorkflow class throws`() { + val error = assertFailsWith { + TestImpostor1::class.workflowIdentifier + } + assertEquals( + "Cannot create WorkflowIdentifier from a KClass of ImpostorWorkflow: " + + TestImpostor1::class.qualifiedName, + error.message + ) + } + + @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 impostor identifier toString()`() { + val id = TestUnsnapshottableImpostor(typeOf()).identifier + assertEquals( + "WorkflowIdentifier(${TestUnsnapshottableImpostor::class.qualifiedName}, " + + "${String::class.java.name} (Kotlin reflection is not available))", + id.toString() + ) + } +} diff --git a/workflow-testing/api/workflow-testing.api b/workflow-testing/api/workflow-testing.api index 2245d35993..940ad14b6b 100644 --- a/workflow-testing/api/workflow-testing.api +++ b/workflow-testing/api/workflow-testing.api @@ -85,6 +85,7 @@ public final class com/squareup/workflow1/testing/RealRenderTester$Expectation$E public final class com/squareup/workflow1/testing/RealRenderTesterKt { public static final fun createRenderChildInvocation (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;)Lcom/squareup/workflow1/testing/RenderTester$RenderChildInvocation; + public static final fun matchesExpectation (Lcom/squareup/workflow1/WorkflowIdentifierType;Lcom/squareup/workflow1/WorkflowIdentifierType;)Z public static final fun realTypeMatchesExpectation (Lcom/squareup/workflow1/WorkflowIdentifier;Lcom/squareup/workflow1/WorkflowIdentifier;)Z } diff --git a/workflow-testing/src/main/java/com/squareup/workflow1/testing/RealRenderTester.kt b/workflow-testing/src/main/java/com/squareup/workflow1/testing/RealRenderTester.kt index 1bc8de3772..4e230c57ae 100644 --- a/workflow-testing/src/main/java/com/squareup/workflow1/testing/RealRenderTester.kt +++ b/workflow-testing/src/main/java/com/squareup/workflow1/testing/RealRenderTester.kt @@ -9,6 +9,9 @@ import com.squareup.workflow1.Workflow import com.squareup.workflow1.WorkflowAction import com.squareup.workflow1.WorkflowAction.Companion.noAction import com.squareup.workflow1.WorkflowIdentifier +import com.squareup.workflow1.WorkflowIdentifierType +import com.squareup.workflow1.WorkflowIdentifierType.Snapshottable +import com.squareup.workflow1.WorkflowIdentifierType.Unsnapshottable import com.squareup.workflow1.WorkflowOutput import com.squareup.workflow1.applyTo import com.squareup.workflow1.identifier @@ -20,7 +23,6 @@ import com.squareup.workflow1.testing.RenderTester.ChildWorkflowMatch.Matched import com.squareup.workflow1.testing.RenderTester.RenderChildInvocation import kotlinx.coroutines.CoroutineScope import kotlin.reflect.KClass -import kotlin.reflect.KType import kotlin.reflect.full.allSupertypes import kotlin.reflect.full.isSuperclassOf import kotlin.reflect.full.isSupertypeOf @@ -354,28 +356,24 @@ internal fun WorkflowIdentifier.realTypeMatchesExpectation( ): Boolean { val expectedType = expected.getRealIdentifierType() val actualType = getRealIdentifierType() + return actualType.matchesExpectation(expectedType) +} +internal fun WorkflowIdentifierType.matchesExpectation(expected: WorkflowIdentifierType): Boolean { return when { - // Expectation is definitely not for this identifier if the identifiers aren't even the same - // type, so don't try to compare them. - expectedType::class != actualType::class -> false - expectedType is KType && actualType is KType -> { - // Comparing an expectation of an unsnapshottable ID with an unsnapshottable ID. - expectedType.isSupertypeOf(actualType) - } - expectedType is KClass<*> && actualType is KClass<*> -> { - // Comparing an expectation of a workflow with a workflow. - expectedType.isSuperclassOf(actualType) || actualType.isJavaMockOf(expectedType) - } - else -> { - error( - "Expected WorkflowIdentifier type to be KType or KClass: " + - "actual: $actualType, expected: $expectedType" - ) - } + this is Snapshottable && expected is Snapshottable -> matchesSnapshottable(expected) + this is Unsnapshottable && expected is Unsnapshottable -> expected.kType.isSupertypeOf(kType) + else -> false } } +private fun Snapshottable.matchesSnapshottable(expected: Snapshottable): Boolean = + kClass?.let { actualKClass -> + expected.kClass?.let { expectedKClass -> + expectedKClass.isSuperclassOf(actualKClass) || actualKClass.isJavaMockOf(expectedKClass) + } + } == true + /** * Falls back to using Java reflection to determine subclass relationship. * diff --git a/workflow-tracing/src/main/java/com/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor.kt b/workflow-tracing/src/main/java/com/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor.kt index d721cc1375..877ab48433 100644 --- a/workflow-tracing/src/main/java/com/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor.kt +++ b/workflow-tracing/src/main/java/com/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor.kt @@ -17,6 +17,8 @@ import com.squareup.workflow1.BaseRenderContext import com.squareup.workflow1.Snapshot import com.squareup.workflow1.WorkflowAction import com.squareup.workflow1.WorkflowIdentifier +import com.squareup.workflow1.WorkflowIdentifierType.Snapshottable +import com.squareup.workflow1.WorkflowIdentifierType.Unsnapshottable import com.squareup.workflow1.WorkflowInterceptor import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession @@ -491,10 +493,11 @@ public class TracingWorkflowInterceptor internal constructor( } private fun WorkflowIdentifier.toLoggingName(): String { - return when (val type = getRealIdentifierType()) { - is KClass<*> -> type.toLoggingName() - is KType -> type.toLoggingName() - else -> toString() + val type = getRealIdentifierType() + return when { + type is Snapshottable && type.kClass != null -> type.kClass!!.toLoggingName() + type is Unsnapshottable -> type.kType.toLoggingName() + else -> type.typeName } }