@@ -9,6 +9,7 @@ import kotlinx.coroutines.CoroutineExceptionHandler
99import kotlinx.coroutines.ExperimentalCoroutinesApi
1010import kotlinx.coroutines.cancel
1111import kotlinx.coroutines.channels.Channel
12+ import kotlinx.coroutines.flow.MutableSharedFlow
1213import kotlinx.coroutines.flow.MutableStateFlow
1314import kotlinx.coroutines.flow.StateFlow
1415import kotlinx.coroutines.flow.map
@@ -21,6 +22,7 @@ import kotlinx.coroutines.test.StandardTestDispatcher
2122import kotlinx.coroutines.test.TestScope
2223import kotlinx.coroutines.test.UnconfinedTestDispatcher
2324import kotlinx.coroutines.test.advanceUntilIdle
25+ import kotlinx.coroutines.test.runCurrent
2426import kotlinx.coroutines.test.runTest
2527import okio.ByteString
2628import kotlin.test.Test
@@ -1180,6 +1182,249 @@ class RenderWorkflowInTest {
11801182 }
11811183 }
11821184
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+ // 2 renderings (initial and then the update.) Not *3* renderings.
1255+ assertEquals(2 , renderCount)
1256+ assertEquals(1 , childHandlerActionExecuted)
1257+ assertEquals(1 , workerActionExecuted)
1258+ }
1259+ }
1260+ }
1261+
1262+ @Test
1263+ fun for_conflate_we_conflate_stacked_actions_into_one_rendering () {
1264+ runtimeTestRunner.runParametrizedTest(
1265+ paramSource = runtimeOptions
1266+ .filter {
1267+ it.first.contains(CONFLATE_STALE_RENDERINGS )
1268+ },
1269+ before = ::setup,
1270+ ) { (runtimeConfig: RuntimeConfig , workflowTracer: WorkflowTracer ? ) ->
1271+ runTest(StandardTestDispatcher ()) {
1272+ check(runtimeConfig.contains(CONFLATE_STALE_RENDERINGS ))
1273+
1274+ var childHandlerActionExecuted = false
1275+ val trigger = MutableSharedFlow <String >()
1276+ val emitted = mutableListOf<String >()
1277+
1278+ val childWorkflow = Workflow .stateful<String , String , String >(
1279+ initialState = " unchanging state" ,
1280+ render = { renderState ->
1281+ runningWorker(
1282+ trigger.asWorker()
1283+ ) {
1284+ action(" " ) {
1285+ state = it
1286+ setOutput(it)
1287+ }
1288+ }
1289+ renderState
1290+ }
1291+ )
1292+ val workflow = Workflow .stateful<String , String , String >(
1293+ initialState = " unchanging state" ,
1294+ render = { renderState ->
1295+ renderChild(childWorkflow) { childOutput ->
1296+ action(" childHandler" ) {
1297+ childHandlerActionExecuted = true
1298+ state = childOutput
1299+ }
1300+ }
1301+ runningWorker(
1302+ trigger.asWorker()
1303+ ) {
1304+ action(" " ) {
1305+ // Update the rendering in order to show conflation.
1306+ state = " $it +update"
1307+ }
1308+ }
1309+ renderState
1310+ }
1311+ )
1312+ val props = MutableStateFlow (Unit )
1313+ val renderings = renderWorkflowIn(
1314+ workflow = workflow,
1315+ scope = backgroundScope,
1316+ props = props,
1317+ runtimeConfig = runtimeConfig,
1318+ workflowTracer = workflowTracer,
1319+ ) {}
1320+
1321+ launch {
1322+ trigger.emit(" changed state" )
1323+ }
1324+ val collectionJob = launch(UnconfinedTestDispatcher (testScheduler)) {
1325+ // Collect this unconfined so we can get all the renderings faster than actions can
1326+ // be processed.
1327+ renderings.collect {
1328+ emitted + = it.rendering
1329+ }
1330+ }
1331+ advanceUntilIdle()
1332+ runCurrent()
1333+
1334+ collectionJob.cancel()
1335+
1336+ // 2 renderings (initial and then the update.) Not *3* renderings.
1337+ assertEquals(2 , emitted.size)
1338+ assertEquals(" changed state+update" , emitted.last())
1339+ assertTrue(childHandlerActionExecuted)
1340+ }
1341+ }
1342+ }
1343+
1344+ @Test
1345+ fun for_conflate_we_do_not_conflate_stacked_actions_into_one_rendering_if_output () {
1346+ runtimeTestRunner.runParametrizedTest(
1347+ paramSource = runtimeOptions
1348+ .filter {
1349+ it.first.contains(CONFLATE_STALE_RENDERINGS )
1350+ },
1351+ before = ::setup,
1352+ ) { (runtimeConfig: RuntimeConfig , workflowTracer: WorkflowTracer ? ) ->
1353+ runTest(StandardTestDispatcher ()) {
1354+ check(runtimeConfig.contains(CONFLATE_STALE_RENDERINGS ))
1355+
1356+ var childHandlerActionExecuted = false
1357+ val trigger = MutableSharedFlow <String >()
1358+ val emitted = mutableListOf<String >()
1359+
1360+ val childWorkflow = Workflow .stateful<String , String , String >(
1361+ initialState = " unchanging state" ,
1362+ render = { renderState ->
1363+ runningWorker(
1364+ trigger.asWorker()
1365+ ) {
1366+ action(" " ) {
1367+ state = it
1368+ setOutput(it)
1369+ }
1370+ }
1371+ renderState
1372+ }
1373+ )
1374+ val workflow = Workflow .stateful<String , String , String >(
1375+ initialState = " unchanging state" ,
1376+ render = { renderState ->
1377+ renderChild(childWorkflow) { childOutput ->
1378+ action(" childHandler" ) {
1379+ childHandlerActionExecuted = true
1380+ state = childOutput
1381+ setOutput(childOutput)
1382+ }
1383+ }
1384+ runningWorker(
1385+ trigger.asWorker()
1386+ ) {
1387+ action(" " ) {
1388+ // Update the rendering in order to show conflation.
1389+ state = " $it +update"
1390+ setOutput(" $it +update" )
1391+ }
1392+ }
1393+ renderState
1394+ }
1395+ )
1396+ val props = MutableStateFlow (Unit )
1397+ val renderings = renderWorkflowIn(
1398+ workflow = workflow,
1399+ scope = backgroundScope,
1400+ props = props,
1401+ runtimeConfig = runtimeConfig,
1402+ workflowTracer = workflowTracer,
1403+ ) {}
1404+
1405+ launch {
1406+ trigger.emit(" changed state" )
1407+ }
1408+ val collectionJob = launch(UnconfinedTestDispatcher (testScheduler)) {
1409+ // Collect this unconfined so we can get all the renderings faster than actions can
1410+ // be processed.
1411+ renderings.collect {
1412+ emitted + = it.rendering
1413+ }
1414+ }
1415+ advanceUntilIdle()
1416+ runCurrent()
1417+
1418+ collectionJob.cancel()
1419+
1420+ // 3 renderings because each had output.
1421+ assertEquals(3 , emitted.size)
1422+ assertEquals(" changed state+update" , emitted.last())
1423+ assertTrue(childHandlerActionExecuted)
1424+ }
1425+ }
1426+ }
1427+
11831428 private class ExpectedException : RuntimeException ()
11841429
11851430 private fun <T1 , T2 > cartesianProduct (
0 commit comments