mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 07:29:14 +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:
@@ -1,3 +1,4 @@
|
|||||||
|
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
|
||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
@@ -100,6 +101,13 @@ tasks.withType<Test>().configureEach {
|
|||||||
integrationTestClasses.forEach { excludeTestsMatching(it) }
|
integrationTestClasses.forEach { excludeTestsMatching(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
testLogging {
|
||||||
|
exceptionFormat = TestExceptionFormat.FULL
|
||||||
|
events("failed")
|
||||||
|
showStackTraces = true
|
||||||
|
showExceptions = true
|
||||||
|
showCauses = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<Test>("integrationTest") {
|
tasks.register<Test>("integrationTest") {
|
||||||
|
|||||||
@@ -13,9 +13,11 @@ import com.subtracks.R
|
|||||||
import com.subtracks.data.repo.DownloadRepository
|
import com.subtracks.data.repo.DownloadRepository
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Our own aggregated progress notification for the downloads the platform engine runs. The engine's
|
* Our own aggregated progress notification for the downloads the platform engine runs. The engine's
|
||||||
@@ -44,7 +46,9 @@ class DownloadNotifier(
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal fun close() {
|
internal fun close() {
|
||||||
|
val job = scope.coroutineContext[Job]
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
|
runBlocking { job?.join() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun build(content: DownloadNotification): Notification =
|
private fun build(content: DownloadNotification): Notification =
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import com.subtracks.data.source.streamLengthSuffix
|
|||||||
import com.subtracks.data.source.subsonic.SubsonicClient
|
import com.subtracks.data.source.subsonic.SubsonicClient
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -25,6 +26,7 @@ import kotlinx.coroutines.flow.combine
|
|||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
@@ -100,7 +102,9 @@ class SourceRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun close() {
|
fun close() {
|
||||||
|
val job = scope.coroutineContext[Job]
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
|
runBlocking { job?.join() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sources(): Flow<List<Source>> = db.sourcesDao().sources()
|
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.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
sealed interface SyncStatus {
|
sealed interface SyncStatus {
|
||||||
data object Idle : SyncStatus
|
data object Idle : SyncStatus
|
||||||
@@ -46,6 +49,12 @@ class SyncManager(
|
|||||||
requests.trySend(Unit)
|
requests.trySend(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun close() {
|
||||||
|
val job = scope.coroutineContext[Job]
|
||||||
|
scope.cancel()
|
||||||
|
runBlocking { job?.join() }
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun runSync() {
|
private suspend fun runSync() {
|
||||||
if (sourceRepository.offline.value) {
|
if (sourceRepository.offline.value) {
|
||||||
showMessage("Offline mode is on")
|
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.model.Source
|
||||||
import com.subtracks.data.repo.ArtworkSeedRepository
|
import com.subtracks.data.repo.ArtworkSeedRepository
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.After
|
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNull
|
import org.junit.Assert.assertNull
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
@@ -30,11 +29,6 @@ class ArtworkSeedDaoTest {
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
|
||||||
fun tearDown() {
|
|
||||||
db.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun deletingASourceCascadesToItsSeeds() =
|
fun deletingASourceCascadesToItsSeeds() =
|
||||||
runTest {
|
runTest {
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import com.subtracks.data.model.SongDownload
|
|||||||
import com.subtracks.data.model.Source
|
import com.subtracks.data.model.Source
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.After
|
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
@@ -39,11 +38,6 @@ class LibraryDaoTest {
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
|
||||||
fun tearDown() {
|
|
||||||
db.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun albumSortsAndStarredFilter() =
|
fun albumSortsAndStarredFilter() =
|
||||||
runTest {
|
runTest {
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ class DownloadRepositoryTest {
|
|||||||
fun tearDown() {
|
fun tearDown() {
|
||||||
repository.close()
|
repository.close()
|
||||||
sources.close()
|
sources.close()
|
||||||
db.close()
|
|
||||||
dir.deleteRecursively()
|
dir.deleteRecursively()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ class LibraryRepositoryTest {
|
|||||||
@After
|
@After
|
||||||
fun tearDown() {
|
fun tearDown() {
|
||||||
sourceRepository.close()
|
sourceRepository.close()
|
||||||
db.close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import com.subtracks.data.model.PlaylistSong
|
|||||||
import com.subtracks.data.model.Song
|
import com.subtracks.data.model.Song
|
||||||
import com.subtracks.data.model.Source
|
import com.subtracks.data.model.Source
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.After
|
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
@@ -35,11 +34,6 @@ class QueueKeysetTest {
|
|||||||
repository = QueueRepository(db)
|
repository = QueueRepository(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
|
||||||
fun tearDown() {
|
|
||||||
db.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun albumKeysetFromMatchesOffsetWindowAtADeepPosition() =
|
fun albumKeysetFromMatchesOffsetWindowAtADeepPosition() =
|
||||||
runTest {
|
runTest {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import com.subtracks.data.model.QueueKind
|
|||||||
import com.subtracks.data.model.Song
|
import com.subtracks.data.model.Song
|
||||||
import com.subtracks.data.model.Source
|
import com.subtracks.data.model.Source
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.After
|
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
@@ -39,11 +38,6 @@ class QueueRepositoryTest {
|
|||||||
repository = QueueRepository(db)
|
repository = QueueRepository(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
|
||||||
fun tearDown() {
|
|
||||||
db.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun wholeAlbumEntryResolvesEveryTrackInOrder() =
|
fun wholeAlbumEntryResolvesEveryTrackInOrder() =
|
||||||
runTest {
|
runTest {
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ class SourceRepositoryTest {
|
|||||||
@After
|
@After
|
||||||
fun tearDown() {
|
fun tearDown() {
|
||||||
repository.close()
|
repository.close()
|
||||||
db.close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -268,7 +267,13 @@ class SourceRepositoryTest {
|
|||||||
repository.setOfflineMode(true)
|
repository.setOfflineMode(true)
|
||||||
withTimeout(5_000) { repository.offline.first { it } }
|
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)
|
assertTrue("expected the stored file, was ${ref?.url}", ref?.url?.startsWith("file:") == true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ class SyncManagerTest {
|
|||||||
|
|
||||||
@After
|
@After
|
||||||
fun tearDown() {
|
fun tearDown() {
|
||||||
|
manager.close()
|
||||||
sourceRepository.close()
|
sourceRepository.close()
|
||||||
db.close()
|
|
||||||
server.shutdown()
|
server.shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.After
|
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -41,11 +40,6 @@ class SyncServiceTest {
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
|
||||||
fun tearDown() {
|
|
||||||
db.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun syncInsertsAndPrunesRemovedRows() =
|
fun syncInsertsAndPrunesRemovedRows() =
|
||||||
runTest {
|
runTest {
|
||||||
|
|||||||
@@ -103,7 +103,6 @@ class PlaybackControllerTest {
|
|||||||
controller.close()
|
controller.close()
|
||||||
sources.close()
|
sources.close()
|
||||||
downloads.close()
|
downloads.close()
|
||||||
db.close()
|
|
||||||
dispatcher.close()
|
dispatcher.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,9 @@ import com.subtracks.playback.FakePlayerHandle
|
|||||||
import com.subtracks.playback.PlaybackController
|
import com.subtracks.playback.PlaybackController
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.asCoroutineDispatcher
|
import kotlinx.coroutines.asCoroutineDispatcher
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.test.resetMain
|
import kotlinx.coroutines.test.resetMain
|
||||||
import kotlinx.coroutines.test.setMain
|
import kotlinx.coroutines.test.setMain
|
||||||
@@ -90,11 +91,10 @@ class DownloadsViewModelTest {
|
|||||||
|
|
||||||
@After
|
@After
|
||||||
fun tearDown() {
|
fun tearDown() {
|
||||||
viewModel.viewModelScope.cancel()
|
runBlocking { viewModel.viewModelScope.coroutineContext[Job]?.cancelAndJoin() }
|
||||||
controller.close()
|
controller.close()
|
||||||
downloadRepository.close()
|
downloadRepository.close()
|
||||||
sourceRepository.close()
|
sourceRepository.close()
|
||||||
db.close()
|
|
||||||
Dispatchers.resetMain()
|
Dispatchers.resetMain()
|
||||||
dispatcher.close()
|
dispatcher.close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,9 @@ import com.subtracks.playback.FakePlayerHandle
|
|||||||
import com.subtracks.playback.PlaybackController
|
import com.subtracks.playback.PlaybackController
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.asCoroutineDispatcher
|
import kotlinx.coroutines.asCoroutineDispatcher
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.test.resetMain
|
import kotlinx.coroutines.test.resetMain
|
||||||
@@ -84,11 +85,10 @@ class QueueViewModelTest {
|
|||||||
|
|
||||||
@After
|
@After
|
||||||
fun tearDown() {
|
fun tearDown() {
|
||||||
viewModel.viewModelScope.cancel()
|
runBlocking { viewModel.viewModelScope.coroutineContext[Job]?.cancelAndJoin() }
|
||||||
controller.close()
|
controller.close()
|
||||||
downloads.close()
|
downloads.close()
|
||||||
sources.close()
|
sources.close()
|
||||||
db.close()
|
|
||||||
Dispatchers.resetMain()
|
Dispatchers.resetMain()
|
||||||
dispatcher.close()
|
dispatcher.close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ import com.subtracks.data.repo.SourceRepository
|
|||||||
import com.subtracks.data.sync.SyncManager
|
import com.subtracks.data.sync.SyncManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.asCoroutineDispatcher
|
import kotlinx.coroutines.asCoroutineDispatcher
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.test.resetMain
|
import kotlinx.coroutines.test.resetMain
|
||||||
import kotlinx.coroutines.test.setMain
|
import kotlinx.coroutines.test.setMain
|
||||||
@@ -72,11 +73,10 @@ class AddSourceViewModelTest {
|
|||||||
|
|
||||||
@After
|
@After
|
||||||
fun tearDown() {
|
fun tearDown() {
|
||||||
createdViewModels.forEach { it.viewModelScope.cancel() }
|
createdViewModels.forEach { runBlocking { it.viewModelScope.coroutineContext[Job]?.cancelAndJoin() } }
|
||||||
server.shutdown()
|
server.shutdown()
|
||||||
downloadRepository.close()
|
downloadRepository.close()
|
||||||
sourceRepository.close()
|
sourceRepository.close()
|
||||||
db.close()
|
|
||||||
Dispatchers.resetMain()
|
Dispatchers.resetMain()
|
||||||
dispatcher.close()
|
dispatcher.close()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user