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