@@ -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,248 @@ 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+ 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+
11831427 private class ExpectedException : RuntimeException ()
11841428
11851429 private fun <T1 , T2 > cartesianProduct (
0 commit comments