@@ -43,6 +43,7 @@ import kotlin.time.Duration
4343import kotlin.time.Duration.Companion.milliseconds
4444import kotlin.time.DurationUnit
4545import kotlinx.cinterop.ExperimentalForeignApi
46+ import kotlinx.coroutines.CoroutineScope
4647import kotlinx.coroutines.delay
4748import kotlinx.coroutines.runBlocking
4849import 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