fix(app-phone): персистентный статус скачивания + докачка с места обрыва + StatusBlock
- DownloadManager: .part-файлы, resume через Range (206/200), status.json (фаза/процент/total), скан диска при старте, guard от повторной качки - DetailsScreen: кнопка «✓ скачано на телефон»/«Продолжить (N%)», чипы из диска - Components: StatusBlock Box→Column (строки статуса перекрывались кнопкой «Удалить»); приоритет бейджа: «✓» done выше «↓ часть» - 8 новых тестов (Range, скан диска, status.json)
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
package pw.binom.viewmate.phone
|
||||
|
||||
import kotlinx.serialization.encodeToString
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class DownloadManagerTest {
|
||||
|
||||
private fun tempDir(): File = Files.createTempDirectory("dm-test").toFile()
|
||||
|
||||
private fun newBaseDir(): File {
|
||||
val base = tempDir()
|
||||
base.deleteOnExit()
|
||||
return base
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rangeHeaderFormedFromPartSize() {
|
||||
val noResume = requestFor("https://s3/mirror/video.mkv", 0)
|
||||
assertNull(noResume.header("Range"), "без .part Range-заголовка быть не должно")
|
||||
|
||||
val resume = requestFor("https://s3/mirror/video.mkv", 1_863_946_240)
|
||||
assertEquals("bytes=1863946240-", resume.header("Range"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun scanFullFileMarksDownloadedAndRemovesPart() {
|
||||
val base = newBaseDir()
|
||||
val dir = File(base, "i1").apply { mkdirs() }
|
||||
File(dir, "video.mkv").writeBytes(ByteArray(10))
|
||||
File(dir, "video.mkv.part").writeBytes(ByteArray(5)) // оставшийся после рестарта
|
||||
|
||||
val result = scanDownloads(base, emptyMap())
|
||||
|
||||
assertEquals(setOf("i1"), result.downloaded)
|
||||
assertTrue(result.progress.isEmpty())
|
||||
assertTrue(!File(dir, "video.mkv.part").exists(), ".part при наличии полного файла удаляется")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun scanPartOnlyMarksDownloadingWithPercent() {
|
||||
val base = newBaseDir()
|
||||
val dir = File(base, "i2").apply { mkdirs() }
|
||||
File(dir, "video.mkv.part").writeBytes(ByteArray(500))
|
||||
val saved = mapOf("i2" to DownloadStatus(totalBytes = 1000))
|
||||
|
||||
val result = scanDownloads(base, saved)
|
||||
|
||||
assertTrue(result.downloaded.isEmpty())
|
||||
assertEquals(
|
||||
DownloadProgress("i2", DownloadPhase.DOWNLOADING, 50, "видео"),
|
||||
result.progress["i2"],
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun scanPartWithoutTotalHasZeroPercent() {
|
||||
val base = newBaseDir()
|
||||
val dir = File(base, "i3").apply { mkdirs() }
|
||||
File(dir, "video.mkv.part").writeBytes(ByteArray(500))
|
||||
|
||||
val result = scanDownloads(base, emptyMap())
|
||||
|
||||
assertEquals(DownloadProgress("i3", DownloadPhase.DOWNLOADING, 0, "видео"), result.progress["i3"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun statusFileWritesAndReads() {
|
||||
val file = File(tempDir(), "status.json")
|
||||
val store = DownloadStatusStore(file)
|
||||
val statuses = mapOf(
|
||||
"i1" to DownloadStatus(phase = "downloading", stage = "видео", percent = 42, totalBytes = 1_000_000, updatedAt = 1234L),
|
||||
"i2" to DownloadStatus(phase = "done", percent = 100, totalBytes = 500, updatedAt = 5678L),
|
||||
)
|
||||
|
||||
store.write(statuses)
|
||||
val read = store.read()
|
||||
|
||||
assertEquals(statuses, read)
|
||||
assertTrue(file.exists())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun statusFileMissingReadsEmpty() {
|
||||
val store = DownloadStatusStore(File(tempDir(), "status.json"))
|
||||
assertTrue(store.read().isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun statusSerializationMatchesSpecKeys() {
|
||||
val text = statusJson.encodeToString(
|
||||
mapOf("i1" to DownloadStatus(phase = "downloading", stage = "видео", percent = 50, totalBytes = 100, updatedAt = 42L)),
|
||||
)
|
||||
assertTrue(text.contains("\"phase\""))
|
||||
assertTrue(text.contains("\"stage\""))
|
||||
assertTrue(text.contains("\"percent\""))
|
||||
assertTrue(text.contains("\"totalBytes\""))
|
||||
assertTrue(text.contains("\"updatedAt\""))
|
||||
assertTrue(text.contains("\"downloading\""))
|
||||
assertTrue(text.contains("\"видео\""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun scanIgnoresNonPartFilesWithoutFinalVideo() {
|
||||
val base = newBaseDir()
|
||||
val dir = File(base, "i4").apply { mkdirs() }
|
||||
File(dir, "some-random.bin").writeBytes(ByteArray(10))
|
||||
|
||||
val result = scanDownloads(base, emptyMap())
|
||||
|
||||
assertTrue(result.downloaded.isEmpty())
|
||||
assertTrue(result.progress.isEmpty())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user