Skip to content

Commit dbd02dc

Browse files
Update CSR to yield() and update tests
1 parent 2ef97ec commit dbd02dc

File tree

3 files changed

+85
-54
lines changed

3 files changed

+85
-54
lines changed

workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
1212
import kotlinx.coroutines.flow.StateFlow
1313
import kotlinx.coroutines.isActive
1414
import kotlinx.coroutines.launch
15+
import kotlinx.coroutines.yield
1516

1617
/**
1718
* Launches the [workflow] in a new coroutine in [scope] and returns a [StateFlow] of its
@@ -170,15 +171,15 @@ public fun <PropsT, OutputT, RenderingT> renderWorkflowIn(
170171
}
171172

172173
scope.launch {
173-
while (isActive) {
174+
outer@ while (isActive) {
174175
// It might look weird to start by processing an action before getting the rendering below,
175176
// but remember the first render pass already occurred above, before this coroutine was even
176177
// launched.
177178
var actionResult: ActionProcessingResult = runner.processAction()
178179

179180
if (shouldShortCircuitForUnchangedState(actionResult)) {
180181
sendOutput(actionResult, onOutput)
181-
continue
182+
continue@outer
182183
}
183184

184185
// After resuming from runner.processAction() our coroutine could now be cancelled, check so
@@ -189,15 +190,23 @@ public fun <PropsT, OutputT, RenderingT> renderWorkflowIn(
189190
var nextRenderAndSnapshot: RenderingAndSnapshot<RenderingT> = runner.nextRendering()
190191

191192
if (runtimeConfig.contains(CONFLATE_STALE_RENDERINGS)) {
192-
while (isActive && actionResult is ActionApplied<*> && actionResult.output == null) {
193+
conflate@ while (isActive && actionResult is ActionApplied<*> && actionResult.output == null) {
194+
// We start by yielding, because if we are on an Unconfined dispatcher, we want to give
195+
// other signals (like Workers listening to the same result) a chance to get dispatched
196+
// and queue their actions.
197+
yield()
193198
// We may have more actions we can process, this rendering could be stale.
194199
actionResult = runner.processAction(waitForAnAction = false)
195200

196201
// If no actions processed, then no new rendering needed. Pass on to UI.
197-
if (actionResult == ActionsExhausted) break
202+
if (actionResult == ActionsExhausted) break@conflate
198203

199204
// Skip rendering if we had unchanged state, keep draining actions.
200-
if (shouldShortCircuitForUnchangedState(actionResult)) continue
205+
if (shouldShortCircuitForUnchangedState(actionResult)) {
206+
// Emit the Output
207+
sendOutput(actionResult, onOutput)
208+
continue@outer
209+
}
201210

202211
// Make sure the runtime has not been cancelled from runner.processAction()
203212
if (!isActive) return@launch

workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RenderWorkflowInTest.kt

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ import kotlinx.coroutines.test.StandardTestDispatcher
2323
import kotlinx.coroutines.test.TestScope
2424
import kotlinx.coroutines.test.UnconfinedTestDispatcher
2525
import kotlinx.coroutines.test.advanceUntilIdle
26-
import kotlinx.coroutines.test.runCurrent
2726
import kotlinx.coroutines.test.runTest
27+
import kotlinx.coroutines.yield
2828
import okio.ByteString
2929
import kotlin.test.Test
3030
import kotlin.test.assertEquals
@@ -1343,6 +1343,7 @@ class RenderWorkflowInTest {
13431343
var childHandlerActionExecuted = 0
13441344
var workerActionExecuted = 0
13451345
val trigger = MutableSharedFlow<String>()
1346+
val outputSet = mutableListOf<String>()
13461347

13471348
val childWorkflow = Workflow.stateful<String, String, String>(
13481349
initialState = "unchanging state",
@@ -1373,6 +1374,7 @@ class RenderWorkflowInTest {
13731374
action("") {
13741375
workerActionExecuted++
13751376
state = it
1377+
setOutput(it)
13761378
}
13771379
}
13781380
renderState.also {
@@ -1387,7 +1389,9 @@ class RenderWorkflowInTest {
13871389
props = props,
13881390
runtimeConfig = runtimeConfig,
13891391
workflowTracer = workflowTracer,
1390-
) {}
1392+
) {
1393+
outputSet.add(it)
1394+
}
13911395

13921396
launch {
13931397
trigger.emit("changed state")
@@ -1398,10 +1402,17 @@ class RenderWorkflowInTest {
13981402
assertEquals(2, renderCount)
13991403
assertEquals(1, childHandlerActionExecuted)
14001404
assertEquals(1, workerActionExecuted)
1405+
assertEquals(1, outputSet.size)
1406+
assertEquals("changed state", outputSet[0])
14011407
}
14021408
}
14031409
}
14041410

1411+
/**
1412+
* This is the same test as [for_conflate_we_do_not_conflate_stacked_actions_into_one_rendering_if_output]
1413+
* except that in that version the handler for the child output also sets output - which is
1414+
* one reason we do not end up conflating.
1415+
*/
14051416
@Test
14061417
fun for_conflate_we_conflate_stacked_actions_into_one_rendering() {
14071418
runtimeTestRunner.runParametrizedTest(
@@ -1411,7 +1422,7 @@ class RenderWorkflowInTest {
14111422
},
14121423
before = ::setup,
14131424
) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) ->
1414-
runTest(StandardTestDispatcher()) {
1425+
runTest(UnconfinedTestDispatcher()) {
14151426
check(runtimeConfig.contains(CONFLATE_STALE_RENDERINGS))
14161427

14171428
var childHandlerActionExecuted = false
@@ -1447,6 +1458,7 @@ class RenderWorkflowInTest {
14471458
action("") {
14481459
// Update the rendering in order to show conflation.
14491460
state = "$it+update"
1461+
setOutput(state)
14501462
}
14511463
}
14521464
renderState
@@ -1459,20 +1471,22 @@ class RenderWorkflowInTest {
14591471
props = props,
14601472
runtimeConfig = runtimeConfig,
14611473
workflowTracer = workflowTracer,
1462-
) {}
1463-
1464-
launch {
1465-
trigger.emit("changed state")
1474+
) {
1475+
// Yield in output so that we ensure that we let the collector of the renderings
1476+
// collect each of them before processing the next action.
1477+
yield()
14661478
}
1467-
val collectionJob = launch(UnconfinedTestDispatcher(testScheduler)) {
1479+
1480+
val collectionJob = launch {
14681481
// Collect this unconfined so we can get all the renderings faster than actions can
14691482
// be processed.
14701483
renderings.collect {
14711484
emitted += it.rendering
14721485
}
14731486
}
1474-
advanceUntilIdle()
1475-
runCurrent()
1487+
launch {
1488+
trigger.emit("changed state")
1489+
}
14761490

14771491
collectionJob.cancel()
14781492

@@ -1493,7 +1507,7 @@ class RenderWorkflowInTest {
14931507
},
14941508
before = ::setup,
14951509
) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?) ->
1496-
runTest(StandardTestDispatcher()) {
1510+
runTest(UnconfinedTestDispatcher()) {
14971511
check(runtimeConfig.contains(CONFLATE_STALE_RENDERINGS))
14981512

14991513
var childHandlerActionExecuted = false
@@ -1543,20 +1557,24 @@ class RenderWorkflowInTest {
15431557
props = props,
15441558
runtimeConfig = runtimeConfig,
15451559
workflowTracer = workflowTracer,
1546-
) {}
1547-
1548-
launch {
1549-
trigger.emit("changed state")
1560+
) {
1561+
// Yield in output so that we ensure that we let the collector of the renderings
1562+
// collect each of them before processing the next action.
1563+
yield()
15501564
}
1551-
val collectionJob = launch(UnconfinedTestDispatcher(testScheduler)) {
1565+
1566+
val collectionJob = launch {
15521567
// Collect this unconfined so we can get all the renderings faster than actions can
15531568
// be processed.
15541569
renderings.collect {
15551570
emitted += it.rendering
15561571
}
15571572
}
1573+
1574+
launch {
1575+
trigger.emit("changed state")
1576+
}
15581577
advanceUntilIdle()
1559-
runCurrent()
15601578

15611579
collectionJob.cancel()
15621580

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

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import com.squareup.workflow1.RuntimeConfigOptions.CONFLATE_STALE_RENDERINGS
44
import com.squareup.workflow1.RuntimeConfigOptions.PARTIAL_TREE_RENDERING
55
import com.squareup.workflow1.RuntimeConfigOptions.RENDER_ONLY_WHEN_STATE_CHANGES
66
import com.squareup.workflow1.testing.headlessIntegrationTest
7+
import kotlinx.coroutines.ExperimentalCoroutinesApi
78
import kotlinx.coroutines.Job
89
import kotlinx.coroutines.awaitCancellation
9-
import kotlinx.coroutines.test.StandardTestDispatcher
10+
import kotlinx.coroutines.test.UnconfinedTestDispatcher
1011
import kotlin.test.Ignore
1112
import kotlin.test.Test
1213

@@ -122,20 +123,45 @@ class WorkflowsLifecycleTests {
122123
}
123124
}
124125

126+
@Test fun childSessionWorkflowStartedWhenExpected() {
127+
runtimeTestRunner.runParametrizedTest(
128+
paramSource = runtimeOptions,
129+
after = ::cleanup,
130+
) { runtimeConfig: RuntimeConfig ->
131+
132+
workflowWithChildSession.headlessIntegrationTest(
133+
runtimeConfig = runtimeConfig
134+
) {
135+
// One time starts but does not stop the child session workflow.
136+
repeat(1) {
137+
val (current, setState) = awaitNextRendering()
138+
setState.invoke(current + 1)
139+
}
140+
141+
assertEquals(1, started, "Child Session Workflow not started 1 time.")
142+
}
143+
}
144+
}
145+
125146
/**
126147
* @see [1093](https://github.com/square/workflow-kotlin/issues/1093)
127148
*
128-
* This test ensconces the currently failing behavior of side effects. We are not currently
129-
* fixing this but rather working around it with [SessionWorkflow].
149+
* This test fails. It is kept and Ignored as a way to ensconce the currently failing behavior
150+
* of side effects with immediate start & stops. We are not currently fixing this but rather
151+
* working around it with [SessionWorkflow].
152+
*
153+
* Compare with [childSessionWorkflowStartAndStoppedWhenHandledSynchronously]
130154
*/
131-
@Ignore
132-
@Test fun sideEffectsStartAndStoppedWhenHandledSynchronously() {
155+
@Ignore("https://github.com/square/workflow-kotlin/issues/1093")
156+
@OptIn(ExperimentalCoroutinesApi::class)
157+
@Test
158+
fun sideEffectsStartAndStoppedWhenHandledSynchronously() {
133159
runtimeTestRunner.runParametrizedTest(
134160
paramSource = runtimeOptions,
135161
after = ::cleanup,
136162
) { runtimeConfig: RuntimeConfig ->
137163

138-
val dispatcher = StandardTestDispatcher()
164+
val dispatcher = UnconfinedTestDispatcher()
139165
workflowWithSideEffects.headlessIntegrationTest(
140166
coroutineContext = dispatcher,
141167
runtimeConfig = runtimeConfig
@@ -146,9 +172,7 @@ class WorkflowsLifecycleTests {
146172
// on two consecutive render passes.
147173
setState.invoke(1)
148174
setState.invoke(2)
149-
dispatcher.scheduler.runCurrent()
150175
awaitNextRendering()
151-
dispatcher.scheduler.runCurrent()
152176
if (!runtimeConfig.contains(CONFLATE_STALE_RENDERINGS)) {
153177
// 2 rendering or 1 depending on runtime config.
154178
awaitNextRendering()
@@ -160,26 +184,6 @@ class WorkflowsLifecycleTests {
160184
}
161185
}
162186

163-
@Test fun childSessionWorkflowStartedWhenExpected() {
164-
runtimeTestRunner.runParametrizedTest(
165-
paramSource = runtimeOptions,
166-
after = ::cleanup,
167-
) { runtimeConfig: RuntimeConfig ->
168-
169-
workflowWithChildSession.headlessIntegrationTest(
170-
runtimeConfig = runtimeConfig
171-
) {
172-
// One time starts but does not stop the child session workflow.
173-
repeat(1) {
174-
val (current, setState) = awaitNextRendering()
175-
setState.invoke(current + 1)
176-
}
177-
178-
assertEquals(1, started, "Child Session Workflow not started 1 time.")
179-
}
180-
}
181-
}
182-
183187
@Test fun childSessionWorkflowStoppedWhenExpected() {
184188
runtimeTestRunner.runParametrizedTest(
185189
paramSource = runtimeOptions,
@@ -205,13 +209,15 @@ class WorkflowsLifecycleTests {
205209
*
206210
* This tests show the working behavior when using a [SessionWorkflow] to track the lifetime.
207211
*/
208-
@Test fun childSessionWorkflowStartAndStoppedWhenHandledSynchronously() {
212+
@OptIn(ExperimentalCoroutinesApi::class)
213+
@Test
214+
fun childSessionWorkflowStartAndStoppedWhenHandledSynchronously() {
209215
runtimeTestRunner.runParametrizedTest(
210216
paramSource = runtimeOptions,
211217
after = ::cleanup,
212218
) { runtimeConfig: RuntimeConfig ->
213219

214-
val dispatcher = StandardTestDispatcher()
220+
val dispatcher = UnconfinedTestDispatcher()
215221
workflowWithChildSession.headlessIntegrationTest(
216222
coroutineContext = dispatcher,
217223
runtimeConfig = runtimeConfig
@@ -222,9 +228,7 @@ class WorkflowsLifecycleTests {
222228
// on two consecutive render passes, synchronously.
223229
setState.invoke(1)
224230
setState.invoke(2)
225-
dispatcher.scheduler.runCurrent()
226231
awaitNextRendering()
227-
dispatcher.scheduler.runCurrent()
228232
if (!runtimeConfig.contains(CONFLATE_STALE_RENDERINGS)) {
229233
// 2 rendering or 1 depending on runtime config.
230234
awaitNextRendering()

0 commit comments

Comments
 (0)