Skip to content

Commit 1066f42

Browse files
authored
Retry flaky memory leaks tests (#2365)
## Release Notes N/A
1 parent c871c63 commit 1066f42

File tree

1 file changed

+117
-83
lines changed
  • compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/leaks

1 file changed

+117
-83
lines changed

compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/leaks/MemoryLeaksTest.kt

Lines changed: 117 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import kotlin.time.Duration
4343
import kotlin.time.Duration.Companion.milliseconds
4444
import kotlin.time.DurationUnit
4545
import kotlinx.cinterop.ExperimentalForeignApi
46+
import kotlinx.coroutines.CoroutineScope
4647
import kotlinx.coroutines.delay
4748
import kotlinx.coroutines.runBlocking
4849
import platform.CoreGraphics.CGRectMake
@@ -60,7 +61,7 @@ class MemoryLeaksTest {
6061
}
6162

6263
@Test
63-
fun testComposeUIViewControllerDisposal() = runBlocking {
64+
fun testComposeUIViewControllerDisposal() = runRepeatingBlocking {
6465
val appDelegate = MockAppDelegate()
6566
var composeViewControllerRef: WeakReference<UIViewController>? = null
6667
var composeLoaded = false
@@ -91,7 +92,7 @@ class MemoryLeaksTest {
9192
}
9293

9394
@Test
94-
fun testComposeUIViewControllerSubviewsDisposal() = runBlocking {
95+
fun testComposeUIViewControllerSubviewsDisposal() = runRepeatingBlocking {
9596
val appDelegate = MockAppDelegate()
9697
val subviewsReferences = mutableListOf<WeakReference<UIView>>()
9798

@@ -166,104 +167,113 @@ class MemoryLeaksTest {
166167

167168
@OptIn(ExperimentalForeignApi::class, ExperimentalFoundationApi::class)
168169
@Test
169-
fun testComposeUIViewControllerSubviewsWithTextInputDisposalAndOldContextMenu() = runBlocking(
170-
newContextMenuEnabled = false
171-
) {
172-
val appDelegate = MockAppDelegate()
173-
val subviewsReferences = mutableListOf<WeakReference<UIView>>()
174-
175-
run {
176-
val controller = ComposeUIViewController({
177-
enforceStrictPlistSanityCheck = false
178-
}) {
179-
val focusRequester = FocusRequester()
180-
TextField(
181-
value = "",
182-
onValueChange = {},
183-
modifier = Modifier.focusRequester(focusRequester)
184-
)
185-
LaunchedEffect(Unit) {
186-
focusRequester.requestFocus()
170+
fun testComposeUIViewControllerSubviewsWithTextInputDisposalAndOldContextMenu() =
171+
runRepeatingBlocking(newContextMenuEnabled = false) {
172+
val appDelegate = MockAppDelegate()
173+
val subviewsReferences = mutableListOf<WeakReference<UIView>>()
174+
175+
run {
176+
val controller = ComposeUIViewController({
177+
enforceStrictPlistSanityCheck = false
178+
}) {
179+
val focusRequester = FocusRequester()
180+
TextField(
181+
value = "",
182+
onValueChange = {},
183+
modifier = Modifier.focusRequester(focusRequester)
184+
)
185+
LaunchedEffect(Unit) {
186+
focusRequester.requestFocus()
187+
}
187188
}
188-
}
189189

190-
appDelegate.setUpWindow(controller)
191-
}
190+
appDelegate.setUpWindow(controller)
191+
}
192192

193-
// Allow run loop to start the application
194-
runApplicationLoop(KeyboardAnimationDelay)
193+
// Allow run loop to start the application
194+
runApplicationLoop(KeyboardAnimationDelay)
195195

196-
collectSubviewsRecursively(
197-
appDelegate.window?.rootViewController?.view!!,
198-
subviewsReferences
199-
)
196+
collectSubviewsRecursively(
197+
appDelegate.window?.rootViewController?.view!!,
198+
subviewsReferences
199+
)
200200

201-
assertEquals(
202-
expected = 5,
203-
actual = subviewsReferences.count(),
204-
message = "Expected 5 subviews: [ComposeView, UserInputView, MetalView, UIKitTransparentContainerView, IntermediateTextInputUIView]" +
205-
", but given: ${subviewsReferences.mapNotNull { it.get()?.let { it::class.simpleName } }}"
206-
)
201+
assertEquals(
202+
expected = 5,
203+
actual = subviewsReferences.count(),
204+
message = "Expected 5 subviews: [ComposeView, UserInputView, MetalView, UIKitTransparentContainerView, IntermediateTextInputUIView]" +
205+
", but given: ${
206+
subviewsReferences.mapNotNull {
207+
it.get()?.let { it::class.simpleName }
208+
}
209+
}"
210+
)
207211

208-
appDelegate.cleanUp()
209-
// In Kotlin, when UITextInput view becomes a first responder, UIKit captures
210-
// strong references on this view. For test purposes, staring another text input session
211-
// to let UIKit release reference to the previous text input view.
212-
startFakeTextInputSession()
212+
appDelegate.cleanUp()
213+
// In Kotlin, when UITextInput view becomes a first responder, UIKit captures
214+
// strong references on this view. For test purposes, staring another text input session
215+
// to let UIKit release reference to the previous text input view.
216+
startFakeTextInputSession()
213217

214-
cleanupMemory()
218+
cleanupMemory()
215219

216-
assertEquals(emptyList(), subviewsReferences.mapNotNull { it.get() })
217-
}
220+
assertEquals(emptyList(), subviewsReferences.mapNotNull { it.get() })
221+
}
218222

219223
@OptIn(ExperimentalForeignApi::class, ExperimentalFoundationApi::class)
220224
@Test
221-
fun testComposeUIViewControllerSubviewsWithTextInputDisposalAndNewContextMenu() = runBlocking(
222-
newContextMenuEnabled = true
223-
) {
224-
val appDelegate = MockAppDelegate()
225-
val subviewsReferences = mutableListOf<WeakReference<UIView>>()
226-
227-
run {
228-
val controller = ComposeUIViewController({
229-
enforceStrictPlistSanityCheck = false
230-
}) {
231-
val focusRequester = FocusRequester()
232-
TextField(
233-
value = "",
234-
onValueChange = {},
235-
modifier = Modifier.focusRequester(focusRequester)
236-
)
237-
LaunchedEffect(Unit) {
238-
focusRequester.requestFocus()
225+
fun testComposeUIViewControllerSubviewsWithTextInputDisposalAndNewContextMenu() =
226+
runRepeatingBlocking(newContextMenuEnabled = true) {
227+
val appDelegate = MockAppDelegate()
228+
val subviewsReferences = mutableListOf<WeakReference<UIView>>()
229+
230+
run {
231+
val controller = ComposeUIViewController({
232+
enforceStrictPlistSanityCheck = false
233+
}) {
234+
val focusRequester = FocusRequester()
235+
TextField(
236+
value = "",
237+
onValueChange = {},
238+
modifier = Modifier.focusRequester(focusRequester)
239+
)
240+
LaunchedEffect(Unit) {
241+
focusRequester.requestFocus()
242+
}
239243
}
240-
}
241244

242-
appDelegate.setUpWindow(controller)
243-
}
245+
appDelegate.setUpWindow(controller)
246+
}
244247

245-
// Allow run loop to start the application
246-
runApplicationLoop(KeyboardAnimationDelay)
248+
// Allow run loop to start the application
249+
runApplicationLoop(KeyboardAnimationDelay)
247250

248-
collectSubviewsRecursively(appDelegate.window?.rootViewController?.view!!, subviewsReferences)
251+
collectSubviewsRecursively(
252+
appDelegate.window?.rootViewController?.view!!,
253+
subviewsReferences
254+
)
249255

250-
assertEquals(
251-
expected = 6,
252-
actual = subviewsReferences.count(),
253-
message = "Expected 6 subviews: [ComposeView, UserInputView, MetalView, UIKitTransparentContainerView, CMPEditMenuView, IntermediateTextInputUIView]" +
254-
", but given: ${subviewsReferences.mapNotNull { it.get()?.let { it::class.simpleName } }}"
255-
)
256+
assertEquals(
257+
expected = 6,
258+
actual = subviewsReferences.count(),
259+
message = "Expected 6 subviews: [ComposeView, UserInputView, MetalView, UIKitTransparentContainerView, CMPEditMenuView, IntermediateTextInputUIView]" +
260+
", but given: ${
261+
subviewsReferences.mapNotNull {
262+
it.get()?.let { it::class.simpleName }
263+
}
264+
}"
265+
)
256266

257-
appDelegate.cleanUp()
258-
// In Kotlin, when UITextInput view becomes a first responder, UIKit captures
259-
// strong references on this view. For test purposes, staring another text input session
260-
// to let UIKit release reference to the previous text input view.
261-
startFakeTextInputSession()
267+
appDelegate.cleanUp()
268+
// In Kotlin, when UITextInput view becomes a first responder, UIKit captures
269+
// strong references on this view. For test purposes, staring another text input session
270+
// to let UIKit release reference to the previous text input view.
271+
startFakeTextInputSession()
262272

263-
cleanupMemory()
273+
cleanupMemory()
264274

265-
assertEquals(emptyList(), subviewsReferences.mapNotNull { it.get() })
266-
}
275+
assertEquals(emptyList(), subviewsReferences.mapNotNull { it.get() })
276+
}
267277

268278
private fun collectSubviewsRecursively(
269279
view: UIView,
@@ -302,13 +312,37 @@ class MemoryLeaksTest {
302312
}
303313

304314
@OptIn(ExperimentalFoundationApi::class)
305-
private fun runBlocking(newContextMenuEnabled: Boolean, block: suspend () -> Unit) {
315+
private fun runRepeatingBlocking(newContextMenuEnabled: Boolean, block: suspend () -> Unit) {
306316
val defaultValue = ComposeFoundationFlags.isNewContextMenuEnabled
307317
try {
308318
ComposeFoundationFlags.isNewContextMenuEnabled = newContextMenuEnabled
309-
runBlocking { block() }
319+
runRepeatingBlocking { block() }
310320
} finally {
311321
ComposeFoundationFlags.isNewContextMenuEnabled = defaultValue
312322
}
313323
}
324+
325+
private fun runRepeatingBlocking(
326+
total: Int = 10,
327+
successRequired: Int = 2,
328+
testBlock: suspend CoroutineScope.(Int) -> Unit
329+
) = runBlocking {
330+
var successCount = 0
331+
var failureCount = 0
332+
repeat(total) {
333+
try {
334+
testBlock(successCount + failureCount)
335+
successCount++
336+
if (successCount >= successRequired) {
337+
return@runBlocking
338+
}
339+
} catch (e: Throwable) {
340+
failureCount++
341+
if (failureCount > total - successRequired) {
342+
throw e
343+
}
344+
cleanupMemory()
345+
}
346+
}
347+
}
314348
}

0 commit comments

Comments
 (0)