diff --git a/redwood-treehouse-host/api/android/redwood-treehouse-host.api b/redwood-treehouse-host/api/android/redwood-treehouse-host.api index b20a56c44f..81f6c00033 100644 --- a/redwood-treehouse-host/api/android/redwood-treehouse-host.api +++ b/redwood-treehouse-host/api/android/redwood-treehouse-host.api @@ -81,6 +81,7 @@ public abstract interface class app/cash/redwood/treehouse/StateStore { public final class app/cash/redwood/treehouse/TreehouseApp { public synthetic fun (Lapp/cash/redwood/treehouse/TreehouseApp$Factory;Lkotlinx/coroutines/CoroutineScope;Lapp/cash/redwood/treehouse/TreehouseApp$Spec;Lapp/cash/redwood/treehouse/EventListener$Factory;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun close ()V public final fun createContent (Lapp/cash/redwood/treehouse/TreehouseContentSource;Lapp/cash/redwood/treehouse/CodeListener;)Lapp/cash/redwood/treehouse/Content; public static synthetic fun createContent$default (Lapp/cash/redwood/treehouse/TreehouseApp;Lapp/cash/redwood/treehouse/TreehouseContentSource;Lapp/cash/redwood/treehouse/CodeListener;ILjava/lang/Object;)Lapp/cash/redwood/treehouse/Content; public final fun getDispatchers ()Lapp/cash/redwood/treehouse/TreehouseDispatchers; diff --git a/redwood-treehouse-host/api/jvm/redwood-treehouse-host.api b/redwood-treehouse-host/api/jvm/redwood-treehouse-host.api index 58ffae507c..b06476021a 100644 --- a/redwood-treehouse-host/api/jvm/redwood-treehouse-host.api +++ b/redwood-treehouse-host/api/jvm/redwood-treehouse-host.api @@ -81,6 +81,7 @@ public abstract interface class app/cash/redwood/treehouse/StateStore { public final class app/cash/redwood/treehouse/TreehouseApp { public synthetic fun (Lapp/cash/redwood/treehouse/TreehouseApp$Factory;Lkotlinx/coroutines/CoroutineScope;Lapp/cash/redwood/treehouse/TreehouseApp$Spec;Lapp/cash/redwood/treehouse/EventListener$Factory;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun close ()V public final fun createContent (Lapp/cash/redwood/treehouse/TreehouseContentSource;Lapp/cash/redwood/treehouse/CodeListener;)Lapp/cash/redwood/treehouse/Content; public static synthetic fun createContent$default (Lapp/cash/redwood/treehouse/TreehouseApp;Lapp/cash/redwood/treehouse/TreehouseContentSource;Lapp/cash/redwood/treehouse/CodeListener;ILjava/lang/Object;)Lapp/cash/redwood/treehouse/Content; public final fun getDispatchers ()Lapp/cash/redwood/treehouse/TreehouseDispatchers; diff --git a/redwood-treehouse-host/api/redwood-treehouse-host.klib.api b/redwood-treehouse-host/api/redwood-treehouse-host.klib.api index 648b7bdf44..fa8b421816 100644 --- a/redwood-treehouse-host/api/redwood-treehouse-host.klib.api +++ b/redwood-treehouse-host/api/redwood-treehouse-host.klib.api @@ -74,6 +74,7 @@ final class <#A: app.cash.redwood.treehouse/AppService> app.cash.redwood.treehou final val dispatchers // app.cash.redwood.treehouse/TreehouseApp.Factory.dispatchers|(){}[0] final fun (): app.cash.redwood.treehouse/TreehouseDispatchers // app.cash.redwood.treehouse/TreehouseApp.Factory.dispatchers.|(){}[0] } + final fun close() // app.cash.redwood.treehouse/TreehouseApp.close|close(){}[0] final fun createContent(app.cash.redwood.treehouse/TreehouseContentSource<#A>, app.cash.redwood.treehouse/CodeListener =...): app.cash.redwood.treehouse/Content // app.cash.redwood.treehouse/TreehouseApp.createContent|createContent(app.cash.redwood.treehouse.TreehouseContentSource<1:0>;app.cash.redwood.treehouse.CodeListener){}[0] final fun restart() // app.cash.redwood.treehouse/TreehouseApp.restart|restart(){}[0] final fun start() // app.cash.redwood.treehouse/TreehouseApp.start|start(){}[0] 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 55ed79794d..72ff0c68ae 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 @@ -19,6 +19,7 @@ import app.cash.redwood.treehouse.leaks.LeakWatcher import assertk.assertThat import assertk.assertions.isEmpty import assertk.assertions.isEqualTo +import assertk.assertions.isNotNull import com.example.redwood.testapp.testing.TextInputValue import kotlin.test.Test import kotlinx.coroutines.flow.first @@ -34,7 +35,7 @@ class LeaksTest { content.bind(view) - content.awaitContent(1) + content.awaitContent(untilChangeCount = 1) val textInputValue = view.views.single() as TextInputValue assertThat(textInputValue.text).isEqualTo("what would you like to see?") @@ -48,7 +49,7 @@ class LeaksTest { textInputValue.onChange!!.invoke("Empty") tester.sendFrame() - content.awaitContent(2) + content.awaitContent(untilChangeCount = 2) assertThat(view.views).isEmpty() // Once the widget is removed, the cycle must be broken and the widget must be unreachable. @@ -82,4 +83,42 @@ class LeaksTest { tester.eventLog.takeEvent("test_app.codeUnloaded()", skipOthers = true) serviceLeakWatcher.assertNotLeaked() } + + @Test + fun eventListenerNotLeaked() = runTest { + val tester = TreehouseTester(this) + tester.eventListenerFactory = RetainEverythingEventListenerFactory(tester.eventLog) + val treehouseApp = tester.loadApp() + val content = tester.content(treehouseApp) + val view = tester.view() + + content.bind(view) + content.awaitContent(untilChangeCount = 1) + + val eventListenerLeakWatcher = LeakWatcher { + (tester.eventListenerFactory as RetainEverythingEventListenerFactory) + .also { + assertThat(it.app).isNotNull() + assertThat(it.manifestUrl).isNotNull() + assertThat(it.zipline).isNotNull() + assertThat(it.ziplineManifest).isNotNull() + } + } + + // Stop referencing our EventListener from our test harness. + tester.eventListenerFactory = FakeEventListener.Factory(tester.eventLog) + + // While the listener is in a running app, it's expected to be in a reference cycle. + eventListenerLeakWatcher.assertObjectInReferenceCycle() + + // It's still in a reference cycle after 'stop', because it can be started again. + treehouseApp.stop() + treehouseApp.zipline.first { it == null } + tester.eventLog.takeEvent("codeUnloaded", skipOthers = true) + eventListenerLeakWatcher.assertObjectInReferenceCycle() + + // But after close, it's unreachable. + treehouseApp.close() + eventListenerLeakWatcher.assertNotLeaked() + } } diff --git a/redwood-treehouse-host/src/appsJvmTest/kotlin/app/cash/redwood/treehouse/RetainEverythingEventListenerFactory.kt b/redwood-treehouse-host/src/appsJvmTest/kotlin/app/cash/redwood/treehouse/RetainEverythingEventListenerFactory.kt new file mode 100644 index 0000000000..a7be65186b --- /dev/null +++ b/redwood-treehouse-host/src/appsJvmTest/kotlin/app/cash/redwood/treehouse/RetainEverythingEventListenerFactory.kt @@ -0,0 +1,44 @@ +/* + * 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 + +import app.cash.zipline.Zipline +import app.cash.zipline.ZiplineManifest + +/** An event listener that keeps a reference to everything it sees, for defensive leak testing. */ +class RetainEverythingEventListenerFactory( + private val eventLog: EventLog, +) : EventListener(), EventListener.Factory { + var app: TreehouseApp<*>? = null + var manifestUrl: String? = null + var zipline: Zipline? = null + var ziplineManifest: ZiplineManifest? = null + + override fun create(app: TreehouseApp<*>, manifestUrl: String?): EventListener { + this.app = app + this.manifestUrl = manifestUrl + return this + } + + override fun codeLoadSuccess(manifest: ZiplineManifest, zipline: Zipline, startValue: Any?) { + this.zipline = zipline + this.ziplineManifest = manifest + } + + override fun codeUnloaded() { + eventLog += "codeUnloaded" + } +} diff --git a/redwood-treehouse-host/src/appsTest/kotlin/app/cash/redwood/treehouse/TreehouseTester.kt b/redwood-treehouse-host/src/appsTest/kotlin/app/cash/redwood/treehouse/TreehouseTester.kt index 98a0cde836..046a54c3d9 100644 --- a/redwood-treehouse-host/src/appsTest/kotlin/app/cash/redwood/treehouse/TreehouseTester.kt +++ b/redwood-treehouse-host/src/appsTest/kotlin/app/cash/redwood/treehouse/TreehouseTester.kt @@ -45,6 +45,10 @@ internal class TreehouseTester( ) { val eventLog = EventLog() + var hostApi: HostApi = FakeHostApi() + + var eventListenerFactory: EventListener.Factory = FakeEventListener.Factory(eventLog) + @OptIn(ExperimentalStdlibApi::class) private val testDispatcher = testScope.coroutineContext[CoroutineDispatcher.Key] as TestDispatcher @@ -96,8 +100,6 @@ internal class TreehouseTester( override fun create(scope: CoroutineScope, dispatchers: TreehouseDispatchers) = frameClock } - var hostApi: HostApi = FakeHostApi() - private val treehouseAppFactory = TreehouseApp.Factory( platform = platform, dispatchers = dispatchers, @@ -133,7 +135,7 @@ internal class TreehouseTester( return treehouseAppFactory.create( appScope = testScope, spec = appSpec, - eventListenerFactory = FakeEventListener.Factory(eventLog), + eventListenerFactory = eventListenerFactory, ) } diff --git a/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/CodeHost.kt b/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/CodeHost.kt index 93fcbe68bf..e63b969bf6 100644 --- a/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/CodeHost.kt +++ b/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/CodeHost.kt @@ -87,9 +87,11 @@ internal abstract class CodeHost( get() = state.codeSession /** Returns a flow that emits a new [CodeSession] each time we should load fresh code. */ - abstract fun codeUpdatesFlow(): Flow> + abstract fun codeUpdatesFlow( + eventListenerFactory: EventListener.Factory, + ): Flow> - fun start() { + fun start(eventListenerFactory: EventListener.Factory) { dispatchers.checkUi() val previous = state @@ -101,7 +103,7 @@ internal abstract class CodeHost( val codeUpdatesScope = newCodeUpdatesScope() state = State.Starting(codeUpdatesScope) - codeUpdatesScope.collectCodeUpdates() + codeUpdatesScope.collectCodeUpdates(eventListenerFactory) } /** This function may only be invoked on [TreehouseDispatchers.zipline]. */ @@ -117,7 +119,7 @@ internal abstract class CodeHost( mutableZipline.value = null } - fun restart() { + fun restart(eventListenerFactory: EventListener.Factory) { dispatchers.checkUi() val previous = state @@ -130,7 +132,7 @@ internal abstract class CodeHost( val codeUpdatesScope = newCodeUpdatesScope() state = State.Starting(codeUpdatesScope) mutableZipline.value = null - codeUpdatesScope.collectCodeUpdates() + codeUpdatesScope.collectCodeUpdates(eventListenerFactory) } fun addListener(listener: Listener) { @@ -146,9 +148,9 @@ internal abstract class CodeHost( private fun newCodeUpdatesScope() = CoroutineScope(SupervisorJob(appScope.coroutineContext.job)) - private fun CoroutineScope.collectCodeUpdates() { + private fun CoroutineScope.collectCodeUpdates(eventListenerFactory: EventListener.Factory) { launch(dispatchers.zipline) { - codeUpdatesFlow().collect { + codeUpdatesFlow(eventListenerFactory).collect { codeSessionLoaded(it) } } diff --git a/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/CodeSession.kt b/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/CodeSession.kt index 70e0412c01..7450a4c356 100644 --- a/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/CodeSession.kt +++ b/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/CodeSession.kt @@ -83,6 +83,7 @@ internal abstract class CodeSession( scope.launch(dispatchers.zipline, start = CoroutineStart.ATOMIC) { ziplineStop() scope.cancel() + eventPublisher.close() // Must be last to prevent lost events. } } diff --git a/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/EventPublisher.kt b/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/EventPublisher.kt index 20e567a6dc..2af1b3715f 100644 --- a/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/EventPublisher.kt +++ b/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/EventPublisher.kt @@ -31,4 +31,6 @@ internal interface EventPublisher { fun onUnknownEventNode(id: Id, tag: EventTag) fun onUncaughtException(exception: Throwable) + + fun close() } diff --git a/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/RealEventPublisher.kt b/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/RealEventPublisher.kt index 6da74abb67..fe25ac851b 100644 --- a/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/RealEventPublisher.kt +++ b/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/RealEventPublisher.kt @@ -29,8 +29,11 @@ import app.cash.zipline.ZiplineManifest import app.cash.zipline.ZiplineService internal class RealEventPublisher( - private val listener: EventListener, + listener: EventListener, ) : EventPublisher { + /** Non-null until this publisher is closed. */ + private var listener: EventListener? = listener + override val ziplineEventListener: app.cash.zipline.EventListener = ZiplineEventListener() inner class ZiplineEventListener : app.cash.zipline.EventListener() { @@ -40,13 +43,13 @@ internal class RealEventPublisher( applicationName: String, manifestUrl: String?, ): Any? { - return listener.codeLoadStart() + return listener!!.codeLoadStart() } override fun ziplineCreated( zipline: Zipline, ) { - listener.ziplineCreated(zipline) + listener!!.ziplineCreated(zipline) } override fun applicationLoadSuccess( @@ -56,7 +59,7 @@ internal class RealEventPublisher( zipline: Zipline, startValue: Any?, ) { - listener.codeLoadSuccess(manifest, zipline, startValue) + listener!!.codeLoadSuccess(manifest, zipline, startValue) } override fun applicationLoadSkipped( @@ -64,7 +67,7 @@ internal class RealEventPublisher( manifestUrl: String, startValue: Any?, ) { - listener.codeLoadSkipped(startValue) + listener!!.codeLoadSkipped(startValue) } override fun applicationLoadSkippedNotFresh( @@ -72,7 +75,7 @@ internal class RealEventPublisher( manifestUrl: String?, startValue: Any?, ) { - listener.codeLoadSkippedNotFresh(startValue) + listener!!.codeLoadSkippedNotFresh(startValue) } override fun applicationLoadFailed( @@ -81,7 +84,7 @@ internal class RealEventPublisher( exception: Exception, startValue: Any?, ) { - listener.codeLoadFailed(exception, startValue) + listener!!.codeLoadFailed(exception, startValue) } override fun bindService( @@ -89,26 +92,26 @@ internal class RealEventPublisher( name: String, service: ZiplineService, ) { - listener.bindService(name, service) + listener!!.bindService(name, service) } override fun callStart( zipline: Zipline, call: Call, ): Any? { - return listener.callStart(call) + return listener!!.callStart(call) } override fun callEnd(zipline: Zipline, call: Call, result: CallResult, startValue: Any?) { - listener.callEnd(call, result, startValue) + listener!!.callEnd(call, result, startValue) } override fun downloadStart(applicationName: String, url: String): Any? { - return listener.downloadStart(url) + return listener!!.downloadStart(url) } override fun downloadEnd(applicationName: String, url: String, startValue: Any?) { - listener.downloadSuccess(url, startValue) + listener!!.downloadSuccess(url, startValue) } override fun downloadFailed( @@ -117,7 +120,7 @@ internal class RealEventPublisher( exception: Exception, startValue: Any?, ) { - listener.downloadFailed(url, exception, startValue) + listener!!.downloadFailed(url, exception, startValue) } override fun manifestVerified( @@ -126,7 +129,7 @@ internal class RealEventPublisher( manifest: ZiplineManifest, verifiedKey: String, ) { - listener.manifestVerified(manifest, verifiedKey) + listener!!.manifestVerified(manifest, verifiedKey) } override fun manifestReady( @@ -134,77 +137,83 @@ internal class RealEventPublisher( manifestUrl: String?, manifest: ZiplineManifest, ) { - listener.manifestReady(manifest) + listener!!.manifestReady(manifest) } override fun moduleLoadStart(zipline: Zipline, moduleId: String): Any? { - return listener.moduleLoadStart(moduleId) + return listener!!.moduleLoadStart(moduleId) } override fun moduleLoadEnd(zipline: Zipline, moduleId: String, startValue: Any?) { - listener.moduleLoadEnd(moduleId, startValue) + listener!!.moduleLoadEnd(moduleId, startValue) } override fun initializerStart(zipline: Zipline, applicationName: String): Any? { - return listener.initializerStart(applicationName) + return listener!!.initializerStart(applicationName) } override fun initializerEnd(zipline: Zipline, applicationName: String, startValue: Any?) { - listener.initializerEnd(applicationName, startValue) + listener!!.initializerEnd(applicationName, startValue) } override fun mainFunctionStart(zipline: Zipline, applicationName: String): Any? { - return listener.mainFunctionStart(applicationName) + return listener!!.mainFunctionStart(applicationName) } override fun mainFunctionEnd(zipline: Zipline, applicationName: String, startValue: Any?) { - listener.mainFunctionEnd(applicationName, startValue) + listener!!.mainFunctionEnd(applicationName, startValue) } override fun manifestParseFailed(applicationName: String, url: String?, exception: Exception) { - listener.manifestParseFailed(exception) + listener!!.manifestParseFailed(exception) } override fun takeService(zipline: Zipline, name: String, service: ZiplineService) { - listener.takeService(name, service) + listener!!.takeService(name, service) } override fun serviceLeaked(zipline: Zipline, name: String) { - listener.serviceLeaked(name) + listener!!.serviceLeaked(name) } override fun ziplineClosed(zipline: Zipline) { - listener.codeUnloaded() + listener!!.codeUnloaded() } } - override val widgetProtocolMismatchHandler = object : ProtocolMismatchHandler { + override val widgetProtocolMismatchHandler = RealProtocolMismatchHandler() + + inner class RealProtocolMismatchHandler : ProtocolMismatchHandler { override fun onUnknownWidget(tag: WidgetTag) { - listener.unknownWidget(tag) + listener!!.unknownWidget(tag) } override fun onUnknownModifier(tag: ModifierTag) { - listener.unknownModifier(tag) + listener!!.unknownModifier(tag) } override fun onUnknownChildren(widgetTag: WidgetTag, tag: ChildrenTag) { - listener.unknownChildren(widgetTag, tag) + listener!!.unknownChildren(widgetTag, tag) } override fun onUnknownProperty(widgetTag: WidgetTag, tag: PropertyTag) { - listener.unknownProperty(widgetTag, tag) + listener!!.unknownProperty(widgetTag, tag) } } override fun onUnknownEvent(widgetTag: WidgetTag, tag: EventTag) { - listener.unknownEvent(widgetTag, tag) + listener!!.unknownEvent(widgetTag, tag) } override fun onUnknownEventNode(id: Id, tag: EventTag) { - listener.unknownEventNode(id, tag) + listener!!.unknownEventNode(id, tag) } override fun onUncaughtException(exception: Throwable) { - listener.uncaughtException(exception) + listener!!.uncaughtException(exception) + } + + override fun close() { + listener = null } } diff --git a/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/TreehouseApp.kt b/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/TreehouseApp.kt index 724c063490..0b915a0f69 100644 --- a/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/TreehouseApp.kt +++ b/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/TreehouseApp.kt @@ -43,8 +43,14 @@ public class TreehouseApp private constructor( private val factory: Factory, private val appScope: CoroutineScope, public val spec: Spec, - private val eventListenerFactory: EventListener.Factory, + eventListenerFactory: EventListener.Factory, ) { + /** This property is confined to [TreehouseDispatchers.ui]. */ + private var closed = false + + /** Non-null until this app is closed. This property is confined to [TreehouseDispatchers.ui]. */ + private var eventListenerFactory: EventListener.Factory? = eventListenerFactory + public val dispatchers: TreehouseDispatchers = factory.dispatchers private val codeHost = object : CodeHost( @@ -53,8 +59,10 @@ public class TreehouseApp private constructor( frameClockFactory = factory.frameClockFactory, stateStore = factory.stateStore, ) { - override fun codeUpdatesFlow(): Flow> { - return ziplineFlow().mapNotNull { loadResult -> + override fun codeUpdatesFlow( + eventListenerFactory: EventListener.Factory, + ): Flow> { + return ziplineFlow(eventListenerFactory).mapNotNull { loadResult -> when (loadResult) { is LoadResult.Failure -> { null // EventListener already notified. @@ -106,7 +114,8 @@ public class TreehouseApp private constructor( * This function may only be invoked on [TreehouseDispatchers.ui]. */ public fun start() { - codeHost.start() + val eventListenerFactory = eventListenerFactory ?: error("closed") + codeHost.start(eventListenerFactory) } /** @@ -124,14 +133,17 @@ public class TreehouseApp private constructor( * This function may only be invoked on [TreehouseDispatchers.ui]. */ public fun restart() { - codeHost.restart() + val eventListenerFactory = eventListenerFactory ?: error("closed") + codeHost.restart(eventListenerFactory) } /** * Continuously polls for updated code, and emits a new [LoadResult] instance when new code is * found. */ - private fun ziplineFlow(): Flow { + private fun ziplineFlow( + eventListenerFactory: EventListener.Factory, + ): Flow { var loader = ZiplineLoader( dispatcher = dispatchers.zipline, manifestVerifier = factory.manifestVerifier, @@ -187,6 +199,15 @@ public class TreehouseApp private constructor( ) } + /** Permanently stop the app and release any resources necessary to start it again. */ + public fun close() { + dispatchers.checkUi() + + closed = true + eventListenerFactory = null + stop() + } + /** * This manages a cache that should be shared by all launched applications. * diff --git a/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/CodeHostTest.kt b/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/CodeHostTest.kt index 95fbd18576..ad465fd400 100644 --- a/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/CodeHostTest.kt +++ b/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/CodeHostTest.kt @@ -57,7 +57,7 @@ class CodeHostTest { content.bind(view1) eventLog.takeEvent("codeListener.onInitialCodeLoading(view1)") - codeHost.start() + codeHost.start(EventListener.NONE) eventLog.takeEvent("codeHostUpdates1.collect()") codeHost.startCodeSession("codeSessionA") eventLog.takeEvent("codeSessionA.start()") @@ -79,7 +79,7 @@ class CodeHostTest { content.bind(view1) eventLog.takeEvent("codeListener.onInitialCodeLoading(view1)") - codeHost.restart() + codeHost.restart(EventListener.NONE) eventLog.takeEvent("codeHostUpdates1.collect()") codeHost.startCodeSession("codeSessionA") eventLog.takeEvent("codeSessionA.start()") @@ -101,7 +101,7 @@ class CodeHostTest { content.bind(view1) eventLog.takeEvent("codeListener.onInitialCodeLoading(view1)") - codeHost.start() + codeHost.start(EventListener.NONE) eventLog.takeEvent("codeHostUpdates1.collect()") codeHost.startCodeSession("codeSessionA") eventLog.takeEvent("codeSessionA.start()") @@ -111,7 +111,7 @@ class CodeHostTest { eventLog.takeEvent("codeSessionA.app.uis[0].close()") eventLog.takeEvent("codeSessionA.stop()") - codeHost.start() + codeHost.start(EventListener.NONE) eventLog.takeEvent("codeHostUpdates2.collect()") codeHost.startCodeSession("codeSessionB") eventLog.takeEvent("codeSessionB.start()") @@ -132,7 +132,7 @@ class CodeHostTest { content.bind(view1) eventLog.takeEvent("codeListener.onInitialCodeLoading(view1)") - codeHost.start() + codeHost.start(EventListener.NONE) eventLog.takeEvent("codeHostUpdates1.collect()") val codeSessionA = codeHost.startCodeSession("codeSessionA") eventLog.takeEvent("codeSessionA.start()") @@ -142,7 +142,7 @@ class CodeHostTest { eventLog.takeEvent("codeSessionA.app.uis[0].close()") eventLog.takeEvent("codeSessionA.stop()") - codeHost.restart() + codeHost.restart(EventListener.NONE) eventLog.takeEvent("codeHostUpdates1.close()") eventLog.takeEvent("codeHostUpdates2.collect()") codeHost.startCodeSession("codeSessionB") @@ -164,7 +164,7 @@ class CodeHostTest { content.bind(view1) eventLog.takeEvent("codeListener.onInitialCodeLoading(view1)") - codeHost.start() + codeHost.start(EventListener.NONE) eventLog.takeEvent("codeHostUpdates1.collect()") val codeSessionA = codeHost.startCodeSession("codeSessionA") eventLog.takeEvent("codeSessionA.start()") @@ -193,7 +193,7 @@ class CodeHostTest { content.bind(view1) eventLog.takeEvent("codeListener.onInitialCodeLoading(view1)") - codeHost.start() + codeHost.start(EventListener.NONE) eventLog.takeEvent("codeHostUpdates1.collect()") val codeSessionA = codeHost.startCodeSession("codeSessionA") eventLog.takeEvent("codeSessionA.start()") @@ -217,11 +217,11 @@ class CodeHostTest { content.bind(view1) eventLog.takeEvent("codeListener.onInitialCodeLoading(view1)") - codeHost.start() + codeHost.start(EventListener.NONE) eventLog.takeEvent("codeHostUpdates1.collect()") eventLog.assertNoEvents() - codeHost.start() + codeHost.start(EventListener.NONE) eventLog.assertNoEvents() codeHost.startCodeSession("codeSessionA") @@ -243,13 +243,13 @@ class CodeHostTest { content.bind(view1) eventLog.takeEvent("codeListener.onInitialCodeLoading(view1)") - codeHost.start() + codeHost.start(EventListener.NONE) eventLog.takeEvent("codeHostUpdates1.collect()") codeHost.startCodeSession("codeSessionA") eventLog.takeEvent("codeSessionA.start()") eventLog.takeEvent("codeSessionA.app.uis[0].start()") - codeHost.start() + codeHost.start(EventListener.NONE) eventLog.assertNoEvents() codeHost.stop() @@ -268,7 +268,7 @@ class CodeHostTest { content.bind(view1) eventLog.takeEvent("codeListener.onInitialCodeLoading(view1)") - codeHost.start() + codeHost.start(EventListener.NONE) eventLog.takeEvent("codeHostUpdates1.collect()") codeHost.startCodeSession("codeSessionA") eventLog.takeEvent("codeSessionA.start()") diff --git a/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/FakeCodeHost.kt b/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/FakeCodeHost.kt index 41b8204e56..c02643df56 100644 --- a/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/FakeCodeHost.kt +++ b/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/FakeCodeHost.kt @@ -39,7 +39,9 @@ internal class FakeCodeHost( * Create a new channel every time we subscribe to code updates. The channel will be closed when * the superclass is done consuming the flow. */ - override fun codeUpdatesFlow(): Flow> { + override fun codeUpdatesFlow( + eventListenerFactory: EventListener.Factory, + ): Flow> { val collectId = nextCollectId++ eventLog += "codeHostUpdates$collectId.collect()" val channel = Channel>(Int.MAX_VALUE) diff --git a/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/FakeEventPublisher.kt b/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/FakeEventPublisher.kt index 1a677b8545..decfb7ae48 100644 --- a/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/FakeEventPublisher.kt +++ b/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/FakeEventPublisher.kt @@ -34,4 +34,7 @@ class FakeEventPublisher : EventPublisher { override fun onUncaughtException(exception: Throwable) { } + + override fun close() { + } } diff --git a/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/TreehouseAppContentTest.kt b/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/TreehouseAppContentTest.kt index f018613633..e7fa6d33fb 100644 --- a/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/TreehouseAppContentTest.kt +++ b/redwood-treehouse-host/src/commonTest/kotlin/app/cash/redwood/treehouse/TreehouseAppContentTest.kt @@ -59,7 +59,7 @@ class TreehouseAppContentTest { @BeforeTest fun setUp() { runBlocking { - codeHost.start() + codeHost.start(EventListener.NONE) eventLog.takeEvent("codeHostUpdates1.collect()") } }