diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/calendar/CalendarsViewModelFlowTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/calendar/CalendarsViewModelFlowTest.kt index 1a7920efdc..e504817753 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/calendar/CalendarsViewModelFlowTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/calendar/CalendarsViewModelFlowTest.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.calendar import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelStore +import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.commons.feeds.FeedContentState import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.cache.LocalCache @@ -33,13 +34,16 @@ import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.job import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain +import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeoutOrNull import org.junit.After import org.junit.Assert.assertEquals @@ -137,14 +141,20 @@ class CalendarsViewModelFlowTest { ) = runBlocking { val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) val store = ViewModelStore() + var modelScope: Job? = null try { val feed = FeedContentState(CalendarAppointmentsFeedFilter(seeEverythingAccount()), scope, LocalCache) val model = ViewModelProvider(store, ViewModelProvider.NewInstanceFactory())[CalendarsViewModel::class.java] + modelScope = model.viewModelScope.coroutineContext.job model.init(pubKey, feed) block(model) } finally { store.clear() scope.cancel() + // Clearing only requests cancellation. The flowOn producers on Dispatchers.Default can + // still be finishing and hand their completion back to the Main collectors; wait for + // them here, while setMain is still in force, not after tearDown's resetMain. + withTimeout(AWAIT_MS) { modelScope?.join() } } } diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/calendar/CalendarsViewModelTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/calendar/CalendarsViewModelTest.kt index 770e0add94..1358a9f67d 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/calendar/CalendarsViewModelTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/calendar/CalendarsViewModelTest.kt @@ -22,13 +22,19 @@ package com.vitorpamplona.amethyst.calendar import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelStore +import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarsViewMode import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarsViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.startOfWeek import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.job +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain +import kotlinx.coroutines.withTimeout import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNull @@ -58,13 +64,24 @@ class CalendarsViewModelTest { store = ViewModelStore() } + private val scopes = mutableListOf() + @After fun tearDown() { store.clear() + // Clearing only requests cancellation: a producer the model runs on Dispatchers.Default + // (its flowOn) can still be finishing, and it hands its completion back to the Main + // collector. Waiting for the scopes keeps that inside setMain, instead of throwing onto a + // background thread after resetMain and failing whichever runTest starts next. + runBlocking { withTimeout(SCOPE_CLOSE_MS) { scopes.joinAll() } } + scopes.clear() Dispatchers.resetMain() } - private fun newModel(): CalendarsViewModel = ViewModelProvider(store, ViewModelProvider.NewInstanceFactory())[CalendarsViewModel::class.java] + private fun newModel(): CalendarsViewModel = + ViewModelProvider(store, ViewModelProvider.NewInstanceFactory())[CalendarsViewModel::class.java].also { + scopes += it.viewModelScope.coroutineContext.job + } @Test fun aFreshScreenOpensOnToday() { @@ -148,4 +165,8 @@ class CalendarsViewModelTest { assertEquals("the lenses must not share one scroll offset", 4, states.distinct().size) } + + companion object { + private const val SCOPE_CLOSE_MS = 5_000L + } }