From 14acb3ff4707e6731289793ac388909993b69301 Mon Sep 17 00:00:00 2001 From: Stephen Edwards Date: Tue, 23 Aug 2022 16:48:45 -0400 Subject: [PATCH] Add Conflate Stale Renderings Runtime Check for empty channels and synchronously return if they are empty. --- .github/workflows/kotlin.yml | 76 +++++++++++++++++++ gradle.properties | 2 +- .../config/AndroidRuntimeConfigTools.kt | 4 + .../config/JvmTestRuntimeConfigTools.kt | 13 ++-- workflow-core/api/workflow-core.api | 4 + .../com/squareup/workflow1/WorkflowAction.kt | 2 + workflow-runtime/api/workflow-runtime.api | 7 +- .../com/squareup/workflow1/RenderWorkflow.kt | 54 +++++++++++-- .../com/squareup/workflow1/RuntimeConfig.kt | 6 ++ .../workflow1/internal/SubtreeManager.kt | 6 +- .../workflow1/internal/WorkflowNode.kt | 9 ++- .../workflow1/internal/WorkflowRunner.kt | 21 +++-- .../workflow1/RenderWorkflowInTest.kt | 34 +++++++++ .../workflow1/internal/WorkflowRunnerTest.kt | 9 ++- 14 files changed, 215 insertions(+), 32 deletions(-) diff --git a/.github/workflows/kotlin.yml b/.github/workflows/kotlin.yml index 4a89524256..7efb792776 100644 --- a/.github/workflows/kotlin.yml +++ b/.github/workflows/kotlin.yml @@ -100,6 +100,34 @@ jobs : with : report_paths : '**/build/test-results/test/TEST-*.xml' + jvm-conflate-runtime-test : + name : Conflate Stale Renderings Runtime JVM Tests + runs-on : ubuntu-latest + timeout-minutes : 20 + steps : + - uses : actions/checkout@v3 + - uses : gradle/wrapper-validation-action@v1 + - name : set up JDK 11 + uses : actions/setup-java@v3 + with : + distribution : 'zulu' + java-version : 11 + + ## Actual task + - uses : gradle/gradle-build-action@v2 + name : Check with Gradle + with : + arguments : | + jvmTest --stacktrace --continue -Pworkflow.runtime=conflate + cache-read-only : false + + # Report as Github Pull Request Check. + - name : Publish Test Report + uses : mikepenz/action-junit-report@v3 + if : always() # always run even if the previous step fails + with : + report_paths : '**/build/test-results/test/TEST-*.xml' + ios-tests : name : iOS Tests runs-on : macos-latest @@ -228,6 +256,54 @@ jobs : name : instrumentation-test-results-${{ matrix.api-level }} path : ./**/build/reports/androidTests/connected/** + conflate-renderings-instrumentation-tests : + name : Conflate Stale Renderings Instrumentation tests + runs-on : macos-latest + timeout-minutes : 45 + strategy : + # Allow tests to continue on other devices if they fail on one device. + fail-fast : false + matrix : + api-level : + - 29 + # Unclear that older versions actually honor command to disable animation. + # Newer versions are reputed to be too slow: https://github.com/ReactiveCircus/android-emulator-runner/issues/222 + steps : + - uses : actions/checkout@v3 + - name : set up JDK 11 + uses : actions/setup-java@v3 + with : + distribution : 'zulu' + java-version : 11 + + ## Build before running tests, using cache. + - uses: gradle/gradle-build-action@v2 + name : Build instrumented tests + with : + # Unfortunately I don't think we can key this cache based on our project property so + # we clean and rebuild. + arguments : | + clean assembleDebugAndroidTest --stacktrace -Pworkflow.runtime=conflate + cache-read-only: false + + ## Actual task + - name : Instrumentation Tests + uses : reactivecircus/android-emulator-runner@v2 + with : + # @ychescale9 suspects Galaxy Nexus is the fastest one + profile : Galaxy Nexus + api-level : ${{ matrix.api-level }} + arch : x86_64 + # Skip the benchmarks as this is running on emulators + script : ./gradlew connectedCheck -x :benchmarks:dungeon-benchmark:connectedCheck -x :benchmarks:performance-poetry:complex-benchmark:connectedCheck -x :benchmarks:performance-poetry:complex-poetry:connectedCheck --stacktrace -Pworkflow.runtime=conflate + + - name : Upload results + if : ${{ always() }} + uses : actions/upload-artifact@v3 + with : + name : instrumentation-test-results-${{ matrix.api-level }} + path : ./**/build/reports/androidTests/connected/** + upload-to-mobiledev : name : mobile.dev | Build & Upload runs-on : ubuntu-latest diff --git a/gradle.properties b/gradle.properties index 0abe598d7f..de5611cae5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ android.useAndroidX=true systemProp.org.gradle.internal.publish.checksums.insecure=true GROUP=com.squareup.workflow1 -VERSION_NAME=1.8.0-beta10-SNAPSHOT +VERSION_NAME=1.8.0-beta10-conflate-SNAPSHOT POM_DESCRIPTION=Square Workflow diff --git a/workflow-config/config-android/src/main/java/com/squareup/workflow1/config/AndroidRuntimeConfigTools.kt b/workflow-config/config-android/src/main/java/com/squareup/workflow1/config/AndroidRuntimeConfigTools.kt index ba9d332fbf..a352341252 100644 --- a/workflow-config/config-android/src/main/java/com/squareup/workflow1/config/AndroidRuntimeConfigTools.kt +++ b/workflow-config/config-android/src/main/java/com/squareup/workflow1/config/AndroidRuntimeConfigTools.kt @@ -1,6 +1,7 @@ package com.squareup.workflow1.config import com.squareup.workflow1.RuntimeConfig +import com.squareup.workflow1.RuntimeConfig.ConflateStaleRenderings import com.squareup.workflow1.RuntimeConfig.RenderPerAction import com.squareup.workflow1.WorkflowExperimentalRuntime @@ -17,12 +18,15 @@ public class AndroidRuntimeConfigTools { * this function, and then pass that to the call to [renderWorkflowIn] as the [RuntimeConfig]. * * Current options are: + * "conflate" : [ConflateStaleRenderings] Process all queued actions before passing rendering + * to the UI layer. * "baseline" : [RenderPerAction] Original Workflow Runtime. Note that this doesn't need to * be specified as it is the current default and is assumed by this utility. */ @WorkflowExperimentalRuntime public fun getAppWorkflowRuntimeConfig(): RuntimeConfig { return when (BuildConfig.WORKFLOW_RUNTIME) { + "conflate" -> ConflateStaleRenderings else -> RenderPerAction } } diff --git a/workflow-config/config-jvm/src/main/java/com/squareup/workflow1/config/JvmTestRuntimeConfigTools.kt b/workflow-config/config-jvm/src/main/java/com/squareup/workflow1/config/JvmTestRuntimeConfigTools.kt index 42c959e81e..16172a373f 100644 --- a/workflow-config/config-jvm/src/main/java/com/squareup/workflow1/config/JvmTestRuntimeConfigTools.kt +++ b/workflow-config/config-jvm/src/main/java/com/squareup/workflow1/config/JvmTestRuntimeConfigTools.kt @@ -1,6 +1,7 @@ package com.squareup.workflow1.config import com.squareup.workflow1.RuntimeConfig +import com.squareup.workflow1.RuntimeConfig.ConflateStaleRenderings import com.squareup.workflow1.RuntimeConfig.RenderPerAction import com.squareup.workflow1.WorkflowExperimentalRuntime @@ -16,16 +17,18 @@ public class JvmTestRuntimeConfigTools { * [RuntimeConfig]. * * Current options are: + * "conflate" : [ConflateStaleRenderings] Process all queued actions before passing rendering + * to the UI layer. * "baseline" : [RenderPerAction] Original Workflow Runtime. Note that this doesn't need to * be specified as it is the current default and is assumed by this utility. */ @OptIn(WorkflowExperimentalRuntime::class) public fun getTestRuntimeConfig(): RuntimeConfig { - return RenderPerAction - // val runtimeConfig = System.getProperty("workflow.runtime", "baseline") - // return when (runtimeConfig) { - // else -> RenderPerAction - // } + val runtimeConfig = System.getProperty("workflow.runtime", "baseline") + return when (runtimeConfig) { + "conflate" -> ConflateStaleRenderings + else -> RenderPerAction + } } } } diff --git a/workflow-core/api/workflow-core.api b/workflow-core/api/workflow-core.api index ccf568ab8a..f93c225e3c 100644 --- a/workflow-core/api/workflow-core.api +++ b/workflow-core/api/workflow-core.api @@ -1,6 +1,10 @@ public abstract interface class com/squareup/workflow1/ActionProcessingResult { } +public final class com/squareup/workflow1/ActionsExhausted : com/squareup/workflow1/ActionProcessingResult { + public static final field INSTANCE Lcom/squareup/workflow1/ActionsExhausted; +} + public abstract interface class com/squareup/workflow1/BaseRenderContext { public abstract fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function10;)Lkotlin/jvm/functions/Function9; public abstract fun eventHandler (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function11;)Lkotlin/jvm/functions/Function10; diff --git a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowAction.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowAction.kt index 5c3e19e106..c1d76a97ed 100644 --- a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowAction.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowAction.kt @@ -119,6 +119,8 @@ public sealed interface ActionProcessingResult public object PropsUpdated : ActionProcessingResult +public object ActionsExhausted : ActionProcessingResult + /** Wrapper around a potentially-nullable [OutputT] value. */ public class WorkflowOutput(public val value: OutputT) : ActionProcessingResult { override fun toString(): String = "WorkflowOutput($value)" diff --git a/workflow-runtime/api/workflow-runtime.api b/workflow-runtime/api/workflow-runtime.api index 667396ac7f..f883bd7f1c 100644 --- a/workflow-runtime/api/workflow-runtime.api +++ b/workflow-runtime/api/workflow-runtime.api @@ -28,6 +28,10 @@ public final class com/squareup/workflow1/RuntimeConfig$Companion { public final fun getDEFAULT_CONFIG ()Lcom/squareup/workflow1/RuntimeConfig; } +public final class com/squareup/workflow1/RuntimeConfig$ConflateStaleRenderings : com/squareup/workflow1/RuntimeConfig { + public static final field INSTANCE Lcom/squareup/workflow1/RuntimeConfig$ConflateStaleRenderings; +} + public final class com/squareup/workflow1/RuntimeConfig$RenderPerAction : com/squareup/workflow1/RuntimeConfig { public static final field INSTANCE Lcom/squareup/workflow1/RuntimeConfig$RenderPerAction; } @@ -272,6 +276,7 @@ public final class com/squareup/workflow1/internal/WorkflowRunner { public final fun cancelRuntime (Ljava/util/concurrent/CancellationException;)V public static synthetic fun cancelRuntime$default (Lcom/squareup/workflow1/internal/WorkflowRunner;Ljava/util/concurrent/CancellationException;ILjava/lang/Object;)V public final fun nextRendering ()Lcom/squareup/workflow1/RenderingAndSnapshot; - public final fun processAction (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun processAction (ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun processAction$default (Lcom/squareup/workflow1/internal/WorkflowRunner;ZLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; } diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt index 23b490317e..e7fbc3eff6 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt @@ -1,5 +1,6 @@ package com.squareup.workflow1 +import com.squareup.workflow1.RuntimeConfig.ConflateStaleRenderings import com.squareup.workflow1.internal.WorkflowRunner import com.squareup.workflow1.internal.chained import kotlinx.coroutines.CancellationException @@ -101,7 +102,7 @@ import kotlinx.coroutines.launch * A [StateFlow] of [RenderingAndSnapshot]s that will emit any time the root workflow creates a new * rendering. */ -@OptIn(ExperimentalCoroutinesApi::class) +@OptIn(ExperimentalCoroutinesApi::class, WorkflowExperimentalRuntime::class) public fun renderWorkflowIn( workflow: Workflow, scope: CoroutineScope, @@ -133,21 +134,60 @@ public fun renderWorkflowIn( } ) + suspend fun renderAndEmitOutput( + runner: WorkflowRunner, + actionResult: ActionProcessingResult?, + onOutput: suspend (OutputT) -> Unit + ): RenderingAndSnapshot { + // After receiving an output from the actions that were processed, + // the next render pass must be done before emitting that output, + // so that the workflow states appear consistent to observers of the outputs and renderings. + val nextRenderAndSnapshot = runner.nextRendering() + + when (actionResult) { + is WorkflowOutput<*> -> { + @Suppress("UNCHECKED_CAST") + val output = actionResult as? WorkflowOutput + output?.let { onOutput(it.value) } + } + else -> {} // no -op + } + + return nextRenderAndSnapshot + } + scope.launch { while (isActive) { - // It might look weird to start by consuming the output before getting the rendering below, + lateinit var nextRenderAndSnapshot: RenderingAndSnapshot + // It might look weird to start by processing an action before getting the rendering below, // but remember the first render pass already occurred above, before this coroutine was even // launched. - val output: WorkflowOutput? = runner.processAction() + var actionResult: ActionProcessingResult? = runner.processAction() - // After resuming from runner.nextOutput() our coroutine could now be cancelled, check so we - // don't surprise anyone with an unexpected rendering pass. Show's over, go home. + // After resuming from runner.processAction() our coroutine could now be cancelled, check so + // we don't surprise anyone with an unexpected rendering pass. Show's over, go home. if (!isActive) return@launch // After receiving an output, the next render pass must be done before emitting that output, // so that the workflow states appear consistent to observers of the outputs and renderings. - renderingsAndSnapshots.value = runner.nextRendering() - output?.let { onOutput(it.value) } + nextRenderAndSnapshot = renderAndEmitOutput(runner, actionResult, onOutput) + + if (runtimeConfig == ConflateStaleRenderings) { + while (actionResult != ActionsExhausted) { + // We have more actions we can process, so this rendering is stale. + actionResult = runner.processAction(waitForAnAction = false) + + if (!isActive) return@launch + + // If no actions processed, then no new rendering needed. + if (actionResult == ActionsExhausted) break + + nextRenderAndSnapshot = renderAndEmitOutput(runner, actionResult, onOutput) + } + } + + // Pass on to the UI. + renderingsAndSnapshots.value = nextRenderAndSnapshot } } diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RuntimeConfig.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RuntimeConfig.kt index 192a1c6607..6d662fb7e1 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RuntimeConfig.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RuntimeConfig.kt @@ -25,6 +25,12 @@ public sealed interface RuntimeConfig { */ public object RenderPerAction : RuntimeConfig + /** + * If we have more actions to process, do so before passing the rendering to the UI layer. + */ + @WorkflowExperimentalRuntime + public object ConflateStaleRenderings : RuntimeConfig + public companion object { public val DEFAULT_CONFIG: RuntimeConfig = RenderPerAction } diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SubtreeManager.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SubtreeManager.kt index 434b16c194..de0c46bc73 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SubtreeManager.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SubtreeManager.kt @@ -134,10 +134,12 @@ internal class SubtreeManager( * Uses [selector] to invoke [WorkflowNode.tick] for every running child workflow this instance * is managing. */ - fun tickChildren(selector: SelectBuilder) { + fun tickChildren(selector: SelectBuilder): Boolean { + var empty = true children.forEachActive { child -> - child.workflowNode.tick(selector) + empty = empty && child.workflowNode.tick(selector) } + return empty } fun createChildSnapshots(): Map { diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt index 11b1e91754..5293330f92 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart.LAZY +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel @@ -150,9 +151,12 @@ internal class WorkflowNode( * * It is an error to call this method after calling [cancel]. */ - fun tick(selector: SelectBuilder) { + @OptIn(ExperimentalCoroutinesApi::class) + fun tick(selector: SelectBuilder): Boolean { // Listen for any child workflow updates. - subtreeManager.tickChildren(selector) + var empty = subtreeManager.tickChildren(selector) + + empty = empty && eventActionsChannel.isEmpty // Listen for any events. with(selector) { @@ -160,6 +164,7 @@ internal class WorkflowNode( return@onReceive applyAction(action) } } + return empty } /** diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowRunner.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowRunner.kt index d2caa09daa..fbfb049f7a 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowRunner.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowRunner.kt @@ -1,14 +1,15 @@ package com.squareup.workflow1.internal import com.squareup.workflow1.ActionProcessingResult +import com.squareup.workflow1.ActionsExhausted import com.squareup.workflow1.PropsUpdated import com.squareup.workflow1.RenderingAndSnapshot import com.squareup.workflow1.RuntimeConfig +import com.squareup.workflow1.RuntimeConfig.ConflateStaleRenderings import com.squareup.workflow1.TreeSnapshot import com.squareup.workflow1.Workflow import com.squareup.workflow1.WorkflowExperimentalRuntime import com.squareup.workflow1.WorkflowInterceptor -import com.squareup.workflow1.WorkflowOutput import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -77,20 +78,16 @@ internal class WorkflowRunner( * coroutine and no others. */ @OptIn(WorkflowExperimentalRuntime::class) - suspend fun processAction(): WorkflowOutput? { + suspend fun processAction(waitForAnAction: Boolean = true): ActionProcessingResult? { // First we block and wait until there is an action to process. - val processingResult: ActionProcessingResult? = select { + return select { onPropsUpdated() // Have the workflow tree build the select to wait for an event/output from Worker. - rootNode.tick(this) - } - - @Suppress("UNCHECKED_CAST") - return when (processingResult) { - PropsUpdated -> null - else -> { - // Unchecked cast as this is the only other option for the sealed interface. - processingResult as WorkflowOutput? + val empty = rootNode.tick(this) + if (!waitForAnAction && runtimeConfig == ConflateStaleRenderings && empty) { + onTimeout(0) { + ActionsExhausted + } } } } 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 59ba80ed49..4204ddfc34 100644 --- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RenderWorkflowInTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RenderWorkflowInTest.kt @@ -1,5 +1,6 @@ package com.squareup.workflow1 +import com.squareup.workflow1.RuntimeConfig.ConflateStaleRenderings import com.squareup.workflow1.RuntimeConfig.RenderPerAction import com.squareup.workflow1.internal.ParameterizedTestRunner import kotlinx.coroutines.CancellationException @@ -43,6 +44,7 @@ class RenderWorkflowInTest { private val runtimeOptions = arrayOf( RenderPerAction, + ConflateStaleRenderings, ).asSequence() private val runtimeTestRunner = ParameterizedTestRunner() @@ -162,6 +164,8 @@ class RenderWorkflowInTest { assertEquals("props: foo", renderings.value.rendering) props.value = "bar" + testScope.advanceUntilIdle() + testScope.runCurrent() assertEquals("props: bar", renderings.value.rendering) } @@ -169,6 +173,9 @@ class RenderWorkflowInTest { private val runtimeMatrix = arrayOf( Pair(RenderPerAction, RenderPerAction), + Pair(RenderPerAction, ConflateStaleRenderings), + Pair(ConflateStaleRenderings, RenderPerAction), + Pair(ConflateStaleRenderings, ConflateStaleRenderings), ).asSequence() private val runtimeMatrixTestRunner = ParameterizedTestRunner>() @@ -206,6 +213,9 @@ class RenderWorkflowInTest { updateState("updated state") } + testScope.advanceUntilIdle() + testScope.runCurrent() + val snapshot = renderings.value.let { (rendering, snapshot) -> val (state, updateState) = rendering runtimeMatrixTestRunner.assertEquals("updated state", state) @@ -266,7 +276,12 @@ class RenderWorkflowInTest { renderings.collect { emitted += it } } sink.send("unchanging state") + testScope.advanceUntilIdle() + testScope.runCurrent() + sink.send("unchanging state") + testScope.advanceUntilIdle() + testScope.runCurrent() scope.cancel() @@ -331,10 +346,14 @@ class RenderWorkflowInTest { assertEquals(0, onOutputCalls) props.value = 1 + testScope.advanceUntilIdle() + testScope.runCurrent() assertEquals(1, renderings.value.rendering) assertEquals(0, onOutputCalls) props.value = 2 + testScope.advanceUntilIdle() + testScope.runCurrent() assertEquals(2, renderings.value.rendering) assertEquals(0, onOutputCalls) } @@ -483,6 +502,8 @@ class RenderWorkflowInTest { assertTrue(testScope.isActive) trigger.complete(Unit) + testScope.advanceUntilIdle() + testScope.runCurrent() assertFalse(testScope.isActive) } } @@ -511,6 +532,8 @@ class RenderWorkflowInTest { assertTrue(testScope.isActive) trigger.complete(Unit) + testScope.advanceUntilIdle() + testScope.runCurrent() assertFalse(testScope.isActive) } } @@ -687,6 +710,7 @@ class RenderWorkflowInTest { assertTrue(pausedTestScope.isActive) pausedTestScope.advanceUntilIdle() + pausedTestScope.runCurrent() assertFalse(pausedTestScope.isActive) } } @@ -721,10 +745,12 @@ class RenderWorkflowInTest { ) .onEach { events += "rendering(${it.rendering})" } .launchIn(pausedTestScope) + pausedTestScope.advanceUntilIdle() pausedTestScope.runCurrent() assertEquals(listOf("rendering({no output})"), events) outputTrigger.complete("output") + pausedTestScope.advanceUntilIdle() pausedTestScope.runCurrent() assertEquals( listOf( @@ -804,6 +830,9 @@ class RenderWorkflowInTest { val renderings = ras.map { it.rendering } .produceIn(testScope) + testScope.advanceUntilIdle() + testScope.runCurrent() + @Suppress("UnusedEquals") assertFailsWith { renderings.tryReceive() @@ -814,6 +843,8 @@ class RenderWorkflowInTest { // Trigger another render pass. props.value += 1 + testScope.advanceUntilIdle() + testScope.runCurrent() } } @@ -856,6 +887,9 @@ class RenderWorkflowInTest { assertTrue(uncaughtExceptions.isEmpty()) props.value += 1 + testScope.advanceUntilIdle() + testScope.runCurrent() + @Suppress("UnusedEquals") assertFailsWith { renderings.tryReceive() 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 4520705faa..d999bbf946 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 @@ -3,6 +3,7 @@ package com.squareup.workflow1.internal import com.squareup.workflow1.NoopWorkflowInterceptor import com.squareup.workflow1.RuntimeConfig import com.squareup.workflow1.RuntimeConfig.Companion +import com.squareup.workflow1.RuntimeConfig.ConflateStaleRenderings import com.squareup.workflow1.RuntimeConfig.RenderPerAction import com.squareup.workflow1.Worker import com.squareup.workflow1.Workflow @@ -31,6 +32,7 @@ internal class WorkflowRunnerTest { private val runtimeOptions = arrayOf( RenderPerAction, + ConflateStaleRenderings, ).asSequence() private fun setup() { @@ -131,7 +133,9 @@ internal class WorkflowRunnerTest { scope.runCurrent() assertTrue(output.isCompleted) - assertNull(output.getCompleted()) + @Suppress("UNCHECKED_CAST") + val outputValue = output.getCompleted() as? WorkflowOutput? + assertNull(outputValue) val rendering = runner.nextRendering().rendering assertEquals("changed", rendering) } @@ -319,10 +323,11 @@ internal class WorkflowRunnerTest { } } + @Suppress("UNCHECKED_CAST") private fun WorkflowRunner<*, T, *>.runTillNextOutput(): WorkflowOutput? = scope.run { val firstOutputDeferred = async { processAction() } runCurrent() - firstOutputDeferred.getCompleted() + firstOutputDeferred.getCompleted() as? WorkflowOutput? } @Suppress("TestFunctionName")