1 /*
<lambda>null2 * Copyright 2024 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.tooling
18
19 import androidx.compose.runtime.Composable
20 import androidx.compose.runtime.Composer
21 import androidx.compose.runtime.DisposableEffect
22 import androidx.compose.runtime.ReusableContent
23 import androidx.compose.runtime.ReusableContentHost
24 import androidx.compose.runtime.getValue
25 import androidx.compose.runtime.key
26 import androidx.compose.runtime.mock.CompositionTestScope
27 import androidx.compose.runtime.mock.compositionTest
28 import androidx.compose.runtime.mutableStateOf
29 import androidx.compose.runtime.remember
30 import androidx.compose.runtime.setValue
31 import androidx.compose.runtime.snapshots.fastForEach
32 import kotlin.test.AfterTest
33 import kotlin.test.BeforeTest
34 import kotlin.test.Test
35 import kotlin.test.assertEquals
36
37 class ErrorTraceTests {
38 @BeforeTest
39 fun setUp() {
40 Composer.setDiagnosticStackTraceEnabled(true)
41 }
42
43 @AfterTest
44 fun tearDown() {
45 Composer.setDiagnosticStackTraceEnabled(false)
46 }
47
48 @Test
49 fun setContent() =
50 exceptionTest("<lambda>(ErrorTraceTests.kt:<unknown line>)") {
51 compose { throwTestException() }
52 }
53
54 @Test
55 fun recompose() =
56 exceptionTest(
57 "<lambda>(ErrorTraceTests.kt:<unknown line>)",
58 ) {
59 var state by mutableStateOf(false)
60 compose {
61 if (state) {
62 throwTestException()
63 }
64 }
65
66 state = true
67 advance()
68 }
69
70 @Test
71 fun setContentLinear() =
72 exceptionTest(
73 "<lambda>(ErrorTraceTests.kt:<unknown line>)",
74 "<lambda>(ErrorTraceComposables.kt:77)",
75 "ReusableComposeNode(Composables.kt:<line number>)",
76 "Linear(ErrorTraceComposables.kt:73)",
77 "<lambda>(ErrorTraceTests.kt:<line number>)"
78 ) {
79 compose { Linear { throwTestException() } }
80 }
81
82 @Test
83 fun recomposeLinear() =
84 exceptionTest(
85 "<lambda>(ErrorTraceTests.kt:<unknown line>)",
86 "<lambda>(ErrorTraceComposables.kt:77)",
87 "ReusableComposeNode(Composables.kt:<line number>)",
88 "Linear(ErrorTraceComposables.kt:73)",
89 "<lambda>(ErrorTraceTests.kt:<line number>)"
90 ) {
91 var state by mutableStateOf(false)
92 compose {
93 Linear {
94 if (state) {
95 throwTestException()
96 }
97 }
98 }
99
100 state = true
101 advance()
102 }
103
104 @Test
105 fun setContentInlineLinear() =
106 exceptionTest(
107 "<lambda>(ErrorTraceTests.kt:<unknown line>)",
108 "<lambda>(ErrorTraceComposables.kt:87)",
109 "ReusableComposeNode(Composables.kt:<line number>)",
110 "InlineLinear(ErrorTraceComposables.kt:83)",
111 "<lambda>(ErrorTraceTests.kt:<line number>)"
112 ) {
113 compose { InlineLinear { throwTestException() } }
114 }
115
116 @Test
117 fun recomposeInlineLinear() =
118 exceptionTest(
119 "<lambda>(ErrorTraceTests.kt:<unknown line>)",
120 "<lambda>(ErrorTraceComposables.kt:87)",
121 "ReusableComposeNode(Composables.kt:<line number>)",
122 "InlineLinear(ErrorTraceComposables.kt:83)",
123 "<lambda>(ErrorTraceTests.kt:<line number>)"
124 ) {
125 var state by mutableStateOf(false)
126
127 compose {
128 InlineLinear {
129 if (state) {
130 throwTestException()
131 }
132 }
133 }
134
135 state = true
136 advance()
137 }
138
139 @Test
140 fun setContentAfterTextInLoopInlineWrapper() =
141 exceptionTest(
142 "<lambda>(ErrorTraceTests.kt:<unknown line>)",
143 "InlineWrapper(ErrorTraceComposables.kt:57)",
144 "<lambda>(ErrorTraceTests.kt:<line number>)"
145 ) {
146 compose {
147 InlineWrapper {
148 repeat(5) { it ->
149 Text("test")
150 if (it > 3) {
151 throwTestException()
152 }
153 }
154 }
155 }
156 }
157
158 @Test
159 fun recomposeAfterTextInLoopInlineWrapper() =
160 exceptionTest(
161 "<lambda>(ErrorTraceTests.kt:<unknown line>)",
162 "InlineWrapper(ErrorTraceComposables.kt:57)",
163 "<lambda>(ErrorTraceTests.kt:<line number>)"
164 ) {
165 var state by mutableStateOf(false)
166
167 compose {
168 InlineWrapper {
169 repeat(5) { it ->
170 Text("test")
171 if (it > 3 && state) {
172 throwTestException()
173 }
174 }
175 }
176 }
177
178 state = true
179 advance()
180 }
181
182 @Test
183 fun setContentAfterTextInLoop() =
184 exceptionTest(
185 "<lambda>(ErrorTraceTests.kt:<unknown line>)",
186 "Repeated(ErrorTraceComposables.kt:94)",
187 "<lambda>(ErrorTraceTests.kt:<line number>)"
188 ) {
189 compose {
190 Repeated(List(10) { it }) {
191 Text("test")
192 throwTestException()
193 }
194 }
195 }
196
197 @Test
198 fun recomposeAfterTextInLoop() =
199 exceptionTest(
200 "<lambda>(ErrorTraceTests.kt:<unknown line>)",
201 "Repeated(ErrorTraceComposables.kt:94)",
202 "<lambda>(ErrorTraceTests.kt:<line number>)"
203 ) {
204 var state by mutableStateOf(false)
205
206 compose {
207 Repeated(List(10) { it }) {
208 Text("test")
209 if (state) {
210 throwTestException()
211 }
212 }
213 }
214
215 state = true
216 advance()
217 }
218
219 @Test
220 fun setContentSubcomposition() =
221 exceptionTest(
222 "<lambda>(ErrorTraceTests.kt:<unknown line>)",
223 "<lambda>(ErrorTraceComposables.kt:66)",
224 "Subcompose(ErrorTraceComposables.kt:62)",
225 "<lambda>(ErrorTraceTests.kt:<line number>)"
226 ) {
227 compose { Subcompose { throwTestException() } }
228 }
229
230 @Test
231 fun recomposeSubcomposition() =
232 exceptionTest(
233 "<lambda>(ErrorTraceTests.kt:<unknown line>)",
234 "<lambda>(ErrorTraceComposables.kt:66)",
235 "Subcompose(ErrorTraceComposables.kt:62)",
236 "<lambda>(ErrorTraceTests.kt:<line number>)"
237 ) {
238 var state by mutableStateOf(false)
239
240 compose {
241 Subcompose {
242 if (state) {
243 throwTestException()
244 }
245 }
246 }
247
248 state = true
249 advance()
250 }
251
252 @Test
253 fun setContentDefaults() =
254 exceptionTest(
255 "<lambda>(ErrorTraceTests.kt:<unknown line>)",
256 "ComposableWithDefaults(ErrorTraceComposables.kt:109)",
257 "<lambda>(ErrorTraceTests.kt:<line number>)"
258 ) {
259 compose { ComposableWithDefaults { throwTestException() } }
260 }
261
262 @Test
263 fun recomposeDefaults() =
264 exceptionTest(
265 "<lambda>(ErrorTraceTests.kt:<unknown line>)",
266 "ComposableWithDefaults(ErrorTraceComposables.kt:109)",
267 "<lambda>(ErrorTraceTests.kt:<line number>)"
268 ) {
269 var state by mutableStateOf(false)
270
271 compose {
272 ComposableWithDefaults {
273 if (state) {
274 throwTestException()
275 }
276 }
277 }
278
279 state = true
280 advance()
281 }
282
283 @Test
284 fun setContentRemember() =
285 exceptionTest(
286 "remember(ErrorTraceTests.kt:<unknown line>)",
287 "<lambda>(ErrorTraceTests.kt:<line number>)"
288 ) {
289 compose { remember { throwTestException() } }
290 }
291
292 @Test
293 fun setContentRememberObserver() =
294 exceptionTest(
295 "remember(Effects.kt:<unknown line>)",
296 "DisposableEffect(Effects.kt:<line number>)",
297 "<lambda>(ErrorTraceTests.kt:<line number>)"
298 ) {
299 compose { DisposableEffect(Unit) { throwTestException() } }
300 }
301
302 @Test
303 fun recomposeRememberObserver() =
304 exceptionTest(
305 "remember(Effects.kt:<unknown line>)",
306 "DisposableEffect(Effects.kt:<line number>)",
307 "<lambda>(ErrorTraceTests.kt:<line number>)"
308 ) {
309 var state by mutableStateOf(false)
310 compose {
311 DisposableEffect(state) {
312 if (state) {
313 throwTestException()
314 }
315 onDispose {}
316 }
317 }
318
319 state = true
320 advance()
321 }
322
323 @Test
324 fun nodeReuse() =
325 exceptionTest(
326 // todo(b/409033128): Investigate why NodeWithCallbacks has unknown line
327 // missing ReusableComposeNode because writer is not set directly to the node group
328 "NodeWithCallbacks(ErrorTraceComposables.kt:<unknown line>)",
329 "<lambda>(ErrorTraceTests.kt:<line number>)",
330 "ReusableContent(Composables.kt:<line number>)",
331 "<lambda>(ErrorTraceTests.kt:<line number>)"
332 ) {
333 var state by mutableStateOf(false)
334 compose {
335 ReusableContent(state) { NodeWithCallbacks(onReuse = { throwTestException() }) }
336 }
337
338 state = true
339 advance()
340 }
341
342 @Test
343 fun nodeDeactivate() =
344 exceptionTest(
345 "ReusableComposeNode(Composables.kt:<unknown line>)",
346 "NodeWithCallbacks(ErrorTraceComposables.kt:122)",
347 "<lambda>(ErrorTraceTests.kt:<line number>)",
348 "ReusableContentHost(Composables.kt:<line number>)",
349 "<lambda>(ErrorTraceTests.kt:<line number>)"
350 ) {
351 var active by mutableStateOf(true)
352 compose {
353 ReusableContentHost(active) {
354 NodeWithCallbacks(onDeactivate = { throwTestException() })
355 }
356 }
357
358 active = false
359 advance()
360 }
361
362 @Test
363 fun setContentNodeAttach() =
364 exceptionTest(
365 "ReusableComposeNode(Composables.kt:<unknown line>)",
366 "NodeWithCallbacks(ErrorTraceComposables.kt:122)",
367 "<lambda>(ErrorTraceTests.kt:<line number>)",
368 "InlineWrapper(ErrorTraceComposables.kt:57)",
369 "<lambda>(ErrorTraceTests.kt:<line number>)"
370 ) {
371 compose { InlineWrapper { NodeWithCallbacks(onAttach = { throwTestException() }) } }
372 }
373
374 @Test
375 fun recomposeNodeAttach() =
376 exceptionTest(
377 "ReusableComposeNode(Composables.kt:<unknown line>)",
378 "NodeWithCallbacks(ErrorTraceComposables.kt:122)",
379 "<lambda>(ErrorTraceTests.kt:<line number>)",
380 "Wrapper(ErrorTraceComposables.kt:149)",
381 "<lambda>(ErrorTraceTests.kt:<line number>)"
382 ) {
383 var state by mutableStateOf(false)
384 compose {
385 Wrapper {
386 if (state) {
387 NodeWithCallbacks(onAttach = { throwTestException() })
388 }
389 }
390 }
391
392 state = true
393 advance()
394 }
395
396 @Test
397 // todo(b/409033128): Investigate why NodeWithCallbacks has an incorrect line
398 fun recomposeNodeAttachInlineWrapper() =
399 exceptionTest(
400 "ReusableComposeNode(Composables.kt:<unknown line>)",
401 "NodeWithCallbacks(ErrorTraceComposables.kt:122)",
402 // (b/380272059): groupless source information is missing here after recomposition
403 // "<lambda>(ErrorTraceTests.kt:<line number>)",
404 // "InlineWrapper(ErrorTraceComposables.kt:148)",
405 "<lambda>(ErrorTraceTests.kt:<line number>)"
406 ) {
407 var state by mutableStateOf(false)
408 compose {
409 InlineWrapper {
410 if (state) {
411 NodeWithCallbacks(onAttach = { throwTestException() })
412 }
413 }
414 }
415
416 state = true
417 advance()
418 }
419
420 @Test
421 fun emptySourceInformation() =
422 exceptionTest(
423 "<lambda>(ErrorTraceTests.kt:<unknown line>)",
424 "<lambda>(ErrorTraceTests.kt:<line number>)",
425 "InlineWrapper(ErrorTraceComposables.kt:57)",
426 "<lambda>(ErrorTraceTests.kt:<line number>)"
427 ) {
428 val list = listOf(1, 2, 3)
429 var content: (@Composable () -> Unit)? = null
430 // some gymnastics to ensure that Kotlin generates a null check
431 if (3 in list) {
432 content = { throwTestException() }
433 }
434
435 compose { InlineWrapper { list.fastForEach { key(it) { content?.invoke() } } } }
436 }
437
438 @Test
439 fun setContentNodeUpdate() =
440 exceptionTest(
441 "ReusableComposeNode(Composables.kt:<unknown line>)",
442 "NodeWithCallbacks(ErrorTraceComposables.kt:122)",
443 "<lambda>(ErrorTraceTests.kt:<line number>)",
444 "Wrapper(ErrorTraceComposables.kt:149)",
445 "<lambda>(ErrorTraceTests.kt:<line number>)"
446 ) {
447 compose { Wrapper { NodeWithCallbacks(onUpdate = { throwTestException() }) } }
448 }
449
450 @Test
451 fun recomposeUpdate() =
452 exceptionTest(
453 "ReusableComposeNode(Composables.kt:<unknown line>)",
454 "NodeWithCallbacks(ErrorTraceComposables.kt:122)",
455 "<lambda>(ErrorTraceTests.kt:<line number>)",
456 "Wrapper(ErrorTraceComposables.kt:149)",
457 "<lambda>(ErrorTraceTests.kt:<line number>)"
458 ) {
459 var state by mutableStateOf(false)
460 compose {
461 Wrapper {
462 if (state) {
463 NodeWithCallbacks(
464 onUpdate =
465 if (state) {
466 { throwTestException() }
467 } else {
468 {}
469 }
470 )
471 }
472 }
473 }
474
475 state = true
476 advance()
477 }
478 }
479
throwTestExceptionnull480 private fun throwTestException(): Nothing = throw TestComposeException()
481
482 private class TestComposeException : Exception("Test exception")
483
484 private const val TestFile = "ErrorTraceComposables.kt"
485 private const val DebugKeepLineNumbers = false
486
487 private fun exceptionTest(vararg trace: String, block: suspend CompositionTestScope.() -> Unit) {
488 assertTrace(trace.toList()) { compositionTest(block) }
489 }
490
assertTracenull491 private fun assertTrace(expected: List<String>, block: () -> Unit) {
492 var exception: TestComposeException? = null
493 try {
494 block()
495 } catch (e: TestComposeException) {
496 exception = e
497 }
498 exception = exception ?: error("Composition exception was not caught or not thrown")
499
500 val composeTrace =
501 exception.suppressedExceptions.firstOrNull { it is DiagnosticComposeException }
502 if (composeTrace == null) {
503 throw exception
504 }
505 val message = composeTrace.message.orEmpty()
506 val frameString =
507 message
508 .substringAfter("Composition stack when thrown:\n")
509 .lines()
510 .filter { it.isNotEmpty() }
511 .map {
512 val trace = it.removePrefix("\tat ")
513 // Only keep the lines in the test file
514 if (trace.contains(TestFile)) {
515 trace
516 } else {
517 val line = trace.substringAfter(':').substringBefore(')')
518 if (line == "<unknown line>" || DebugKeepLineNumbers) {
519 trace
520 } else {
521 trace.replace(line, "<line number>")
522 }
523 }
524 }
525 .joinToString(",\n") { "\"$it\"" }
526
527 val expectedString = expected.joinToString(",\n") { "\"$it\"" }
528 assertEquals(expectedString, frameString)
529 }
530