mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 23:21:59 +01:00
bound the downloads handed to the platform, and confirm bulk deletes
Two things a 3k-song playlist turned up, plus the hero's downloaded state. - The platform runs about eight downloads at a time, but a bulk action handed it every song at once. Three thousand pending requests meant a notification flood, and a poll tick that rewrote every row's bytes/total, so the whole list churned ten times a second. Now a bulk action only records the songs and hands over at most IN_FLIGHT_LIMIT, oldest first, promoting the next as each finishes, so our row state mirrors what is actually running and a cancel only has to reach what is in flight. - Cancelling was therefore unusable at that size. It now touches at most the in-flight requests instead of thousands, and the rows wait behind it. - A queued row with no engine id is waiting its turn, not lost, so reconcile no longer re-enqueues the whole list on every pass; a row whose engine entry vanished goes back to waiting and the next promote picks it up. A song that cannot be handed over fails instead of being retried every tick. - Requests follow the order the screen shows: artist songs resolve through the same albums ordering the artist page uses, so they were the one list that did not. - The hero's fully-downloaded state shows the downloaded glyph rather than a bin, and tapping it asks before deleting, naming the album or playlist and the space it frees.
This commit is contained in:
@@ -560,11 +560,10 @@ class DownloadRepositoryTest {
|
||||
fun cancellingWhileAListIsStillQueuingRemovesTheWholeList() =
|
||||
runTest {
|
||||
seedLibrary()
|
||||
seedSongs(3..40)
|
||||
engine.enqueueDelayMs = 1
|
||||
seedSongs(3..400)
|
||||
|
||||
val queuing = launch(Dispatchers.IO) { repository.downloadAll(1, DownloadList.Album, "al1") }
|
||||
await { engine.requests.size >= 3 }
|
||||
await { allRows().size >= 50 }
|
||||
val cancelling = launch(Dispatchers.IO) { repository.cancelAll(1, DownloadList.Album, "al1") }
|
||||
queuing.join()
|
||||
cancelling.join()
|
||||
@@ -572,6 +571,50 @@ class DownloadRepositoryTest {
|
||||
assertTrue("expected the whole list cancelled, ${db.downloadDao().all().size} rows left", db.downloadDao().all().isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aLongListHandsThePlatformOnlyTheInFlightLimit() =
|
||||
runTest {
|
||||
seedLibrary()
|
||||
seedSongs(3..40)
|
||||
|
||||
runBlocking { repository.downloadAll(1, DownloadList.Album, "al1") }
|
||||
|
||||
assertEquals(8, engine.requests.size)
|
||||
assertEquals(32, db.downloadDao().all().count { it.status == DownloadStatus.Queued && it.engineId == null })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun finishingADownloadHandsOverTheNextSongInListOrder() =
|
||||
runTest {
|
||||
seedLibrary()
|
||||
seedSongs(3..12)
|
||||
runBlocking { repository.downloadAll(1, DownloadList.Album, "al1") }
|
||||
val first = engine.requests.first()
|
||||
|
||||
engine.complete(first.first, total = 10)
|
||||
writeFile(1, first.second.path.substringAfterLast('/'), "audio")
|
||||
runBlocking { repository.reconcile() }
|
||||
|
||||
assertEquals(9, engine.requests.size)
|
||||
assertEquals((1..9).map { "s$it" }, requestedSongIds())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun reconcilingAWaitingListDoesNotHandItOverWholesale() =
|
||||
runTest {
|
||||
seedLibrary()
|
||||
seedSongs(3..40)
|
||||
runBlocking {
|
||||
(1..40).forEach { track ->
|
||||
db.downloadDao().upsert(SongDownload(1, "s$track", DownloadStatus.Queued))
|
||||
}
|
||||
}
|
||||
|
||||
runBlocking { repository.reconcile() }
|
||||
|
||||
assertEquals(8, engine.requests.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun oneSongFailingToQueueDoesNotAbandonTheRest() =
|
||||
runTest {
|
||||
@@ -613,6 +656,19 @@ class DownloadRepositoryTest {
|
||||
assertNull(row(1, "s2"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun downloadedBytesCountsTheCompletedFilesOfTheList() =
|
||||
runTest {
|
||||
seedLibrary()
|
||||
writeFile(1, "s1", "12345")
|
||||
db.downloadDao().upsert(SongDownload(1, "s1", DownloadStatus.Completed, engineId = 11))
|
||||
writeFile(1, "s2", "1234")
|
||||
db.downloadDao().upsert(SongDownload(1, "s2", DownloadStatus.Running, engineId = 22))
|
||||
writeFile(1, "s3", "123")
|
||||
|
||||
assertEquals(5L, repository.downloadedBytes(1, DownloadList.Album, "al1"))
|
||||
}
|
||||
|
||||
private fun requestedSongIds(): List<String> = engine.requests.map { it.second.path.substringAfterLast('/') }
|
||||
|
||||
private fun seedSongs(tracks: IntRange) {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.subtracks.ui.components
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.subtracks.ui.theme.SubtracksTheme
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.GraphicsMode
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||
class DeleteDownloadsDialogTest {
|
||||
@get:Rule
|
||||
val composeRule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun theDialogNamesTheThingAndTheSpaceItFrees() {
|
||||
render(bytes = 1_500_000)
|
||||
|
||||
composeRule.onNodeWithText("Kid A", substring = true).assertIsDisplayed()
|
||||
composeRule.onNodeWithText("1.5 MB", substring = true).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteConfirms() {
|
||||
var confirms = 0
|
||||
render(onConfirm = { confirms++ })
|
||||
|
||||
composeRule.onNodeWithText("Delete").performClick()
|
||||
|
||||
assertEquals(1, confirms)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cancelDismisses() {
|
||||
var dismisses = 0
|
||||
render(onDismiss = { dismisses++ })
|
||||
|
||||
composeRule.onNodeWithText("Cancel").performClick()
|
||||
|
||||
assertEquals(1, dismisses)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bytesReadAsUnits() {
|
||||
assertEquals("512 B", formatBytes(512))
|
||||
assertEquals("1.0 kB", formatBytes(1_024))
|
||||
assertEquals("1.5 MB", formatBytes(1_500_000))
|
||||
assertEquals("2.0 GB", formatBytes(2_000_000_000))
|
||||
}
|
||||
|
||||
private fun render(
|
||||
bytes: Long = 0,
|
||||
onConfirm: () -> Unit = {},
|
||||
onDismiss: () -> Unit = {},
|
||||
) {
|
||||
composeRule.setContent {
|
||||
SubtracksTheme {
|
||||
DeleteDownloadsDialog(name = "Kid A", bytes = bytes, onConfirm = onConfirm, onDismiss = onDismiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user