diff --git a/workflow-runtime/build.gradle.kts b/workflow-runtime/build.gradle.kts index 9df5fffe11..a4ffd5cae8 100644 --- a/workflow-runtime/build.gradle.kts +++ b/workflow-runtime/build.gradle.kts @@ -27,22 +27,24 @@ kotlin { } } } + ios() sourceSets { - val jvmMain by getting { + all { + languageSettings.apply { + optIn("kotlin.RequiresOptIn") + } + } + val commonMain by getting { dependencies { - compileOnly(libs.jetbrains.annotations) - api(project(":workflow-core")) - api(libs.kotlin.jdk6) api(libs.kotlinx.coroutines.core) } } - val jvmTest by getting { + val commonTest by getting { dependencies { - implementation(libs.kotlinx.coroutines.test) + implementation(libs.kotlinx.coroutines.test.common) implementation(libs.kotlin.test.jdk) - implementation(libs.kotlin.reflect) } } } diff --git a/workflow-runtime/src/jvmMain/baseline-prof.txt b/workflow-runtime/src/commonMain/baseline-prof.txt similarity index 100% rename from workflow-runtime/src/jvmMain/baseline-prof.txt rename to workflow-runtime/src/commonMain/baseline-prof.txt diff --git a/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt similarity index 100% rename from workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt diff --git a/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/RenderingAndSnapshot.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderingAndSnapshot.kt similarity index 100% rename from workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/RenderingAndSnapshot.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderingAndSnapshot.kt diff --git a/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/SimpleLoggingWorkflowInterceptor.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/SimpleLoggingWorkflowInterceptor.kt similarity index 98% rename from workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/SimpleLoggingWorkflowInterceptor.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/SimpleLoggingWorkflowInterceptor.kt index eceb2efd96..2b6e624338 100644 --- a/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/SimpleLoggingWorkflowInterceptor.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/SimpleLoggingWorkflowInterceptor.kt @@ -113,7 +113,7 @@ public open class SimpleLoggingWorkflowInterceptor : WorkflowInterceptor { println(text) } - protected open fun logError(text: String): Unit = System.err.println(text) + protected open fun logError(text: String): Unit = println("ERROR: $text") private fun formatLogMessage( name: String, diff --git a/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/TreeSnapshot.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/TreeSnapshot.kt similarity index 100% rename from workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/TreeSnapshot.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/TreeSnapshot.kt diff --git a/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt similarity index 100% rename from workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt diff --git a/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/ActiveStagingList.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/ActiveStagingList.kt similarity index 100% rename from workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/ActiveStagingList.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/ActiveStagingList.kt diff --git a/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/ChainedWorkflowInterceptor.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/ChainedWorkflowInterceptor.kt similarity index 100% rename from workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/ChainedWorkflowInterceptor.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/ChainedWorkflowInterceptor.kt diff --git a/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/IdCounter.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/IdCounter.kt similarity index 100% rename from workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/IdCounter.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/IdCounter.kt diff --git a/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/InlineLinkedList.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/InlineLinkedList.kt similarity index 100% rename from workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/InlineLinkedList.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/InlineLinkedList.kt diff --git a/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/RealRenderContext.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/RealRenderContext.kt similarity index 100% rename from workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/RealRenderContext.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/RealRenderContext.kt diff --git a/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/SideEffectNode.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SideEffectNode.kt similarity index 100% rename from workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/SideEffectNode.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SideEffectNode.kt diff --git a/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/SubtreeManager.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SubtreeManager.kt similarity index 100% rename from workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/SubtreeManager.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SubtreeManager.kt diff --git a/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/Throwables.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/Throwables.kt similarity index 100% rename from workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/Throwables.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/Throwables.kt diff --git a/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/WorkflowChildNode.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowChildNode.kt similarity index 100% rename from workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/WorkflowChildNode.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowChildNode.kt diff --git a/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt similarity index 100% rename from workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt diff --git a/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/WorkflowNodeId.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNodeId.kt similarity index 100% rename from workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/WorkflowNodeId.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNodeId.kt diff --git a/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/WorkflowRunner.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowRunner.kt similarity index 100% rename from workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/WorkflowRunner.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowRunner.kt diff --git a/workflow-runtime/src/jvmTest/java/com/squareup/workflow1/RecordingWorkflowInterceptor.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RecordingWorkflowInterceptor.kt similarity index 100% rename from workflow-runtime/src/jvmTest/java/com/squareup/workflow1/RecordingWorkflowInterceptor.kt rename to workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RecordingWorkflowInterceptor.kt diff --git a/workflow-runtime/src/jvmTest/java/com/squareup/workflow1/RenderWorkflowInTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RenderWorkflowInTest.kt similarity index 73% rename from workflow-runtime/src/jvmTest/java/com/squareup/workflow1/RenderWorkflowInTest.kt rename to workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RenderWorkflowInTest.kt index 6d66c23df8..642165491f 100644 --- a/workflow-runtime/src/jvmTest/java/com/squareup/workflow1/RenderWorkflowInTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RenderWorkflowInTest.kt @@ -2,15 +2,14 @@ package com.squareup.workflow1 import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers.Unconfined import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map @@ -18,11 +17,14 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.produceIn import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import kotlinx.coroutines.plus import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runCurrent import okio.ByteString -import org.junit.After -import org.junit.Test +import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertFalse @@ -34,31 +36,22 @@ import kotlin.test.assertTrue @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) class RenderWorkflowInTest { - // TestCoroutineScope doesn't actually create a Job, so isActive will always return true unless - // explicitly give it a job. - /** - * A [CoroutineScope] that will fail the test if it has uncaught exceptions after the test - * completes. Use this scope to test success cases. + * A [TestScope] that will not run until explicitly told to. */ - private val expectedSuccessScope = TestCoroutineScope(Job()) + private val pausedTestScope = TestScope() /** - * A [TestCoroutineScope] that will _not_ fail the test if it has uncaught exceptions after the - * test completes. Use this scope to test failure cases. + * A [TestScope] that will automatically dispatch enqueued routines. */ - private val allowedToFailScope = TestCoroutineScope(Job()) - - @After fun tearDown() { - expectedSuccessScope.cleanupTestCoroutines() - } + private val testScope = TestScope(UnconfinedTestDispatcher()) @Test fun `initial rendering is calculated synchronously`() { val props = MutableStateFlow("foo") val workflow = Workflow.stateless { "props: $it" } // Don't allow the workflow runtime to actually start. - expectedSuccessScope.pauseDispatcher() - val renderings = renderWorkflowIn(workflow, expectedSuccessScope, props) {} + + val renderings = renderWorkflowIn(workflow, pausedTestScope, props) {} assertEquals("props: foo", renderings.value.rendering) } @@ -66,8 +59,8 @@ class RenderWorkflowInTest { val props = MutableStateFlow("foo") val workflow = Workflow.stateless { "props: $it" } - expectedSuccessScope.cancel() - val renderings = renderWorkflowIn(workflow, expectedSuccessScope, props) {} + pausedTestScope.cancel() + val renderings = renderWorkflowIn(workflow, pausedTestScope, props) {} assertEquals("props: foo", renderings.value.rendering) } @@ -81,10 +74,10 @@ class RenderWorkflowInTest { } } - expectedSuccessScope.cancel() - renderWorkflowIn(workflow, expectedSuccessScope, MutableStateFlow(Unit)) {} + testScope.cancel() + renderWorkflowIn(workflow, testScope, MutableStateFlow(Unit)) {} + testScope.advanceUntilIdle() - expectedSuccessScope.advanceUntilIdle() assertFalse(sideEffectWasRan) } @@ -100,23 +93,22 @@ class RenderWorkflowInTest { renderChild(childWorkflow) } - expectedSuccessScope.cancel() - renderWorkflowIn(workflow, expectedSuccessScope, MutableStateFlow(Unit)) {} + testScope.cancel() + renderWorkflowIn(workflow, testScope, MutableStateFlow(Unit)) {} + testScope.advanceUntilIdle() - expectedSuccessScope.advanceUntilIdle() assertFalse(sideEffectWasRan) } @Test fun `new renderings are emitted on update`() { val props = MutableStateFlow("foo") val workflow = Workflow.stateless { "props: $it" } - val renderings = renderWorkflowIn(workflow, expectedSuccessScope, props) {} + val renderings = renderWorkflowIn(workflow, testScope, props) {} - expectedSuccessScope.advanceUntilIdle() assertEquals("props: foo", renderings.value.rendering) props.value = "bar" - expectedSuccessScope.advanceUntilIdle() + assertEquals("props: bar", renderings.value.rendering) } @@ -136,7 +128,7 @@ class RenderWorkflowInTest { } ) val props = MutableStateFlow(Unit) - val renderings = renderWorkflowIn(workflow, expectedSuccessScope, props) {} + val renderings = renderWorkflowIn(workflow, testScope, props) {} // Interact with the workflow to change the state. renderings.value.rendering.let { (state, updateState) -> @@ -152,7 +144,7 @@ class RenderWorkflowInTest { } // Create a new scope to launch a second runtime to restore. - val restoreScope = TestCoroutineScope() + val restoreScope = TestScope() val restoredRenderings = renderWorkflowIn(workflow, restoreScope, props, initialSnapshot = snapshot) {} assertEquals("updated state", restoredRenderings.value.rendering.first) @@ -177,7 +169,7 @@ class RenderWorkflowInTest { } ) val props = MutableStateFlow(Unit) - val renderings = renderWorkflowIn(workflow, expectedSuccessScope, props) {} + val renderings = renderWorkflowIn(workflow, testScope, props) {} val emitted = mutableListOf>() val scope = CoroutineScope(Unconfined) @@ -204,17 +196,15 @@ class RenderWorkflowInTest { ) { action { setOutput(it) } } } val receivedOutputs = mutableListOf() - renderWorkflowIn( - workflow, expectedSuccessScope, MutableStateFlow(Unit) - ) { receivedOutputs += it } + renderWorkflowIn(workflow, testScope, MutableStateFlow(Unit)) { + receivedOutputs += it + } assertTrue(receivedOutputs.isEmpty()) trigger.trySend("foo").isSuccess - expectedSuccessScope.advanceUntilIdle() assertEquals(listOf("foo"), receivedOutputs) trigger.trySend("bar").isSuccess - expectedSuccessScope.advanceUntilIdle() assertEquals(listOf("foo", "bar"), receivedOutputs) } @@ -222,17 +212,15 @@ class RenderWorkflowInTest { val workflow = Workflow.stateless { props -> props } var onOutputCalls = 0 val props = MutableStateFlow(0) - val renderings = renderWorkflowIn(workflow, expectedSuccessScope, props) { onOutputCalls++ } + val renderings = renderWorkflowIn(workflow, testScope, props) { onOutputCalls++ } assertEquals(0, renderings.value.rendering) assertEquals(0, onOutputCalls) props.value = 1 - expectedSuccessScope.advanceUntilIdle() assertEquals(1, renderings.value.rendering) assertEquals(0, onOutputCalls) props.value = 2 - expectedSuccessScope.advanceUntilIdle() assertEquals(2, renderings.value.rendering) assertEquals(0, onOutputCalls) } @@ -246,12 +234,10 @@ class RenderWorkflowInTest { val workflow = Workflow.stateless { throw ExpectedException() } - expectedSuccessScope.pauseDispatcher() assertFailsWith { - renderWorkflowIn(workflow, expectedSuccessScope, MutableStateFlow(Unit)) {} + renderWorkflowIn(workflow, testScope, MutableStateFlow(Unit)) {} } - expectedSuccessScope.advanceUntilIdle() - assertTrue(expectedSuccessScope.isActive) + assertTrue(testScope.isActive) } @Test @@ -265,9 +251,8 @@ class RenderWorkflowInTest { } assertFailsWith { - renderWorkflowIn(workflow, expectedSuccessScope, MutableStateFlow(Unit)) {} + renderWorkflowIn(workflow, testScope, MutableStateFlow(Unit)) {} } - expectedSuccessScope.advanceUntilIdle() assertFalse(sideEffectWasRan) } @@ -289,9 +274,8 @@ class RenderWorkflowInTest { } assertFailsWith { - renderWorkflowIn(workflow, expectedSuccessScope, MutableStateFlow(Unit)) {} + renderWorkflowIn(workflow, testScope, MutableStateFlow(Unit)) {} } - expectedSuccessScope.advanceUntilIdle() assertTrue(sideEffectWasRan) assertNotNull(cancellationException) val realCause = generateSequence(cancellationException) { it.cause } @@ -313,9 +297,8 @@ class RenderWorkflowInTest { } assertFailsWith { - renderWorkflowIn(workflow, expectedSuccessScope, MutableStateFlow(Unit)) {} + renderWorkflowIn(workflow, testScope, MutableStateFlow(Unit)) {} } - expectedSuccessScope.advanceUntilIdle() assertFalse(sideEffectWasRan) } @@ -331,14 +314,12 @@ class RenderWorkflowInTest { } } ) - renderWorkflowIn(workflow, allowedToFailScope, MutableStateFlow(Unit)) {} + renderWorkflowIn(workflow, testScope, MutableStateFlow(Unit)) {} - allowedToFailScope.advanceUntilIdle() - assertTrue(allowedToFailScope.isActive) + assertTrue(testScope.isActive) trigger.complete(Unit) - allowedToFailScope.advanceUntilIdle() - assertFalse(allowedToFailScope.isActive) + assertFalse(testScope.isActive) } @Test fun `exception from action fails parent scope`() { @@ -351,14 +332,12 @@ class RenderWorkflowInTest { } } } - renderWorkflowIn(workflow, allowedToFailScope, MutableStateFlow(Unit)) {} + renderWorkflowIn(workflow, testScope, MutableStateFlow(Unit)) {} - allowedToFailScope.advanceUntilIdle() - assertTrue(allowedToFailScope.isActive) + assertTrue(testScope.isActive) trigger.complete(Unit) - allowedToFailScope.advanceUntilIdle() - assertFalse(allowedToFailScope.isActive) + assertFalse(testScope.isActive) } @Test fun `cancelling scope cancels runtime`() { @@ -370,12 +349,11 @@ class RenderWorkflowInTest { } } } - renderWorkflowIn(workflow, expectedSuccessScope, MutableStateFlow(Unit)) {} + renderWorkflowIn(workflow, testScope, MutableStateFlow(Unit)) {} assertNull(cancellationException) - assertTrue(expectedSuccessScope.isActive) + assertTrue(testScope.isActive) - expectedSuccessScope.cancel() - expectedSuccessScope.advanceUntilIdle() + testScope.cancel() assertTrue(cancellationException is CancellationException) assertNull(cancellationException!!.cause) } @@ -387,17 +365,17 @@ class RenderWorkflowInTest { renderCount++ runningWorker(Worker.from { trigger.await() }) { action { - expectedSuccessScope.cancel() + testScope.cancel() } } } - renderWorkflowIn(workflow, expectedSuccessScope, MutableStateFlow(Unit)) {} - assertTrue(expectedSuccessScope.isActive) + renderWorkflowIn(workflow, testScope, MutableStateFlow(Unit)) {} + assertTrue(testScope.isActive) assertTrue(renderCount == 1) trigger.complete(Unit) - expectedSuccessScope.advanceUntilIdle() - assertFalse(expectedSuccessScope.isActive) + testScope.advanceUntilIdle() + assertFalse(testScope.isActive) assertEquals(1, renderCount, "Should not render after CoroutineScope is canceled.") } @@ -410,29 +388,26 @@ class RenderWorkflowInTest { } } } - renderWorkflowIn(workflow, expectedSuccessScope, MutableStateFlow(Unit)) {} + renderWorkflowIn(workflow, testScope, MutableStateFlow(Unit)) {} assertNull(cancellationException) - assertTrue(expectedSuccessScope.isActive) + assertTrue(testScope.isActive) - expectedSuccessScope.cancel(CancellationException("fail!", ExpectedException())) - expectedSuccessScope.advanceUntilIdle() + testScope.cancel(CancellationException("fail!", ExpectedException())) assertTrue(cancellationException is CancellationException) assertTrue(cancellationException!!.cause is ExpectedException) } @Test fun `error from renderings collector doesn't fail parent scope`() { val workflow = Workflow.stateless {} - val renderings = renderWorkflowIn(workflow, expectedSuccessScope, MutableStateFlow(Unit)) {} + val renderings = renderWorkflowIn(workflow, testScope, MutableStateFlow(Unit)) {} // Collect in separate scope so we actually test that the parent scope is failed when it's // different from the collecting scope. - val collectScope = CoroutineScope(Unconfined) + val collectScope = TestScope(UnconfinedTestDispatcher()) collectScope.launch { renderings.collect { throw ExpectedException() } } - - expectedSuccessScope.advanceUntilIdle() - assertTrue(expectedSuccessScope.isActive) + assertTrue(testScope.isActive) assertFalse(collectScope.isActive) } @@ -447,15 +422,14 @@ class RenderWorkflowInTest { } } } - val renderings = renderWorkflowIn(workflow, allowedToFailScope, MutableStateFlow(Unit)) {} + val renderings = renderWorkflowIn(workflow, pausedTestScope, MutableStateFlow(Unit)) {} - allowedToFailScope.pauseDispatcher() - allowedToFailScope.launch { + pausedTestScope.launch { renderings.collect { throw ExpectedException() } } assertNull(cancellationException) - allowedToFailScope.advanceUntilIdle() + pausedTestScope.advanceUntilIdle() assertTrue(cancellationException is CancellationException) assertTrue(cancellationException!!.cause is ExpectedException) } @@ -466,18 +440,16 @@ class RenderWorkflowInTest { val workflow = Workflow.stateless { runningWorker(Worker.from { trigger.await() }) { action { setOutput(Unit) } } } - renderWorkflowIn(workflow, allowedToFailScope, MutableStateFlow(Unit)) { + renderWorkflowIn(workflow, pausedTestScope, MutableStateFlow(Unit)) { throw ExpectedException() } - assertTrue(allowedToFailScope.isActive) + assertTrue(pausedTestScope.isActive) - allowedToFailScope.pauseDispatcher() trigger.complete(Unit) - assertTrue(allowedToFailScope.isActive) + assertTrue(pausedTestScope.isActive) - allowedToFailScope.resumeDispatcher() - allowedToFailScope.advanceUntilIdle() - assertFalse(allowedToFailScope.isActive) + pausedTestScope.advanceUntilIdle() + assertFalse(pausedTestScope.isActive) } @Test fun `output is emitted before next render pass`() { @@ -496,19 +468,22 @@ class RenderWorkflowInTest { } ) val events = mutableListOf() + renderWorkflowIn( - workflow, expectedSuccessScope, MutableStateFlow(Unit) - ) { events += "output($it)" } + workflow, pausedTestScope, MutableStateFlow(Unit), onOutput = { events += "output($it)" } + ) .onEach { events += "rendering(${it.rendering})" } - .launchIn(expectedSuccessScope) + .launchIn(pausedTestScope) + pausedTestScope.runCurrent() assertEquals(listOf("rendering({no output})"), events) outputTrigger.complete("output") + pausedTestScope.runCurrent() assertEquals( listOf( "rendering({no output})", + "output(output)", "rendering(output)", - "output(output)" ), events ) @@ -526,17 +501,19 @@ class RenderWorkflowInTest { render = { _, _ -> } ) val props = MutableStateFlow(0) - val snapshot = renderWorkflowIn(workflow, expectedSuccessScope, props) {} + val uncaughtExceptions = mutableListOf() + val exceptionHandler = CoroutineExceptionHandler { _, throwable -> + uncaughtExceptions += throwable + } + val snapshot = renderWorkflowIn(workflow, testScope + exceptionHandler, props) {} .value .snapshot assertFailsWith { snapshot.toByteString() } - expectedSuccessScope.advanceUntilIdle() - assertTrue(expectedSuccessScope.uncaughtExceptions.isEmpty()) + assertTrue(uncaughtExceptions.isEmpty()) props.value += 1 assertFailsWith { snapshot.toByteString() } - expectedSuccessScope.advanceUntilIdle() } // https://github.com/square/workflow-kotlin/issues/224 @@ -552,18 +529,20 @@ class RenderWorkflowInTest { FailRendering(props) } val props = MutableStateFlow(0) - val ras = renderWorkflowIn(workflow, expectedSuccessScope, props) {} + val uncaughtExceptions = mutableListOf() + val exceptionHandler = CoroutineExceptionHandler { _, throwable -> + uncaughtExceptions += throwable + } + val ras = renderWorkflowIn(workflow, testScope + exceptionHandler, props) {} val renderings = ras.map { it.rendering } - .produceIn(expectedSuccessScope) + .produceIn(testScope) @Suppress("UnusedEquals") assertFailsWith { renderings.tryReceive().getOrNull()!!.equals(Unit) } - expectedSuccessScope.advanceUntilIdle() - assertTrue(expectedSuccessScope.uncaughtExceptions.isEmpty()) + assertTrue(uncaughtExceptions.isEmpty()) // Trigger another render pass. props.value += 1 - expectedSuccessScope.advanceUntilIdle() } // https://github.com/square/workflow-kotlin/issues/224 @@ -579,19 +558,21 @@ class RenderWorkflowInTest { FailRendering(props) } val props = MutableStateFlow(0) - val ras = renderWorkflowIn(workflow, expectedSuccessScope, props) {} + val uncaughtExceptions = mutableListOf() + val exceptionHandler = CoroutineExceptionHandler { _, throwable -> + uncaughtExceptions += throwable + } + val ras = renderWorkflowIn(workflow, testScope + exceptionHandler, props) {} val renderings = ras.map { it.rendering } - .produceIn(expectedSuccessScope) + .produceIn(testScope) @Suppress("UnusedEquals") assertFailsWith { renderings.tryReceive().getOrNull().hashCode() } - expectedSuccessScope.advanceUntilIdle() - assertTrue(expectedSuccessScope.uncaughtExceptions.isEmpty()) + assertTrue(uncaughtExceptions.isEmpty()) props.value += 1 @Suppress("UnusedEquals") assertFailsWith { renderings.tryReceive().getOrNull().hashCode() } - expectedSuccessScope.advanceUntilIdle() } private class ExpectedException : RuntimeException() diff --git a/workflow-runtime/src/jvmTest/java/com/squareup/workflow1/RenderingAndSnapshotTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RenderingAndSnapshotTest.kt similarity index 96% rename from workflow-runtime/src/jvmTest/java/com/squareup/workflow1/RenderingAndSnapshotTest.kt rename to workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RenderingAndSnapshotTest.kt index b307b96a0b..d02132989b 100644 --- a/workflow-runtime/src/jvmTest/java/com/squareup/workflow1/RenderingAndSnapshotTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RenderingAndSnapshotTest.kt @@ -1,6 +1,6 @@ package com.squareup.workflow1 -import org.junit.Test +import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotEquals import kotlin.test.assertSame diff --git a/workflow-runtime/src/jvmTest/java/com/squareup/workflow1/SimpleLoggingWorkflowInterceptorTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/SimpleLoggingWorkflowInterceptorTest.kt similarity index 100% rename from workflow-runtime/src/jvmTest/java/com/squareup/workflow1/SimpleLoggingWorkflowInterceptorTest.kt rename to workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/SimpleLoggingWorkflowInterceptorTest.kt diff --git a/workflow-runtime/src/jvmTest/java/com/squareup/workflow1/TreeSnapshotTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/TreeSnapshotTest.kt similarity index 99% rename from workflow-runtime/src/jvmTest/java/com/squareup/workflow1/TreeSnapshotTest.kt rename to workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/TreeSnapshotTest.kt index 0341fedeec..5f56255224 100644 --- a/workflow-runtime/src/jvmTest/java/com/squareup/workflow1/TreeSnapshotTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/TreeSnapshotTest.kt @@ -3,8 +3,8 @@ package com.squareup.workflow1 import com.squareup.workflow1.internal.WorkflowNodeId import com.squareup.workflow1.internal.id import okio.ByteString -import org.junit.Test import kotlin.reflect.typeOf +import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull import kotlin.test.assertTrue diff --git a/workflow-runtime/src/jvmTest/java/com/squareup/workflow1/WorkflowInterceptorTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/WorkflowInterceptorTest.kt similarity index 93% rename from workflow-runtime/src/jvmTest/java/com/squareup/workflow1/WorkflowInterceptorTest.kt rename to workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/WorkflowInterceptorTest.kt index fd88900a98..f53335429c 100644 --- a/workflow-runtime/src/jvmTest/java/com/squareup/workflow1/WorkflowInterceptorTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/WorkflowInterceptorTest.kt @@ -5,8 +5,10 @@ package com.squareup.workflow1 import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestScope import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext.Key import kotlin.test.Test @@ -16,9 +18,10 @@ import kotlin.test.assertSame import kotlin.test.assertTrue import kotlin.test.fail +@OptIn(ExperimentalCoroutinesApi::class) internal class WorkflowInterceptorTest { - @Test fun `intercept() returns workflow when Noop`() { + @Test fun `intercept returns workflow when Noop`() { val interceptor = NoopWorkflowInterceptor val workflow = Workflow.rendering("hello") .asStatefulWorkflow() @@ -26,7 +29,7 @@ internal class WorkflowInterceptorTest { assertSame(workflow, intercepted) } - @Test fun `intercept() intercepts calls to initialState()`() { + @Test fun `intercept intercepts calls to initialState`() { val recorder = RecordingWorkflowInterceptor() val intercepted = recorder.intercept(TestWorkflow, TestWorkflow.session) @@ -36,7 +39,7 @@ internal class WorkflowInterceptorTest { assertEquals(listOf("BEGIN|onInitialState", "END|onInitialState"), recorder.consumeEventNames()) } - @Test fun `intercept() intercepts calls to onPropsChanged()`() { + @Test fun `intercept intercepts calls to onPropsChanged`() { val recorder = RecordingWorkflowInterceptor() val intercepted = recorder.intercept(TestWorkflow, TestWorkflow.session) @@ -46,7 +49,7 @@ internal class WorkflowInterceptorTest { assertEquals(listOf("BEGIN|onPropsChanged", "END|onPropsChanged"), recorder.consumeEventNames()) } - @Test fun `intercept() intercepts calls to render()`() { + @Test fun `intercept intercepts calls to render`() { val recorder = RecordingWorkflowInterceptor() val intercepted = recorder.intercept(TestWorkflow, TestWorkflow.session) val fakeContext = object : BaseRenderContext { @@ -71,7 +74,7 @@ internal class WorkflowInterceptorTest { assertEquals(listOf("BEGIN|onRender", "END|onRender"), recorder.consumeEventNames()) } - @Test fun `intercept() intercepts calls to snapshotState()`() { + @Test fun `intercept intercepts calls to snapshotState`() { val recorder = RecordingWorkflowInterceptor() val intercepted = recorder.intercept(TestWorkflow, TestWorkflow.session) @@ -83,7 +86,7 @@ internal class WorkflowInterceptorTest { ) } - @Test fun `intercept() intercepts calls to actionSink send`() { + @Test fun `intercept intercepts calls to actionSink send`() { val recorder = RecordingWorkflowInterceptor() val intercepted = recorder.intercept(TestActionWorkflow, TestActionWorkflow.session) val actions = mutableListOf>() @@ -118,7 +121,7 @@ internal class WorkflowInterceptorTest { ) } - @Test fun `intercept() intercepts side effects`() { + @Test fun `intercept intercepts side effects`() { val recorder = RecordingWorkflowInterceptor() val workflow = TestSideEffectWorkflow() val intercepted = recorder.intercept(workflow, workflow.session) @@ -153,7 +156,7 @@ internal class WorkflowInterceptorTest { ) } - @Test fun `intercept() uses interceptor's context for side effect`() { + @Test fun `intercept uses interceptor's context for side effect`() { val recorder = object : RecordingWorkflowInterceptor() { override fun onRender( renderProps: P, @@ -170,7 +173,7 @@ internal class WorkflowInterceptorTest { sideEffect: suspend () -> Unit, proceed: (key: String, sideEffect: suspend () -> Unit) -> Unit ) { - CoroutineScope(TestElement).launch { + TestScope(TestElement).launch { proceed(key, sideEffect) } } diff --git a/workflow-runtime/src/jvmTest/java/com/squareup/workflow1/WorkflowOperatorsTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/WorkflowOperatorsTest.kt similarity index 93% rename from workflow-runtime/src/jvmTest/java/com/squareup/workflow1/WorkflowOperatorsTest.kt rename to workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/WorkflowOperatorsTest.kt index f3875e58b2..62a395e015 100644 --- a/workflow-runtime/src/jvmTest/java/com/squareup/workflow1/WorkflowOperatorsTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/WorkflowOperatorsTest.kt @@ -10,7 +10,8 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.plus -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.fail @@ -36,7 +37,7 @@ class WorkflowOperatorsTest { val childWorkflow = object : StateFlowWorkflow("child", trigger) {} val mappedWorkflow = childWorkflow.mapRendering { "mapped: $it" } - runBlockingTest { + runTest(UnconfinedTestDispatcher()) { val renderings = mutableListOf() val workflowJob = Job(coroutineContext[Job]) renderWorkflowIn(mappedWorkflow, this + workflowJob, MutableStateFlow(Unit)) {} @@ -45,11 +46,9 @@ class WorkflowOperatorsTest { assertEquals(listOf("mapped: initial"), renderings) trigger.value = "foo" - advanceUntilIdle() assertEquals(listOf("mapped: initial", "mapped: foo"), renderings) trigger.value = "bar" - advanceUntilIdle() assertEquals(listOf("mapped: initial", "mapped: foo", "mapped: bar"), renderings) workflowJob.cancel() @@ -68,7 +67,7 @@ class WorkflowOperatorsTest { ).toString() } - runBlockingTest { + runTest(UnconfinedTestDispatcher()) { val renderings = mutableListOf() val workflowJob = Job(coroutineContext[Job]) renderWorkflowIn(parentWorkflow, this + workflowJob, MutableStateFlow(Unit)) {} @@ -82,7 +81,6 @@ class WorkflowOperatorsTest { ) trigger1.value = "foo" - advanceUntilIdle() assertEquals( listOf( "[rendering1: initial1, rendering2: initial2]", @@ -92,7 +90,6 @@ class WorkflowOperatorsTest { ) trigger2.value = "bar" - advanceUntilIdle() assertEquals( listOf( "[rendering1: initial1, rendering2: initial2]", @@ -118,7 +115,7 @@ class WorkflowOperatorsTest { ).toString() } - runBlockingTest { + runTest(UnconfinedTestDispatcher()) { val renderings = mutableListOf() val workflowJob = Job(coroutineContext[Job]) renderWorkflowIn(parentWorkflow, this + workflowJob, MutableStateFlow(Unit)) {} @@ -132,7 +129,6 @@ class WorkflowOperatorsTest { ) trigger1.value = "foo" - advanceUntilIdle() assertEquals( listOf( "[initial1, rendering2: initial2]", @@ -142,7 +138,6 @@ class WorkflowOperatorsTest { ) trigger2.value = "bar" - advanceUntilIdle() assertEquals( listOf( "[initial1, rendering2: initial2]", @@ -156,7 +151,8 @@ class WorkflowOperatorsTest { } } - @Test fun `mapRendering with same upstream workflow in two different passes doesn't restart`() { + @Test + fun `mapRendering with same upstream workflow in two different passes doesn't restart`() { val trigger = MutableStateFlow("initial") val childWorkflow = object : StateFlowWorkflow("child", trigger) {} val parentWorkflow = Workflow.stateless { props -> @@ -168,7 +164,7 @@ class WorkflowOperatorsTest { } val props = MutableStateFlow(0) - runBlockingTest { + runTest(UnconfinedTestDispatcher()) { val renderings = mutableListOf() val workflowJob = Job(coroutineContext[Job]) renderWorkflowIn(parentWorkflow, this + workflowJob, props) {} @@ -183,7 +179,6 @@ class WorkflowOperatorsTest { assertEquals(1, childWorkflow.starts) trigger.value = "foo" - advanceUntilIdle() assertEquals(1, childWorkflow.starts) assertEquals( listOf( @@ -195,7 +190,6 @@ class WorkflowOperatorsTest { props.value = 1 trigger.value = "bar" - advanceUntilIdle() assertEquals(1, childWorkflow.starts) assertEquals( listOf( diff --git a/workflow-runtime/src/jvmTest/java/com/squareup/workflow1/internal/ActiveStagingListTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/ActiveStagingListTest.kt similarity index 100% rename from workflow-runtime/src/jvmTest/java/com/squareup/workflow1/internal/ActiveStagingListTest.kt rename to workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/ActiveStagingListTest.kt diff --git a/workflow-runtime/src/jvmTest/java/com/squareup/workflow1/internal/ChainedWorkflowInterceptorTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/ChainedWorkflowInterceptorTest.kt similarity index 97% rename from workflow-runtime/src/jvmTest/java/com/squareup/workflow1/internal/ChainedWorkflowInterceptorTest.kt rename to workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/ChainedWorkflowInterceptorTest.kt index 1b4d9403d8..896f775703 100644 --- a/workflow-runtime/src/jvmTest/java/com/squareup/workflow1/internal/ChainedWorkflowInterceptorTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/ChainedWorkflowInterceptorTest.kt @@ -18,8 +18,7 @@ import com.squareup.workflow1.rendering import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel -import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertSame @@ -80,11 +79,10 @@ internal class ChainedWorkflowInterceptorTest { } } val chained = listOf(interceptor1, interceptor2).chained() - val scope = TestCoroutineScope(Job()) - chained.onSessionStarted(scope, TestSession) - scope.advanceUntilIdle() - scope.cancel() + runTest { + chained.onSessionStarted(this, TestSession) + } assertEquals(listOf("started1", "started2", "cancelled1", "cancelled2"), events) } diff --git a/workflow-runtime/src/jvmTest/java/com/squareup/workflow1/internal/InlineLinkedListTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/InlineLinkedListTest.kt similarity index 100% rename from workflow-runtime/src/jvmTest/java/com/squareup/workflow1/internal/InlineLinkedListTest.kt rename to workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/InlineLinkedListTest.kt diff --git a/workflow-runtime/src/jvmTest/java/com/squareup/workflow1/internal/RealRenderContextTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/RealRenderContextTest.kt similarity index 100% rename from workflow-runtime/src/jvmTest/java/com/squareup/workflow1/internal/RealRenderContextTest.kt rename to workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/RealRenderContextTest.kt diff --git a/workflow-runtime/src/jvmTest/java/com/squareup/workflow1/internal/SubtreeManagerTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/SubtreeManagerTest.kt similarity index 98% rename from workflow-runtime/src/jvmTest/java/com/squareup/workflow1/internal/SubtreeManagerTest.kt rename to workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/SubtreeManagerTest.kt index f45053fd04..f50cbbc31c 100644 --- a/workflow-runtime/src/jvmTest/java/com/squareup/workflow1/internal/SubtreeManagerTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/SubtreeManagerTest.kt @@ -9,8 +9,8 @@ import com.squareup.workflow1.WorkflowAction import com.squareup.workflow1.WorkflowOutput import com.squareup.workflow1.action import com.squareup.workflow1.applyTo +import com.squareup.workflow1.identifier import com.squareup.workflow1.internal.SubtreeManagerTest.TestWorkflow.Rendering -import com.squareup.workflow1.workflowIdentifier import kotlinx.coroutines.Dispatchers.Unconfined import kotlinx.coroutines.async import kotlinx.coroutines.runBlocking @@ -134,7 +134,7 @@ internal class SubtreeManagerTest { manager.render(workflow, "props", "foo", handler = { fail() }) } assertEquals( - "Expected keys to be unique for ${TestWorkflow::class.workflowIdentifier}: key=\"foo\"", + "Expected keys to be unique for ${workflow.identifier}: key=\"foo\"", error.message ) } diff --git a/workflow-runtime/src/jvmTest/java/com/squareup/workflow1/internal/WorkflowNodeTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowNodeTest.kt similarity index 99% rename from workflow-runtime/src/jvmTest/java/com/squareup/workflow1/internal/WorkflowNodeTest.kt rename to workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowNodeTest.kt index 59cb61e2cb..f2da7651e5 100644 --- a/workflow-runtime/src/jvmTest/java/com/squareup/workflow1/internal/WorkflowNodeTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowNodeTest.kt @@ -1,4 +1,4 @@ -@file:Suppress("EXPERIMENTAL_API_USAGE") +@file:Suppress("EXPERIMENTAL_API_USAGE", "DEPRECATION") package com.squareup.workflow1.internal diff --git a/workflow-runtime/src/jvmTest/java/com/squareup/workflow1/internal/WorkflowRunnerTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowRunnerTest.kt similarity index 87% rename from workflow-runtime/src/jvmTest/java/com/squareup/workflow1/internal/WorkflowRunnerTest.kt rename to workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowRunnerTest.kt index de901602c0..ef64ca080c 100644 --- a/workflow-runtime/src/jvmTest/java/com/squareup/workflow1/internal/WorkflowRunnerTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowRunnerTest.kt @@ -3,22 +3,23 @@ package com.squareup.workflow1.internal import com.squareup.workflow1.NoopWorkflowInterceptor import com.squareup.workflow1.Worker import com.squareup.workflow1.Workflow +import com.squareup.workflow1.WorkflowOutput import com.squareup.workflow1.action import com.squareup.workflow1.runningWorker import com.squareup.workflow1.stateful import com.squareup.workflow1.stateless import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.test.TestCoroutineDispatcher -import org.junit.Test +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 @@ -27,12 +28,7 @@ import kotlin.test.assertTrue @OptIn(ExperimentalCoroutinesApi::class) internal class WorkflowRunnerTest { - private val dispatcher = TestCoroutineDispatcher() - private val scope = CoroutineScope(dispatcher) - - @BeforeTest fun setUp() { - dispatcher.pauseDispatcher() - } + private val scope = TestScope() @AfterTest fun tearDown() { scope.cancel() @@ -57,10 +53,11 @@ internal class WorkflowRunnerTest { val props = MutableStateFlow("initial") val runner = WorkflowRunner(workflow, props) runner.nextRendering() - val output = scope.async { runner.nextOutput() } - dispatcher.resumeDispatcher() - assertTrue(output.isActive) + val outputDeferred = scope.async { runner.nextOutput() } + + scope.runCurrent() + assertTrue(outputDeferred.isActive) } @Test fun `initial nextOutput() handles props changed after initialization`() { @@ -79,7 +76,7 @@ internal class WorkflowRunnerTest { assertTrue(output.isActive) // Resume the dispatcher to start the coroutines and process the new props value. - dispatcher.resumeDispatcher() + scope.runCurrent() assertTrue(output.isCompleted) assertNull(output.getCompleted()) @@ -101,13 +98,11 @@ internal class WorkflowRunnerTest { } ) val runner = WorkflowRunner(workflow, MutableStateFlow(Unit)) - dispatcher.resumeDispatcher() val initialRendering = runner.nextRendering().rendering assertEquals("initial", initialRendering) - val output = scope.async { runner.nextOutput() } - .getCompleted() + val output = runner.runTillNextOutput() assertEquals("output: work", output?.value) val updatedRendering = runner.nextRendering().rendering @@ -133,19 +128,15 @@ internal class WorkflowRunnerTest { val initialRendering = runner.nextRendering().rendering assertEquals("initial props|initial state(initial props)", initialRendering) - dispatcher.resumeDispatcher() - // The order in which props update and workflow update are processed is deterministic, based // on the order they appear in the select block in nextOutput. - val firstOutput = scope.async { runner.nextOutput() } - .getCompleted() + val firstOutput = runner.runTillNextOutput() // First update will be props, so no output value. assertNull(firstOutput) val secondRendering = runner.nextRendering().rendering assertEquals("changed props|initial state(initial props)", secondRendering) - val secondOutput = scope.async { runner.nextOutput() } - .getCompleted() + val secondOutput = runner.runTillNextOutput() assertEquals("output: work", secondOutput?.value) val thirdRendering = runner.nextRendering().rendering assertEquals("changed props|state: work", thirdRendering) @@ -156,14 +147,14 @@ internal class WorkflowRunnerTest { val runner = WorkflowRunner(workflow, MutableStateFlow(Unit)) runner.nextRendering() val output = scope.async { runner.nextOutput() } - dispatcher.resumeDispatcher() + scope.runCurrent() assertTrue(output.isActive) // nextOutput is run on the scope passed to the runner, so it shouldn't be affected by this // call. runner.cancelRuntime() - dispatcher.advanceUntilIdle() + scope.advanceUntilIdle() assertTrue(output.isActive) } @@ -178,12 +169,12 @@ internal class WorkflowRunnerTest { } val runner = WorkflowRunner(workflow, MutableStateFlow(Unit)) runner.nextRendering() - dispatcher.resumeDispatcher() + scope.runCurrent() assertNull(cancellationException) runner.cancelRuntime() - dispatcher.advanceUntilIdle() + scope.advanceUntilIdle() assertNotNull(cancellationException) val causes = generateSequence(cancellationException) { it.cause } assertTrue(causes.all { it is CancellationException }) @@ -194,12 +185,12 @@ internal class WorkflowRunnerTest { val runner = WorkflowRunner(workflow, MutableStateFlow(Unit)) runner.nextRendering() val output = scope.async { runner.nextOutput() } - dispatcher.resumeDispatcher() + scope.runCurrent() assertTrue(output.isActive) scope.cancel("foo") - dispatcher.advanceUntilIdle() + scope.advanceUntilIdle() assertTrue(output.isCancelled) val realCause = output.getCompletionExceptionOrNull() assertEquals("foo", realCause?.message) @@ -217,18 +208,24 @@ internal class WorkflowRunnerTest { val runner = WorkflowRunner(workflow, MutableStateFlow(Unit)) runner.nextRendering() val output = scope.async { runner.nextOutput() } - dispatcher.resumeDispatcher() + scope.runCurrent() assertTrue(output.isActive) assertNull(cancellationException) scope.cancel("foo") - dispatcher.advanceUntilIdle() + scope.advanceUntilIdle() assertTrue(output.isCancelled) assertNotNull(cancellationException) assertEquals("foo", cancellationException!!.message) } + private fun WorkflowRunner<*, T, *>.runTillNextOutput(): WorkflowOutput? = scope.run { + val firstOutputDeferred = async { nextOutput() } + runCurrent() + firstOutputDeferred.getCompleted() + } + @Suppress("TestFunctionName") private fun WorkflowRunner( workflow: Workflow,