Skip to content

Commit aecdb30

Browse files
HS0204criticalAY
authored andcommitted
test: add tests for potential update skip
- reproduce and verify the concern where lastValue check against StateFlow.value may skip UI updates during lifecycle transitions
1 parent 757f342 commit aecdb30

1 file changed

Lines changed: 76 additions & 0 deletions

File tree

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright (c) 2026 HS0204(Hanseul Lee) <lhanseul0204@gmail.com>
3+
*
4+
* This program is free software; you can redistribute it and/or modify it under
5+
* the terms of the GNU General Public License as published by the Free Software
6+
* Foundation; either version 3 of the License, or (at your option) any later
7+
* version.
8+
*
9+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
10+
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
11+
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License along with
14+
* this program. If not, see <http://www.gnu.org/licenses/>.
15+
*/
16+
package com.ichi2.anki.utils.ext
17+
18+
import android.content.Intent
19+
import com.ichi2.anki.AnkiActivity
20+
import com.ichi2.anki.RobolectricTest
21+
import kotlinx.coroutines.ExperimentalForInheritanceCoroutinesApi
22+
import kotlinx.coroutines.flow.FlowCollector
23+
import kotlinx.coroutines.flow.MutableStateFlow
24+
import kotlinx.coroutines.flow.StateFlow
25+
import org.junit.Assert.assertEquals
26+
import org.junit.Test
27+
import org.junit.runner.RunWith
28+
import org.robolectric.Robolectric
29+
import org.robolectric.RobolectricTestRunner
30+
31+
// test for the deduplication logic in StateFlow.launchCollectionInLifecycleScope
32+
@RunWith(RobolectricTestRunner::class)
33+
class StateFlowLifecycleCollectionTest : RobolectricTest() {
34+
// scenario: background > value changes > resume triggers race
35+
// to check StateFlow logic ensure that all updates are delivered and none are potentially skipped
36+
@Test
37+
fun `concurrent value change does not skip UI update`() {
38+
val controller = Robolectric.buildActivity(AnkiActivity::class.java, Intent())
39+
val flow = MutableStateFlow(0)
40+
val concurrentFlow =
41+
ConcurrentStateFlow(flow) { emitted ->
42+
if (emitted == 1) flow.value = 2
43+
}
44+
val updates = mutableListOf<Int>()
45+
46+
controller.create().start().resume()
47+
with(controller.get()) {
48+
concurrentFlow.launchCollectionInLifecycleScope { updates.add(it) }
49+
}
50+
advanceRobolectricLooper()
51+
52+
assertEquals(listOf(0), updates)
53+
54+
controller.pause().stop()
55+
advanceRobolectricLooper()
56+
flow.value = 1
57+
controller.start().resume()
58+
advanceRobolectricLooper()
59+
60+
assertEquals(listOf(0, 1, 2), updates)
61+
}
62+
63+
// simulates a concurrent value change between emission and the dedup check
64+
@OptIn(ExperimentalForInheritanceCoroutinesApi::class)
65+
private class ConcurrentStateFlow(
66+
private val delegate: MutableStateFlow<Int>,
67+
private val onEmit: (Int) -> Unit,
68+
) : StateFlow<Int> by delegate {
69+
override suspend fun collect(collector: FlowCollector<Int>): Nothing {
70+
delegate.collect { emitted ->
71+
onEmit(emitted)
72+
collector.emit(emitted)
73+
}
74+
}
75+
}
76+
}

0 commit comments

Comments
 (0)