diff --git a/redwood-treehouse-host/src/appsJvmTest/kotlin/app/cash/redwood/treehouse/LeaksTest.kt b/redwood-treehouse-host/src/appsJvmTest/kotlin/app/cash/redwood/treehouse/LeaksTest.kt index aa5dba93d1..6493d4b4ad 100644 --- a/redwood-treehouse-host/src/appsJvmTest/kotlin/app/cash/redwood/treehouse/LeaksTest.kt +++ b/redwood-treehouse-host/src/appsJvmTest/kotlin/app/cash/redwood/treehouse/LeaksTest.kt @@ -95,7 +95,7 @@ class LeaksTest { val view = tester.view() content.bind(view) - content.awaitContent(untilChangeCount = 1) + content.awaitContent() val eventListenerLeakWatcher = LeakWatcher { (tester.eventListenerFactory as RetainEverythingEventListenerFactory) @@ -156,6 +156,36 @@ class LeaksTest { treehouseApp.stop() } + @Test + fun codeListenerNotLeaked() = runTest { + val tester = TreehouseTester(this) + val treehouseApp = tester.loadApp() + val view = tester.view() + + var codeListener: CodeListener? = RetainEverythingCodeListener(tester.eventLog) + val content = treehouseApp.createContent( + source = { app -> app.launchForTester() }, + codeListener = codeListener!!, + ) + val codeListenerLeakWatcher = LeakWatcher { codeListener } + + // Stop referencing the CodeListener from our test harness. + codeListener = null + + // One a view is bound, the code listener is in a reference cycle. + content.bind(view) + tester.eventLog.takeEvent("onCodeLoaded", skipOthers = true) + codeListenerLeakWatcher.assertObjectInReferenceCycle() + + // When the view is unbound, the code listener is no longer reachable. + content.unbind() + tester.eventLog.takeEvent("onCodeDetached", skipOthers = true) + treehouseApp.dispatchers.awaitLaunchedTasks() + codeListenerLeakWatcher.assertNotLeaked() + + treehouseApp.stop() + } + /** * This is unfortunate. Some cleanup functions launch jobs on another dispatcher and we don't have * a natural way to wait for those jobs to complete. So we launch empty jobs on each dispatcher, diff --git a/redwood-treehouse-host/src/appsJvmTest/kotlin/app/cash/redwood/treehouse/RetainEverythingCodeListener.kt b/redwood-treehouse-host/src/appsJvmTest/kotlin/app/cash/redwood/treehouse/RetainEverythingCodeListener.kt new file mode 100644 index 0000000000..e14328bd0f --- /dev/null +++ b/redwood-treehouse-host/src/appsJvmTest/kotlin/app/cash/redwood/treehouse/RetainEverythingCodeListener.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package app.cash.redwood.treehouse + +class RetainEverythingCodeListener( + private val eventLog: EventLog, +) : CodeListener() { + private var app: TreehouseApp<*>? = null + private var view: TreehouseView<*>? = null + + override fun onInitialCodeLoading(app: TreehouseApp<*>, view: TreehouseView<*>) { + this.app = app + this.view = view + } + + override fun onCodeLoaded(app: TreehouseApp<*>, view: TreehouseView<*>, initial: Boolean) { + this.app = app + this.view = view + eventLog += "onCodeLoaded" + } + + override fun onCodeDetached(app: TreehouseApp<*>, view: TreehouseView<*>, exception: Throwable?) { + this.app = app + this.view = view + eventLog += "onCodeDetached" + } +}