address review: join the controller scope, keep DB closes where nothing leaks

PlaybackController.now exposes awaitStopped() so the tests that own its DB can join its scope before db.close(); close() stays cancel-only so AppModuleTest (Main dispatcher) cannot deadlock. Restores db.close() in the tests that have no leaking scope, closes the SyncManager AddSourceViewModelTest was leaking, replaces the duplicated poll with the existing awaitCoverArt helper, and trims the testLogging block to the one line that does anything.
This commit is contained in:
austinried
2026-10-04 00:30:05 +00:00
parent 84df71771e
commit 1128af165d
15 changed files with 53 additions and 15 deletions
-4
View File
@@ -103,10 +103,6 @@ tasks.withType<Test>().configureEach {
}
testLogging {
exceptionFormat = TestExceptionFormat.FULL
events("failed")
showStackTraces = true
showExceptions = true
showCauses = true
}
}
@@ -173,6 +173,10 @@ class PlaybackController(
scope.cancel()
}
internal suspend fun awaitStopped() {
scope.coroutineContext[Job]?.join()
}
private val _ready = MutableStateFlow(false)
val ready: StateFlow<Boolean> = _ready
@@ -9,6 +9,7 @@ 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
@@ -29,6 +30,11 @@ class ArtworkSeedDaoTest {
.build()
}
@After
fun tearDown() {
db.close()
}
@Test
fun deletingASourceCascadesToItsSeeds() =
runTest {
@@ -17,6 +17,7 @@ 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
@@ -38,6 +39,11 @@ class LibraryDaoTest {
.build()
}
@After
fun tearDown() {
db.close()
}
@Test
fun albumSortsAndStarredFilter() =
runTest {
@@ -93,6 +93,7 @@ class DownloadRepositoryTest {
fun tearDown() {
repository.close()
sources.close()
db.close()
dir.deleteRecursively()
}
@@ -51,6 +51,7 @@ class LibraryRepositoryTest {
@After
fun tearDown() {
sourceRepository.close()
db.close()
}
@Test
@@ -12,6 +12,7 @@ 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
@@ -34,6 +35,11 @@ class QueueKeysetTest {
repository = QueueRepository(db)
}
@After
fun tearDown() {
db.close()
}
@Test
fun albumKeysetFromMatchesOffsetWindowAtADeepPosition() =
runTest {
@@ -15,6 +15,7 @@ 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
@@ -38,6 +39,11 @@ class QueueRepositoryTest {
repository = QueueRepository(db)
}
@After
fun tearDown() {
db.close()
}
@Test
fun wholeAlbumEntryResolvesEveryTrackInOrder() =
runTest {
@@ -61,6 +61,7 @@ class SourceRepositoryTest {
@After
fun tearDown() {
repository.close()
db.close()
}
@Test
@@ -267,15 +268,9 @@ class SourceRepositoryTest {
repository.setOfflineMode(true)
withTimeout(5_000) { repository.offline.first { it } }
var ref: CoverArtRef? = repository.coverArt("art-1")
withTimeout(5_000) {
while (ref?.url?.startsWith("file:") != true) {
delay(10)
ref = repository.coverArt("art-1")
}
}
val ref = awaitCoverArt("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:"))
}
private suspend fun awaitActiveSourceId(): Long = withTimeout(5_000) { repository.activeSourceId().first { it != null }!! }
@@ -59,6 +59,7 @@ class SyncManagerTest {
fun tearDown() {
manager.close()
sourceRepository.close()
db.close()
server.shutdown()
}
@@ -21,6 +21,7 @@ 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
@@ -40,6 +41,11 @@ class SyncServiceTest {
.build()
}
@After
fun tearDown() {
db.close()
}
@Test
fun syncInsertsAndPrunesRemovedRows() =
runTest {
@@ -101,8 +101,10 @@ class PlaybackControllerTest {
@After
fun tearDown() {
controller.close()
runBlocking { controller.awaitStopped() }
sources.close()
downloads.close()
db.close()
dispatcher.close()
}
@@ -93,8 +93,10 @@ class DownloadsViewModelTest {
fun tearDown() {
runBlocking { viewModel.viewModelScope.coroutineContext[Job]?.cancelAndJoin() }
controller.close()
runBlocking { controller.awaitStopped() }
downloadRepository.close()
sourceRepository.close()
db.close()
Dispatchers.resetMain()
dispatcher.close()
}
@@ -87,8 +87,10 @@ class QueueViewModelTest {
fun tearDown() {
runBlocking { viewModel.viewModelScope.coroutineContext[Job]?.cancelAndJoin() }
controller.close()
runBlocking { controller.awaitStopped() }
downloads.close()
sources.close()
db.close()
Dispatchers.resetMain()
dispatcher.close()
}
@@ -46,6 +46,7 @@ class AddSourceViewModelTest {
private lateinit var downloadRepository: DownloadRepository
private lateinit var server: MockWebServer
private val createdViewModels = mutableListOf<AddSourceViewModel>()
private val syncManagers = mutableListOf<SyncManager>()
@Before
fun setUp() {
@@ -74,9 +75,11 @@ class AddSourceViewModelTest {
@After
fun tearDown() {
createdViewModels.forEach { runBlocking { it.viewModelScope.coroutineContext[Job]?.cancelAndJoin() } }
syncManagers.forEach { it.close() }
server.shutdown()
downloadRepository.close()
sourceRepository.close()
db.close()
Dispatchers.resetMain()
dispatcher.close()
}
@@ -133,9 +136,10 @@ class AddSourceViewModelTest {
assertNotNull("the active source must survive the refused delete", db.sourcesDao().sourceOnce(sourceId))
}
private fun viewModel(sourceId: Long? = null): AddSourceViewModel =
AddSourceViewModel(sourceRepository, SyncManager(db, sourceRepository, QueueRepository(db)), downloadRepository, sourceId)
.also { createdViewModels += it }
private fun viewModel(sourceId: Long? = null): AddSourceViewModel {
val syncManager = SyncManager(db, sourceRepository, QueueRepository(db)).also { syncManagers += it }
return AddSourceViewModel(sourceRepository, syncManager, downloadRepository, sourceId).also { createdViewModels += it }
}
private fun ok() = MockResponse().setBody("<subsonic-response status=\"ok\" version=\"1.16.1\"/>")