@@ -9,6 +9,7 @@ import kotlinx.coroutines.CoroutineExceptionHandler
9
9
import kotlinx.coroutines.ExperimentalCoroutinesApi
10
10
import kotlinx.coroutines.cancel
11
11
import kotlinx.coroutines.channels.Channel
12
+ import kotlinx.coroutines.flow.MutableSharedFlow
12
13
import kotlinx.coroutines.flow.MutableStateFlow
13
14
import kotlinx.coroutines.flow.StateFlow
14
15
import kotlinx.coroutines.flow.map
@@ -21,6 +22,7 @@ import kotlinx.coroutines.test.StandardTestDispatcher
21
22
import kotlinx.coroutines.test.TestScope
22
23
import kotlinx.coroutines.test.UnconfinedTestDispatcher
23
24
import kotlinx.coroutines.test.advanceUntilIdle
25
+ import kotlinx.coroutines.test.runCurrent
24
26
import kotlinx.coroutines.test.runTest
25
27
import okio.ByteString
26
28
import kotlin.test.Test
@@ -1180,6 +1182,248 @@ class RenderWorkflowInTest {
1180
1182
}
1181
1183
}
1182
1184
1185
+ @Test
1186
+ fun for_render_on_change_only_and_conflate_we_drain_action_but_do_not_render_no_state_changed () {
1187
+ runtimeTestRunner.runParametrizedTest(
1188
+ paramSource = runtimeOptions.filter {
1189
+ it.first.contains(RENDER_ONLY_WHEN_STATE_CHANGES ) && it.first.contains(
1190
+ CONFLATE_STALE_RENDERINGS
1191
+ )
1192
+ },
1193
+ before = ::setup,
1194
+ ) { (runtimeConfig: RuntimeConfig , workflowTracer: WorkflowTracer ? ) ->
1195
+ runTest(UnconfinedTestDispatcher ()) {
1196
+ check(runtimeConfig.contains(CONFLATE_STALE_RENDERINGS ))
1197
+ check(runtimeConfig.contains(RENDER_ONLY_WHEN_STATE_CHANGES ))
1198
+
1199
+ var renderCount = 0
1200
+ var childHandlerActionExecuted = 0
1201
+ var workerActionExecuted = 0
1202
+ val trigger = MutableSharedFlow <String >()
1203
+
1204
+ val childWorkflow = Workflow .stateful<String , String , String >(
1205
+ initialState = " unchanging state" ,
1206
+ render = { renderState ->
1207
+ runningWorker(
1208
+ trigger.asWorker()
1209
+ ) {
1210
+ action(" " ) {
1211
+ state = it
1212
+ setOutput(it)
1213
+ }
1214
+ }
1215
+ renderState
1216
+ }
1217
+ )
1218
+ val workflow = Workflow .stateful<String , String , String >(
1219
+ initialState = " unchanging state" ,
1220
+ render = { renderState ->
1221
+ renderChild(childWorkflow) { childOutput ->
1222
+ action(" childHandler" ) {
1223
+ childHandlerActionExecuted++
1224
+ state = childOutput
1225
+ }
1226
+ }
1227
+ runningWorker(
1228
+ trigger.asWorker()
1229
+ ) {
1230
+ action(" " ) {
1231
+ workerActionExecuted++
1232
+ state = it
1233
+ }
1234
+ }
1235
+ renderState.also {
1236
+ renderCount++
1237
+ }
1238
+ }
1239
+ )
1240
+ val props = MutableStateFlow (Unit )
1241
+ renderWorkflowIn(
1242
+ workflow = workflow,
1243
+ scope = backgroundScope,
1244
+ props = props,
1245
+ runtimeConfig = runtimeConfig,
1246
+ workflowTracer = workflowTracer,
1247
+ ) {}
1248
+
1249
+ launch {
1250
+ trigger.emit(" changed state" )
1251
+ }
1252
+ advanceUntilIdle()
1253
+
1254
+ assertEquals(2 , renderCount)
1255
+ assertEquals(1 , childHandlerActionExecuted)
1256
+ assertEquals(1 , workerActionExecuted)
1257
+ }
1258
+ }
1259
+ }
1260
+
1261
+ @Test
1262
+ fun for_conflate_we_conflate_stacked_actions_into_one_rendering () {
1263
+ runtimeTestRunner.runParametrizedTest(
1264
+ paramSource = runtimeOptions
1265
+ .filter {
1266
+ it.first.contains(CONFLATE_STALE_RENDERINGS )
1267
+ },
1268
+ before = ::setup,
1269
+ ) { (runtimeConfig: RuntimeConfig , workflowTracer: WorkflowTracer ? ) ->
1270
+ runTest(StandardTestDispatcher ()) {
1271
+ check(runtimeConfig.contains(CONFLATE_STALE_RENDERINGS ))
1272
+
1273
+ var childHandlerActionExecuted = false
1274
+ val trigger = MutableSharedFlow <String >()
1275
+ val emitted = mutableListOf<String >()
1276
+
1277
+ val childWorkflow = Workflow .stateful<String , String , String >(
1278
+ initialState = " unchanging state" ,
1279
+ render = { renderState ->
1280
+ runningWorker(
1281
+ trigger.asWorker()
1282
+ ) {
1283
+ action(" " ) {
1284
+ state = it
1285
+ setOutput(it)
1286
+ }
1287
+ }
1288
+ renderState
1289
+ }
1290
+ )
1291
+ val workflow = Workflow .stateful<String , String , String >(
1292
+ initialState = " unchanging state" ,
1293
+ render = { renderState ->
1294
+ renderChild(childWorkflow) { childOutput ->
1295
+ action(" childHandler" ) {
1296
+ childHandlerActionExecuted = true
1297
+ state = childOutput
1298
+ }
1299
+ }
1300
+ runningWorker(
1301
+ trigger.asWorker()
1302
+ ) {
1303
+ action(" " ) {
1304
+ // Update the rendering in order to show conflation.
1305
+ state = " $it +update"
1306
+ }
1307
+ }
1308
+ renderState
1309
+ }
1310
+ )
1311
+ val props = MutableStateFlow (Unit )
1312
+ val renderings = renderWorkflowIn(
1313
+ workflow = workflow,
1314
+ scope = backgroundScope,
1315
+ props = props,
1316
+ runtimeConfig = runtimeConfig,
1317
+ workflowTracer = workflowTracer,
1318
+ ) {}
1319
+
1320
+ launch {
1321
+ trigger.emit(" changed state" )
1322
+ }
1323
+ val collectionJob = launch(UnconfinedTestDispatcher (testScheduler)) {
1324
+ // Collect this unconfined so we can get all the renderings faster than actions can
1325
+ // be processed.
1326
+ renderings.collect {
1327
+ emitted + = it.rendering
1328
+ }
1329
+ }
1330
+ advanceUntilIdle()
1331
+ runCurrent()
1332
+
1333
+ collectionJob.cancel()
1334
+
1335
+ // 2 renderings (initial and then the update.) Not *3* renderings.
1336
+ assertEquals(2 , emitted.size)
1337
+ assertEquals(" changed state+update" , emitted.last())
1338
+ assertTrue(childHandlerActionExecuted)
1339
+ }
1340
+ }
1341
+ }
1342
+
1343
+ @Test
1344
+ fun for_conflate_we_do_not_conflate_stacked_actions_into_one_rendering_if_output () {
1345
+ runtimeTestRunner.runParametrizedTest(
1346
+ paramSource = runtimeOptions
1347
+ .filter {
1348
+ it.first.contains(CONFLATE_STALE_RENDERINGS )
1349
+ },
1350
+ before = ::setup,
1351
+ ) { (runtimeConfig: RuntimeConfig , workflowTracer: WorkflowTracer ? ) ->
1352
+ runTest(StandardTestDispatcher ()) {
1353
+ check(runtimeConfig.contains(CONFLATE_STALE_RENDERINGS ))
1354
+
1355
+ var childHandlerActionExecuted = false
1356
+ val trigger = MutableSharedFlow <String >()
1357
+ val emitted = mutableListOf<String >()
1358
+
1359
+ val childWorkflow = Workflow .stateful<String , String , String >(
1360
+ initialState = " unchanging state" ,
1361
+ render = { renderState ->
1362
+ runningWorker(
1363
+ trigger.asWorker()
1364
+ ) {
1365
+ action(" " ) {
1366
+ state = it
1367
+ setOutput(it)
1368
+ }
1369
+ }
1370
+ renderState
1371
+ }
1372
+ )
1373
+ val workflow = Workflow .stateful<String , String , String >(
1374
+ initialState = " unchanging state" ,
1375
+ render = { renderState ->
1376
+ renderChild(childWorkflow) { childOutput ->
1377
+ action(" childHandler" ) {
1378
+ childHandlerActionExecuted = true
1379
+ state = childOutput
1380
+ setOutput(childOutput)
1381
+ }
1382
+ }
1383
+ runningWorker(
1384
+ trigger.asWorker()
1385
+ ) {
1386
+ action(" " ) {
1387
+ // Update the rendering in order to show conflation.
1388
+ state = " $it +update"
1389
+ setOutput(" $it +update" )
1390
+ }
1391
+ }
1392
+ renderState
1393
+ }
1394
+ )
1395
+ val props = MutableStateFlow (Unit )
1396
+ val renderings = renderWorkflowIn(
1397
+ workflow = workflow,
1398
+ scope = backgroundScope,
1399
+ props = props,
1400
+ runtimeConfig = runtimeConfig,
1401
+ workflowTracer = workflowTracer,
1402
+ ) {}
1403
+
1404
+ launch {
1405
+ trigger.emit(" changed state" )
1406
+ }
1407
+ val collectionJob = launch(UnconfinedTestDispatcher (testScheduler)) {
1408
+ // Collect this unconfined so we can get all the renderings faster than actions can
1409
+ // be processed.
1410
+ renderings.collect {
1411
+ emitted + = it.rendering
1412
+ }
1413
+ }
1414
+ advanceUntilIdle()
1415
+ runCurrent()
1416
+
1417
+ collectionJob.cancel()
1418
+
1419
+ // 3 renderings because each had output.
1420
+ assertEquals(3 , emitted.size)
1421
+ assertEquals(" changed state+update" , emitted.last())
1422
+ assertTrue(childHandlerActionExecuted)
1423
+ }
1424
+ }
1425
+ }
1426
+
1183
1427
private class ExpectedException : RuntimeException ()
1184
1428
1185
1429
private fun <T1 , T2 > cartesianProduct (
0 commit comments