From b8f49db13dbad5ceaa02136478a566a47106309f Mon Sep 17 00:00:00 2001 From: Stephen Edwards Date: Fri, 20 Jun 2025 11:52:21 -0400 Subject: [PATCH 1/2] Migrate Parametrized Tests to Burst Publish workflow-runtime-android module because we forgot to originally. --- workflow-core/api/workflow-core.api | 17 + .../com/squareup/workflow1/RuntimeConfig.kt | 80 + workflow-runtime-android/build.gradle.kts | 1 + .../dependencies/releaseRuntimeClasspath.txt | 67 + workflow-runtime/build.gradle.kts | 1 + .../workflow1/RenderWorkflowInTest.kt | 1960 +++++++---------- .../internal/ParameterizedTestRunner.kt | 67 - .../workflow1/internal/WorkflowRunnerTest.kt | 438 ++-- workflow-testing/build.gradle.kts | 1 + .../workflow1/ParameterizedTestRunner.kt | 70 - .../StatefulWorkflowEventHandlerTest.kt | 379 ++-- .../StatelessWorkflowEventHandlerTest.kt | 379 ++-- .../workflow1/WorkflowsLifecycleTests.kt | 203 +- 13 files changed, 1646 insertions(+), 2017 deletions(-) create mode 100644 workflow-runtime-android/dependencies/releaseRuntimeClasspath.txt delete mode 100644 workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/ParameterizedTestRunner.kt delete mode 100644 workflow-testing/src/test/java/com/squareup/workflow1/ParameterizedTestRunner.kt diff --git a/workflow-core/api/workflow-core.api b/workflow-core/api/workflow-core.api index 567217d4f0..eca46db5b0 100644 --- a/workflow-core/api/workflow-core.api +++ b/workflow-core/api/workflow-core.api @@ -178,6 +178,23 @@ public final class com/squareup/workflow1/RuntimeConfigOptions$Companion { public final fun getRENDER_PER_ACTION ()Ljava/util/Set; } +public final class com/squareup/workflow1/RuntimeConfigOptions$Companion$RuntimeOptions : java/lang/Enum { + public static final field CONFLATE Lcom/squareup/workflow1/RuntimeConfigOptions$Companion$RuntimeOptions; + public static final field DEFAULT Lcom/squareup/workflow1/RuntimeConfigOptions$Companion$RuntimeOptions; + public static final field RENDER_ONLY Lcom/squareup/workflow1/RuntimeConfigOptions$Companion$RuntimeOptions; + public static final field RENDER_ONLY_CONFLATE Lcom/squareup/workflow1/RuntimeConfigOptions$Companion$RuntimeOptions; + public static final field RENDER_ONLY_CONFLATE_PARTIAL Lcom/squareup/workflow1/RuntimeConfigOptions$Companion$RuntimeOptions; + public static final field RENDER_ONLY_CONFLATE_PARTIAL_STABLE Lcom/squareup/workflow1/RuntimeConfigOptions$Companion$RuntimeOptions; + public static final field RENDER_ONLY_CONFLATE_STABLE Lcom/squareup/workflow1/RuntimeConfigOptions$Companion$RuntimeOptions; + public static final field RENDER_ONLY_PARTIAL Lcom/squareup/workflow1/RuntimeConfigOptions$Companion$RuntimeOptions; + public static final field RENDER_ONLY_PARTIAL_STABLE Lcom/squareup/workflow1/RuntimeConfigOptions$Companion$RuntimeOptions; + public static final field STABLE Lcom/squareup/workflow1/RuntimeConfigOptions$Companion$RuntimeOptions; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public final fun getRuntimeConfig ()Ljava/util/Set; + public static fun valueOf (Ljava/lang/String;)Lcom/squareup/workflow1/RuntimeConfigOptions$Companion$RuntimeOptions; + public static fun values ()[Lcom/squareup/workflow1/RuntimeConfigOptions$Companion$RuntimeOptions; +} + public abstract class com/squareup/workflow1/SessionWorkflow : com/squareup/workflow1/StatefulWorkflow { public fun ()V public final fun initialState (Ljava/lang/Object;Lcom/squareup/workflow1/Snapshot;)Ljava/lang/Object; diff --git a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/RuntimeConfig.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/RuntimeConfig.kt index 5c31f2ac15..8f5f70f62c 100644 --- a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/RuntimeConfig.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/RuntimeConfig.kt @@ -66,6 +66,14 @@ public enum class RuntimeConfigOptions { */ @WorkflowExperimentalRuntime STABLE_EVENT_HANDLERS, + + // /** + // * If we have more actions to process that are queued on nodes not affected by the last + // * action application, then we will continue to process those actions before another render + // * pass. + // */ + // @WorkflowExperimentalRuntime + // DRAIN_EXCLUSIVE_ACTIONS, ; public companion object { @@ -82,5 +90,77 @@ public enum class RuntimeConfigOptions { */ @WorkflowExperimentalRuntime public val ALL: RuntimeConfig = entries.toSet() + + /** + * Enum of all reasonable config options. Used especially for parameterized testing. + */ + @WorkflowExperimentalRuntime + enum class RuntimeOptions( + val runtimeConfig: RuntimeConfig + ) { + DEFAULT(RENDER_PER_ACTION), + RENDER_ONLY(setOf(RENDER_ONLY_WHEN_STATE_CHANGES)), + CONFLATE(setOf(CONFLATE_STALE_RENDERINGS)), + STABLE(setOf(STABLE_EVENT_HANDLERS)), + + // DEA(setOf(DRAIN_EXCLUSIVE_ACTIONS)), + RENDER_ONLY_CONFLATE(setOf(RENDER_ONLY_WHEN_STATE_CHANGES, CONFLATE_STALE_RENDERINGS)), + RENDER_ONLY_PARTIAL(setOf(RENDER_ONLY_WHEN_STATE_CHANGES, PARTIAL_TREE_RENDERING)), + + // RENDER_ONLY_DEA(setOf(RENDER_ONLY_WHEN_STATE_CHANGES, DRAIN_EXCLUSIVE_ACTIONS)), + RENDER_ONLY_CONFLATE_STABLE( + setOf(RENDER_ONLY_WHEN_STATE_CHANGES, CONFLATE_STALE_RENDERINGS, STABLE_EVENT_HANDLERS) + ), + RENDER_ONLY_CONFLATE_PARTIAL( + setOf(RENDER_ONLY_WHEN_STATE_CHANGES, CONFLATE_STALE_RENDERINGS, PARTIAL_TREE_RENDERING) + ), + + // RENDER_ONLY_CONFLATE_DEA( + // setOf(RENDER_ONLY_WHEN_STATE_CHANGES, CONFLATE_STALE_RENDERINGS, DRAIN_EXCLUSIVE_ACTIONS) + // ), + RENDER_ONLY_PARTIAL_STABLE( + setOf(RENDER_ONLY_WHEN_STATE_CHANGES, PARTIAL_TREE_RENDERING, STABLE_EVENT_HANDLERS) + ), + + // RENDER_ONLY_PARTIAL_DEA( + // setOf(RENDER_ONLY_WHEN_STATE_CHANGES, PARTIAL_TREE_RENDERING, DRAIN_EXCLUSIVE_ACTIONS) + // ), + // RENDER_ONLY_DEA_STABLE( + // setOf(RENDER_ONLY_WHEN_STATE_CHANGES, DRAIN_EXCLUSIVE_ACTIONS, STABLE_EVENT_HANDLERS) + // ), + RENDER_ONLY_CONFLATE_PARTIAL_STABLE( + setOf( + RENDER_ONLY_WHEN_STATE_CHANGES, + CONFLATE_STALE_RENDERINGS, + PARTIAL_TREE_RENDERING, + STABLE_EVENT_HANDLERS, + ) + ), + // RENDER_ONLY_CONFLATE_PARTIAL_DEA( + // setOf( + // RENDER_ONLY_WHEN_STATE_CHANGES, + // CONFLATE_STALE_RENDERINGS, + // PARTIAL_TREE_RENDERING, + // DRAIN_EXCLUSIVE_ACTIONS, + // ) + // ), + // RENDER_ONLY_PARTIAL_STABLE_DEA( + // setOf( + // RENDER_ONLY_WHEN_STATE_CHANGES, + // PARTIAL_TREE_RENDERING, + // STABLE_EVENT_HANDLERS, + // DRAIN_EXCLUSIVE_ACTIONS, + // ) + // ), + // RENDER_ONLY_CONFLATE_PARTIAL_STABLE_DEA( + // setOf( + // RENDER_ONLY_WHEN_STATE_CHANGES, + // CONFLATE_STALE_RENDERINGS, + // PARTIAL_TREE_RENDERING, + // STABLE_EVENT_HANDLERS, + // DRAIN_EXCLUSIVE_ACTIONS, + // ) + // ), + } } } diff --git a/workflow-runtime-android/build.gradle.kts b/workflow-runtime-android/build.gradle.kts index 51e2837ffb..1dfc2c7b36 100644 --- a/workflow-runtime-android/build.gradle.kts +++ b/workflow-runtime-android/build.gradle.kts @@ -4,6 +4,7 @@ plugins { id("android-defaults") id("android-ui-tests") id("app.cash.burst") + id("published") } android { diff --git a/workflow-runtime-android/dependencies/releaseRuntimeClasspath.txt b/workflow-runtime-android/dependencies/releaseRuntimeClasspath.txt new file mode 100644 index 0000000000..20e8bd0fa9 --- /dev/null +++ b/workflow-runtime-android/dependencies/releaseRuntimeClasspath.txt @@ -0,0 +1,67 @@ +androidx.activity:activity-ktx:1.7.0 +androidx.activity:activity:1.7.0 +androidx.annotation:annotation-experimental:1.4.1 +androidx.annotation:annotation-jvm:1.8.1 +androidx.annotation:annotation:1.8.1 +androidx.arch.core:core-common:2.2.0 +androidx.arch.core:core-runtime:2.2.0 +androidx.autofill:autofill:1.0.0 +androidx.collection:collection-jvm:1.4.4 +androidx.collection:collection-ktx:1.4.4 +androidx.collection:collection:1.4.4 +androidx.compose.runtime:runtime-android:1.7.2 +androidx.compose.runtime:runtime-saveable-android:1.7.2 +androidx.compose.runtime:runtime-saveable:1.7.2 +androidx.compose.runtime:runtime:1.7.2 +androidx.compose.ui:ui-android:1.7.2 +androidx.compose.ui:ui-geometry-android:1.7.2 +androidx.compose.ui:ui-geometry:1.7.2 +androidx.compose.ui:ui-graphics-android:1.7.2 +androidx.compose.ui:ui-graphics:1.7.2 +androidx.compose.ui:ui-text-android:1.7.2 +androidx.compose.ui:ui-text:1.7.2 +androidx.compose.ui:ui-unit-android:1.7.2 +androidx.compose.ui:ui-unit:1.7.2 +androidx.compose.ui:ui-util-android:1.7.2 +androidx.compose.ui:ui-util:1.7.2 +androidx.compose:compose-bom:2024.09.02 +androidx.concurrent:concurrent-futures:1.1.0 +androidx.core:core-ktx:1.12.0 +androidx.core:core:1.12.0 +androidx.customview:customview-poolingcontainer:1.0.0 +androidx.emoji2:emoji2:1.2.0 +androidx.graphics:graphics-path:1.0.1 +androidx.interpolator:interpolator:1.0.0 +androidx.lifecycle:lifecycle-common-jvm:2.8.7 +androidx.lifecycle:lifecycle-common:2.8.7 +androidx.lifecycle:lifecycle-livedata-core:2.8.7 +androidx.lifecycle:lifecycle-process:2.8.7 +androidx.lifecycle:lifecycle-runtime-android:2.8.7 +androidx.lifecycle:lifecycle-runtime-compose-android:2.8.7 +androidx.lifecycle:lifecycle-runtime-compose:2.8.7 +androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.7 +androidx.lifecycle:lifecycle-runtime-ktx:2.8.7 +androidx.lifecycle:lifecycle-runtime:2.8.7 +androidx.lifecycle:lifecycle-viewmodel-android:2.8.7 +androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7 +androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.7 +androidx.lifecycle:lifecycle-viewmodel:2.8.7 +androidx.profileinstaller:profileinstaller:1.3.1 +androidx.savedstate:savedstate-ktx:1.2.1 +androidx.savedstate:savedstate:1.2.1 +androidx.startup:startup-runtime:1.1.1 +androidx.tracing:tracing:1.0.0 +androidx.versionedparcelable:versionedparcelable:1.1.1 +com.google.guava:listenablefuture:1.0 +com.squareup.okio:okio-jvm:3.3.0 +com.squareup.okio:okio:3.3.0 +org.jetbrains.kotlin:kotlin-bom:2.0.21 +org.jetbrains.kotlin:kotlin-stdlib-common:2.0.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.0.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.0.21 +org.jetbrains.kotlin:kotlin-stdlib:2.0.21 +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 +org.jetbrains:annotations:23.0.0 diff --git a/workflow-runtime/build.gradle.kts b/workflow-runtime/build.gradle.kts index d789bda882..3530f2a9cc 100644 --- a/workflow-runtime/build.gradle.kts +++ b/workflow-runtime/build.gradle.kts @@ -3,6 +3,7 @@ import com.squareup.workflow1.buildsrc.iosWithSimulatorArm64 plugins { id("kotlin-multiplatform") id("published") + id("app.cash.burst") } kotlin { diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RenderWorkflowInTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RenderWorkflowInTest.kt index 504203d52c..2b4033d710 100644 --- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RenderWorkflowInTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RenderWorkflowInTest.kt @@ -1,12 +1,14 @@ package com.squareup.workflow1 +import app.cash.burst.Burst import com.squareup.workflow1.RuntimeConfigOptions.CONFLATE_STALE_RENDERINGS +import com.squareup.workflow1.RuntimeConfigOptions.Companion.RuntimeOptions +import com.squareup.workflow1.RuntimeConfigOptions.Companion.RuntimeOptions.DEFAULT import com.squareup.workflow1.RuntimeConfigOptions.PARTIAL_TREE_RENDERING import com.squareup.workflow1.RuntimeConfigOptions.RENDER_ONLY_WHEN_STATE_CHANGES import com.squareup.workflow1.WorkflowInterceptor.RenderPassSkipped import com.squareup.workflow1.WorkflowInterceptor.RenderPassesComplete import com.squareup.workflow1.WorkflowInterceptor.RuntimeLoopOutcome -import com.squareup.workflow1.internal.ParameterizedTestRunner import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineExceptionHandler @@ -23,505 +25,413 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import kotlinx.coroutines.yield import okio.ByteString +import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNotSame +import kotlin.test.assertNull import kotlin.test.assertTrue @OptIn(ExperimentalCoroutinesApi::class, WorkflowExperimentalRuntime::class) -class RenderWorkflowInTest { - +@Burst +class RenderWorkflowInTest( + useTracer: Boolean = false, + useUnconfined: Boolean = true, + private val runtime: RuntimeOptions = DEFAULT +) { + + private val runtimeConfig = runtime.runtimeConfig private val traces: StringBuilder = StringBuilder() - private val testTracer: WorkflowTracer = object : WorkflowTracer { - var prefix: String = "" - override fun beginSection(label: String) { - traces.appendLine("${prefix}Starting$label") - prefix += " " - } + private val testTracer: WorkflowTracer? = if (useTracer) { + object : WorkflowTracer { + var prefix: String = "" + override fun beginSection(label: String) { + traces.appendLine("${prefix}Starting$label") + prefix += " " + } - override fun endSection() { - prefix = prefix.substring(0, prefix.length - 2) - traces.appendLine("${prefix}Ending") + override fun endSection() { + prefix = prefix.substring(0, prefix.length - 2) + traces.appendLine("${prefix}Ending") + } } + } else { + null } - private val runtimes = setOf( - RuntimeConfigOptions.RENDER_PER_ACTION, - setOf(RENDER_ONLY_WHEN_STATE_CHANGES), - setOf(CONFLATE_STALE_RENDERINGS), - setOf(CONFLATE_STALE_RENDERINGS, RENDER_ONLY_WHEN_STATE_CHANGES), - setOf(RENDER_ONLY_WHEN_STATE_CHANGES, PARTIAL_TREE_RENDERING), - setOf(CONFLATE_STALE_RENDERINGS, RENDER_ONLY_WHEN_STATE_CHANGES, PARTIAL_TREE_RENDERING), - ) - - private val tracerOptions = setOf( - null, - testTracer - ) - private val myStandardTestDispatcher = StandardTestDispatcher() - private val dispatcherOptions = setOf( - UnconfinedTestDispatcher(), - myStandardTestDispatcher - ) - - private val runtimeOptions: Sequence> = - cartesianProduct( - runtimes.asSequence(), - tracerOptions.asSequence(), - dispatcherOptions.asSequence() - ) + private val dispatcherUsed = + if (useUnconfined) UnconfinedTestDispatcher() else myStandardTestDispatcher - private val runtimeTestRunner = - ParameterizedTestRunner>() + private fun advanceIfStandard() { + if (dispatcherUsed == myStandardTestDispatcher) { + dispatcherUsed.scheduler.advanceUntilIdle() + dispatcherUsed.scheduler.runCurrent() + } + } - private fun setup() { + @BeforeTest + public fun setup() { traces.clear() } - @Test fun initial_rendering_is_calculated_synchronously() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { - val props = MutableStateFlow("foo") - val workflow = Workflow.stateless { "props: $it" } - // Don't allow the workflow runtime to actually start if this is a [StandardTestDispatcher]. + @Test fun initial_rendering_is_calculated_synchronously() = runTest(dispatcherUsed) { + val props = MutableStateFlow("foo") + val workflow = Workflow.stateless { "props: $it" } + // Don't allow the workflow runtime to actually start if this is a [StandardTestDispatcher]. - val renderings = renderWorkflowIn( - workflow = workflow, - scope = backgroundScope, - props = props, - runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, - ) {} - assertEquals("props: foo", renderings.value.rendering) - } - } + val renderings = renderWorkflowIn( + workflow = workflow, + scope = backgroundScope, + props = props, + runtimeConfig = runtimeConfig, + workflowTracer = testTracer, + ) {} + assertEquals("props: foo", renderings.value.rendering) } - @Test fun initial_rendering_is_reported_through_interceptor() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { - val props = MutableStateFlow("foo") - val workflow = Workflow.stateless { "props: $it" } + @Test fun initial_rendering_is_reported_through_interceptor() = runTest(dispatcherUsed) { + val props = MutableStateFlow("foo") + val workflow = Workflow.stateless { "props: $it" } - val hasReportedRendering = Mutex(locked = true) - val testInterceptor = object : WorkflowInterceptor { - override fun onRuntimeLoopTick(outcome: RuntimeLoopOutcome) { - if (outcome is RenderPassesComplete<*>) { - assertEquals("props: foo", outcome.renderingAndSnapshot.rendering) - hasReportedRendering.unlock() - } - } + val hasReportedRendering = Mutex(locked = true) + val testInterceptor = object : WorkflowInterceptor { + override fun onRuntimeLoopTick(outcome: RuntimeLoopOutcome) { + if (outcome is RenderPassesComplete<*>) { + assertEquals("props: foo", outcome.renderingAndSnapshot.rendering) + hasReportedRendering.unlock() } - renderWorkflowIn( - workflow = workflow, - scope = backgroundScope, - props = props, - interceptors = listOf(testInterceptor), - runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, - ) {} - hasReportedRendering.lock() } } + renderWorkflowIn( + workflow = workflow, + scope = backgroundScope, + props = props, + interceptors = listOf(testInterceptor), + runtimeConfig = runtimeConfig, + workflowTracer = testTracer, + ) {} + hasReportedRendering.lock() } - @Test fun modified_rendering_is_returned() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { - val props = MutableStateFlow("foo") - val workflow = Workflow.stateless { "props: $it" } + @Test fun modified_rendering_is_returned() = runTest(dispatcherUsed) { + val props = MutableStateFlow("foo") + val workflow = Workflow.stateless { "props: $it" } - val interceptedRenderings = mutableListOf() - val testInterceptor = object : WorkflowInterceptor { - override fun onRuntimeLoopTick(outcome: RuntimeLoopOutcome) { - if (outcome is RenderPassesComplete<*>) { - interceptedRenderings.add(outcome.renderingAndSnapshot.rendering) - } - } + val interceptedRenderings = mutableListOf() + val testInterceptor = object : WorkflowInterceptor { + override fun onRuntimeLoopTick(outcome: RuntimeLoopOutcome) { + if (outcome is RenderPassesComplete<*>) { + interceptedRenderings.add(outcome.renderingAndSnapshot.rendering) } - - renderWorkflowIn( - workflow = workflow, - scope = backgroundScope, - props = props, - interceptors = listOf(testInterceptor), - runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, - ) {} - assertEquals(1, interceptedRenderings.size, "Should have intercepted 1 rendering.") - assertEquals( - "props: foo", - interceptedRenderings[0], - "Should intercept 'props: foo' as a rendering." - ) } } + + renderWorkflowIn( + workflow = workflow, + scope = backgroundScope, + props = props, + interceptors = listOf(testInterceptor), + runtimeConfig = runtimeConfig, + workflowTracer = testTracer, + ) {} + assertEquals(1, interceptedRenderings.size, "Should have intercepted 1 rendering.") + assertEquals( + "props: foo", + interceptedRenderings[0], + "Should intercept 'props: foo' as a rendering." + ) } - @Test fun initial_rendering_is_calculated_when_scope_cancelled_before_start() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { - val props = MutableStateFlow("foo") - val workflow = Workflow.stateless { "props: $it" } - - val testScope = TestScope(dispatcher) - testScope.cancel() - val renderings = renderWorkflowIn( - workflow = workflow, - scope = testScope, - props = props, - runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, - ) {} - assertEquals("props: foo", renderings.value.rendering) - } + @Test fun initial_rendering_is_calculated_when_scope_cancelled_before_start() = + runTest(dispatcherUsed) { + val props = MutableStateFlow("foo") + val workflow = Workflow.stateless { "props: $it" } + + val testScope = TestScope(dispatcherUsed) + testScope.cancel() + val renderings = renderWorkflowIn( + workflow = workflow, + scope = testScope, + props = props, + runtimeConfig = runtimeConfig, + workflowTracer = testTracer, + ) {} + assertEquals("props: foo", renderings.value.rendering) } - } @Test - fun `side_effects_from_initial_rendering_in_root_workflow_are_never_started_when_scope_cancelled_before_start`() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { - var sideEffectWasRan = false - val workflow = Workflow.stateless { - runningSideEffect("test") { - sideEffectWasRan = true - } + fun `side_effects_from_initial_rendering_in_root_workflow_are_never_started_when_scope_cancelled_before_start`() = + runTest(dispatcherUsed) { + var sideEffectWasRan = false + val workflow = Workflow.stateless { + runningSideEffect("test") { + sideEffectWasRan = true } - - val testScope = TestScope(dispatcher) - testScope.cancel() - renderWorkflowIn( - workflow, - testScope, - MutableStateFlow(Unit), - runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, - ) {} - advanceIfStandard(dispatcher) - - assertFalse(sideEffectWasRan) } + + val testScope = TestScope(dispatcherUsed) + testScope.cancel() + renderWorkflowIn( + workflow, + testScope, + MutableStateFlow(Unit), + runtimeConfig = runtimeConfig, + workflowTracer = testTracer, + ) {} + advanceIfStandard() + + assertFalse(sideEffectWasRan) } - } @Test - fun `side_effects_from_initial_rendering_in_non_root_workflow_are_never_started_when_scope_cancelled_before_start`() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { - var sideEffectWasRan = false - val childWorkflow = Workflow.stateless { - runningSideEffect("test") { - sideEffectWasRan = true - } + fun `side_effects_from_initial_rendering_in_non_root_workflow_are_never_started_when_scope_cancelled_before_start`() = + runTest(dispatcherUsed) { + var sideEffectWasRan = false + val childWorkflow = Workflow.stateless { + runningSideEffect("test") { + sideEffectWasRan = true } - val workflow = Workflow.stateless { - renderChild(childWorkflow) - } - - val testScope = TestScope(dispatcher) - testScope.cancel() - renderWorkflowIn( - workflow = workflow, - scope = testScope, - props = MutableStateFlow(Unit), - runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, - ) {} - advanceIfStandard(dispatcher) - - assertFalse(sideEffectWasRan) } + val workflow = Workflow.stateless { + renderChild(childWorkflow) + } + + val testScope = TestScope(dispatcherUsed) + testScope.cancel() + renderWorkflowIn( + workflow = workflow, + scope = testScope, + props = MutableStateFlow(Unit), + runtimeConfig = runtimeConfig, + workflowTracer = testTracer, + ) {} + advanceIfStandard() + + assertFalse(sideEffectWasRan) } - } - @Test fun new_renderings_are_emitted_on_update() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { - val props = MutableStateFlow("foo") - val workflow = Workflow.stateless { "props: $it" } - val renderings = renderWorkflowIn( - workflow = workflow, - scope = backgroundScope, - props = props, - runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, - ) {} - advanceIfStandard(dispatcher) + @Test fun new_renderings_are_emitted_on_update() = runTest(dispatcherUsed) { + val props = MutableStateFlow("foo") + val workflow = Workflow.stateless { "props: $it" } + val renderings = renderWorkflowIn( + workflow = workflow, + scope = backgroundScope, + props = props, + runtimeConfig = runtimeConfig, + workflowTracer = testTracer, + ) {} + advanceIfStandard() - assertEquals("props: foo", renderings.value.rendering) + assertEquals("props: foo", renderings.value.rendering) - props.value = "bar" - advanceIfStandard(dispatcher) + props.value = "bar" + advanceIfStandard() - assertEquals("props: bar", renderings.value.rendering) - } - } + assertEquals("props: bar", renderings.value.rendering) } - @Test fun new_renderings_are_emitted_to_interceptor() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { - val props = MutableStateFlow("foo") - val workflow = Workflow.stateless { "props: $it" } + @Test fun new_renderings_are_emitted_to_interceptor() = runTest(dispatcherUsed) { + val props = MutableStateFlow("foo") + val workflow = Workflow.stateless { "props: $it" } - val interceptedRenderings = mutableListOf() - val testInterceptor = object : WorkflowInterceptor { - override fun onRuntimeLoopTick(outcome: RuntimeLoopOutcome) { - if (outcome is RenderPassesComplete<*>) { - interceptedRenderings.add(outcome.renderingAndSnapshot.rendering) - } - } + val interceptedRenderings = mutableListOf() + val testInterceptor = object : WorkflowInterceptor { + override fun onRuntimeLoopTick(outcome: RuntimeLoopOutcome) { + if (outcome is RenderPassesComplete<*>) { + interceptedRenderings.add(outcome.renderingAndSnapshot.rendering) } - - renderWorkflowIn( - workflow = workflow, - scope = backgroundScope, - props = props, - interceptors = listOf(testInterceptor), - runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, - ) {} - advanceIfStandard(dispatcher) - - assertEquals(1, interceptedRenderings.size, "Should have intercepted 1 rendering.") - assertEquals( - "props: foo", - interceptedRenderings[0], - "Should intercept 'props: foo' as a rendering." - ) - - props.value = "bar" - advanceIfStandard(dispatcher) - - assertEquals(2, interceptedRenderings.size, "Should have intercepted 2 rendering.") - assertEquals( - "props: bar", - interceptedRenderings[1], - "Should intercept 'props: bar' as a rendering." - ) } } - } - private val runtimeMatrix: Sequence> = - cartesianProduct( - runtimes.asSequence(), - runtimes.asSequence(), - dispatcherOptions.asSequence(), + renderWorkflowIn( + workflow = workflow, + scope = backgroundScope, + props = props, + interceptors = listOf(testInterceptor), + runtimeConfig = runtimeConfig, + workflowTracer = testTracer, + ) {} + advanceIfStandard() + + assertEquals(1, interceptedRenderings.size, "Should have intercepted 1 rendering.") + assertEquals( + "props: foo", + interceptedRenderings[0], + "Should intercept 'props: foo' as a rendering." ) - private val runtimeMatrixTestRunner = - ParameterizedTestRunner>() - - @Test fun saves_to_and_restores_from_snapshot() { - runtimeMatrixTestRunner.runParametrizedTest( - paramSource = runtimeMatrix, - before = ::setup, - ) { (runtimeConfig1, runtimeConfig2, dispatcher) -> - runTest(dispatcher) { - val workflow = Workflow.stateful Unit>>( - initialState = { _, snapshot -> - snapshot?.bytes?.parse { it.readUtf8WithLength() } ?: "initial state" - }, - snapshot = { state -> - Snapshot.write { it.writeUtf8WithLength(state) } - }, - render = { _, renderState -> - Pair( - renderState, - { newState -> actionSink.send(action("") { state = newState }) } - ) - } - ) - val props = MutableStateFlow(Unit) - val renderings = renderWorkflowIn( - workflow = workflow, - scope = backgroundScope, - props = props, - runtimeConfig = runtimeConfig1, - workflowTracer = null, - ) {} - advanceIfStandard(dispatcher) + props.value = "bar" + advanceIfStandard() - // Interact with the workflow to change the state. - renderings.value.rendering.let { (state, updateState) -> - runtimeMatrixTestRunner.assertEquals("initial state", state) - updateState("updated state") - } - advanceIfStandard(dispatcher) + assertEquals(2, interceptedRenderings.size, "Should have intercepted 2 rendering.") + assertEquals( + "props: bar", + interceptedRenderings[1], + "Should intercept 'props: bar' as a rendering." + ) + } - val snapshot = renderings.value.let { (rendering, snapshot) -> - val (state, updateState) = rendering - runtimeMatrixTestRunner.assertEquals("updated state", state) - updateState("ignored rendering") - return@let snapshot - } - advanceIfStandard(dispatcher) - - // Create a new scope to launch a second runtime to restore. - val restoreScope = TestScope(dispatcher) - val restoredRenderings = - renderWorkflowIn( - workflow = workflow, - scope = restoreScope, - props = props, - initialSnapshot = snapshot, - workflowTracer = null, - runtimeConfig = runtimeConfig2 - ) {} - advanceIfStandard(dispatcher) - runtimeMatrixTestRunner.assertEquals( - "updated state", - restoredRenderings.value.rendering.first + @Test fun saves_to_and_restores_from_snapshot( + runtime2: RuntimeOptions = DEFAULT + ) = runTest(dispatcherUsed) { + val workflow = Workflow.stateful Unit>>( + initialState = { _, snapshot -> + snapshot?.bytes?.parse { it.readUtf8WithLength() } ?: "initial state" + }, + snapshot = { state -> + Snapshot.write { it.writeUtf8WithLength(state) } + }, + render = { _, renderState -> + Pair( + renderState, + { newState -> actionSink.send(action("") { state = newState }) } ) } + ) + val props = MutableStateFlow(Unit) + val renderings = renderWorkflowIn( + workflow = workflow, + scope = backgroundScope, + props = props, + runtimeConfig = runtimeConfig, + workflowTracer = null, + ) {} + advanceIfStandard() + + // Interact with the workflow to change the state. + renderings.value.rendering.let { (state, updateState) -> + assertEquals("initial state", state) + updateState("updated state") + } + advanceIfStandard() + + val snapshot = renderings.value.let { (rendering, snapshot) -> + val (state, updateState) = rendering + assertEquals("updated state", state) + updateState("ignored rendering") + return@let snapshot } + advanceIfStandard() + + // Create a new scope to launch a second runtime to restore. + val restoreScope = TestScope(dispatcherUsed) + val restoredRenderings = + renderWorkflowIn( + workflow = workflow, + scope = restoreScope, + props = props, + initialSnapshot = snapshot, + workflowTracer = null, + runtimeConfig = runtime2.runtimeConfig + ) {} + advanceIfStandard() + assertEquals( + "updated state", + restoredRenderings.value.rendering.first + ) } // https://github.com/square/workflow-kotlin/issues/223 - @Test fun snapshots_are_lazy() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { - lateinit var sink: Sink - var snapped = false - - val workflow = Workflow.stateful( - initialState = { _, _ -> "unchanging state" }, - snapshot = { - Snapshot.of { - snapped = true - ByteString.of(1) - } - }, - render = { _, renderState -> - sink = actionSink.contraMap { action("") { state = it } } - renderState - } - ) - val props = MutableStateFlow(Unit) - val renderings = renderWorkflowIn( - workflow = workflow, - scope = backgroundScope, - props = props, - runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, - ) {} - advanceIfStandard(dispatcher) - - val emitted = mutableListOf>() - val collectionJob = launch { - renderings.collect { emitted += it } - } - advanceIfStandard(dispatcher) - - if (runtimeConfig.contains(RENDER_ONLY_WHEN_STATE_CHANGES)) { - // we have to change state then or it won't render. - sink.send("changing state") - } else { - sink.send("unchanging state") + @Test fun snapshots_are_lazy() = runTest(dispatcherUsed) { + lateinit var sink: Sink + var snapped = false + + val workflow = Workflow.stateful( + initialState = { _, _ -> "unchanging state" }, + snapshot = { + Snapshot.of { + snapped = true + ByteString.of(1) } - advanceIfStandard(dispatcher) - - if (runtimeConfig.contains(RENDER_ONLY_WHEN_STATE_CHANGES)) { - // we have to change state then or it won't render. - sink.send("changing state, again") - } else { - sink.send("unchanging state") - } - advanceIfStandard(dispatcher) + }, + render = { _, renderState -> + sink = actionSink.contraMap { action("") { state = it } } + renderState + } + ) + val props = MutableStateFlow(Unit) + val renderings = renderWorkflowIn( + workflow = workflow, + scope = backgroundScope, + props = props, + runtimeConfig = runtimeConfig, + workflowTracer = testTracer, + ) {} + advanceIfStandard() - collectionJob.cancel() + val emitted = mutableListOf>() + val collectionJob = launch { + renderings.collect { emitted += it } + } + advanceIfStandard() - assertFalse(snapped) - assertNotSame( - emitted[0].snapshot.workflowSnapshot, - emitted[1].snapshot.workflowSnapshot - ) - assertNotSame( - emitted[1].snapshot.workflowSnapshot, - emitted[2].snapshot.workflowSnapshot - ) - } + if (runtimeConfig.contains(RENDER_ONLY_WHEN_STATE_CHANGES)) { + // we have to change state then or it won't render. + sink.send("changing state") + } else { + sink.send("unchanging state") } - } + advanceIfStandard() - @Test fun onOutput_called_when_output_emitted() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { - val trigger = Channel() - val workflow = Workflow.stateless { - runningWorker( - trigger.receiveAsFlow() - .asWorker() - ) { action("") { setOutput(it) } } - } - val receivedOutputs = mutableListOf() - renderWorkflowIn( - workflow = workflow, - scope = backgroundScope, - props = MutableStateFlow(Unit), - runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, - ) { - receivedOutputs += it - } - advanceIfStandard(dispatcher) - assertTrue(receivedOutputs.isEmpty()) + if (runtimeConfig.contains(RENDER_ONLY_WHEN_STATE_CHANGES)) { + // we have to change state then or it won't render. + sink.send("changing state, again") + } else { + sink.send("unchanging state") + } + advanceIfStandard() - assertTrue(trigger.trySend("foo").isSuccess) - advanceIfStandard(dispatcher) - assertEquals(listOf("foo"), receivedOutputs) + collectionJob.cancel() - assertTrue(trigger.trySend("bar").isSuccess) - advanceIfStandard(dispatcher) - assertEquals(listOf("foo", "bar"), receivedOutputs) - } - } + assertFalse(snapped) + assertNotSame( + emitted[0].snapshot.workflowSnapshot, + emitted[1].snapshot.workflowSnapshot + ) + assertNotSame( + emitted[1].snapshot.workflowSnapshot, + emitted[2].snapshot.workflowSnapshot + ) } - private fun advanceIfStandard(dispatcher: TestDispatcher) { - if (dispatcher == myStandardTestDispatcher) { - dispatcher.scheduler.advanceUntilIdle() - dispatcher.scheduler.runCurrent() + @Test fun onOutput_called_when_output_emitted() = runTest(dispatcherUsed) { + val trigger = Channel() + val workflow = Workflow.stateless { + runningWorker( + trigger.receiveAsFlow() + .asWorker() + ) { action("") { setOutput(it) } } + } + val receivedOutputs = mutableListOf() + renderWorkflowIn( + workflow = workflow, + scope = backgroundScope, + props = MutableStateFlow(Unit), + runtimeConfig = runtimeConfig, + workflowTracer = testTracer, + ) { + receivedOutputs += it } + advanceIfStandard() + assertTrue(receivedOutputs.isEmpty()) + + assertTrue(trigger.trySend("foo").isSuccess) + advanceIfStandard() + assertEquals(listOf("foo"), receivedOutputs) + + assertTrue(trigger.trySend("bar").isSuccess) + advanceIfStandard() + assertEquals(listOf("foo", "bar"), receivedOutputs) } /** @@ -535,53 +445,47 @@ class RenderWorkflowInTest { * See [onOutput_called_after_rendering_emitted_and_collected] for alternate behaviour with * a different dispatcher for the runtime. */ - @Test fun onOutput_called_after_rendering_emitted() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { - val trigger = Channel() - val workflow = Workflow.stateful( - initialState = "initial", - render = { renderState -> - runningWorker( - trigger.receiveAsFlow() - .asWorker() - ) { - action("") { - state = it - setOutput(it) - } + @Test fun onOutput_called_after_rendering_emitted() = + runTest(dispatcherUsed) { + val trigger = Channel() + val workflow = Workflow.stateful( + initialState = "initial", + render = { renderState -> + runningWorker( + trigger.receiveAsFlow() + .asWorker() + ) { + action("") { + state = it + setOutput(it) } - renderState } - ) - - val receivedOutputs = mutableListOf() - lateinit var renderings: StateFlow> - renderings = renderWorkflowIn( - workflow = workflow, - scope = backgroundScope, - props = MutableStateFlow(Unit), - runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, - ) { it: String -> - receivedOutputs += it - // The value of the updated rendering has already been set by the time onOutput is - // called - assertEquals(it, renderings.value.rendering) + renderState } - advanceIfStandard(dispatcher) + ) + + val receivedOutputs = mutableListOf() + lateinit var renderings: StateFlow> + renderings = renderWorkflowIn( + workflow = workflow, + scope = backgroundScope, + props = MutableStateFlow(Unit), + runtimeConfig = runtimeConfig, + workflowTracer = testTracer, + ) { it: String -> + receivedOutputs += it + // The value of the updated rendering has already been set by the time onOutput is + // called + assertEquals(it, renderings.value.rendering) + } + advanceIfStandard() - assertTrue(receivedOutputs.isEmpty()) + assertTrue(receivedOutputs.isEmpty()) - assertTrue(trigger.trySend("foo").isSuccess) - advanceIfStandard(dispatcher) - assertEquals(listOf("foo"), receivedOutputs) - } + assertTrue(trigger.trySend("foo").isSuccess) + advanceIfStandard() + assertEquals(listOf("foo"), receivedOutputs) } - } /** * A different form of [onOutput_called_after_rendering_emitted]. Here we launch the workflow @@ -596,11 +500,8 @@ class RenderWorkflowInTest { * Then when we let the runtime's scheduler go ahead, it will have already been populated. */ @Test fun onOutput_called_after_rendering_emitted_and_collected() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions.filter { it.third != myStandardTestDispatcher }, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { + if (dispatcherUsed != myStandardTestDispatcher) { + runTest(dispatcherUsed) { val trigger = Channel() val workflow = Workflow.stateful( initialState = "initial", @@ -627,7 +528,7 @@ class RenderWorkflowInTest { scope = testScope, props = MutableStateFlow(Unit), runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, + workflowTracer = testTracer, ) { it: String -> // The list collecting the renderings already contains it by the time onOutput is fired. assertTrue(emittedRenderings.contains(it)) @@ -654,619 +555,526 @@ class RenderWorkflowInTest { } } - @Test fun tracer_includes_expected_sections() = runTest(UnconfinedTestDispatcher()) { - // Only test default so we only have one 'golden value' to assert against. - // We are only testing the tracer correctness here, which should be agnostic of runtime. - // We include 'tracers' in the other test to test against unexpected side effects. - val runtimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG - val workflowTracer = testTracer - setup() - val trigger = Channel() - val workflow = Workflow.stateful( - initialState = "initial", - render = { renderState -> - runningWorker( - trigger.receiveAsFlow() - .asWorker() - ) { - action("") { - state = it - setOutput(it) - } - } - renderState - } - ) - - val emittedRenderings = mutableListOf() - val receivedOutputs = mutableListOf() - val renderings = renderWorkflowIn( - workflow = workflow, - scope = backgroundScope, - props = MutableStateFlow(Unit), - runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, - onOutput = {} - ) - assertTrue(receivedOutputs.isEmpty()) - - val collectionJob = launch { - renderings.collect { rendering: RenderingAndSnapshot -> - emittedRenderings += rendering.rendering - } - } - - assertTrue(trigger.trySend("foo").isSuccess) - - assertEquals(EXPECTED_TRACE, traces.toString().trim()) + @Test fun tracer_includes_expected_sections() { + if (runtime == DEFAULT && testTracer != null) { + runTest(UnconfinedTestDispatcher()) { + // Only test default so we only have one 'golden value' to assert against. + // We are only testing the tracer correctness here, which should be agnostic of runtime. + // We include 'tracers' in the other test to test against unexpected side effects. - collectionJob.cancel() - } + val trigger = Channel() + val workflow = Workflow.stateful( + initialState = "initial", + render = { renderState -> + runningWorker( + trigger.receiveAsFlow() + .asWorker() + ) { + action("") { + state = it + setOutput(it) + } + } + renderState + } + ) - @Test fun onOutput_is_not_called_when_no_output_emitted() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { - val workflow = Workflow.stateless { props -> props } - var onOutputCalls = 0 - val props = MutableStateFlow(0) + val emittedRenderings = mutableListOf() + val receivedOutputs = mutableListOf() val renderings = renderWorkflowIn( workflow = workflow, scope = backgroundScope, - props = props, + props = MutableStateFlow(Unit), runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, - ) { onOutputCalls++ } - advanceIfStandard(dispatcher) - assertEquals(0, renderings.value.rendering) - assertEquals(0, onOutputCalls) - - props.value = 1 - advanceIfStandard(dispatcher) - assertEquals(1, renderings.value.rendering) - assertEquals(0, onOutputCalls) - - props.value = 2 - advanceIfStandard(dispatcher) - assertEquals(2, renderings.value.rendering) - assertEquals(0, onOutputCalls) + workflowTracer = testTracer, + onOutput = {} + ) + assertTrue(receivedOutputs.isEmpty()) + + val collectionJob = launch { + renderings.collect { rendering: RenderingAndSnapshot -> + emittedRenderings += rendering.rendering + } + } + + assertTrue(trigger.trySend("foo").isSuccess) + + assertEquals(EXPECTED_TRACE, traces.toString().trim()) + + collectionJob.cancel() } } } + @Test fun onOutput_is_not_called_when_no_output_emitted() = + runTest(dispatcherUsed) { + val workflow = Workflow.stateless { props -> props } + var onOutputCalls = 0 + val props = MutableStateFlow(0) + val renderings = renderWorkflowIn( + workflow = workflow, + scope = backgroundScope, + props = props, + runtimeConfig = runtimeConfig, + workflowTracer = testTracer, + ) { onOutputCalls++ } + advanceIfStandard() + assertEquals(0, renderings.value.rendering) + assertEquals(0, onOutputCalls) + + props.value = 1 + advanceIfStandard() + assertEquals(1, renderings.value.rendering) + assertEquals(0, onOutputCalls) + + props.value = 2 + advanceIfStandard() + assertEquals(2, renderings.value.rendering) + assertEquals(0, onOutputCalls) + } + /** * Since the initial render occurs before launching the coroutine, an exception thrown from it * doesn't implicitly cancel the scope. If it did, the reception would be reported twice: once to * the caller, and once to the scope. */ - @Test fun exception_from_initial_render_does_not_fail_parent_scope() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { - val workflow = Workflow.stateless { - throw ExpectedException() - } - assertFailsWith { - renderWorkflowIn( - workflow = workflow, - scope = backgroundScope, - props = MutableStateFlow(Unit), - runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, - ) {} - } - assertTrue(backgroundScope.isActive) + @Test fun exception_from_initial_render_does_not_fail_parent_scope() = + runTest(dispatcherUsed) { + val workflow = Workflow.stateless { + throw ExpectedException() } - } - } - - @Test - fun `side_effects_from_initial_rendering_in_root_workflow_are_never_started_when_initial_render_of_root_workflow_fails`() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { - var sideEffectWasRan = false - val workflow = Workflow.stateless { - runningSideEffect("test") { - sideEffectWasRan = true - } - throw ExpectedException() - } - - assertFailsWith { - renderWorkflowIn( - workflow = workflow, - scope = backgroundScope, - props = MutableStateFlow(Unit), - runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, - ) {} - } - assertFalse(sideEffectWasRan) + assertFailsWith { + renderWorkflowIn( + workflow = workflow, + scope = backgroundScope, + props = MutableStateFlow(Unit), + runtimeConfig = runtimeConfig, + workflowTracer = testTracer, + ) {} } + assertTrue(backgroundScope.isActive) } - } @Test - fun `side_effects_from_initial_rendering_in_non_root_workflow_are_cancelled_when_initial_render_of_root_workflow_fails`() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { - var sideEffectWasRan = false - var cancellationException: Throwable? = null - val childWorkflow = Workflow.stateless { - runningSideEffect("test") { - sideEffectWasRan = true - suspendCancellableCoroutine { continuation -> - continuation.invokeOnCancellation { cause -> cancellationException = cause } - } - } - } - val workflow = Workflow.stateless { - renderChild(childWorkflow) - throw ExpectedException() - } - - assertFailsWith { - renderWorkflowIn( - workflow = workflow, - scope = backgroundScope, - props = MutableStateFlow(Unit), - runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, - ) {} - } - advanceIfStandard(dispatcher) - if (dispatcher != myStandardTestDispatcher) { - // Side effect will never actually be started unless the dispatcher is eager. - assertTrue(sideEffectWasRan) - assertNotNull(cancellationException) - val realCause = generateSequence(cancellationException) { it.cause } - .firstOrNull { it !is CancellationException } - assertTrue(realCause is ExpectedException) + fun `side_effects_from_initial_rendering_in_root_workflow_are_never_started_when_initial_render_of_root_workflow_fails`() = + runTest(dispatcherUsed) { + var sideEffectWasRan = false + val workflow = Workflow.stateless { + runningSideEffect("test") { + sideEffectWasRan = true } + throw ExpectedException() } - } - } - - @Test - fun `side_effects_from_initial_rendering_in_non_root_workflow_are_never_started_when_initial_render_of_non_root_workflow_fails`() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { - var sideEffectWasRan = false - val childWorkflow = Workflow.stateless { - runningSideEffect("test") { - sideEffectWasRan = true - } - throw ExpectedException() - } - val workflow = Workflow.stateless { - renderChild(childWorkflow) - } - assertFailsWith { - renderWorkflowIn( - workflow = workflow, - scope = backgroundScope, - props = MutableStateFlow(Unit), - runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, - ) {} - } - assertFalse(sideEffectWasRan) - } - } - } - - @Test fun exception_from_non_initial_render_fails_parent_scope() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { - val trigger = CompletableDeferred() - // Throws an exception when trigger is completed. - val workflow = Workflow.stateful( - initialState = { false }, - render = { _, throwNow -> - runningWorker(Worker.from { trigger.await() }) { action("") { state = true } } - if (throwNow) { - throw ExpectedException() - } - } - ) - val testScope = TestScope(dispatcher) + assertFailsWith { renderWorkflowIn( workflow = workflow, - scope = testScope, + scope = backgroundScope, props = MutableStateFlow(Unit), runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, + workflowTracer = testTracer, ) {} - - assertTrue(testScope.isActive) - - trigger.complete(Unit) - advanceIfStandard(dispatcher) - - assertFalse(testScope.isActive) } + assertFalse(sideEffectWasRan) } - } - @Test fun exception_from_action_fails_parent_scope() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { - val trigger = CompletableDeferred() - // Throws an exception when trigger is completed. - val workflow = Workflow.stateless { - runningWorker(Worker.from { trigger.await() }) { - action("") { - throw ExpectedException() - } + @Test + fun `side_effects_from_initial_rendering_in_non_root_workflow_are_cancelled_when_initial_render_of_root_workflow_fails`() = + runTest(dispatcherUsed) { + var sideEffectWasRan = false + var cancellationException: Throwable? = null + val childWorkflow = Workflow.stateless { + runningSideEffect("test") { + sideEffectWasRan = true + suspendCancellableCoroutine { continuation -> + continuation.invokeOnCancellation { cause -> cancellationException = cause } } } - val testScope = TestScope(dispatcher) + } + val workflow = Workflow.stateless { + renderChild(childWorkflow) + throw ExpectedException() + } + + assertFailsWith { renderWorkflowIn( workflow = workflow, - scope = testScope, + scope = backgroundScope, props = MutableStateFlow(Unit), runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, + workflowTracer = testTracer, ) {} - - assertTrue(testScope.isActive) - - trigger.complete(Unit) - advanceIfStandard(dispatcher) - - assertFalse(testScope.isActive) + } + advanceIfStandard() + if (dispatcherUsed != myStandardTestDispatcher) { + // Side effect will never actually be started unless the dispatcher is eager. + assertTrue(sideEffectWasRan) + assertNotNull(cancellationException) + val realCause = generateSequence(cancellationException) { it.cause } + .firstOrNull { it !is CancellationException } + assertTrue(realCause is ExpectedException) } } - } - @Test fun cancelling_scope_cancels_runtime() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { - var cancellationException: Throwable? = null - val workflow = Workflow.stateless { - runningSideEffect(key = "test1") { - suspendCancellableCoroutine { continuation -> - continuation.invokeOnCancellation { cause -> cancellationException = cause } - } - } + @Test + fun `side_effects_from_initial_rendering_in_non_root_workflow_are_never_started_when_initial_render_of_non_root_workflow_fails`() = + runTest(dispatcherUsed) { + var sideEffectWasRan = false + val childWorkflow = Workflow.stateless { + runningSideEffect("test") { + sideEffectWasRan = true } - val testScope = TestScope(dispatcher) + throw ExpectedException() + } + val workflow = Workflow.stateless { + renderChild(childWorkflow) + } + + assertFailsWith { renderWorkflowIn( workflow = workflow, - scope = testScope, + scope = backgroundScope, props = MutableStateFlow(Unit), runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, + workflowTracer = testTracer, ) {} - assertNull(cancellationException) - assertTrue(testScope.isActive) - advanceIfStandard(dispatcher) - - testScope.cancel() - - advanceIfStandard(dispatcher) - - assertTrue(cancellationException is CancellationException) - assertNull(cancellationException!!.cause) } + assertFalse(sideEffectWasRan) } - } - @Test fun cancelling_scope_in_action_cancels_runtime_and_does_not_render_again() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { - val testScope = TestScope(dispatcher) - val trigger = CompletableDeferred() - var renderCount = 0 - val workflow = Workflow.stateless { - renderCount++ - runningWorker(Worker.from { trigger.await() }) { - action("") { - testScope.cancel() - } + @Test fun exception_from_non_initial_render_fails_parent_scope() = + runTest(dispatcherUsed) { + val trigger = CompletableDeferred() + // Throws an exception when trigger is completed. + val workflow = Workflow.stateful( + initialState = { false }, + render = { _, throwNow -> + runningWorker(Worker.from { trigger.await() }) { action("") { state = true } } + if (throwNow) { + throw ExpectedException() } } - renderWorkflowIn( - workflow = workflow, - scope = testScope, - props = MutableStateFlow(Unit), - runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, - ) {} - advanceIfStandard(dispatcher) + ) + val testScope = TestScope(dispatcherUsed) + renderWorkflowIn( + workflow = workflow, + scope = testScope, + props = MutableStateFlow(Unit), + runtimeConfig = runtimeConfig, + workflowTracer = testTracer, + ) {} + + assertTrue(testScope.isActive) + + trigger.complete(Unit) + advanceIfStandard() + + assertFalse(testScope.isActive) + } - assertTrue(testScope.isActive) - assertTrue(renderCount == 1) + @Test fun exception_from_action_fails_parent_scope() = + runTest(dispatcherUsed) { + val trigger = CompletableDeferred() + // Throws an exception when trigger is completed. + val workflow = Workflow.stateless { + runningWorker(Worker.from { trigger.await() }) { + action("") { + throw ExpectedException() + } + } + } + val testScope = TestScope(dispatcherUsed) + renderWorkflowIn( + workflow = workflow, + scope = testScope, + props = MutableStateFlow(Unit), + runtimeConfig = runtimeConfig, + workflowTracer = testTracer, + ) {} - trigger.complete(Unit) + assertTrue(testScope.isActive) - advanceIfStandard(dispatcher) + trigger.complete(Unit) + advanceIfStandard() - assertFalse(testScope.isActive) - assertEquals( - 1, - renderCount, - "Should not render after CoroutineScope is canceled." - ) - } + assertFalse(testScope.isActive) } - } - @Test fun failing_scope_cancels_runtime() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { - var cancellationException: Throwable? = null - val workflow = Workflow.stateless { - runningSideEffect(key = "failing") { - suspendCancellableCoroutine { continuation -> - continuation.invokeOnCancellation { cause -> cancellationException = cause } - } + @Test fun cancelling_scope_cancels_runtime() = + runTest(dispatcherUsed) { + var cancellationException: Throwable? = null + val workflow = Workflow.stateless { + runningSideEffect(key = "test1") { + suspendCancellableCoroutine { continuation -> + continuation.invokeOnCancellation { cause -> cancellationException = cause } } } - val testScope = TestScope(dispatcher) - renderWorkflowIn( - workflow = workflow, - scope = testScope, - props = MutableStateFlow(Unit), - runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, - ) {} - advanceIfStandard(dispatcher) - assertNull(cancellationException) - assertTrue(testScope.isActive) - - testScope.cancel(CancellationException("fail!", ExpectedException())) - advanceIfStandard(dispatcher) - assertTrue(cancellationException is CancellationException) - assertTrue(cancellationException!!.cause is ExpectedException) } + val testScope = TestScope(dispatcherUsed) + renderWorkflowIn( + workflow = workflow, + scope = testScope, + props = MutableStateFlow(Unit), + runtimeConfig = runtimeConfig, + workflowTracer = testTracer, + ) {} + assertNull(cancellationException) + assertTrue(testScope.isActive) + advanceIfStandard() + + testScope.cancel() + + advanceIfStandard() + + assertTrue(cancellationException is CancellationException) + assertNull(cancellationException!!.cause) } - } - @Test fun error_from_renderings_collector_does_not_fail_parent_scope() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { - val workflow = Workflow.stateless {} - val testScope = TestScope(dispatcher) - val renderings = renderWorkflowIn( - workflow = workflow, - scope = testScope, - props = MutableStateFlow(Unit), - runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, - ) {} - - // Collect in separate scope so we actually test that the parent scope is failed when it's - // different from the collecting scope. - val collectScope = TestScope(dispatcher) - collectScope.launch { - renderings.collect { throw ExpectedException() } + @Test fun cancelling_scope_in_action_cancels_runtime_and_does_not_render_again() = + runTest(dispatcherUsed) { + val testScope = TestScope(dispatcherUsed) + val trigger = CompletableDeferred() + var renderCount = 0 + val workflow = Workflow.stateless { + renderCount++ + runningWorker(Worker.from { trigger.await() }) { + action("") { + testScope.cancel() + } } - advanceIfStandard(dispatcher) - assertTrue(testScope.isActive) - assertFalse(collectScope.isActive) } + renderWorkflowIn( + workflow = workflow, + scope = testScope, + props = MutableStateFlow(Unit), + runtimeConfig = runtimeConfig, + workflowTracer = testTracer, + ) {} + advanceIfStandard() + + assertTrue(testScope.isActive) + assertTrue(renderCount == 1) + + trigger.complete(Unit) + + advanceIfStandard() + + assertFalse(testScope.isActive) + assertEquals( + 1, + renderCount, + "Should not render after CoroutineScope is canceled." + ) } - } - @Test fun exception_from_onOutput_fails_parent_scope() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { - val trigger = CompletableDeferred() - // Emits a Unit when trigger is completed. - val workflow = Workflow.stateless { - runningWorker(Worker.from { trigger.await() }) { action("") { setOutput(Unit) } } - } - val testScope = TestScope(dispatcher) - renderWorkflowIn( - workflow = workflow, - scope = testScope, - props = MutableStateFlow(Unit), - runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, - ) { - throw ExpectedException() + @Test fun failing_scope_cancels_runtime() = + runTest(dispatcherUsed) { + var cancellationException: Throwable? = null + val workflow = Workflow.stateless { + runningSideEffect(key = "failing") { + suspendCancellableCoroutine { continuation -> + continuation.invokeOnCancellation { cause -> cancellationException = cause } + } } - advanceIfStandard(dispatcher) - assertTrue(testScope.isActive) + } + val testScope = TestScope(dispatcherUsed) + renderWorkflowIn( + workflow = workflow, + scope = testScope, + props = MutableStateFlow(Unit), + runtimeConfig = runtimeConfig, + workflowTracer = testTracer, + ) {} + advanceIfStandard() + assertNull(cancellationException) + assertTrue(testScope.isActive) + + testScope.cancel(CancellationException("fail!", ExpectedException())) + advanceIfStandard() + assertTrue(cancellationException is CancellationException) + assertTrue(cancellationException!!.cause is ExpectedException) + } - trigger.complete(Unit) - advanceIfStandard(dispatcher) - assertFalse(testScope.isActive) + @Test fun error_from_renderings_collector_does_not_fail_parent_scope() = + runTest(dispatcherUsed) { + val workflow = Workflow.stateless {} + val testScope = TestScope(dispatcherUsed) + val renderings = renderWorkflowIn( + workflow = workflow, + scope = testScope, + props = MutableStateFlow(Unit), + runtimeConfig = runtimeConfig, + workflowTracer = testTracer, + ) {} + + // Collect in separate scope so we actually test that the parent scope is failed when it's + // different from the collecting scope. + val collectScope = TestScope(dispatcherUsed) + collectScope.launch { + renderings.collect { throw ExpectedException() } } + advanceIfStandard() + assertTrue(testScope.isActive) + assertFalse(collectScope.isActive) } - } - // https://github.com/square/workflow-kotlin/issues/224 - @Test fun exceptions_from_Snapshots_do_not_fail_runtime() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { - val workflow = Workflow.stateful( - snapshot = { - Snapshot.of { - throw ExpectedException() - } - }, - initialState = { _, _ -> }, - render = { _, _ -> } - ) - val props = MutableStateFlow(0) - val uncaughtExceptions = mutableListOf() - val exceptionHandler = CoroutineExceptionHandler { _, throwable -> - uncaughtExceptions += throwable - } - val mutex = Mutex(locked = true) - backgroundScope.launch(exceptionHandler) { - val snapshot = renderWorkflowIn( - workflow = workflow, - scope = this, - props = props, - runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, - ) {} - .value - .snapshot - - assertFailsWith { snapshot.toByteString() } - assertTrue(uncaughtExceptions.isEmpty()) - - props.value += 1 - assertFailsWith { snapshot.toByteString() } - mutex.unlock() - } - // wait for snapshotting. - mutex.lock() + @Test fun exception_from_onOutput_fails_parent_scope() = + runTest(dispatcherUsed) { + val trigger = CompletableDeferred() + // Emits a Unit when trigger is completed. + val workflow = Workflow.stateless { + runningWorker(Worker.from { trigger.await() }) { action("") { setOutput(Unit) } } + } + val testScope = TestScope(dispatcherUsed) + renderWorkflowIn( + workflow = workflow, + scope = testScope, + props = MutableStateFlow(Unit), + runtimeConfig = runtimeConfig, + workflowTracer = testTracer, + ) { + throw ExpectedException() } + advanceIfStandard() + assertTrue(testScope.isActive) + + trigger.complete(Unit) + advanceIfStandard() + assertFalse(testScope.isActive) } - } // https://github.com/square/workflow-kotlin/issues/224 - @Test fun exceptions_from_renderings_equals_methods_do_not_fail_runtime() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { - @Suppress("EqualsOrHashCode", "unused") - class FailRendering(val value: Int) { - override fun equals(other: Any?): Boolean { + @Test fun exceptions_from_Snapshots_do_not_fail_runtime() = + runTest(dispatcherUsed) { + val workflow = Workflow.stateful( + snapshot = { + Snapshot.of { throw ExpectedException() } - } + }, + initialState = { _, _ -> }, + render = { _, _ -> } + ) + val props = MutableStateFlow(0) + val uncaughtExceptions = mutableListOf() + val exceptionHandler = CoroutineExceptionHandler { _, throwable -> + uncaughtExceptions += throwable + } + val mutex = Mutex(locked = true) + backgroundScope.launch(exceptionHandler) { + val snapshot = renderWorkflowIn( + workflow = workflow, + scope = this, + props = props, + runtimeConfig = runtimeConfig, + workflowTracer = testTracer, + ) {} + .value + .snapshot - val workflow = Workflow.stateless { props -> - FailRendering(props) - } - val props = MutableStateFlow(0) - val uncaughtExceptions = mutableListOf() - val exceptionHandler = CoroutineExceptionHandler { _, throwable -> - uncaughtExceptions += throwable - } - val mutex = Mutex(locked = true) - backgroundScope.launch(exceptionHandler) { - val ras = renderWorkflowIn( - workflow = workflow, - scope = this, - props = props, - runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, - ) {} - val renderings = ras.map { it.rendering } - - @Suppress("UnusedEquals") - assertFailsWith { - renderings.collect { - it.equals(Unit) - } - } - assertTrue(uncaughtExceptions.isEmpty()) + assertFailsWith { snapshot.toByteString() } + assertTrue(uncaughtExceptions.isEmpty()) - // Trigger another render pass. - props.value += 1 - advanceIfStandard(dispatcher) - mutex.unlock() - } - mutex.lock() + props.value += 1 + assertFailsWith { snapshot.toByteString() } + mutex.unlock() } + // wait for snapshotting. + mutex.lock() } - } // https://github.com/square/workflow-kotlin/issues/224 - @Test fun exceptions_from_renderings_hashCode_methods_do_not_fail_runtime() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { - @Suppress("EqualsOrHashCode") - data class FailRendering(val value: Int) { - override fun hashCode(): Int { - throw ExpectedException() - } + @Test fun exceptions_from_renderings_equals_methods_do_not_fail_runtime() = + runTest(dispatcherUsed) { + @Suppress("EqualsOrHashCode", "unused") + class FailRendering(val value: Int) { + override fun equals(other: Any?): Boolean { + throw ExpectedException() } + } + + val workflow = Workflow.stateless { props -> + FailRendering(props) + } + val props = MutableStateFlow(0) + val uncaughtExceptions = mutableListOf() + val exceptionHandler = CoroutineExceptionHandler { _, throwable -> + uncaughtExceptions += throwable + } + val mutex = Mutex(locked = true) + backgroundScope.launch(exceptionHandler) { + val ras = renderWorkflowIn( + workflow = workflow, + scope = this, + props = props, + runtimeConfig = runtimeConfig, + workflowTracer = testTracer, + ) {} + val renderings = ras.map { it.rendering } - val workflow = Workflow.stateless { props -> - FailRendering(props) + @Suppress("UnusedEquals") + assertFailsWith { + renderings.collect { + it.equals(Unit) + } } - val props = MutableStateFlow(0) - val uncaughtExceptions = mutableListOf() - val exceptionHandler = CoroutineExceptionHandler { _, throwable -> - uncaughtExceptions += throwable + assertTrue(uncaughtExceptions.isEmpty()) + + // Trigger another render pass. + props.value += 1 + advanceIfStandard() + mutex.unlock() + } + mutex.lock() + } + + // https://github.com/square/workflow-kotlin/issues/224 + @Test fun exceptions_from_renderings_hashCode_methods_do_not_fail_runtime() = + runTest(dispatcherUsed) { + @Suppress("EqualsOrHashCode") + data class FailRendering(val value: Int) { + override fun hashCode(): Int { + throw ExpectedException() } - val mutex = Mutex(locked = true) - backgroundScope.launch(exceptionHandler) { - val ras = renderWorkflowIn( - workflow = workflow, - scope = this, - props = props, - runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, - ) {} - val renderings = ras.map { it.rendering } - - assertFailsWith { - renderings.collect { - it.hashCode() - } - } - assertTrue(uncaughtExceptions.isEmpty()) + } + + val workflow = Workflow.stateless { props -> + FailRendering(props) + } + val props = MutableStateFlow(0) + val uncaughtExceptions = mutableListOf() + val exceptionHandler = CoroutineExceptionHandler { _, throwable -> + uncaughtExceptions += throwable + } + val mutex = Mutex(locked = true) + backgroundScope.launch(exceptionHandler) { + val ras = renderWorkflowIn( + workflow = workflow, + scope = this, + props = props, + runtimeConfig = runtimeConfig, + workflowTracer = testTracer, + ) {} + val renderings = ras.map { it.rendering } - // Trigger another render pass. - props.value += 1 - advanceIfStandard(dispatcher) - mutex.unlock() + assertFailsWith { + renderings.collect { + it.hashCode() + } } - mutex.lock() + assertTrue(uncaughtExceptions.isEmpty()) + + // Trigger another render pass. + props.value += 1 + advanceIfStandard() + mutex.unlock() } + mutex.lock() } - } @Test fun for_render_on_state_change_only_we_do_not_render_if_state_not_changed() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions.filter { - it.first.contains(RENDER_ONLY_WHEN_STATE_CHANGES) - }, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { + if (runtimeConfig.contains(RENDER_ONLY_WHEN_STATE_CHANGES)) { + runTest(dispatcherUsed) { check(runtimeConfig.contains(RENDER_ONLY_WHEN_STATE_CHANGES)) lateinit var sink: Sink @@ -1283,7 +1091,7 @@ class RenderWorkflowInTest { scope = backgroundScope, props = props, runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, + workflowTracer = testTracer, ) {} val emitted = mutableListOf>() @@ -1292,7 +1100,7 @@ class RenderWorkflowInTest { } sink.send("unchanging state") - advanceIfStandard(dispatcher) + advanceIfStandard() collectionJob.cancel() assertEquals(1, emitted.size) @@ -1301,13 +1109,8 @@ class RenderWorkflowInTest { } @Test fun for_render_on_state_change_only_we_report_skipped_in_interceptor() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions.filter { - it.first.contains(RENDER_ONLY_WHEN_STATE_CHANGES) - }, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { + if (runtimeConfig.contains(RENDER_ONLY_WHEN_STATE_CHANGES)) { + runTest(dispatcherUsed) { check(runtimeConfig.contains(RENDER_ONLY_WHEN_STATE_CHANGES)) lateinit var sink: Sink val interceptedRenderings = mutableListOf() @@ -1336,7 +1139,7 @@ class RenderWorkflowInTest { props = props, interceptors = listOf(testInterceptor), runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, + workflowTracer = testTracer, ) {} val emitted = mutableListOf>() @@ -1345,7 +1148,7 @@ class RenderWorkflowInTest { } sink.send("unchanging state") - advanceIfStandard(dispatcher) + advanceIfStandard() collectionJob.cancel() assertEquals(1, emitted.size) @@ -1356,13 +1159,8 @@ class RenderWorkflowInTest { } @Test fun for_render_on_state_change_only_we_render_if_state_changed() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions.filter { - it.first.contains(RENDER_ONLY_WHEN_STATE_CHANGES) - }, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { + if (runtimeConfig.contains(RENDER_ONLY_WHEN_STATE_CHANGES)) { + runTest(dispatcherUsed) { check(runtimeConfig.contains(RENDER_ONLY_WHEN_STATE_CHANGES)) lateinit var sink: Sink @@ -1379,7 +1177,7 @@ class RenderWorkflowInTest { scope = backgroundScope, props = props, runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, + workflowTracer = testTracer, ) {} val emitted = mutableListOf>() @@ -1387,9 +1185,9 @@ class RenderWorkflowInTest { renderings.collect { emitted += it } } - advanceIfStandard(dispatcher) + advanceIfStandard() sink.send("changing state") - advanceIfStandard(dispatcher) + advanceIfStandard() assertEquals(2, emitted.size) collectionJob.cancel() @@ -1399,13 +1197,8 @@ class RenderWorkflowInTest { @Test fun `for_partial_tree_rendering_we_do_not_render_nodes_if_state_not_changed_even_in_render_pass`() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions.filter { - it.first.contains(PARTIAL_TREE_RENDERING) - }, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { + if (runtimeConfig.contains(PARTIAL_TREE_RENDERING)) { + runTest(dispatcherUsed) { check(runtimeConfig.contains(PARTIAL_TREE_RENDERING)) val trigger = MutableSharedFlow() @@ -1455,12 +1248,12 @@ class RenderWorkflowInTest { scope = backgroundScope, props = props, runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, + workflowTracer = testTracer, ) {} - advanceIfStandard(dispatcher) + advanceIfStandard() trigger.emit("state 1") // same value as the child starts with. - advanceIfStandard(dispatcher) + advanceIfStandard() assertEquals(2, parentRenderCount) assertEquals(1, childRenderCount) @@ -1469,13 +1262,8 @@ class RenderWorkflowInTest { } @Test fun for_partial_tree_rendering_we_render_nodes_if_state_changed() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions.filter { - it.first.contains(PARTIAL_TREE_RENDERING) - }, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { + if (runtimeConfig.contains(PARTIAL_TREE_RENDERING)) { + runTest(dispatcherUsed) { check(runtimeConfig.contains(PARTIAL_TREE_RENDERING)) val trigger = MutableSharedFlow() @@ -1525,12 +1313,12 @@ class RenderWorkflowInTest { scope = backgroundScope, props = props, runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, + workflowTracer = testTracer, ) {} - advanceIfStandard(dispatcher) + advanceIfStandard() trigger.emit("state 1") // different value than the child starts with. - advanceIfStandard(dispatcher) + advanceIfStandard() assertEquals(3, parentRenderCount) // Parent needs to be rendered 3x, but child only 2x as the 3rd time its the same. @@ -1541,15 +1329,11 @@ class RenderWorkflowInTest { @Test fun for_render_on_change_only_and_conflate_we_drain_action_but_do_not_render_no_state_changed() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions.filter { - it.first.contains(RENDER_ONLY_WHEN_STATE_CHANGES) && it.first.contains( - CONFLATE_STALE_RENDERINGS - ) - }, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { + if (runtimeConfig.contains(RENDER_ONLY_WHEN_STATE_CHANGES) && runtimeConfig.contains( + CONFLATE_STALE_RENDERINGS + ) + ) { + runTest(dispatcherUsed) { check(runtimeConfig.contains(CONFLATE_STALE_RENDERINGS)) check(runtimeConfig.contains(RENDER_ONLY_WHEN_STATE_CHANGES)) @@ -1600,16 +1384,16 @@ class RenderWorkflowInTest { scope = backgroundScope, props = props, runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, + workflowTracer = testTracer, ) { outputSet.add(it) } - advanceIfStandard(dispatcher) + advanceIfStandard() launch { trigger.emit("changed state") } - advanceIfStandard(dispatcher) + advanceIfStandard() // 2 renderings (initial and then the update.) Not *3* renderings. assertEquals(2, renderCount) @@ -1628,14 +1412,8 @@ class RenderWorkflowInTest { */ @Test fun for_conflate_we_conflate_stacked_actions_into_one_rendering() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions - .filter { - it.first.contains(CONFLATE_STALE_RENDERINGS) - }, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { + if (runtimeConfig.contains(CONFLATE_STALE_RENDERINGS)) { + runTest(dispatcherUsed) { check(runtimeConfig.contains(CONFLATE_STALE_RENDERINGS)) var childHandlerActionExecuted = false @@ -1683,13 +1461,13 @@ class RenderWorkflowInTest { scope = backgroundScope, props = props, runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, + workflowTracer = testTracer, ) { // Yield in output so that we ensure that we let the collector of the renderings // collect each of them before processing the next action. yield() } - advanceIfStandard(dispatcher) + advanceIfStandard() val collectionJob = launch { // Collect this unconfined so we can get all the renderings faster than actions can @@ -1698,11 +1476,11 @@ class RenderWorkflowInTest { emitted += it.rendering } } - advanceIfStandard(dispatcher) + advanceIfStandard() launch { trigger.emit("changed state") } - advanceIfStandard(dispatcher) + advanceIfStandard() collectionJob.cancel() @@ -1716,14 +1494,8 @@ class RenderWorkflowInTest { @Test fun for_conflate_we_do_not_conflate_stacked_actions_into_one_rendering_if_output() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions - .filter { - it.first.contains(CONFLATE_STALE_RENDERINGS) - }, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { + if (runtimeConfig.contains(CONFLATE_STALE_RENDERINGS)) { + runTest(dispatcherUsed) { check(runtimeConfig.contains(CONFLATE_STALE_RENDERINGS)) var childHandlerActionExecuted = false @@ -1772,7 +1544,7 @@ class RenderWorkflowInTest { scope = backgroundScope, props = props, runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, + workflowTracer = testTracer, ) { // Yield in output so that we ensure that we let the collector of the renderings // collect each of them before processing the next action. @@ -1786,12 +1558,12 @@ class RenderWorkflowInTest { emitted += it.rendering } } - advanceIfStandard(dispatcher) + advanceIfStandard() launch { trigger.emit("changed state") } - advanceIfStandard(dispatcher) + advanceIfStandard() collectionJob.cancel() @@ -1805,15 +1577,10 @@ class RenderWorkflowInTest { @Test fun for_conflate_and_render_only_when_state_changed_we_do_not_conflate_stacked_actions_into_one_rendering_if_previous_rendering_changed() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions - .filter { - it.first.contains(CONFLATE_STALE_RENDERINGS) && - it.first.contains(RENDER_ONLY_WHEN_STATE_CHANGES) - }, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { + if (runtimeConfig.contains(CONFLATE_STALE_RENDERINGS) && + runtimeConfig.contains(RENDER_ONLY_WHEN_STATE_CHANGES) + ) { + runTest(dispatcherUsed) { check(runtimeConfig.contains(CONFLATE_STALE_RENDERINGS)) check(runtimeConfig.contains(RENDER_ONLY_WHEN_STATE_CHANGES)) @@ -1860,7 +1627,7 @@ class RenderWorkflowInTest { scope = backgroundScope, props = props, runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, + workflowTracer = testTracer, ) { } val collectionJob = launch { @@ -1870,12 +1637,12 @@ class RenderWorkflowInTest { emitted += it.rendering } } - advanceIfStandard(dispatcher) + advanceIfStandard() launch { trigger.emit("changed state") } - advanceIfStandard(dispatcher) + advanceIfStandard() collectionJob.cancel() @@ -1889,15 +1656,10 @@ class RenderWorkflowInTest { @Test fun for_conflate_and_render_only_when_state_changed_we_do_not_render_again_if_only_previous_rendering_changed() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions - .filter { - it.first.contains(CONFLATE_STALE_RENDERINGS) && - it.first.contains(RENDER_ONLY_WHEN_STATE_CHANGES) - }, - before = ::setup, - ) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) -> - runTest(dispatcher) { + if (runtimeConfig.contains(CONFLATE_STALE_RENDERINGS) && + runtimeConfig.contains(RENDER_ONLY_WHEN_STATE_CHANGES) + ) { + runTest(dispatcherUsed) { check(runtimeConfig.contains(CONFLATE_STALE_RENDERINGS)) check(runtimeConfig.contains(RENDER_ONLY_WHEN_STATE_CHANGES)) @@ -1947,7 +1709,7 @@ class RenderWorkflowInTest { scope = backgroundScope, props = props, runtimeConfig = runtimeConfig, - workflowTracer = workflowTracer, + workflowTracer = testTracer, ) { } val collectionJob = launch { @@ -1957,12 +1719,12 @@ class RenderWorkflowInTest { emitted += it.rendering } } - advanceIfStandard(dispatcher) + advanceIfStandard() launch { trigger.emit("changed state") } - advanceIfStandard(dispatcher) + advanceIfStandard() collectionJob.cancel() @@ -1978,30 +1740,6 @@ class RenderWorkflowInTest { private class ExpectedException : RuntimeException() - private fun cartesianProduct( - set1: Sequence, - set2: Sequence - ): Sequence> { - return set1.flatMap { set1Item -> set2.map { set2Item -> set1Item to set2Item } } - } - - private fun cartesianProduct( - set1: Sequence, - set2: Sequence, - set3: Sequence - ): Sequence> { - return set1.flatMap { set1Item -> set2.map { set2Item -> set1Item to set2Item } } - .flatMap { (set1Item, set2Item) -> - set3.map { set3Item -> - Triple( - set1Item, - set2Item, - set3Item - ) - } - } - } - companion object { internal val EXPECTED_TRACE: String = """ StartingCreateWorkerWorkflow diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/ParameterizedTestRunner.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/ParameterizedTestRunner.kt deleted file mode 100644 index 794107b358..0000000000 --- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/ParameterizedTestRunner.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.squareup.workflow1.internal - -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertNotSame -import kotlin.test.assertNull -import kotlin.test.assertTrue - -/** - * Simple parameterized test as we are in KMP commonTest code and don't have junit - * libraries like jupiter. - * - * We do our best to tell you what the parameter was when the failure occurred by wrapping - * assertions from kotlin.test and injecting our own message. - */ -class ParameterizedTestRunner

{ - - var currentParam: P? = null - - fun runParametrizedTest( - paramSource: Sequence

, - before: () -> Unit = {}, - after: () -> Unit = {}, - test: ParameterizedTestRunner

.(param: P) -> Unit - ) { - paramSource.forEach { - before() - currentParam = it - test(it) - after() - } - } - - fun assertEquals(expected: T, actual: T) { - assertEquals(expected, actual, message = "Using: ${currentParam?.toString()}") - } - - fun assertEquals(expected: T, actual: T, originalMessage: String) { - assertEquals(expected, actual, message = "$originalMessage; Using: ${currentParam?.toString()}") - } - - fun assertTrue(statement: Boolean) { - assertTrue(statement, message = "Using: ${currentParam?.toString()}") - } - - fun assertFalse(statement: Boolean) { - assertFalse(statement, message = "Using: ${currentParam?.toString()}") - } - - inline fun assertFailsWith(block: () -> Unit) { - assertFailsWith(message = "Using: ${currentParam?.toString()}", block) - } - - fun assertNotSame(illegal: T, actual: T) { - assertNotSame(illegal, actual, message = "Using: ${currentParam?.toString()}") - } - - fun assertNotNull(actual: T?) { - assertNotNull(actual, message = "Using: ${currentParam?.toString()}") - } - - fun assertNull(actual: Any?) { - assertNull(actual, message = "Using: ${currentParam?.toString()}") - } -} diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowRunnerTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowRunnerTest.kt index 045f57bb5b..0097939327 100644 --- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowRunnerTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowRunnerTest.kt @@ -1,12 +1,12 @@ package com.squareup.workflow1.internal +import app.cash.burst.Burst import com.squareup.workflow1.ActionApplied import com.squareup.workflow1.NoopWorkflowInterceptor import com.squareup.workflow1.RuntimeConfig import com.squareup.workflow1.RuntimeConfigOptions -import com.squareup.workflow1.RuntimeConfigOptions.CONFLATE_STALE_RENDERINGS -import com.squareup.workflow1.RuntimeConfigOptions.PARTIAL_TREE_RENDERING -import com.squareup.workflow1.RuntimeConfigOptions.RENDER_ONLY_WHEN_STATE_CHANGES +import com.squareup.workflow1.RuntimeConfigOptions.Companion.RuntimeOptions +import com.squareup.workflow1.RuntimeConfigOptions.Companion.RuntimeOptions.DEFAULT import com.squareup.workflow1.Worker import com.squareup.workflow1.Workflow import com.squareup.workflow1.WorkflowExperimentalRuntime @@ -24,308 +24,240 @@ import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runCurrent +import kotlin.test.AfterTest +import kotlin.test.BeforeTest import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue @OptIn(ExperimentalCoroutinesApi::class, WorkflowExperimentalRuntime::class) -internal class WorkflowRunnerTest { +@Burst +internal class WorkflowRunnerTest( + runtime: RuntimeOptions = DEFAULT +) { private lateinit var scope: TestScope + private val runtimeConfig = runtime.runtimeConfig - private val runtimeOptions = arrayOf( - RuntimeConfigOptions.RENDER_PER_ACTION, - setOf(RENDER_ONLY_WHEN_STATE_CHANGES), - setOf(CONFLATE_STALE_RENDERINGS), - setOf(CONFLATE_STALE_RENDERINGS, RENDER_ONLY_WHEN_STATE_CHANGES), - setOf(RENDER_ONLY_WHEN_STATE_CHANGES, PARTIAL_TREE_RENDERING), - setOf(CONFLATE_STALE_RENDERINGS, RENDER_ONLY_WHEN_STATE_CHANGES, PARTIAL_TREE_RENDERING), - ).asSequence() - - private fun setup() { + @BeforeTest + public fun setup() { scope = TestScope() } - private fun tearDown() { + @AfterTest + public fun tearDown() { scope.cancel() } - private val runtimeTestRunner = ParameterizedTestRunner() - @Test fun initial_nextRendering_returns_initial_rendering() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - after = ::tearDown, - ) { runtimeConfig: RuntimeConfig -> - - val workflow = Workflow.stateless { "foo" } - val runner = WorkflowRunner( - workflow, - MutableStateFlow(Unit), - runtimeConfig - ) - val rendering = runner.nextRendering().rendering - assertEquals("foo", rendering) - } + val workflow = Workflow.stateless { "foo" } + val runner = WorkflowRunner( + workflow, + MutableStateFlow(Unit), + runtimeConfig + ) + val rendering = runner.nextRendering().rendering + assertEquals("foo", rendering) } @Test fun initial_nextRendering_uses_initial_props() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - after = ::tearDown, - ) { runtimeConfig: RuntimeConfig -> - - val workflow = Workflow.stateless { it } - val runner = WorkflowRunner( - workflow, - MutableStateFlow("foo"), - runtimeConfig - ) - val rendering = runner.nextRendering().rendering - assertEquals("foo", rendering) - } + val workflow = Workflow.stateless { it } + val runner = WorkflowRunner( + workflow, + MutableStateFlow("foo"), + runtimeConfig + ) + val rendering = runner.nextRendering().rendering + assertEquals("foo", rendering) } @Test fun initial_awaitAndApplyActions_does_not_handle_initial_props() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - after = ::tearDown, - ) { runtimeConfig: RuntimeConfig -> - - val workflow = Workflow.stateless { it } - val props = MutableStateFlow("initial") - val runner = WorkflowRunner( - workflow, - props, - runtimeConfig - ) - runner.nextRendering() - - val outputDeferred = scope.async { runner.awaitAndApplyAction() } - - scope.runCurrent() - assertTrue(outputDeferred.isActive) - } + val workflow = Workflow.stateless { it } + val props = MutableStateFlow("initial") + val runner = WorkflowRunner( + workflow, + props, + runtimeConfig + ) + runner.nextRendering() + + val outputDeferred = scope.async { runner.awaitAndApplyAction() } + + scope.runCurrent() + assertTrue(outputDeferred.isActive) } @Test fun initial_awaitAndApplyActions_handles_props_changed_after_initialization() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - after = ::tearDown, - ) { runtimeConfig: RuntimeConfig -> - - val workflow = Workflow.stateless { it } - val props = MutableStateFlow("initial") - // The dispatcher is paused, so the produceIn coroutine won't start yet. - val runner = WorkflowRunner( - workflow, - props, - runtimeConfig - ) - // The initial value will be read during initialization, so we can change it any time after - // that. - props.value = "changed" - - // Get the runner into the state where it's waiting for a props update. - val initialRendering = runner.nextRendering().rendering - assertEquals("initial", initialRendering) - val output = scope.async { runner.awaitAndApplyAction() } - assertTrue(output.isActive) - - // Resume the dispatcher to start the coroutines and process the new props value. - scope.runCurrent() - - assertTrue(output.isCompleted) - @Suppress("UNCHECKED_CAST") - val outputValue = output.getCompleted() as? ActionApplied? - assertNull(outputValue) - val rendering = runner.nextRendering().rendering - assertEquals("changed", rendering) - } + val workflow = Workflow.stateless { it } + val props = MutableStateFlow("initial") + // The dispatcher is paused, so the produceIn coroutine won't start yet. + val runner = WorkflowRunner( + workflow, + props, + runtimeConfig + ) + // The initial value will be read during initialization, so we can change it any time after + // that. + props.value = "changed" + + // Get the runner into the state where it's waiting for a props update. + val initialRendering = runner.nextRendering().rendering + assertEquals("initial", initialRendering) + val output = scope.async { runner.awaitAndApplyAction() } + assertTrue(output.isActive) + + // Resume the dispatcher to start the coroutines and process the new props value. + scope.runCurrent() + + assertTrue(output.isCompleted) + @Suppress("UNCHECKED_CAST") + val outputValue = output.getCompleted() as? ActionApplied? + assertNull(outputValue) + val rendering = runner.nextRendering().rendering + assertEquals("changed", rendering) } @Test fun awaitAndApplyActions_handles_workflow_update() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - after = ::tearDown, - ) { runtimeConfig: RuntimeConfig -> - - val workflow = Workflow.stateful( - initialState = { "initial" }, - render = { _, renderState -> - runningWorker(Worker.from { "work" }) { - action("") { - state = "state: $it" - setOutput("output: $it") - } + val workflow = Workflow.stateful( + initialState = { "initial" }, + render = { _, renderState -> + runningWorker(Worker.from { "work" }) { + action("") { + state = "state: $it" + setOutput("output: $it") } - return@stateful renderState } - ) - val runner = - WorkflowRunner(workflow, MutableStateFlow(Unit), runtimeConfig) + return@stateful renderState + } + ) + val runner = + WorkflowRunner(workflow, MutableStateFlow(Unit), runtimeConfig) - val initialRendering = runner.nextRendering().rendering - assertEquals("initial", initialRendering) + val initialRendering = runner.nextRendering().rendering + assertEquals("initial", initialRendering) - val actionResult = runner.runTillNextActionResult() - assertEquals("output: work", actionResult!!.output!!.value) + val actionResult = runner.runTillNextActionResult() + assertEquals("output: work", actionResult!!.output!!.value) - val updatedRendering = runner.nextRendering().rendering - assertEquals("state: work", updatedRendering) - } + val updatedRendering = runner.nextRendering().rendering + assertEquals("state: work", updatedRendering) } - @Test fun awaitAndProcessActions_handles_concurrent_props_change_and_workflow_update() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - after = ::tearDown, - ) { runtimeConfig: RuntimeConfig -> - - val workflow = Workflow.stateful( - initialState = { "initial state($it)" }, - render = { renderProps, renderState -> - runningWorker(Worker.from { "work" }) { - action("") { - state = "state: $it" - setOutput("output: $it") - } + @Test fun awaitAndApplyActions_handles_concurrent_props_change_and_workflow_update() { + val workflow = Workflow.stateful( + initialState = { "initial state($it)" }, + render = { renderProps, renderState -> + runningWorker(Worker.from { "work" }) { + action("") { + state = "state: $it" + setOutput("output: $it") } - return@stateful "$renderProps|$renderState" } - ) - val props = MutableStateFlow("initial props") - val runner = WorkflowRunner(workflow, props, runtimeConfig) - props.value = "changed props" - val initialRendering = runner.nextRendering().rendering - assertEquals("initial props|initial state(initial props)", initialRendering) - - // The order in which props update and workflow update are processed is deterministic, based - // on the order they appear in the select block in processActions. - val firstActionResult = runner.runTillNextActionResult() - // First update will be props, so no output value. - assertNull(firstActionResult) - val secondRendering = runner.nextRendering().rendering - assertEquals("changed props|initial state(initial props)", secondRendering) - - val secondActionResult = runner.runTillNextActionResult() - assertEquals("output: work", secondActionResult!!.output!!.value) - val thirdRendering = runner.nextRendering().rendering - assertEquals("changed props|state: work", thirdRendering) - } + return@stateful "$renderProps|$renderState" + } + ) + val props = MutableStateFlow("initial props") + val runner = WorkflowRunner(workflow, props, runtimeConfig) + props.value = "changed props" + val initialRendering = runner.nextRendering().rendering + assertEquals("initial props|initial state(initial props)", initialRendering) + + // The order in which props update and workflow update are processed is deterministic, based + // on the order they appear in the select block in processActions. + val firstActionResult = runner.runTillNextActionResult() + // First update will be props, so no output value. + assertNull(firstActionResult) + val secondRendering = runner.nextRendering().rendering + assertEquals("changed props|initial state(initial props)", secondRendering) + + val secondActionResult = runner.runTillNextActionResult() + assertEquals("output: work", secondActionResult!!.output!!.value) + val thirdRendering = runner.nextRendering().rendering + assertEquals("changed props|state: work", thirdRendering) } @Test fun cancelRuntime_does_not_interrupt_awaitAndApplyActions() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - after = ::tearDown, - ) { runtimeConfig: RuntimeConfig -> - val workflow = Workflow.stateless {} - val runner = - WorkflowRunner(workflow, MutableStateFlow(Unit), runtimeConfig) - runner.nextRendering() - val output = scope.async { runner.awaitAndApplyAction() } - scope.runCurrent() - assertTrue(output.isActive) - - // processActions is run on the scope passed to the runner, so it shouldn't be affected by this - // call. - runner.cancelRuntime() - - scope.advanceUntilIdle() - assertTrue(output.isActive) - } + val workflow = Workflow.stateless {} + val runner = + WorkflowRunner(workflow, MutableStateFlow(Unit), runtimeConfig) + runner.nextRendering() + val output = scope.async { runner.awaitAndApplyAction() } + scope.runCurrent() + assertTrue(output.isActive) + + // processActions is run on the scope passed to the runner, so it shouldn't be affected by this + // call. + runner.cancelRuntime() + + scope.advanceUntilIdle() + assertTrue(output.isActive) } @Test fun cancelRuntime_cancels_runtime() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - after = ::tearDown, - ) { runtimeConfig: RuntimeConfig -> - - var cancellationException: Throwable? = null - val workflow = Workflow.stateless { - runningSideEffect(key = "test side effect") { - suspendCancellableCoroutine { continuation -> - continuation.invokeOnCancellation { cause -> cancellationException = cause } - } + var cancellationException: Throwable? = null + val workflow = Workflow.stateless { + runningSideEffect(key = "test side effect") { + suspendCancellableCoroutine { continuation -> + continuation.invokeOnCancellation { cause -> cancellationException = cause } } } - val runner = - WorkflowRunner(workflow, MutableStateFlow(Unit), runtimeConfig) - runner.nextRendering() - scope.runCurrent() - assertNull(cancellationException) - - runner.cancelRuntime() - - scope.advanceUntilIdle() - assertNotNull(cancellationException) - val causes = generateSequence(cancellationException) { it.cause } - assertTrue(causes.all { it is CancellationException }) } + val runner = + WorkflowRunner(workflow, MutableStateFlow(Unit), runtimeConfig) + runner.nextRendering() + scope.runCurrent() + assertNull(cancellationException) + + runner.cancelRuntime() + + scope.advanceUntilIdle() + assertNotNull(cancellationException) + val causes = generateSequence(cancellationException) { it.cause } + assertTrue(causes.all { it is CancellationException }) } @Test fun cancelling_scope_interrupts_awaitAndApplyActions() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - after = ::tearDown, - ) { runtimeConfig: RuntimeConfig -> - - val workflow = Workflow.stateless {} - val runner = - WorkflowRunner(workflow, MutableStateFlow(Unit), runtimeConfig) - runner.nextRendering() - val actionResult = scope.async { runner.awaitAndApplyAction() } - scope.runCurrent() - assertTrue(actionResult.isActive) - - scope.cancel("foo") - - scope.advanceUntilIdle() - assertTrue(actionResult.isCancelled) - val realCause = actionResult.getCompletionExceptionOrNull() - assertEquals("foo", realCause?.message) - } + val workflow = Workflow.stateless {} + val runner = + WorkflowRunner(workflow, MutableStateFlow(Unit), runtimeConfig) + runner.nextRendering() + val actionResult = scope.async { runner.awaitAndApplyAction() } + scope.runCurrent() + assertTrue(actionResult.isActive) + + scope.cancel("foo") + + scope.advanceUntilIdle() + assertTrue(actionResult.isCancelled) + val realCause = actionResult.getCompletionExceptionOrNull() + assertEquals("foo", realCause?.message) } @Test fun cancelling_scope_cancels_runtime() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - before = ::setup, - after = ::tearDown, - ) { runtimeConfig: RuntimeConfig -> - - var cancellationException: Throwable? = null - val workflow = Workflow.stateless { - runningSideEffect(key = "test") { - suspendCancellableCoroutine { continuation -> - continuation.invokeOnCancellation { cause -> cancellationException = cause } - } + var cancellationException: Throwable? = null + val workflow = Workflow.stateless { + runningSideEffect(key = "test") { + suspendCancellableCoroutine { continuation -> + continuation.invokeOnCancellation { cause -> cancellationException = cause } } } - val runner = - WorkflowRunner(workflow, MutableStateFlow(Unit), runtimeConfig) - runner.nextRendering() - val actionResult = scope.async { runner.awaitAndApplyAction() } - scope.runCurrent() - assertTrue(actionResult.isActive) - assertNull(cancellationException) - - scope.cancel("foo") - - scope.advanceUntilIdle() - assertTrue(actionResult.isCancelled) - assertNotNull(cancellationException) - assertEquals("foo", cancellationException!!.message) } + val runner = + WorkflowRunner(workflow, MutableStateFlow(Unit), runtimeConfig) + runner.nextRendering() + val actionResult = scope.async { runner.awaitAndApplyAction() } + scope.runCurrent() + assertTrue(actionResult.isActive) + assertNull(cancellationException) + + scope.cancel("foo") + + scope.advanceUntilIdle() + assertTrue(actionResult.isCancelled) + assertNotNull(cancellationException) + assertEquals("foo", cancellationException!!.message) } @Suppress("UNCHECKED_CAST") diff --git a/workflow-testing/build.gradle.kts b/workflow-testing/build.gradle.kts index c471e45c56..efd28001c3 100644 --- a/workflow-testing/build.gradle.kts +++ b/workflow-testing/build.gradle.kts @@ -4,6 +4,7 @@ plugins { id("java-library") id("kotlin-jvm") id("published") + id("app.cash.burst") } tasks.withType { diff --git a/workflow-testing/src/test/java/com/squareup/workflow1/ParameterizedTestRunner.kt b/workflow-testing/src/test/java/com/squareup/workflow1/ParameterizedTestRunner.kt deleted file mode 100644 index e2a7f85dfb..0000000000 --- a/workflow-testing/src/test/java/com/squareup/workflow1/ParameterizedTestRunner.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.squareup.workflow1 - -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertNotSame -import kotlin.test.assertNull -import kotlin.test.assertTrue - -/** - * This file is copied from workflow-runtime:commonTest so our tests that test across the runtime - * look consistent. We could have used a JUnit library like Jupiter, but didn't. - * - * This file is copied so as to avoid creating a workflow-core-testing module (for now). - * - * We do our best to tell you what the parameter was when the failure occured by wrapping - * assertions from kotlin.test and injecting our own message. - */ -@WorkflowExperimentalApi -class ParameterizedTestRunner

{ - - var currentParam: P? = null - - fun runParametrizedTest( - paramSource: Sequence

, - before: () -> Unit = {}, - after: () -> Unit = {}, - test: ParameterizedTestRunner

.(param: P) -> Unit - ) { - paramSource.forEach { - before() - currentParam = it - test(it) - after() - } - } - - fun assertEquals(expected: T, actual: T) { - assertEquals(expected, actual, message = "Using: ${currentParam?.toString()}") - } - - fun assertEquals(expected: T, actual: T, originalMessage: String) { - assertEquals(expected, actual, message = "$originalMessage; Using: ${currentParam?.toString()}") - } - - fun assertTrue(statement: Boolean) { - assertTrue(statement, message = "Using: ${currentParam?.toString()}") - } - - fun assertFalse(statement: Boolean) { - assertFalse(statement, message = "Using: ${currentParam?.toString()}") - } - - inline fun assertFailsWith(block: () -> Unit) { - assertFailsWith(message = "Using: ${currentParam?.toString()}", block) - } - - fun assertNotSame(illegal: T, actual: T) { - assertNotSame(illegal, actual, message = "Using: ${currentParam?.toString()}") - } - - fun assertNotNull(actual: T?) { - assertNotNull(actual, message = "Using: ${currentParam?.toString()}") - } - - fun assertNull(actual: Any?) { - assertNull(actual, message = "Using: ${currentParam?.toString()}") - } -} diff --git a/workflow-testing/src/test/java/com/squareup/workflow1/StatefulWorkflowEventHandlerTest.kt b/workflow-testing/src/test/java/com/squareup/workflow1/StatefulWorkflowEventHandlerTest.kt index d3200b1ed1..90fb6057ca 100644 --- a/workflow-testing/src/test/java/com/squareup/workflow1/StatefulWorkflowEventHandlerTest.kt +++ b/workflow-testing/src/test/java/com/squareup/workflow1/StatefulWorkflowEventHandlerTest.kt @@ -1,266 +1,249 @@ +@file:Suppress("JUnitMalformedDeclaration") + package com.squareup.workflow1 +import app.cash.burst.Burst +import com.squareup.workflow1.RuntimeConfigOptions.Companion.RENDER_PER_ACTION import com.squareup.workflow1.RuntimeConfigOptions.STABLE_EVENT_HANDLERS import com.squareup.workflow1.testing.WorkflowTestParams import com.squareup.workflow1.testing.launchForTestingFromStartWith import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotSame import kotlin.test.assertSame /** * A lot of duplication here with [StatelessWorkflowEventHandlerTest] */ -@OptIn(WorkflowExperimentalApi::class, WorkflowExperimentalRuntime::class) -class StatefulWorkflowEventHandlerTest { - private data class Params( - val remember: Boolean?, - val runtimeConfig: RuntimeConfig - ) { - val remembering = remember ?: runtimeConfig.contains(STABLE_EVENT_HANDLERS) - } +@OptIn(WorkflowExperimentalRuntime::class) +@Burst +class StatefulWorkflowEventHandlerTest( + private val remembering: Boolean = false, + stableEventHandlers: Boolean = false, +) { - private val rememberValues = sequenceOf(true, false, null) - private val configValues = sequenceOf(emptySet(), setOf(STABLE_EVENT_HANDLERS)) - private val values = rememberValues.flatMap { remember -> - configValues.map { Params(remember, it) } + private val runtimeConfig = if (stableEventHandlers) { + setOf( + STABLE_EVENT_HANDLERS + ) + } else { + RENDER_PER_ACTION } - private val parameterizedTestRunner = ParameterizedTestRunner() @Test fun eventHandler0() { - parameterizedTestRunner.runParametrizedTest(values) { params -> - Workflow.stateful(Unit) { - eventHandler("", remember = params.remembering) { setOutput("yay") } - }.launchForTestingFromStartWith( - testParams = WorkflowTestParams(runtimeConfig = params.runtimeConfig) - ) { - val first = awaitNextRendering() - first.invoke() - assertEquals("yay", awaitNextOutput()) - val next = awaitNextRendering() - if (params.remembering) { - assertSame(first, next) - } else { - assertNotSame(first, next) - } + Workflow.stateful(Unit) { + eventHandler("", remember = remembering) { setOutput("yay") } + }.launchForTestingFromStartWith( + testParams = WorkflowTestParams(runtimeConfig = runtimeConfig) + ) { + val first = awaitNextRendering() + first.invoke() + assertEquals("yay", awaitNextOutput()) + val next = awaitNextRendering() + if (remembering) { + assertSame(first, next) + } else { + assertNotSame(first, next) } } } @Test fun eventHandler1() { - parameterizedTestRunner.runParametrizedTest(values) { params -> - Workflow.stateful Unit>(Unit) { - eventHandler("", remember = params.remembering) { e1 -> - setOutput(e1) - } - }.launchForTestingFromStartWith( - testParams = WorkflowTestParams(runtimeConfig = params.runtimeConfig) - ) { - val first = awaitNextRendering() - first.invoke("yay") - assertEquals("yay", awaitNextOutput()) - val next = awaitNextRendering() - if (params.remembering) { - assertSame(first, next) - } else { - assertNotSame(first, next) - } + Workflow.stateful Unit>(Unit) { + eventHandler("", remember = remembering) { e1 -> + setOutput(e1) + } + }.launchForTestingFromStartWith( + testParams = WorkflowTestParams(runtimeConfig = runtimeConfig) + ) { + val first = awaitNextRendering() + first.invoke("yay") + assertEquals("yay", awaitNextOutput()) + val next = awaitNextRendering() + if (remembering) { + assertSame(first, next) + } else { + assertNotSame(first, next) } } } @Test fun eventHandler2() { - parameterizedTestRunner.runParametrizedTest(values) { params -> - Workflow.stateful Unit>(Unit) { - eventHandler("", remember = params.remembering) { e1, e2 -> - setOutput("$e1-$e2") - } - }.launchForTestingFromStartWith( - testParams = WorkflowTestParams(runtimeConfig = params.runtimeConfig) - ) { - val first = awaitNextRendering() - first.invoke("a", "b") - assertEquals("a-b", awaitNextOutput()) - val next = awaitNextRendering() - if (params.remembering) { - assertSame(first, next) - } else { - assertNotSame(first, next) - } + Workflow.stateful Unit>(Unit) { + eventHandler("", remember = remembering) { e1, e2 -> + setOutput("$e1-$e2") + } + }.launchForTestingFromStartWith( + testParams = WorkflowTestParams(runtimeConfig = runtimeConfig) + ) { + val first = awaitNextRendering() + first.invoke("a", "b") + assertEquals("a-b", awaitNextOutput()) + val next = awaitNextRendering() + if (remembering) { + assertSame(first, next) + } else { + assertNotSame(first, next) } } } @Test fun eventHandler3() { - parameterizedTestRunner.runParametrizedTest(values) { params -> - Workflow.stateful Unit>(Unit) { - eventHandler("", remember = params.remembering) { e1, e2, e3 -> - setOutput("$e1-$e2-$e3") - } - }.launchForTestingFromStartWith( - testParams = WorkflowTestParams(runtimeConfig = params.runtimeConfig) - ) { - val first = awaitNextRendering() - first.invoke("a", "b", "c") - assertEquals("a-b-c", awaitNextOutput()) - val next = awaitNextRendering() - if (params.remembering) { - assertSame(first, next) - } else { - assertNotSame(first, next) - } + Workflow.stateful Unit>(Unit) { + eventHandler("", remember = remembering) { e1, e2, e3 -> + setOutput("$e1-$e2-$e3") + } + }.launchForTestingFromStartWith( + testParams = WorkflowTestParams(runtimeConfig = runtimeConfig) + ) { + val first = awaitNextRendering() + first.invoke("a", "b", "c") + assertEquals("a-b-c", awaitNextOutput()) + val next = awaitNextRendering() + if (remembering) { + assertSame(first, next) + } else { + assertNotSame(first, next) } } } @Test fun eventHandler4() { - parameterizedTestRunner.runParametrizedTest(values) { params -> - Workflow.stateful Unit>(Unit) { - eventHandler("", remember = params.remembering) { e1, e2, e3, e4 -> - setOutput("$e1-$e2-$e3-$e4") - } - }.launchForTestingFromStartWith( - testParams = WorkflowTestParams(runtimeConfig = params.runtimeConfig) - ) { - val first = awaitNextRendering() - first.invoke("a", "b", "c", "d") - assertEquals("a-b-c-d", awaitNextOutput()) - val next = awaitNextRendering() - if (params.remembering) { - assertSame(first, next) - } else { - assertNotSame(first, next) - } + Workflow.stateful Unit>(Unit) { + eventHandler("", remember = remembering) { e1, e2, e3, e4 -> + setOutput("$e1-$e2-$e3-$e4") + } + }.launchForTestingFromStartWith( + testParams = WorkflowTestParams(runtimeConfig = runtimeConfig) + ) { + val first = awaitNextRendering() + first.invoke("a", "b", "c", "d") + assertEquals("a-b-c-d", awaitNextOutput()) + val next = awaitNextRendering() + if (remembering) { + assertSame(first, next) + } else { + assertNotSame(first, next) } } } @Test fun eventHandler5() { - parameterizedTestRunner.runParametrizedTest(values) { params -> - Workflow.stateful Unit>(Unit) { - eventHandler("", remember = params.remembering) { e1, e2, e3, e4, e5 -> - setOutput("$e1-$e2-$e3-$e4-$e5") - } - }.launchForTestingFromStartWith( - testParams = WorkflowTestParams(runtimeConfig = params.runtimeConfig) - ) { - val first = awaitNextRendering() - first.invoke("a", "b", "c", "d", "e") - assertEquals("a-b-c-d-e", awaitNextOutput()) - val next = awaitNextRendering() - if (params.remembering) { - assertSame(first, next) - } else { - assertNotSame(first, next) - } + Workflow.stateful Unit>(Unit) { + eventHandler("", remember = remembering) { e1, e2, e3, e4, e5 -> + setOutput("$e1-$e2-$e3-$e4-$e5") + } + }.launchForTestingFromStartWith( + testParams = WorkflowTestParams(runtimeConfig = runtimeConfig) + ) { + val first = awaitNextRendering() + first.invoke("a", "b", "c", "d", "e") + assertEquals("a-b-c-d-e", awaitNextOutput()) + val next = awaitNextRendering() + if (remembering) { + assertSame(first, next) + } else { + assertNotSame(first, next) } } } @Test fun eventHandler6() { - parameterizedTestRunner.runParametrizedTest(values) { params -> - Workflow.stateful Unit>(Unit) { - eventHandler("", remember = params.remembering) { e1, e2, e3, e4, e5, e6 -> - setOutput("$e1-$e2-$e3-$e4-$e5-$e6") - } - }.launchForTestingFromStartWith( - testParams = WorkflowTestParams(runtimeConfig = params.runtimeConfig) - ) { - val first = awaitNextRendering() - first.invoke("a", "b", "c", "d", "e", "f") - assertEquals("a-b-c-d-e-f", awaitNextOutput()) - val next = awaitNextRendering() - if (params.remembering) { - assertSame(first, next) - } else { - assertNotSame(first, next) - } + Workflow.stateful Unit>(Unit) { + eventHandler("", remember = remembering) { e1, e2, e3, e4, e5, e6 -> + setOutput("$e1-$e2-$e3-$e4-$e5-$e6") + } + }.launchForTestingFromStartWith( + testParams = WorkflowTestParams(runtimeConfig = runtimeConfig) + ) { + val first = awaitNextRendering() + first.invoke("a", "b", "c", "d", "e", "f") + assertEquals("a-b-c-d-e-f", awaitNextOutput()) + val next = awaitNextRendering() + if (remembering) { + assertSame(first, next) + } else { + assertNotSame(first, next) } } } @Test fun eventHandler7() { - parameterizedTestRunner.runParametrizedTest(values) { params -> - Workflow.stateful Unit>(Unit) { - eventHandler("", remember = params.remembering) { e1, e2, e3, e4, e5, e6, e7 -> - setOutput("$e1-$e2-$e3-$e4-$e5-$e6-$e7") - } - }.launchForTestingFromStartWith( - testParams = WorkflowTestParams(runtimeConfig = params.runtimeConfig) - ) { - val first = awaitNextRendering() - first.invoke("a", "b", "c", "d", "e", "f", "g") - assertEquals("a-b-c-d-e-f-g", awaitNextOutput()) - val next = awaitNextRendering() - if (params.remembering) { - assertSame(first, next) - } else { - assertNotSame(first, next) - } + Workflow.stateful Unit>(Unit) { + eventHandler("", remember = remembering) { e1, e2, e3, e4, e5, e6, e7 -> + setOutput("$e1-$e2-$e3-$e4-$e5-$e6-$e7") + } + }.launchForTestingFromStartWith( + testParams = WorkflowTestParams(runtimeConfig = runtimeConfig) + ) { + val first = awaitNextRendering() + first.invoke("a", "b", "c", "d", "e", "f", "g") + assertEquals("a-b-c-d-e-f-g", awaitNextOutput()) + val next = awaitNextRendering() + if (remembering) { + assertSame(first, next) + } else { + assertNotSame(first, next) } } } @Test fun eventHandler8() { - parameterizedTestRunner.runParametrizedTest(values) { params -> - Workflow.stateful Unit>(Unit) { - eventHandler("", remember = params.remembering) { e1, e2, e3, e4, e5, e6, e7, e8 -> - setOutput("$e1-$e2-$e3-$e4-$e5-$e6-$e7-$e8") - } - }.launchForTestingFromStartWith( - testParams = WorkflowTestParams(runtimeConfig = params.runtimeConfig) - ) { - val first = awaitNextRendering() - first.invoke("a", "b", "c", "d", "e", "f", "g", "h") - assertEquals("a-b-c-d-e-f-g-h", awaitNextOutput()) - val next = awaitNextRendering() - if (params.remembering) { - assertSame(first, next) - } else { - assertNotSame(first, next) - } + Workflow.stateful Unit>(Unit) { + eventHandler("", remember = remembering) { e1, e2, e3, e4, e5, e6, e7, e8 -> + setOutput("$e1-$e2-$e3-$e4-$e5-$e6-$e7-$e8") + } + }.launchForTestingFromStartWith( + testParams = WorkflowTestParams(runtimeConfig = runtimeConfig) + ) { + val first = awaitNextRendering() + first.invoke("a", "b", "c", "d", "e", "f", "g", "h") + assertEquals("a-b-c-d-e-f-g-h", awaitNextOutput()) + val next = awaitNextRendering() + if (remembering) { + assertSame(first, next) + } else { + assertNotSame(first, next) } } } @Test fun eventHandler9() { - parameterizedTestRunner.runParametrizedTest(values) { params -> - Workflow.stateful Unit>(Unit) { - eventHandler("", remember = params.remembering) { e1, e2, e3, e4, e5, e6, e7, e8, e9 -> - setOutput("$e1-$e2-$e3-$e4-$e5-$e6-$e7-$e8-$e9") - } - }.launchForTestingFromStartWith( - testParams = WorkflowTestParams(runtimeConfig = params.runtimeConfig) - ) { - val first = awaitNextRendering() - first.invoke("a", "b", "c", "d", "e", "f", "g", "h", "i") - assertEquals("a-b-c-d-e-f-g-h-i", awaitNextOutput()) - val next = awaitNextRendering() - if (params.remembering) { - assertSame(first, next) - } else { - assertNotSame(first, next) - } + Workflow.stateful Unit>(Unit) { + eventHandler("", remember = remembering) { e1, e2, e3, e4, e5, e6, e7, e8, e9 -> + setOutput("$e1-$e2-$e3-$e4-$e5-$e6-$e7-$e8-$e9") + } + }.launchForTestingFromStartWith( + testParams = WorkflowTestParams(runtimeConfig = runtimeConfig) + ) { + val first = awaitNextRendering() + first.invoke("a", "b", "c", "d", "e", "f", "g", "h", "i") + assertEquals("a-b-c-d-e-f-g-h-i", awaitNextOutput()) + val next = awaitNextRendering() + if (remembering) { + assertSame(first, next) + } else { + assertNotSame(first, next) } } } @Test fun eventHandler10() { - parameterizedTestRunner.runParametrizedTest(values) { params -> - Workflow.stateful Unit>(Unit) { - eventHandler("", remember = params.remembering) { e1, e2, e3, e4, e5, e6, e7, e8, e9, e10 -> - setOutput("$e1-$e2-$e3-$e4-$e5-$e6-$e7-$e8-$e9-$e10") - } - }.launchForTestingFromStartWith( - testParams = WorkflowTestParams(runtimeConfig = params.runtimeConfig) - ) { - val first = awaitNextRendering() - first.invoke("a", "b", "c", "d", "e", "f", "g", "h", "i", "k") - assertEquals("a-b-c-d-e-f-g-h-i-k", awaitNextOutput()) - val next = awaitNextRendering() - if (params.remembering) { - assertSame(first, next) - } else { - assertNotSame(first, next) - } + Workflow.stateful Unit>(Unit) { + eventHandler("", remember = remembering) { e1, e2, e3, e4, e5, e6, e7, e8, e9, e10 -> + setOutput("$e1-$e2-$e3-$e4-$e5-$e6-$e7-$e8-$e9-$e10") + } + }.launchForTestingFromStartWith( + testParams = WorkflowTestParams(runtimeConfig = runtimeConfig) + ) { + val first = awaitNextRendering() + first.invoke("a", "b", "c", "d", "e", "f", "g", "h", "i", "k") + assertEquals("a-b-c-d-e-f-g-h-i-k", awaitNextOutput()) + val next = awaitNextRendering() + if (remembering) { + assertSame(first, next) + } else { + assertNotSame(first, next) } } } diff --git a/workflow-testing/src/test/java/com/squareup/workflow1/StatelessWorkflowEventHandlerTest.kt b/workflow-testing/src/test/java/com/squareup/workflow1/StatelessWorkflowEventHandlerTest.kt index 6f0ab5db23..7be50b0b17 100644 --- a/workflow-testing/src/test/java/com/squareup/workflow1/StatelessWorkflowEventHandlerTest.kt +++ b/workflow-testing/src/test/java/com/squareup/workflow1/StatelessWorkflowEventHandlerTest.kt @@ -1,266 +1,249 @@ +@file:Suppress("JUnitMalformedDeclaration") + package com.squareup.workflow1 +import app.cash.burst.Burst +import com.squareup.workflow1.RuntimeConfigOptions.Companion.RENDER_PER_ACTION import com.squareup.workflow1.RuntimeConfigOptions.STABLE_EVENT_HANDLERS import com.squareup.workflow1.testing.WorkflowTestParams import com.squareup.workflow1.testing.launchForTestingFromStartWith import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotSame import kotlin.test.assertSame /** * A lot of duplication here with [StatefulWorkflowEventHandlerTest] */ -@OptIn(WorkflowExperimentalApi::class, WorkflowExperimentalRuntime::class) -class StatelessWorkflowEventHandlerTest { - private data class Params( - val remember: Boolean?, - val runtimeConfig: RuntimeConfig - ) { - val remembering = remember ?: runtimeConfig.contains(STABLE_EVENT_HANDLERS) - } +@OptIn(WorkflowExperimentalRuntime::class) +@Burst +class StatelessWorkflowEventHandlerTest( + private val remembering: Boolean = false, + stableEventHandlers: Boolean = false, +) { - private val rememberValues = sequenceOf(true, false, null) - private val configValues = sequenceOf(emptySet(), setOf(STABLE_EVENT_HANDLERS)) - private val values = rememberValues.flatMap { remember -> - configValues.map { Params(remember, it) } + private val runtimeConfig = if (stableEventHandlers) { + setOf( + STABLE_EVENT_HANDLERS + ) + } else { + RENDER_PER_ACTION } - private val parameterizedTestRunner = ParameterizedTestRunner() @Test fun eventHandler0() { - parameterizedTestRunner.runParametrizedTest(values) { params -> - Workflow.stateless Unit> { - eventHandler("", remember = params.remembering) { setOutput("yay") } - }.launchForTestingFromStartWith( - testParams = WorkflowTestParams(runtimeConfig = params.runtimeConfig) - ) { - val first = awaitNextRendering() - first.invoke() - assertEquals("yay", awaitNextOutput()) - val next = awaitNextRendering() - if (params.remembering) { - assertSame(first, next) - } else { - assertNotSame(first, next) - } + Workflow.stateless Unit> { + eventHandler("", remember = remembering) { setOutput("yay") } + }.launchForTestingFromStartWith( + testParams = WorkflowTestParams(runtimeConfig = runtimeConfig) + ) { + val first = awaitNextRendering() + first.invoke() + assertEquals("yay", awaitNextOutput()) + val next = awaitNextRendering() + if (remembering) { + assertSame(first, next) + } else { + assertNotSame(first, next) } } } @Test fun eventHandler1() { - parameterizedTestRunner.runParametrizedTest(values) { params -> - Workflow.stateless Unit> { - eventHandler("", remember = params.remembering) { e1 -> - setOutput(e1) - } - }.launchForTestingFromStartWith( - testParams = WorkflowTestParams(runtimeConfig = params.runtimeConfig) - ) { - val first = awaitNextRendering() - first.invoke("yay") - assertEquals("yay", awaitNextOutput()) - val next = awaitNextRendering() - if (params.remembering) { - assertSame(first, next) - } else { - assertNotSame(first, next) - } + Workflow.stateless Unit> { + eventHandler("", remember = remembering) { e1 -> + setOutput(e1) + } + }.launchForTestingFromStartWith( + testParams = WorkflowTestParams(runtimeConfig = runtimeConfig) + ) { + val first = awaitNextRendering() + first.invoke("yay") + assertEquals("yay", awaitNextOutput()) + val next = awaitNextRendering() + if (remembering) { + assertSame(first, next) + } else { + assertNotSame(first, next) } } } @Test fun eventHandler2() { - parameterizedTestRunner.runParametrizedTest(values) { params -> - Workflow.stateless Unit> { - eventHandler("", remember = params.remembering) { e1, e2 -> - setOutput("$e1-$e2") - } - }.launchForTestingFromStartWith( - testParams = WorkflowTestParams(runtimeConfig = params.runtimeConfig) - ) { - val first = awaitNextRendering() - first.invoke("a", "b") - assertEquals("a-b", awaitNextOutput()) - val next = awaitNextRendering() - if (params.remembering) { - assertSame(first, next) - } else { - assertNotSame(first, next) - } + Workflow.stateless Unit> { + eventHandler("", remember = remembering) { e1, e2 -> + setOutput("$e1-$e2") + } + }.launchForTestingFromStartWith( + testParams = WorkflowTestParams(runtimeConfig = runtimeConfig) + ) { + val first = awaitNextRendering() + first.invoke("a", "b") + assertEquals("a-b", awaitNextOutput()) + val next = awaitNextRendering() + if (remembering) { + assertSame(first, next) + } else { + assertNotSame(first, next) } } } @Test fun eventHandler3() { - parameterizedTestRunner.runParametrizedTest(values) { params -> - Workflow.stateless Unit> { - eventHandler("", remember = params.remembering) { e1, e2, e3 -> - setOutput("$e1-$e2-$e3") - } - }.launchForTestingFromStartWith( - testParams = WorkflowTestParams(runtimeConfig = params.runtimeConfig) - ) { - val first = awaitNextRendering() - first.invoke("a", "b", "c") - assertEquals("a-b-c", awaitNextOutput()) - val next = awaitNextRendering() - if (params.remembering) { - assertSame(first, next) - } else { - assertNotSame(first, next) - } + Workflow.stateless Unit> { + eventHandler("", remember = remembering) { e1, e2, e3 -> + setOutput("$e1-$e2-$e3") + } + }.launchForTestingFromStartWith( + testParams = WorkflowTestParams(runtimeConfig = runtimeConfig) + ) { + val first = awaitNextRendering() + first.invoke("a", "b", "c") + assertEquals("a-b-c", awaitNextOutput()) + val next = awaitNextRendering() + if (remembering) { + assertSame(first, next) + } else { + assertNotSame(first, next) } } } @Test fun eventHandler4() { - parameterizedTestRunner.runParametrizedTest(values) { params -> - Workflow.stateless Unit> { - eventHandler("", remember = params.remembering) { e1, e2, e3, e4 -> - setOutput("$e1-$e2-$e3-$e4") - } - }.launchForTestingFromStartWith( - testParams = WorkflowTestParams(runtimeConfig = params.runtimeConfig) - ) { - val first = awaitNextRendering() - first.invoke("a", "b", "c", "d") - assertEquals("a-b-c-d", awaitNextOutput()) - val next = awaitNextRendering() - if (params.remembering) { - assertSame(first, next) - } else { - assertNotSame(first, next) - } + Workflow.stateless Unit> { + eventHandler("", remember = remembering) { e1, e2, e3, e4 -> + setOutput("$e1-$e2-$e3-$e4") + } + }.launchForTestingFromStartWith( + testParams = WorkflowTestParams(runtimeConfig = runtimeConfig) + ) { + val first = awaitNextRendering() + first.invoke("a", "b", "c", "d") + assertEquals("a-b-c-d", awaitNextOutput()) + val next = awaitNextRendering() + if (remembering) { + assertSame(first, next) + } else { + assertNotSame(first, next) } } } @Test fun eventHandler5() { - parameterizedTestRunner.runParametrizedTest(values) { params -> - Workflow.stateless Unit> { - eventHandler("", remember = params.remembering) { e1, e2, e3, e4, e5 -> - setOutput("$e1-$e2-$e3-$e4-$e5") - } - }.launchForTestingFromStartWith( - testParams = WorkflowTestParams(runtimeConfig = params.runtimeConfig) - ) { - val first = awaitNextRendering() - first.invoke("a", "b", "c", "d", "e") - assertEquals("a-b-c-d-e", awaitNextOutput()) - val next = awaitNextRendering() - if (params.remembering) { - assertSame(first, next) - } else { - assertNotSame(first, next) - } + Workflow.stateless Unit> { + eventHandler("", remember = remembering) { e1, e2, e3, e4, e5 -> + setOutput("$e1-$e2-$e3-$e4-$e5") + } + }.launchForTestingFromStartWith( + testParams = WorkflowTestParams(runtimeConfig = runtimeConfig) + ) { + val first = awaitNextRendering() + first.invoke("a", "b", "c", "d", "e") + assertEquals("a-b-c-d-e", awaitNextOutput()) + val next = awaitNextRendering() + if (remembering) { + assertSame(first, next) + } else { + assertNotSame(first, next) } } } @Test fun eventHandler6() { - parameterizedTestRunner.runParametrizedTest(values) { params -> - Workflow.stateless Unit> { - eventHandler("", remember = params.remembering) { e1, e2, e3, e4, e5, e6 -> - setOutput("$e1-$e2-$e3-$e4-$e5-$e6") - } - }.launchForTestingFromStartWith( - testParams = WorkflowTestParams(runtimeConfig = params.runtimeConfig) - ) { - val first = awaitNextRendering() - first.invoke("a", "b", "c", "d", "e", "f") - assertEquals("a-b-c-d-e-f", awaitNextOutput()) - val next = awaitNextRendering() - if (params.remembering) { - assertSame(first, next) - } else { - assertNotSame(first, next) - } + Workflow.stateless Unit> { + eventHandler("", remember = remembering) { e1, e2, e3, e4, e5, e6 -> + setOutput("$e1-$e2-$e3-$e4-$e5-$e6") + } + }.launchForTestingFromStartWith( + testParams = WorkflowTestParams(runtimeConfig = runtimeConfig) + ) { + val first = awaitNextRendering() + first.invoke("a", "b", "c", "d", "e", "f") + assertEquals("a-b-c-d-e-f", awaitNextOutput()) + val next = awaitNextRendering() + if (remembering) { + assertSame(first, next) + } else { + assertNotSame(first, next) } } } @Test fun eventHandler7() { - parameterizedTestRunner.runParametrizedTest(values) { params -> - Workflow.stateless Unit> { - eventHandler("", remember = params.remembering) { e1, e2, e3, e4, e5, e6, e7 -> - setOutput("$e1-$e2-$e3-$e4-$e5-$e6-$e7") - } - }.launchForTestingFromStartWith( - testParams = WorkflowTestParams(runtimeConfig = params.runtimeConfig) - ) { - val first = awaitNextRendering() - first.invoke("a", "b", "c", "d", "e", "f", "g") - assertEquals("a-b-c-d-e-f-g", awaitNextOutput()) - val next = awaitNextRendering() - if (params.remembering) { - assertSame(first, next) - } else { - assertNotSame(first, next) - } + Workflow.stateless Unit> { + eventHandler("", remember = remembering) { e1, e2, e3, e4, e5, e6, e7 -> + setOutput("$e1-$e2-$e3-$e4-$e5-$e6-$e7") + } + }.launchForTestingFromStartWith( + testParams = WorkflowTestParams(runtimeConfig = runtimeConfig) + ) { + val first = awaitNextRendering() + first.invoke("a", "b", "c", "d", "e", "f", "g") + assertEquals("a-b-c-d-e-f-g", awaitNextOutput()) + val next = awaitNextRendering() + if (remembering) { + assertSame(first, next) + } else { + assertNotSame(first, next) } } } @Test fun eventHandler8() { - parameterizedTestRunner.runParametrizedTest(values) { params -> - Workflow.stateless Unit> { - eventHandler("", remember = params.remembering) { e1, e2, e3, e4, e5, e6, e7, e8 -> - setOutput("$e1-$e2-$e3-$e4-$e5-$e6-$e7-$e8") - } - }.launchForTestingFromStartWith( - testParams = WorkflowTestParams(runtimeConfig = params.runtimeConfig) - ) { - val first = awaitNextRendering() - first.invoke("a", "b", "c", "d", "e", "f", "g", "h") - assertEquals("a-b-c-d-e-f-g-h", awaitNextOutput()) - val next = awaitNextRendering() - if (params.remembering) { - assertSame(first, next) - } else { - assertNotSame(first, next) - } + Workflow.stateless Unit> { + eventHandler("", remember = remembering) { e1, e2, e3, e4, e5, e6, e7, e8 -> + setOutput("$e1-$e2-$e3-$e4-$e5-$e6-$e7-$e8") + } + }.launchForTestingFromStartWith( + testParams = WorkflowTestParams(runtimeConfig = runtimeConfig) + ) { + val first = awaitNextRendering() + first.invoke("a", "b", "c", "d", "e", "f", "g", "h") + assertEquals("a-b-c-d-e-f-g-h", awaitNextOutput()) + val next = awaitNextRendering() + if (remembering) { + assertSame(first, next) + } else { + assertNotSame(first, next) } } } @Test fun eventHandler9() { - parameterizedTestRunner.runParametrizedTest(values) { params -> - Workflow.stateless Unit> { - eventHandler("", remember = params.remembering) { e1, e2, e3, e4, e5, e6, e7, e8, e9 -> - setOutput("$e1-$e2-$e3-$e4-$e5-$e6-$e7-$e8-$e9") - } - }.launchForTestingFromStartWith( - testParams = WorkflowTestParams(runtimeConfig = params.runtimeConfig) - ) { - val first = awaitNextRendering() - first.invoke("a", "b", "c", "d", "e", "f", "g", "h", "i") - assertEquals("a-b-c-d-e-f-g-h-i", awaitNextOutput()) - val next = awaitNextRendering() - if (params.remembering) { - assertSame(first, next) - } else { - assertNotSame(first, next) - } + Workflow.stateless Unit> { + eventHandler("", remember = remembering) { e1, e2, e3, e4, e5, e6, e7, e8, e9 -> + setOutput("$e1-$e2-$e3-$e4-$e5-$e6-$e7-$e8-$e9") + } + }.launchForTestingFromStartWith( + testParams = WorkflowTestParams(runtimeConfig = runtimeConfig) + ) { + val first = awaitNextRendering() + first.invoke("a", "b", "c", "d", "e", "f", "g", "h", "i") + assertEquals("a-b-c-d-e-f-g-h-i", awaitNextOutput()) + val next = awaitNextRendering() + if (remembering) { + assertSame(first, next) + } else { + assertNotSame(first, next) } } } @Test fun eventHandler10() { - parameterizedTestRunner.runParametrizedTest(values) { params -> - Workflow.stateless Unit> { - eventHandler("", remember = params.remembering) { e1, e2, e3, e4, e5, e6, e7, e8, e9, e10 -> - setOutput("$e1-$e2-$e3-$e4-$e5-$e6-$e7-$e8-$e9-$e10") - } - }.launchForTestingFromStartWith( - testParams = WorkflowTestParams(runtimeConfig = params.runtimeConfig) - ) { - val first = awaitNextRendering() - first.invoke("a", "b", "c", "d", "e", "f", "g", "h", "i", "k") - assertEquals("a-b-c-d-e-f-g-h-i-k", awaitNextOutput()) - val next = awaitNextRendering() - if (params.remembering) { - assertSame(first, next) - } else { - assertNotSame(first, next) - } + Workflow.stateless Unit> { + eventHandler("", remember = remembering) { e1, e2, e3, e4, e5, e6, e7, e8, e9, e10 -> + setOutput("$e1-$e2-$e3-$e4-$e5-$e6-$e7-$e8-$e9-$e10") + } + }.launchForTestingFromStartWith( + testParams = WorkflowTestParams(runtimeConfig = runtimeConfig) + ) { + val first = awaitNextRendering() + first.invoke("a", "b", "c", "d", "e", "f", "g", "h", "i", "k") + assertEquals("a-b-c-d-e-f-g-h-i-k", awaitNextOutput()) + val next = awaitNextRendering() + if (remembering) { + assertSame(first, next) + } else { + assertNotSame(first, next) } } } diff --git a/workflow-testing/src/test/java/com/squareup/workflow1/WorkflowsLifecycleTests.kt b/workflow-testing/src/test/java/com/squareup/workflow1/WorkflowsLifecycleTests.kt index 5f3b6841b8..7d2d035570 100644 --- a/workflow-testing/src/test/java/com/squareup/workflow1/WorkflowsLifecycleTests.kt +++ b/workflow-testing/src/test/java/com/squareup/workflow1/WorkflowsLifecycleTests.kt @@ -1,8 +1,11 @@ +@file:Suppress("JUnitMalformedDeclaration") + package com.squareup.workflow1 +import app.cash.burst.Burst import com.squareup.workflow1.RuntimeConfigOptions.CONFLATE_STALE_RENDERINGS -import com.squareup.workflow1.RuntimeConfigOptions.PARTIAL_TREE_RENDERING -import com.squareup.workflow1.RuntimeConfigOptions.RENDER_ONLY_WHEN_STATE_CHANGES +import com.squareup.workflow1.RuntimeConfigOptions.Companion.RuntimeOptions +import com.squareup.workflow1.RuntimeConfigOptions.Companion.RuntimeOptions.DEFAULT import com.squareup.workflow1.testing.headlessIntegrationTest import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job @@ -10,23 +13,19 @@ import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlin.test.Ignore import kotlin.test.Test +import kotlin.test.assertEquals /** * Most of these tests are motivated by [1093](https://github.com/square/workflow-kotlin/issues/1093). */ @OptIn(WorkflowExperimentalRuntime::class, WorkflowExperimentalApi::class) -class WorkflowsLifecycleTests { - - private val runtimeOptions: Sequence = arrayOf( - RuntimeConfigOptions.RENDER_PER_ACTION, - setOf(RENDER_ONLY_WHEN_STATE_CHANGES), - setOf(CONFLATE_STALE_RENDERINGS), - setOf(CONFLATE_STALE_RENDERINGS, RENDER_ONLY_WHEN_STATE_CHANGES), - setOf(RENDER_ONLY_WHEN_STATE_CHANGES, PARTIAL_TREE_RENDERING), - setOf(CONFLATE_STALE_RENDERINGS, RENDER_ONLY_WHEN_STATE_CHANGES, PARTIAL_TREE_RENDERING), - ).asSequence() - - private val runtimeTestRunner = ParameterizedTestRunner() +@Burst +class WorkflowsLifecycleTests( + private val runtime: RuntimeOptions = DEFAULT +) { + + private val runtimeConfig = runtime.runtimeConfig + private var started = 0 private var cancelled = 0 private val workflowWithSideEffects: @@ -84,62 +83,44 @@ class WorkflowsLifecycleTests { } @Test fun sideEffectsStartedWhenExpected() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - after = ::cleanup, - ) { runtimeConfig: RuntimeConfig -> - - workflowWithSideEffects.headlessIntegrationTest( - runtimeConfig = runtimeConfig - ) { - // One time starts but does not stop the side effect. - repeat(1) { - val (current, setState) = awaitNextRendering() - setState.invoke(current + 1) - } - - assertEquals(1, started, "Side Effect not started 1 time.") + workflowWithSideEffects.headlessIntegrationTest( + runtimeConfig = runtimeConfig + ) { + // One time starts but does not stop the side effect. + repeat(1) { + val (current, setState) = awaitNextRendering() + setState.invoke(current + 1) } + + assertEquals(1, started, "Side Effect not started 1 time.") } } @Test fun sideEffectsStoppedWhenExpected() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - after = ::cleanup, - ) { runtimeConfig: RuntimeConfig -> - - workflowWithSideEffects.headlessIntegrationTest( - runtimeConfig = runtimeConfig - ) { - // Twice will start and stop the side effect. - repeat(2) { - val (current, setState) = awaitNextRendering() - setState.invoke(current + 1) - } - assertEquals(1, started, "Side Effect not started 1 time.") - assertEquals(1, cancelled, "Side Effect not cancelled 1 time.") + workflowWithSideEffects.headlessIntegrationTest( + runtimeConfig = runtimeConfig + ) { + // Twice will start and stop the side effect. + repeat(2) { + val (current, setState) = awaitNextRendering() + setState.invoke(current + 1) } + assertEquals(1, started, "Side Effect not started 1 time.") + assertEquals(1, cancelled, "Side Effect not cancelled 1 time.") } } @Test fun childSessionWorkflowStartedWhenExpected() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - after = ::cleanup, - ) { runtimeConfig: RuntimeConfig -> - - workflowWithChildSession.headlessIntegrationTest( - runtimeConfig = runtimeConfig - ) { - // One time starts but does not stop the child session workflow. - repeat(1) { - val (current, setState) = awaitNextRendering() - setState.invoke(current + 1) - } - - assertEquals(1, started, "Child Session Workflow not started 1 time.") + workflowWithChildSession.headlessIntegrationTest( + runtimeConfig = runtimeConfig + ) { + // One time starts but does not stop the child session workflow. + repeat(1) { + val (current, setState) = awaitNextRendering() + setState.invoke(current + 1) } + + assertEquals(1, started, "Child Session Workflow not started 1 time.") } } @@ -156,51 +137,39 @@ class WorkflowsLifecycleTests { @OptIn(ExperimentalCoroutinesApi::class) @Test fun sideEffectsStartAndStoppedWhenHandledSynchronously() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - after = ::cleanup, - ) { runtimeConfig: RuntimeConfig -> - - val dispatcher = UnconfinedTestDispatcher() - workflowWithSideEffects.headlessIntegrationTest( - coroutineContext = dispatcher, - runtimeConfig = runtimeConfig - ) { - - val (_, setState) = awaitNextRendering() - // 2 actions queued up - should start the side effect and then stop it - // on two consecutive render passes. - setState.invoke(1) - setState.invoke(2) + val dispatcher = UnconfinedTestDispatcher() + workflowWithSideEffects.headlessIntegrationTest( + coroutineContext = dispatcher, + runtimeConfig = runtimeConfig + ) { + + val (_, setState) = awaitNextRendering() + // 2 actions queued up - should start the side effect and then stop it + // on two consecutive render passes. + setState.invoke(1) + setState.invoke(2) + awaitNextRendering() + if (!runtimeConfig.contains(CONFLATE_STALE_RENDERINGS)) { + // 2 rendering or 1 depending on runtime config. awaitNextRendering() - if (!runtimeConfig.contains(CONFLATE_STALE_RENDERINGS)) { - // 2 rendering or 1 depending on runtime config. - awaitNextRendering() - } - - assertEquals(1, started, "Side Effect not started 1 time.") - assertEquals(1, cancelled, "Side Effect not cancelled 1 time.") } + + assertEquals(1, started, "Side Effect not started 1 time.") + assertEquals(1, cancelled, "Side Effect not cancelled 1 time.") } } @Test fun childSessionWorkflowStoppedWhenExpected() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - after = ::cleanup, - ) { runtimeConfig: RuntimeConfig -> - - workflowWithChildSession.headlessIntegrationTest( - runtimeConfig = runtimeConfig - ) { - // Twice will start and stop the child session workflow. - repeat(2) { - val (current, setState) = awaitNextRendering() - setState.invoke(current + 1) - } - assertEquals(1, started, "Child Session Workflow not started 1 time.") - assertEquals(1, cancelled, "Child Session Workflow not cancelled 1 time.") + workflowWithChildSession.headlessIntegrationTest( + runtimeConfig = runtimeConfig + ) { + // Twice will start and stop the child session workflow. + repeat(2) { + val (current, setState) = awaitNextRendering() + setState.invoke(current + 1) } + assertEquals(1, started, "Child Session Workflow not started 1 time.") + assertEquals(1, cancelled, "Child Session Workflow not cancelled 1 time.") } } @@ -212,31 +181,25 @@ class WorkflowsLifecycleTests { @OptIn(ExperimentalCoroutinesApi::class) @Test fun childSessionWorkflowStartAndStoppedWhenHandledSynchronously() { - runtimeTestRunner.runParametrizedTest( - paramSource = runtimeOptions, - after = ::cleanup, - ) { runtimeConfig: RuntimeConfig -> - - val dispatcher = UnconfinedTestDispatcher() - workflowWithChildSession.headlessIntegrationTest( - coroutineContext = dispatcher, - runtimeConfig = runtimeConfig - ) { - - val (_, setState) = awaitNextRendering() - // 2 actions queued up - should start the child session workflow and then stop it - // on two consecutive render passes, synchronously. - setState.invoke(1) - setState.invoke(2) + val dispatcher = UnconfinedTestDispatcher() + workflowWithChildSession.headlessIntegrationTest( + coroutineContext = dispatcher, + runtimeConfig = runtimeConfig + ) { + + val (_, setState) = awaitNextRendering() + // 2 actions queued up - should start the child session workflow and then stop it + // on two consecutive render passes, synchronously. + setState.invoke(1) + setState.invoke(2) + awaitNextRendering() + if (!runtimeConfig.contains(CONFLATE_STALE_RENDERINGS)) { + // 2 rendering or 1 depending on runtime config. awaitNextRendering() - if (!runtimeConfig.contains(CONFLATE_STALE_RENDERINGS)) { - // 2 rendering or 1 depending on runtime config. - awaitNextRendering() - } - - assertEquals(1, started, "Child Session Workflow not started 1 time.") - assertEquals(1, cancelled, "Child Session Workflow not cancelled 1 time.") } + + assertEquals(1, started, "Child Session Workflow not started 1 time.") + assertEquals(1, cancelled, "Child Session Workflow not cancelled 1 time.") } } } From a3bf0ad00bd65b865fa08cff92763340f1eb1051 Mon Sep 17 00:00:00 2001 From: steve-the-edwards <8658187+steve-the-edwards@users.noreply.github.com> Date: Thu, 3 Jul 2025 19:33:22 +0000 Subject: [PATCH 2/2] Apply changes from artifactsDump Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- artifacts.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/artifacts.json b/artifacts.json index 3890a37a88..21b078b895 100644 --- a/artifacts.json +++ b/artifacts.json @@ -125,6 +125,15 @@ "javaVersion": 8, "publicationName": "kotlinMultiplatform" }, + { + "gradlePath": ":workflow-runtime-android", + "group": "com.squareup.workflow1", + "artifactId": "workflow-runtime-android", + "description": "Workflow Runtime Android", + "packaging": "aar", + "javaVersion": 8, + "publicationName": "maven" + }, { "gradlePath": ":workflow-rx2", "group": "com.squareup.workflow1",