mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-15 00:42:54 +01:00
sync: reset queues only for successfully changed origins
This commit is contained in:
@@ -5,18 +5,27 @@ import android.content.res.Resources
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.subtracks.TEST_TIMEOUT_MS
|
||||
import com.subtracks.awaitUntil
|
||||
import com.subtracks.cancelAndJoinBlocking
|
||||
import com.subtracks.data.db.SubtracksDatabase
|
||||
import com.subtracks.data.db.createTestDatabase
|
||||
import com.subtracks.data.download.ArtworkStore
|
||||
import com.subtracks.data.download.FakeDownloadEngine
|
||||
import com.subtracks.data.model.DownloadStatus
|
||||
import com.subtracks.data.model.SongDownload
|
||||
import com.subtracks.data.model.Source
|
||||
import com.subtracks.data.prefs.UserPreferences
|
||||
import com.subtracks.data.prefs.fakeUserPreferences
|
||||
import com.subtracks.data.repo.DownloadRepository
|
||||
import com.subtracks.data.repo.NetworkServerActionSink
|
||||
import com.subtracks.data.repo.QueueRepository
|
||||
import com.subtracks.data.repo.SourceRepository
|
||||
import com.subtracks.data.repo.replace
|
||||
import com.subtracks.data.repo.setShuffle
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withTimeout
|
||||
@@ -28,6 +37,7 @@ import okhttp3.mockwebserver.RecordedRequest
|
||||
import okhttp3.mockwebserver.SocketPolicy
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@@ -100,6 +110,39 @@ class SyncManagerTest {
|
||||
assertTrue(messages.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aNoOpSyncPreservesTheQueueResolutionCache() =
|
||||
runBlocking {
|
||||
val healthy = healthyDispatcher()
|
||||
server.dispatcher =
|
||||
object : Dispatcher() {
|
||||
override fun dispatch(request: RecordedRequest): MockResponse =
|
||||
when {
|
||||
request.path?.startsWith("/rest/search3.view") == true -> MockResponse().setBody(TWO_SONGS)
|
||||
request.path?.startsWith("/rest/getPlaylists.view") == true -> MockResponse().setBody(PLAYLISTS)
|
||||
request.path?.startsWith("/rest/getPlaylist.view") == true -> MockResponse().setBody(PLAYLIST)
|
||||
else -> healthy.dispatch(request)
|
||||
}
|
||||
}
|
||||
sourceRepository.addSource("Local", server.url("/").toString(), "u", "p", useTokenAuth = false)
|
||||
manager.requestSync()
|
||||
withTimeout(TEST_TIMEOUT_MS) { manager.status.first { it is SyncStatus.Success } }
|
||||
queueRepository.replace(listOf(queueRepository.playlistEntry(1, "p1")))
|
||||
val snapshot = queueRepository.snapshot()
|
||||
assertEquals(2L, snapshot.size)
|
||||
assertEquals(listOf("s1"), queueRepository.range(snapshot, 0, 0).map { it.item.song.id })
|
||||
assertEquals(listOf("s2"), queueRepository.range(snapshot, 1, 1).map { it.item.song.id })
|
||||
|
||||
manager.requestSync()
|
||||
withTimeout(TEST_TIMEOUT_MS) { manager.status.drop(1).first { it is SyncStatus.Success } }
|
||||
val offsetSeeks = queueRepository.offsetSeeks
|
||||
val keysetSeeks = queueRepository.keysetSeeks
|
||||
assertEquals(listOf("s2"), queueRepository.range(snapshot, 1, 1).map { it.item.song.id })
|
||||
|
||||
assertEquals(offsetSeeks, queueRepository.offsetSeeks)
|
||||
assertEquals(keysetSeeks + 1, queueRepository.keysetSeeks)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aFailedSyncDoesNotAdvanceTheQueueContentVersion() =
|
||||
runBlocking {
|
||||
@@ -112,6 +155,19 @@ class SyncManagerTest {
|
||||
assertEquals(before, queueRepository.snapshot().version)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aFailedSyncDoesNotResetTheQueue() =
|
||||
runBlocking {
|
||||
db.sourcesDao().upsertSource(Source(1, "Local", "http://localhost", true, 0))
|
||||
queueRepository.replace(listOf(queueRepository.albumEntry(1, "al1").copy(rangeStart = 0, rangeEnd = 0)))
|
||||
val before = queueRepository.snapshot().entries.map { it.entry }
|
||||
|
||||
manager.requestSync()
|
||||
withTimeout(TEST_TIMEOUT_MS) { manager.status.first { it is SyncStatus.Failed } }
|
||||
|
||||
assertEquals(before, queueRepository.snapshot().entries.map { it.entry })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aSuccessfulSyncMirrorsTheLibraryIntoRoom() =
|
||||
runBlocking {
|
||||
@@ -169,6 +225,162 @@ class SyncManagerTest {
|
||||
assertEquals(listOf("al1"), db.libraryDao().albumIds(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aFailedSyncCarriesCommittedOriginsIntoTheNextSuccessfulReset() =
|
||||
runBlocking {
|
||||
val healthy = healthyDispatcher()
|
||||
server.dispatcher =
|
||||
object : Dispatcher() {
|
||||
override fun dispatch(request: RecordedRequest): MockResponse =
|
||||
if (request.path?.startsWith("/rest/getPlaylists.view") == true) {
|
||||
MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)
|
||||
} else {
|
||||
healthy.dispatch(request)
|
||||
}
|
||||
}
|
||||
sourceRepository.addSource("Local", server.url("/").toString(), "u", "p", useTokenAuth = false)
|
||||
val edited = queueRepository.albumEntry(1, "al1").copy(rangeStart = 0, rangeEnd = 0)
|
||||
queueRepository.replace(listOf(edited))
|
||||
|
||||
manager.requestSync()
|
||||
withTimeout(TEST_TIMEOUT_MS) { manager.status.first { it is SyncStatus.Failed } }
|
||||
assertEquals(
|
||||
0L,
|
||||
queueRepository
|
||||
.snapshot()
|
||||
.entries
|
||||
.single()
|
||||
.entry.rangeStart,
|
||||
)
|
||||
|
||||
assertTrue(db.queueDao().pendingSyncOrigins(1).isNotEmpty())
|
||||
manager =
|
||||
SyncManager(
|
||||
db,
|
||||
sourceRepository,
|
||||
queueRepository,
|
||||
NetworkServerActionSink(sourceRepository),
|
||||
preferences = preferences,
|
||||
showMessage = { messages += it.resolve(resources) },
|
||||
scope = scope,
|
||||
)
|
||||
server.dispatcher = healthy
|
||||
manager.requestSync()
|
||||
withTimeout(TEST_TIMEOUT_MS) { manager.status.first { it is SyncStatus.Success } }
|
||||
|
||||
assertNull(
|
||||
queueRepository
|
||||
.snapshot()
|
||||
.entries
|
||||
.single()
|
||||
.entry.rangeStart,
|
||||
)
|
||||
assertTrue(db.queueDao().pendingSyncOrigins(1).isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aFailedSyncPreservesResolvedQueueShuffle() =
|
||||
runBlocking {
|
||||
server.dispatcher = healthyDispatcher()
|
||||
sourceRepository.addSource("Local", server.url("/").toString(), "u", "p", useTokenAuth = false)
|
||||
manager.requestSync()
|
||||
withTimeout(TEST_TIMEOUT_MS) { manager.status.first { it is SyncStatus.Success } }
|
||||
queueRepository.replace(listOf(queueRepository.albumEntry(1, "al1")))
|
||||
queueRepository.setShuffle(42L)
|
||||
assertTrue(queueRepository.snapshot().shuffled)
|
||||
val healthy = healthyDispatcher()
|
||||
server.dispatcher =
|
||||
object : Dispatcher() {
|
||||
override fun dispatch(request: RecordedRequest): MockResponse =
|
||||
when {
|
||||
request.path?.startsWith("/rest/search3.view") == true -> {
|
||||
MockResponse().setBody(TWO_SONGS)
|
||||
}
|
||||
|
||||
request.path?.startsWith("/rest/getPlaylists.view") == true -> {
|
||||
MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)
|
||||
}
|
||||
|
||||
else -> {
|
||||
healthy.dispatch(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
manager.requestSync()
|
||||
withTimeout(TEST_TIMEOUT_MS) { manager.status.first { it is SyncStatus.Failed } }
|
||||
|
||||
val snapshot = queueRepository.snapshot()
|
||||
assertTrue(snapshot.shuffled)
|
||||
assertEquals(1L, snapshot.contextSize)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aFailedSongPruneDoesNotResetAnEditedDownloadedQueue() =
|
||||
runBlocking {
|
||||
val healthy = healthyDispatcher()
|
||||
server.dispatcher =
|
||||
object : Dispatcher() {
|
||||
override fun dispatch(request: RecordedRequest): MockResponse =
|
||||
if (request.path?.startsWith("/rest/search3.view") == true) {
|
||||
MockResponse().setBody(TWO_SONGS)
|
||||
} else {
|
||||
healthy.dispatch(request)
|
||||
}
|
||||
}
|
||||
sourceRepository.addSource("Local", server.url("/").toString(), "u", "p", useTokenAuth = false)
|
||||
manager.requestSync()
|
||||
withTimeout(TEST_TIMEOUT_MS) { manager.status.first { it is SyncStatus.Success } }
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
val downloadsDir = File(context.cacheDir, "sync-downloads-${System.nanoTime()}")
|
||||
val downloads =
|
||||
DownloadRepository(
|
||||
db,
|
||||
sourceRepository,
|
||||
FakeDownloadEngine(),
|
||||
downloadsDir,
|
||||
artworkStore = ArtworkStore(File(context.cacheDir, "art")),
|
||||
artworkFetcher = { ByteArray(0) },
|
||||
scope = scope,
|
||||
)
|
||||
File(downloadsDir, "1").mkdirs()
|
||||
File(downloadsDir, "1/s2").writeText("audio")
|
||||
db.downloadDao().upsert(SongDownload(1, "s2", DownloadStatus.Completed, downloadedAt = 1))
|
||||
downloads.start()
|
||||
downloads.awaitLoaded()
|
||||
awaitUntil("waiting for download baseline") { downloads.localUri("s2") != null }
|
||||
queueRepository.replace(listOf(queueRepository.downloadedEntry(1).copy(rangeStart = 1)))
|
||||
server.dispatcher =
|
||||
object : Dispatcher() {
|
||||
override fun dispatch(request: RecordedRequest): MockResponse =
|
||||
when {
|
||||
request.path?.startsWith("/rest/search3.view") == true -> {
|
||||
MockResponse().setBody(SONG)
|
||||
}
|
||||
|
||||
request.path?.startsWith("/rest/getPlaylists.view") == true -> {
|
||||
MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)
|
||||
}
|
||||
|
||||
else -> {
|
||||
healthy.dispatch(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
manager.requestSync()
|
||||
withTimeout(TEST_TIMEOUT_MS) { manager.status.first { it is SyncStatus.Failed } }
|
||||
|
||||
assertEquals(
|
||||
1L,
|
||||
queueRepository
|
||||
.snapshot()
|
||||
.entries
|
||||
.single()
|
||||
.entry.rangeStart,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun requestsWhileRunningCoalesceIntoOneSync() =
|
||||
runBlocking {
|
||||
@@ -253,6 +465,24 @@ class SyncManagerTest {
|
||||
"<subsonic-response status=\"ok\" version=\"1.16.1\"><searchResult3>" +
|
||||
"<song id=\"s1\" title=\"Song One\" albumId=\"al1\" artistId=\"ar1\" track=\"1\"/>" +
|
||||
"</searchResult3></subsonic-response>"
|
||||
|
||||
const val TWO_SONGS =
|
||||
"<subsonic-response status=\"ok\" version=\"1.16.1\"><searchResult3>" +
|
||||
"<song id=\"s1\" title=\"Song One\" albumId=\"al1\" artistId=\"ar1\" track=\"1\"/>" +
|
||||
"<song id=\"s2\" title=\"Song Two\" albumId=\"al1\" artistId=\"ar1\" track=\"2\"/>" +
|
||||
"</searchResult3></subsonic-response>"
|
||||
|
||||
const val PLAYLISTS =
|
||||
"<subsonic-response status=\"ok\" version=\"1.16.1\"><playlists>" +
|
||||
"<playlist id=\"p1\" name=\"Playlist\" songCount=\"2\"/>" +
|
||||
"</playlists></subsonic-response>"
|
||||
|
||||
const val PLAYLIST =
|
||||
"<subsonic-response status=\"ok\" version=\"1.16.1\">" +
|
||||
"<playlist id=\"p1\" name=\"Playlist\" songCount=\"2\">" +
|
||||
"<entry id=\"s1\" title=\"Song One\" albumId=\"al1\" artistId=\"ar1\" track=\"1\"/>" +
|
||||
"<entry id=\"s2\" title=\"Song Two\" albumId=\"al1\" artistId=\"ar1\" track=\"2\"/>" +
|
||||
"</playlist></subsonic-response>"
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -18,6 +18,8 @@ import com.subtracks.data.model.DownloadStatus
|
||||
import com.subtracks.data.model.ListDownloadStatus
|
||||
import com.subtracks.data.model.Playlist
|
||||
import com.subtracks.data.model.PlaylistSong
|
||||
import com.subtracks.data.model.QueueKind
|
||||
import com.subtracks.data.model.QueueOrigin
|
||||
import com.subtracks.data.model.Song
|
||||
import com.subtracks.data.model.SongDownload
|
||||
import com.subtracks.data.model.Source
|
||||
@@ -1868,6 +1870,278 @@ class SyncServiceTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun playlistMembershipChangesReportOnlyTheirOwnOrigins() =
|
||||
runTest {
|
||||
insertSource()
|
||||
val source =
|
||||
FakeMusicSource(
|
||||
songs = listOf(song("s1"), song("s2")),
|
||||
playlists = listOf(playlist("p1"), playlist("p2")),
|
||||
playlistSongs =
|
||||
listOf(
|
||||
PlaylistSong(1, "p1", "s1", 0),
|
||||
PlaylistSong(1, "p2", "s2", 0),
|
||||
),
|
||||
)
|
||||
SyncService(db, source).sync()
|
||||
|
||||
source.playlistSongs =
|
||||
listOf(
|
||||
PlaylistSong(1, "p1", "s2", 0),
|
||||
PlaylistSong(1, "p2", "s2", 0),
|
||||
)
|
||||
|
||||
val summary = SyncService(db, source).sync()
|
||||
|
||||
assertEquals(setOf(QueueOrigin(QueueKind.Playlist, 1, "p1")), summary.changedOrigins)
|
||||
|
||||
source.playlistSongs = listOf(PlaylistSong(1, "p2", "s2", 0))
|
||||
|
||||
val deletionSummary = SyncService(db, source).sync()
|
||||
|
||||
assertEquals(setOf(QueueOrigin(QueueKind.Playlist, 1, "p1")), deletionSummary.changedOrigins)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun albumAndSongChangesReportOnlyAffectedOrigins() =
|
||||
runTest {
|
||||
insertSource()
|
||||
val source =
|
||||
FakeMusicSource(
|
||||
artists = listOf(artist("a1"), artist("a2"), artist("a3")),
|
||||
albums =
|
||||
listOf(
|
||||
album("al1"),
|
||||
album("al2").copy(artistId = "a2"),
|
||||
album("al3").copy(artistId = "a3"),
|
||||
),
|
||||
songs =
|
||||
listOf(
|
||||
song("s1").copy(genres = listOf("Rock")),
|
||||
song("s2").copy(albumId = "al2", artistId = "a2", genres = listOf("Jazz")),
|
||||
),
|
||||
)
|
||||
SyncService(db, source).sync()
|
||||
|
||||
source.albums =
|
||||
listOf(
|
||||
album("al1").copy(name = "Renamed"),
|
||||
album("al2").copy(artistId = "a2"),
|
||||
album("al3").copy(artistId = "a3"),
|
||||
)
|
||||
source.songs =
|
||||
listOf(
|
||||
song("s1").copy(
|
||||
albumId = "al3",
|
||||
artistId = "a3",
|
||||
genres = listOf("Electronic"),
|
||||
starred = 1234,
|
||||
),
|
||||
song("s2").copy(albumId = "al2", artistId = "a2", genres = listOf("Jazz")),
|
||||
)
|
||||
|
||||
val summary = SyncService(db, source).sync()
|
||||
|
||||
assertEquals(
|
||||
setOf(
|
||||
QueueOrigin(QueueKind.Album, 1, "al1"),
|
||||
QueueOrigin(QueueKind.Album, 1, "al3"),
|
||||
QueueOrigin(QueueKind.Artist, 1, "a1"),
|
||||
QueueOrigin(QueueKind.Artist, 1, "a3"),
|
||||
QueueOrigin(QueueKind.Genre, 1, "Rock"),
|
||||
QueueOrigin(QueueKind.Genre, 1, "Electronic"),
|
||||
QueueOrigin(QueueKind.Starred, 1, ""),
|
||||
QueueOrigin(QueueKind.Song, 1, "s1"),
|
||||
),
|
||||
summary.changedOrigins,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deletedSongsReportOnlyTheirPreviousOrigins() =
|
||||
runTest {
|
||||
insertSource()
|
||||
val source =
|
||||
FakeMusicSource(
|
||||
artists = listOf(artist("a1"), artist("a2")),
|
||||
albums = listOf(album("al1"), album("al2").copy(artistId = "a2")),
|
||||
songs =
|
||||
listOf(
|
||||
song("s1").copy(genres = listOf("Rock"), starred = 1234),
|
||||
song("s2").copy(albumId = "al2", artistId = "a2", genres = listOf("Jazz")),
|
||||
),
|
||||
)
|
||||
SyncService(db, source).sync()
|
||||
|
||||
source.songs = listOf(song("s2").copy(albumId = "al2", artistId = "a2", genres = listOf("Jazz")))
|
||||
|
||||
val summary = SyncService(db, source).sync()
|
||||
|
||||
assertEquals(
|
||||
setOf(
|
||||
QueueOrigin(QueueKind.Album, 1, "al1"),
|
||||
QueueOrigin(QueueKind.Artist, 1, "a1"),
|
||||
QueueOrigin(QueueKind.Genre, 1, "Rock"),
|
||||
QueueOrigin(QueueKind.Starred, 1, ""),
|
||||
QueueOrigin(QueueKind.Song, 1, "s1"),
|
||||
),
|
||||
summary.changedOrigins,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun albumCreditChangesReportBothArtistOrigins() =
|
||||
runTest {
|
||||
insertSource()
|
||||
val plural = listOf(ArtistRef("a2", "Artist a2"))
|
||||
val source =
|
||||
FakeMusicSource(
|
||||
artists = listOf(artist("a1"), artist("a2"), artist("a3")),
|
||||
albums = listOf(album("al1").copy(artists = plural)),
|
||||
songs = listOf(song("s1")),
|
||||
)
|
||||
SyncService(db, source).sync()
|
||||
|
||||
source.albums = listOf(album("al1").copy(artistId = "a3", artists = plural, year = 2024))
|
||||
|
||||
val summary = SyncService(db, source).sync()
|
||||
|
||||
assertTrue(summary.changedOrigins.contains(QueueOrigin(QueueKind.Artist, 1, "a1")))
|
||||
assertTrue(summary.changedOrigins.contains(QueueOrigin(QueueKind.Artist, 1, "a3")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun songPresenceChangesReportReferencingPlaylistOrigins() =
|
||||
runTest {
|
||||
insertSource()
|
||||
val source =
|
||||
FakeMusicSource(
|
||||
artists = listOf(artist("a1")),
|
||||
albums = listOf(album("al1")),
|
||||
songs = listOf(song("s1"), song("s2")),
|
||||
playlists = listOf(playlist("p1")),
|
||||
playlistSongs = listOf(PlaylistSong(1, "p1", "s1", 0), PlaylistSong(1, "p1", "s2", 1)),
|
||||
)
|
||||
SyncService(db, source).sync()
|
||||
|
||||
source.songs = listOf(song("s2"))
|
||||
val deleted = SyncService(db, source).sync()
|
||||
assertTrue(deleted.changedOrigins.contains(QueueOrigin(QueueKind.Playlist, 1, "p1")))
|
||||
|
||||
source.songs = listOf(song("s1"), song("s2"))
|
||||
val restored = SyncService(db, source).sync()
|
||||
assertTrue(restored.changedOrigins.contains(QueueOrigin(QueueKind.Playlist, 1, "p1")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun downloadedSongSortChangesReportDownloadedOrigins() =
|
||||
runTest {
|
||||
insertSource()
|
||||
val source =
|
||||
FakeMusicSource(artists = listOf(artist("a1")), albums = listOf(album("al1")), songs = listOf(song("s1"), song("s2")))
|
||||
SyncService(db, source).sync()
|
||||
db.downloadDao().upsert(SongDownload(1, "s1", DownloadStatus.Completed))
|
||||
db.downloadDao().upsert(SongDownload(1, "s2", DownloadStatus.Completed))
|
||||
|
||||
source.songs = listOf(song("s1").copy(title = "Zzz", sortName = "Zzz"), song("s2").copy(title = "Zulu"))
|
||||
val summary = SyncService(db, source).sync()
|
||||
|
||||
assertTrue(summary.changedOrigins.contains(QueueOrigin(QueueKind.Downloaded, 1, "")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun albumChangesReportArtistsCreditedOnlyOnTracks() =
|
||||
runTest {
|
||||
insertSource()
|
||||
val source =
|
||||
FakeMusicSource(
|
||||
artists = listOf(artist("a1"), artist("a2")),
|
||||
albums = listOf(album("al1")),
|
||||
songs = listOf(song("s1").copy(artistId = "a2")),
|
||||
)
|
||||
SyncService(db, source).sync()
|
||||
|
||||
source.albums = listOf(album("al1").copy(year = 2024))
|
||||
val summary = SyncService(db, source).sync()
|
||||
|
||||
assertTrue(summary.changedOrigins.contains(QueueOrigin(QueueKind.Artist, 1, "a2")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun albumPruningReportsArtistsCreditedOnlyOnRemainingTracks() =
|
||||
runTest {
|
||||
insertSource()
|
||||
val source =
|
||||
FakeMusicSource(
|
||||
artists = listOf(artist("a1"), artist("a2")),
|
||||
albums = listOf(album("al1")),
|
||||
songs = listOf(song("s1").copy(artistId = "a2")),
|
||||
)
|
||||
SyncService(db, source).sync()
|
||||
|
||||
source.albums = emptyList()
|
||||
val summary = SyncService(db, source).sync()
|
||||
|
||||
assertTrue(summary.changedOrigins.contains(QueueOrigin(QueueKind.Artist, 1, "a2")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun albumChangesIgnoreSongsWithoutTrackArtists() =
|
||||
runTest {
|
||||
insertSource()
|
||||
val source =
|
||||
FakeMusicSource(
|
||||
artists = listOf(artist("a1")),
|
||||
albums = listOf(album("al1")),
|
||||
songs = listOf(song("s1").copy(artistId = null)),
|
||||
)
|
||||
SyncService(db, source).sync()
|
||||
|
||||
source.albums = listOf(album("al1").copy(year = 2024))
|
||||
val summary = SyncService(db, source).sync()
|
||||
|
||||
assertTrue(summary.changedOrigins.contains(QueueOrigin(QueueKind.Album, 1, "al1")))
|
||||
assertTrue(summary.changedOrigins.none { it.kind == QueueKind.Artist && it.refId.isBlank() })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun committedOriginsArePersistedWithoutManagerRetention() =
|
||||
runTest {
|
||||
insertSource()
|
||||
val source = FakeMusicSource(artists = listOf(artist("a1")), albums = listOf(album("al1")), songs = listOf(song("s1")))
|
||||
SyncService(db, source).sync()
|
||||
|
||||
source.albums = listOf(album("al1").copy(name = "Changed"))
|
||||
SyncService(db, source).sync()
|
||||
|
||||
val pending = db.queueDao().pendingSyncOrigins(1)
|
||||
assertTrue(pending.any { it.kind == QueueKind.Album && it.refId == "al1" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun albumPruningReportsArtistsCreditedOnlyByTheDeletedAlbum() =
|
||||
runTest {
|
||||
insertSource()
|
||||
val source =
|
||||
FakeMusicSource(
|
||||
artists = listOf(artist("a1"), artist("a2")),
|
||||
albums = listOf(album("al1"), album("al2")),
|
||||
songs =
|
||||
listOf(
|
||||
song("s1").copy(artistId = "a2"),
|
||||
song("s2").copy(albumId = "al2", artistId = "a2"),
|
||||
),
|
||||
)
|
||||
SyncService(db, source).sync()
|
||||
|
||||
source.albums = listOf(album("al2"))
|
||||
source.songs = listOf(song("s2").copy(albumId = "al2", artistId = "a2"))
|
||||
val summary = SyncService(db, source).sync()
|
||||
|
||||
assertTrue(summary.changedOrigins.contains(QueueOrigin(QueueKind.Artist, 1, "a1")))
|
||||
}
|
||||
|
||||
private suspend fun totalChanges(): Long =
|
||||
db.useWriterConnection { connection ->
|
||||
connection.usePrepared("SELECT total_changes()") { statement ->
|
||||
|
||||
Reference in New Issue
Block a user