Skip to content

Commit 3c7e88d

Browse files
Merge pull request #1481 from square/sedwards/deprecated-launch-scheduler-mode
Add DeprecatedLaunchSchedulerMode for phased migration of launchForTesting* APIs
2 parents 9ad608a + 6f5e930 commit 3c7e88d

5 files changed

Lines changed: 200 additions & 7 deletions

File tree

workflow-testing/api/workflow-testing.api

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
public final class com/squareup/workflow1/testing/DeprecatedLaunchSchedulerMode : java/lang/Enum {
2+
public static final field LEGACY_UNCONFINED Lcom/squareup/workflow1/testing/DeprecatedLaunchSchedulerMode;
3+
public static final field VIRTUAL_TIME_STANDARD Lcom/squareup/workflow1/testing/DeprecatedLaunchSchedulerMode;
4+
public static fun getEntries ()Lkotlin/enums/EnumEntries;
5+
public static fun valueOf (Ljava/lang/String;)Lcom/squareup/workflow1/testing/DeprecatedLaunchSchedulerMode;
6+
public static fun values ()[Lcom/squareup/workflow1/testing/DeprecatedLaunchSchedulerMode;
7+
}
8+
19
public final class com/squareup/workflow1/testing/RenderIdempotencyChecker : com/squareup/workflow1/WorkflowInterceptor {
210
public static final field INSTANCE Lcom/squareup/workflow1/testing/RenderIdempotencyChecker;
311
public fun onInitialState (Ljava/lang/Object;Lcom/squareup/workflow1/Snapshot;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
@@ -131,9 +139,10 @@ public final class com/squareup/workflow1/testing/WorkerTesterKt {
131139

132140
public final class com/squareup/workflow1/testing/WorkflowTestParams {
133141
public fun <init> ()V
134-
public fun <init> (Lcom/squareup/workflow1/testing/WorkflowTestParams$StartMode;ZLjava/util/Set;)V
135-
public synthetic fun <init> (Lcom/squareup/workflow1/testing/WorkflowTestParams$StartMode;ZLjava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
142+
public fun <init> (Lcom/squareup/workflow1/testing/WorkflowTestParams$StartMode;ZLjava/util/Set;Lcom/squareup/workflow1/testing/DeprecatedLaunchSchedulerMode;)V
143+
public synthetic fun <init> (Lcom/squareup/workflow1/testing/WorkflowTestParams$StartMode;ZLjava/util/Set;Lcom/squareup/workflow1/testing/DeprecatedLaunchSchedulerMode;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
136144
public final fun getCheckRenderIdempotence ()Z
145+
public final fun getDeprecatedLaunchSchedulerMode ()Lcom/squareup/workflow1/testing/DeprecatedLaunchSchedulerMode;
137146
public final fun getRuntimeConfig ()Ljava/util/Set;
138147
public final fun getStartFrom ()Lcom/squareup/workflow1/testing/WorkflowTestParams$StartMode;
139148
}

workflow-testing/src/main/java/com/squareup/workflow1/testing/WorkflowTestParams.kt

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,33 @@ import com.squareup.workflow1.testing.WorkflowTestParams.StartMode.StartFromWork
1313
import kotlinx.coroutines.CoroutineScope
1414
import org.jetbrains.annotations.TestOnly
1515

16+
/**
17+
* Controls the coroutine dispatcher used by the deprecated `launchForTesting*` helpers.
18+
*
19+
* This exists to allow a phased migration away from the deprecated APIs. The default
20+
* [LEGACY_UNCONFINED] preserves the pre-1.25.1 behavior where coroutines dispatched immediately,
21+
* so existing tests continue to pass without changes. [VIRTUAL_TIME_STANDARD] opts in to the
22+
* newer [kotlinx.coroutines.test.StandardTestDispatcher] behavior where coroutines must be
23+
* explicitly advanced.
24+
*
25+
* This enum only affects the deprecated `launchForTesting*` functions. The recommended
26+
* `renderForTest` API already accepts a `coroutineContext` parameter directly.
27+
*/
28+
public enum class DeprecatedLaunchSchedulerMode {
29+
/**
30+
* Uses [kotlinx.coroutines.test.UnconfinedTestDispatcher] — coroutines dispatch immediately.
31+
* This matches the behavior of the deprecated APIs before the 1.25.1 migration to
32+
* [kotlinx.coroutines.test.StandardTestDispatcher].
33+
*/
34+
LEGACY_UNCONFINED,
35+
36+
/**
37+
* Uses [kotlinx.coroutines.test.StandardTestDispatcher] — coroutines require explicit
38+
* advancement via `advanceUntilSettled()` or similar scheduler control.
39+
*/
40+
VIRTUAL_TIME_STANDARD
41+
}
42+
1643
/**
1744
* Defines configuration for workflow testing infrastructure such as `testRender`, `testFromStart`.
1845
* and `test`.
@@ -27,12 +54,20 @@ import org.jetbrains.annotations.TestOnly
2754
* @param runtimeConfig Runtime configuration to apply. If `null` we use
2855
* [JvmTestRuntimeConfigTools.getTestRuntimeConfig][com.squareup.workflow1.config.JvmTestRuntimeConfigTools.getTestRuntimeConfig]
2956
* instead.
57+
* @param deprecatedLaunchSchedulerMode Controls which dispatcher the deprecated `launchForTesting*`
58+
* helpers use. Defaults to [DeprecatedLaunchSchedulerMode.LEGACY_UNCONFINED] to preserve pre-1.25.1
59+
* behavior. Set to [DeprecatedLaunchSchedulerMode.VIRTUAL_TIME_STANDARD] to opt in to virtual-time
60+
* semantics. Has no effect on the recommended `renderForTest` API. Note: if the `context` parameter
61+
* passed to a `launchForTesting*` function contains a dispatcher, that dispatcher takes precedence
62+
* over the one selected by this mode.
3063
*/
3164
@TestOnly
3265
public class WorkflowTestParams<out StateT>(
3366
public val startFrom: StartMode<StateT> = StartFresh,
3467
public val checkRenderIdempotence: Boolean = true,
35-
public val runtimeConfig: RuntimeConfig? = null
68+
public val runtimeConfig: RuntimeConfig? = null,
69+
public val deprecatedLaunchSchedulerMode: DeprecatedLaunchSchedulerMode =
70+
DeprecatedLaunchSchedulerMode.LEGACY_UNCONFINED
3671
) {
3772
/**
3873
* Defines how to start the workflow for tests.

workflow-testing/src/main/java/com/squareup/workflow1/testing/WorkflowTestRuntime.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import com.squareup.workflow1.Workflow
99
import com.squareup.workflow1.testing.WorkflowTestParams.StartMode.StartFromState
1010
import kotlinx.coroutines.DelicateCoroutinesApi
1111
import kotlinx.coroutines.ExperimentalCoroutinesApi
12+
import kotlinx.coroutines.Job
1213
import kotlinx.coroutines.flow.MutableStateFlow
1314
import kotlinx.coroutines.test.StandardTestDispatcher
15+
import kotlinx.coroutines.test.UnconfinedTestDispatcher
1416
import kotlinx.coroutines.withTimeout
1517
import org.jetbrains.annotations.TestOnly
1618
import kotlin.coroutines.CoroutineContext
@@ -248,11 +250,18 @@ public fun <T, PropsT, StateT, OutputT, RenderingT>
248250
block: suspend WorkflowTestRuntime<PropsT, OutputT, RenderingT>.() -> T
249251
): T {
250252
val propsFlow = MutableStateFlow(props)
253+
val schedulerContext = when (testParams.deprecatedLaunchSchedulerMode) {
254+
DeprecatedLaunchSchedulerMode.LEGACY_UNCONFINED -> UnconfinedTestDispatcher()
255+
DeprecatedLaunchSchedulerMode.VIRTUAL_TIME_STANDARD -> StandardTestDispatcher()
256+
}
257+
// Strip Job from the caller's context so it doesn't interfere with the runtime scope's
258+
// lifecycle management. Other context elements (names, coroutine names, etc.) are preserved.
259+
val safeContext = context.minusKey(Job)
251260
var result: T? = null
252261
renderForTest(
253262
props = propsFlow,
254263
testParams = testParams,
255-
coroutineContext = StandardTestDispatcher(),
264+
coroutineContext = schedulerContext + safeContext,
256265
) {
257266
val runtime = WorkflowTestRuntime(propsFlow, this)
258267
result = runtime.block()

workflow-testing/src/test/java/com/squareup/workflow1/WorkerCompositionIntegrationTest.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,6 @@ internal class WorkerCompositionIntegrationTest {
234234
}
235235
}
236236

237-
// Test removed: The deprecated `launchForTestingWith` shim now delegates to `renderForTest`
238-
// which always uses `StandardTestDispatcher()`. The `context` parameter is no longer forwarded
239-
// to the workflow runtime scope. Use `renderForTest` directly to control the coroutine context.
237+
// Note: The `context` parameter is now honored again (minus Job elements) via the
238+
// DeprecatedLaunchSchedulerMode bridge. Use `renderForTest` directly for full context control.
240239
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
@file:Suppress("DEPRECATION")
2+
@file:OptIn(ExperimentalCoroutinesApi::class)
3+
4+
package com.squareup.workflow1.testing
5+
6+
import com.squareup.workflow1.Worker
7+
import com.squareup.workflow1.Workflow
8+
import com.squareup.workflow1.action
9+
import com.squareup.workflow1.runningWorker
10+
import com.squareup.workflow1.stateful
11+
import kotlinx.coroutines.ExperimentalCoroutinesApi
12+
import kotlinx.coroutines.TimeoutCancellationException
13+
import kotlinx.coroutines.delay
14+
import kotlin.test.Test
15+
import kotlin.test.assertEquals
16+
import kotlin.test.assertFailsWith
17+
import kotlin.test.assertTrue
18+
19+
/**
20+
* Tests verifying the [DeprecatedLaunchSchedulerMode] migration bridge for the deprecated
21+
* `launchForTesting*` APIs.
22+
*
23+
* These tests lock the semantics of each mode to prevent regressions during phased migration.
24+
*/
25+
internal class DeprecatedLaunchSchedulerModeTest {
26+
27+
// -- LEGACY_UNCONFINED mode tests --
28+
29+
@Test fun `legacy mode - side effects are immediately observable`() {
30+
var renderCount = 0
31+
val workflow = Workflow.stateful<Unit, String, Nothing, () -> Unit>(
32+
initialState = { "initial" },
33+
render = { _, _ ->
34+
renderCount++
35+
eventHandler("update") {
36+
state = "updated"
37+
}
38+
}
39+
)
40+
41+
workflow.launchForTestingFromStartWith(
42+
testParams = WorkflowTestParams(
43+
checkRenderIdempotence = false,
44+
deprecatedLaunchSchedulerMode = DeprecatedLaunchSchedulerMode.LEGACY_UNCONFINED
45+
)
46+
) {
47+
val onUpdate = awaitNextRendering()
48+
val countBeforeAction = renderCount
49+
// Invoke the event handler — with UnconfinedTestDispatcher the action dispatches
50+
// immediately, so the render count increases synchronously without advanceUntilSettled().
51+
onUpdate()
52+
assertTrue(renderCount > countBeforeAction)
53+
}
54+
}
55+
56+
@Test fun `legacy mode - delay worker does not auto-complete`() {
57+
val workflow = Workflow.stateful<Unit, String, String, Unit>(
58+
initialState = { "waiting" },
59+
render = { _, _ ->
60+
runningWorker(
61+
Worker.from {
62+
delay(5_000)
63+
"done"
64+
}
65+
) {
66+
action("workerResult") { setOutput(it) }
67+
}
68+
}
69+
)
70+
71+
workflow.launchForTestingFromStartWith(
72+
testParams = WorkflowTestParams(
73+
deprecatedLaunchSchedulerMode = DeprecatedLaunchSchedulerMode.LEGACY_UNCONFINED
74+
)
75+
) {
76+
// The delay-based worker should NOT have completed — virtual time is not auto-advanced.
77+
assertFailsWith<TimeoutCancellationException> {
78+
awaitNextOutput(timeoutMs = 100)
79+
}
80+
}
81+
}
82+
83+
// -- VIRTUAL_TIME_STANDARD mode tests --
84+
85+
@Test fun `virtual time mode - side effects require advanceUntilSettled`() {
86+
var renderCount = 0
87+
val workflow = Workflow.stateful<Unit, String, Nothing, () -> Unit>(
88+
initialState = { "initial" },
89+
render = { _, _ ->
90+
renderCount++
91+
eventHandler("update") {
92+
state = "updated"
93+
}
94+
}
95+
)
96+
97+
workflow.launchForTestingFromStartWith(
98+
testParams = WorkflowTestParams(
99+
checkRenderIdempotence = false,
100+
deprecatedLaunchSchedulerMode = DeprecatedLaunchSchedulerMode.VIRTUAL_TIME_STANDARD
101+
)
102+
) {
103+
val onUpdate = awaitNextRendering()
104+
val countBeforeAction = renderCount
105+
onUpdate()
106+
// With StandardTestDispatcher, the action is queued but not yet processed.
107+
assertEquals(countBeforeAction, renderCount)
108+
// advanceUntilSettled() processes the queued action and triggers a new render.
109+
advanceUntilSettled()
110+
assertTrue(renderCount > countBeforeAction)
111+
assertTrue(hasRendering)
112+
}
113+
}
114+
115+
@Test fun `virtual time mode - delay worker completes with advance`() {
116+
val workflow = Workflow.stateful<Unit, String, String, Unit>(
117+
initialState = { "waiting" },
118+
render = { _, _ ->
119+
runningWorker(
120+
Worker.from {
121+
delay(5_000)
122+
"done"
123+
}
124+
) {
125+
action("workerResult") { setOutput(it) }
126+
}
127+
}
128+
)
129+
130+
workflow.launchForTestingFromStartWith(
131+
testParams = WorkflowTestParams(
132+
deprecatedLaunchSchedulerMode = DeprecatedLaunchSchedulerMode.VIRTUAL_TIME_STANDARD
133+
)
134+
) {
135+
// After advancing (done internally by awaitNextOutput -> advanceUntilSettled),
136+
// the delay completes and the worker emits output.
137+
val output = awaitNextOutput()
138+
assertEquals("done", output)
139+
}
140+
}
141+
}

0 commit comments

Comments
 (0)