mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 23:21:59 +01:00
stop poisoning the unit suite from Room teardown races
The ThemeTest failure was cross-test contamination, not a theme bug. A Room invalidation-tracker query keeps running on a cancelled flow collector after the in-memory DB is closed or finalised, throws "no such table: sources", and kotlinx-coroutines-test records it and fails the next Compose test with UncaughtExceptionsBeforeTest. Which test it lands on is just ordering/load luck. Close repositories the way DownloadRepository already did: cancel and join the scope, so Room work cannot outlive close. Stop closing the in-memory test DBs; the DB is dropped and collected once the test releases it, instead of being torn down under a pending query. Wait for the stored ref in offlineModeStillServesDownloadedArtwork instead of asserting once, since it was waiting on a different flow than the state coverArt reads. Print full stack traces for failed tests in CI.
This commit is contained in:
@@ -13,9 +13,11 @@ import com.subtracks.R
|
||||
import com.subtracks.data.repo.DownloadRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/**
|
||||
* Our own aggregated progress notification for the downloads the platform engine runs. The engine's
|
||||
@@ -44,7 +46,9 @@ class DownloadNotifier(
|
||||
}
|
||||
|
||||
internal fun close() {
|
||||
val job = scope.coroutineContext[Job]
|
||||
scope.cancel()
|
||||
runBlocking { job?.join() }
|
||||
}
|
||||
|
||||
private fun build(content: DownloadNotification): Notification =
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.subtracks.data.source.streamLengthSuffix
|
||||
import com.subtracks.data.source.subsonic.SubsonicClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -25,6 +26,7 @@ import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
@@ -100,7 +102,9 @@ class SourceRepository(
|
||||
}
|
||||
|
||||
fun close() {
|
||||
val job = scope.coroutineContext[Job]
|
||||
scope.cancel()
|
||||
runBlocking { job?.join() }
|
||||
}
|
||||
|
||||
fun sources(): Flow<List<Source>> = db.sourcesDao().sources()
|
||||
|
||||
@@ -7,11 +7,14 @@ import com.subtracks.data.repo.SourceRepository
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
sealed interface SyncStatus {
|
||||
data object Idle : SyncStatus
|
||||
@@ -46,6 +49,12 @@ class SyncManager(
|
||||
requests.trySend(Unit)
|
||||
}
|
||||
|
||||
fun close() {
|
||||
val job = scope.coroutineContext[Job]
|
||||
scope.cancel()
|
||||
runBlocking { job?.join() }
|
||||
}
|
||||
|
||||
private suspend fun runSync() {
|
||||
if (sourceRepository.offline.value) {
|
||||
showMessage("Offline mode is on")
|
||||
|
||||
@@ -9,7 +9,6 @@ import com.subtracks.data.model.ArtworkSeed
|
||||
import com.subtracks.data.model.Source
|
||||
import com.subtracks.data.repo.ArtworkSeedRepository
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Before
|
||||
@@ -30,11 +29,6 @@ class ArtworkSeedDaoTest {
|
||||
.build()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deletingASourceCascadesToItsSeeds() =
|
||||
runTest {
|
||||
|
||||
@@ -17,7 +17,6 @@ import com.subtracks.data.model.SongDownload
|
||||
import com.subtracks.data.model.Source
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
@@ -39,11 +38,6 @@ class LibraryDaoTest {
|
||||
.build()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun albumSortsAndStarredFilter() =
|
||||
runTest {
|
||||
|
||||
@@ -93,7 +93,6 @@ class DownloadRepositoryTest {
|
||||
fun tearDown() {
|
||||
repository.close()
|
||||
sources.close()
|
||||
db.close()
|
||||
dir.deleteRecursively()
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,6 @@ class LibraryRepositoryTest {
|
||||
@After
|
||||
fun tearDown() {
|
||||
sourceRepository.close()
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -12,7 +12,6 @@ import com.subtracks.data.model.PlaylistSong
|
||||
import com.subtracks.data.model.Song
|
||||
import com.subtracks.data.model.Source
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
@@ -35,11 +34,6 @@ class QueueKeysetTest {
|
||||
repository = QueueRepository(db)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun albumKeysetFromMatchesOffsetWindowAtADeepPosition() =
|
||||
runTest {
|
||||
|
||||
@@ -15,7 +15,6 @@ import com.subtracks.data.model.QueueKind
|
||||
import com.subtracks.data.model.Song
|
||||
import com.subtracks.data.model.Source
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
@@ -39,11 +38,6 @@ class QueueRepositoryTest {
|
||||
repository = QueueRepository(db)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun wholeAlbumEntryResolvesEveryTrackInOrder() =
|
||||
runTest {
|
||||
|
||||
@@ -61,7 +61,6 @@ class SourceRepositoryTest {
|
||||
@After
|
||||
fun tearDown() {
|
||||
repository.close()
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -268,7 +267,13 @@ class SourceRepositoryTest {
|
||||
repository.setOfflineMode(true)
|
||||
withTimeout(5_000) { repository.offline.first { it } }
|
||||
|
||||
val ref = repository.coverArt("art-1")
|
||||
var ref: CoverArtRef? = repository.coverArt("art-1")
|
||||
withTimeout(5_000) {
|
||||
while (ref?.url?.startsWith("file:") != true) {
|
||||
delay(10)
|
||||
ref = repository.coverArt("art-1")
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue("expected the stored file, was ${ref?.url}", ref?.url?.startsWith("file:") == true)
|
||||
}
|
||||
|
||||
@@ -57,8 +57,8 @@ class SyncManagerTest {
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
manager.close()
|
||||
sourceRepository.close()
|
||||
db.close()
|
||||
server.shutdown()
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@@ -41,11 +40,6 @@ class SyncServiceTest {
|
||||
.build()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun syncInsertsAndPrunesRemovedRows() =
|
||||
runTest {
|
||||
|
||||
@@ -103,7 +103,6 @@ class PlaybackControllerTest {
|
||||
controller.close()
|
||||
sources.close()
|
||||
downloads.close()
|
||||
db.close()
|
||||
dispatcher.close()
|
||||
}
|
||||
|
||||
|
||||
@@ -24,8 +24,9 @@ import com.subtracks.playback.FakePlayerHandle
|
||||
import com.subtracks.playback.PlaybackController
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.setMain
|
||||
@@ -90,11 +91,10 @@ class DownloadsViewModelTest {
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
viewModel.viewModelScope.cancel()
|
||||
runBlocking { viewModel.viewModelScope.coroutineContext[Job]?.cancelAndJoin() }
|
||||
controller.close()
|
||||
downloadRepository.close()
|
||||
sourceRepository.close()
|
||||
db.close()
|
||||
Dispatchers.resetMain()
|
||||
dispatcher.close()
|
||||
}
|
||||
|
||||
@@ -24,8 +24,9 @@ import com.subtracks.playback.FakePlayerHandle
|
||||
import com.subtracks.playback.PlaybackController
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
@@ -84,11 +85,10 @@ class QueueViewModelTest {
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
viewModel.viewModelScope.cancel()
|
||||
runBlocking { viewModel.viewModelScope.coroutineContext[Job]?.cancelAndJoin() }
|
||||
controller.close()
|
||||
downloads.close()
|
||||
sources.close()
|
||||
db.close()
|
||||
Dispatchers.resetMain()
|
||||
dispatcher.close()
|
||||
}
|
||||
|
||||
@@ -16,8 +16,9 @@ import com.subtracks.data.repo.SourceRepository
|
||||
import com.subtracks.data.sync.SyncManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.setMain
|
||||
@@ -72,11 +73,10 @@ class AddSourceViewModelTest {
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
createdViewModels.forEach { it.viewModelScope.cancel() }
|
||||
createdViewModels.forEach { runBlocking { it.viewModelScope.coroutineContext[Job]?.cancelAndJoin() } }
|
||||
server.shutdown()
|
||||
downloadRepository.close()
|
||||
sourceRepository.close()
|
||||
db.close()
|
||||
Dispatchers.resetMain()
|
||||
dispatcher.close()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user