1 /*
<lambda>null2 * Copyright 2021 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.compose.runtime
18
19 import androidx.collection.IntObjectMap
20 import androidx.collection.MutableIntObjectMap
21 import androidx.compose.runtime.mock.CompositionTestScope
22 import androidx.compose.runtime.mock.NonReusableText
23 import androidx.compose.runtime.mock.compositionTest
24 import androidx.compose.runtime.mock.expectNoChanges
25 import androidx.compose.runtime.mock.revalidate
26 import androidx.compose.runtime.mock.validate
27 import kotlin.test.Test
28 import kotlin.test.assertEquals
29 import kotlin.test.assertNotEquals
30
31 class CompoundHashKeyTests {
32 @Test // b/157905524
33 fun testWithSubCompose() = compositionTest {
34 val outerKeys = mutableListOf<CompositeKeyHashCode>()
35 val innerKeys = mutableListOf<CompositeKeyHashCode>()
36 val invalidates = mutableListOf<RecomposeScope>()
37 fun invalidateComposition() {
38 invalidates.forEach { it.invalidate() }
39 invalidates.clear()
40 }
41 @Composable
42 fun recordHashKeys() {
43 invalidates.add(currentRecomposeScope)
44 outerKeys.add(currentCompositeKeyHashCode)
45 TestSubcomposition {
46 invalidates.add(currentRecomposeScope)
47 innerKeys.add(currentCompositeKeyHashCode)
48 }
49 }
50
51 val firstOuter = mutableListOf<CompositeKeyHashCode>()
52 val firstInner = mutableListOf<CompositeKeyHashCode>()
53 compose { (0..1).forEach { key(it) { recordHashKeys() } } }
54 assertEquals(2, outerKeys.size)
55 assertEquals(2, innerKeys.size)
56 assertNotEquals(outerKeys[0], outerKeys[1])
57 assertNotEquals(innerKeys[0], innerKeys[1])
58
59 firstOuter.addAll(outerKeys)
60 outerKeys.clear()
61 firstInner.addAll(innerKeys)
62 innerKeys.clear()
63 invalidateComposition()
64
65 expectNoChanges()
66
67 assertEquals(firstInner, innerKeys)
68 assertEquals(firstOuter, outerKeys)
69 }
70
71 @Test // b/195185633
72 fun testEnumKeys() = compositionTest {
73 val testClass = EnumTestClass()
74 compose { testClass.Test() }
75
76 val originalKey = testClass.currentKey
77 testClass.scope.invalidate()
78 advance()
79
80 assertEquals(originalKey, testClass.currentKey)
81 }
82
83 @Test // b/263760668
84 fun testReusableContentNodeKeys() = compositionTest {
85 var keyOnEnter = CompositeKeyHashCode(-1)
86 var keyOnExit = CompositeKeyHashCode(-1)
87
88 var contentKey by mutableStateOf(0)
89
90 compose {
91 ReusableContent(contentKey) {
92 keyOnEnter = currentCompositeKeyHashCode
93
94 NonReusableText("$contentKey")
95
96 keyOnExit = currentCompositeKeyHashCode
97 }
98 }
99
100 assertEquals(keyOnEnter, keyOnExit)
101
102 contentKey = 1
103 advance()
104
105 assertEquals(keyOnEnter, keyOnExit)
106 }
107
108 @Test // b/287537290
109 fun adjacentCallsProduceUniqueKeys() = compositionTest {
110 expectUniqueHashCodes {
111 A()
112 A()
113 }
114 }
115
116 @Test // b/287537290
117 fun indirectConditionalCallsProduceUniqueKeys() = compositionTest {
118 expectUniqueHashCodes {
119 C(condition = true)
120 C(condition = true)
121 }
122 }
123
124 @Test // b/287537290
125 fun repeatedCallsProduceUniqueKeys() = compositionTest {
126 expectUniqueHashCodes { repeat(10) { A() } }
127 }
128
129 @Test
130 fun uniqueKeysGenerateUniqueCompositeKeys() = compositionTest {
131 expectUniqueHashCodes {
132 key(1) { A() }
133 key(2) { A() }
134 }
135 }
136
137 @Test
138 fun duplicateKeysGenerateDuplicateCompositeKeys() = compositionTest {
139 expectHashCodes(duplicateCount = 4) { listOf(1, 2, 1, 2, 1, 2).forEach { key(it) { A() } } }
140 }
141
142 @Test
143 fun compositeKeysAreConsistentBetweenOnRecreate() = compositionTest {
144 val state = mutableStateOf(true)
145 var markers = expectUniqueHashCodes {
146 C(state.value)
147 C(!state.value)
148 }
149 repeat(4) {
150 state.value = !state.value
151 markers = retraceConsistentWith(markers)
152 }
153 }
154
155 @Test // regression test for b/382670037
156 fun nestedCompositeHashKeyIsConsistentOnRecompose() = compositionTest {
157 lateinit var scope: RecomposeScope
158 var lastRecordedHash = CompositeKeyHashCode(-1)
159
160 compose {
161 RestartGroup {}
162 key(0) {
163 RestartGroup {
164 scope = currentRecomposeScope
165 lastRecordedHash = currentCompositeKeyHashCode
166 }
167 }
168 }
169
170 validate {}
171 val originalHash = lastRecordedHash
172 lastRecordedHash = -1
173 scope.invalidate()
174 expectNoChanges()
175 revalidate()
176 assertEquals(originalHash, lastRecordedHash)
177 }
178
179 @Test
180 fun compositeKeysMoveWithKeys() = compositionTest {
181 val list = mutableStateListOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
182 var markers = expectUniqueHashCodes {
183 for (item in list) {
184 key(item) { A() }
185 }
186 }
187 list.reverse()
188 markers = retraceConsistentWith(markers)
189 list.reverse()
190 markers = retraceConsistentWith(markers)
191 list.shuffle()
192 markers = retraceConsistentWith(markers)
193 list.shuffle()
194 retraceConsistentWith(markers)
195 }
196 }
197
198 private class EnumTestClass {
199 var currentKey = EmptyCompositeKeyHashCode
200 lateinit var scope: RecomposeScope
201 val state = mutableStateOf(0)
202 private val config = mutableStateOf(Config.A)
203
204 @Composable
Testnull205 fun Test() {
206 key(config.value) { Child() }
207 }
208
209 @Composable
Childnull210 private fun Child() {
211 scope = currentRecomposeScope
212 currentKey = currentCompositeKeyHashCode
213 }
214
215 enum class Config {
216 A,
217 B
218 }
219 }
220
221 private var hashTraceRecomposeState = mutableStateOf(0)
222 private var hashTrace = mutableListOf<CompositeKeyHashCode>()
223 private var markerToHash = MutableIntObjectMap<CompositeKeyHashCode>()
224
225 private var marker = 100
226
newMarkernull227 private fun newMarker() = marker++
228
229 private data class TraceResult(
230 val trace: List<CompositeKeyHashCode>,
231 val markers: MutableIntObjectMap<CompositeKeyHashCode>
232 )
233
234 private fun CompositionTestScope.composeTrace(content: @Composable () -> Unit): TraceResult {
235 hashTrace = mutableListOf()
236 markerToHash = MutableIntObjectMap()
237 compose(content)
238 val result = TraceResult(hashTrace, markerToHash)
239 hashTrace = mutableListOf()
240 markerToHash = MutableIntObjectMap()
241 return result
242 }
243
retracenull244 private fun CompositionTestScope.retrace(): TraceResult {
245 hashTraceRecomposeState.value++
246 hashTrace = mutableListOf()
247 markerToHash = MutableIntObjectMap()
248 advance()
249 val result = TraceResult(hashTrace, markerToHash)
250 hashTrace = mutableListOf()
251 markerToHash = MutableIntObjectMap()
252 return result
253 }
254
CompositionTestScopenull255 private fun CompositionTestScope.retraceConsistentWith(
256 markers: IntObjectMap<CompositeKeyHashCode>
257 ): IntObjectMap<CompositeKeyHashCode> {
258 val recomposeMarkers = retrace().markers
259 return markers.mergedWith(recomposeMarkers) { key, existing, merged ->
260 error("Inconsistent marker $key expected hash $existing but found $merged")
261 }
262 }
263
CompositionTestScopenull264 private fun CompositionTestScope.expectUniqueHashCodes(content: @Composable () -> Unit) =
265 expectHashCodes(duplicateCount = 0, content)
266
267 private fun CompositionTestScope.expectHashCodes(
268 duplicateCount: Int,
269 content: @Composable () -> Unit
270 ): IntObjectMap<CompositeKeyHashCode> {
271 val (hashCodes, markers) = composeTrace(content)
272 val uniqueCodes = hashCodes.distinct()
273 assertEquals(
274 hashCodes.size,
275 uniqueCodes.size + duplicateCount,
276 if (duplicateCount == 0)
277 "Non-unique codes detected. " +
278 "Count of unique hash codes doesn't match the number of codes collected"
279 else
280 "Expected $duplicateCount keys but found but found ${
281 hashCodes.size - uniqueCodes.size
282 }"
283 )
284 val (recomposeTrace, recomposeMarkers) = retrace()
285 assertEquals(hashCodes.size, recomposeTrace.size)
286 hashCodes.forEachIndexed { index, code ->
287 assertEquals(code, recomposeTrace[index], "Unexpected hash code at $index")
288 }
289
290 return markers.mergedWith(recomposeMarkers) { key, existing, merged ->
291 error("Inconsistent marker $key expected hash $existing but found $merged")
292 }
293 }
294
IntObjectMapnull295 private fun IntObjectMap<CompositeKeyHashCode>.mergedWith(
296 other: IntObjectMap<CompositeKeyHashCode>,
297 inconsistent:
298 (
299 key: Int, existingValue: CompositeKeyHashCode, mergedValue: CompositeKeyHashCode
300 ) -> CompositeKeyHashCode
301 ): IntObjectMap<CompositeKeyHashCode> {
302 val result = MutableIntObjectMap<CompositeKeyHashCode>()
303 forEach { key, value ->
304 var mergedValue = value
305 if (key in other) {
306 val otherValue = other[key] ?: EmptyCompositeKeyHashCode
307 if (otherValue != value) mergedValue = inconsistent(key, value, otherValue)
308 }
309 result[key] = mergedValue
310 }
311 other.forEach { key, value ->
312 if (key !in this) {
313 result[key] = value
314 }
315 }
316 return result
317 }
318
319 @Composable
<lambda>null320 private fun A(marker: Int = remember { newMarker() }) {
321 hashTraceRecomposeState.value
322 val hash = currentCompositeKeyHashCode
323 hashTrace.add(hash)
324 markerToHash[marker] = hash
325 }
326
327 @Composable
Bnull328 private fun B() {
329 A()
330 A()
331 A()
332 }
333
334 @Composable
Cnull335 private fun C(condition: Boolean) {
336 val one = remember { newMarker() }
337 val two = remember { newMarker() }
338 val three = remember { newMarker() }
339 if (condition) {
340 A(one)
341 A(two)
342 A(three)
343 }
344 }
345