fix(app-phone): звук при watch (диагностика+подстраховка mirrorByItem) + качка не воскресает после удаления
- onWatch: явные логи в каждой точке отказа (watch result / audio source / уже играет / не скачано / mediaDir пуст), свежий mirrorByItem если files не догрузился, звук стартует с позиции очков при уже идущем видео - resolveAudioSource → resolveAudioSourceDetailed (AudioSourceResult с причиной null) - DownloadManager: поколения generations — delete() инвалидирует старую корутину (не воскрешает качку), ensureActive перед .part/rename, jobs.remove(itemId, job) - Тесты 62/0 (в т.ч. поколения — поймали null != 0L) Инфраструктурный блокер (не код): SeaweedFS на Server3 76.66 в IO-шторме (load ~184, wa 20%, чужой yt-dlp на хосте) — большие GET с s3.binom.pw висят/рвутся с ~14:00; качка на очки FAILED по этой причине.
This commit is contained in:
@@ -1,8 +1,14 @@
|
||||
package pw.binom.viewmate.phone
|
||||
|
||||
import pw.binom.viewmate.core.media.MirrorAudio
|
||||
import pw.binom.viewmate.core.media.MirrorFiles
|
||||
import pw.binom.viewmate.core.media.MirrorVideo
|
||||
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 AudioSyncPlayerTest {
|
||||
|
||||
@@ -59,4 +65,70 @@ class AudioSyncPlayerTest {
|
||||
assertNull(d.seekToMs)
|
||||
assertNull(d.playing)
|
||||
}
|
||||
|
||||
// --- resolveAudioSourceDetailed (чистая функция, ветки причины «нет звука») ---
|
||||
|
||||
private fun mirrorFiles(audios: List<MirrorAudio> = listOf()) = MirrorFiles(
|
||||
video = MirrorVideo(key = "v", url = "https://s3/mirror/video.mkv"),
|
||||
audios = audios,
|
||||
)
|
||||
|
||||
private fun audioDir(): File = Files.createTempDirectory("audio-src-test").toFile()
|
||||
|
||||
@Test
|
||||
fun sourceIsNullWhenMirrorFilesMissing() {
|
||||
val result = resolveAudioSourceDetailed(null, 0, { File("/x", "audio-$it.ogg") }) { "signed:$it" }
|
||||
assertNull(result.source)
|
||||
assertTrue(result.reason.contains("null"), "причина должна указывать на отсутствующее зеркало: ${result.reason}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sourceIsNullWhenAudiosEmpty() {
|
||||
val result = resolveAudioSourceDetailed(mirrorFiles(), 0, { File("/x", "audio-$it.ogg") }) { "signed:$it" }
|
||||
assertNull(result.source)
|
||||
assertTrue(result.reason.contains("audios пусто"), "причина должна указывать на пустой список дорожек: ${result.reason}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun usesRequestedAudioIndex() {
|
||||
val dir = audioDir()
|
||||
File(dir, "audio-1.ogg").writeBytes(ByteArray(5))
|
||||
val audios = listOf(
|
||||
MirrorAudio(index = 0, key = "a0", url = "https://s3/audio-0.ogg"),
|
||||
MirrorAudio(index = 1, key = "a1", url = "https://s3/audio-1.ogg"),
|
||||
)
|
||||
val result = resolveAudioSourceDetailed(mirrorFiles(audios), 1, { File(dir, "audio-$it.ogg") }) { "signed:$it" }
|
||||
assertEquals(File(dir, "audio-1.ogg").absolutePath, result.source)
|
||||
assertTrue(result.reason.contains("локальный файл"), "выбрана именно дорожка #1")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fallsBackToFirstAudioWhenIndexMissing() {
|
||||
val dir = audioDir()
|
||||
File(dir, "audio-0.ogg").writeBytes(ByteArray(5))
|
||||
val audios = listOf(
|
||||
MirrorAudio(index = 0, key = "a0", url = "https://s3/audio-0.ogg"),
|
||||
MirrorAudio(index = 2, key = "a2", url = "https://s3/audio-2.ogg"),
|
||||
)
|
||||
val result = resolveAudioSourceDetailed(mirrorFiles(audios), 7, { File(dir, "audio-$it.ogg") }) { "signed:$it" }
|
||||
assertEquals(File(dir, "audio-0.ogg").absolutePath, result.source, "нет #7 — берём первую дорожку")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun usesLocalFileWhenDownloaded() {
|
||||
val dir = audioDir()
|
||||
val local = File(dir, "audio-3.ogg").apply { writeBytes(ByteArray(10)) }
|
||||
val audios = listOf(MirrorAudio(index = 3, key = "a3", url = "https://s3/audio-3.ogg"))
|
||||
val result = resolveAudioSourceDetailed(mirrorFiles(audios), 3, { File(dir, "audio-$it.ogg") }) { "signed:$it" }
|
||||
assertEquals(local.absolutePath, result.source)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun streamsPresignedUrlWhenNoLocalFile() {
|
||||
val dir = audioDir()
|
||||
val audios = listOf(MirrorAudio(index = 0, key = "a0", url = "https://s3/audio-0.ogg"))
|
||||
val result = resolveAudioSourceDetailed(mirrorFiles(audios), 0, { File(dir, "audio-$it.ogg") }) { "signed:$it" }
|
||||
assertEquals("signed:https://s3/audio-0.ogg", result.source)
|
||||
assertTrue(result.reason.contains("стриминг"), "без локального файла — стриминг: ${result.reason}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package pw.binom.viewmate.phone
|
||||
|
||||
import kotlinx.serialization.encodeToString
|
||||
import pw.binom.viewmate.core.media.MirrorAudio
|
||||
import pw.binom.viewmate.core.media.MirrorFiles
|
||||
import pw.binom.viewmate.core.media.MirrorVideo
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import kotlin.test.Test
|
||||
@@ -115,4 +118,183 @@ class DownloadManagerTest {
|
||||
assertTrue(result.downloaded.isEmpty())
|
||||
assertTrue(result.progress.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fileKeysFromMirrorCoversVideoAndAllAudios() {
|
||||
val files = MirrorFiles(
|
||||
video = MirrorVideo(key = "v", url = "http://v"),
|
||||
audios = listOf(
|
||||
MirrorAudio(index = 0, key = "a0", url = "http://a0"),
|
||||
MirrorAudio(index = 1, key = "a1", url = "http://a1"),
|
||||
),
|
||||
)
|
||||
assertEquals(listOf("video", "audio-0", "audio-1"), fileKeys(files))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isFullyOnDiskRequiresAllFilesNoParts() {
|
||||
val base = newBaseDir()
|
||||
val dir = File(base, "i").apply { mkdirs() }
|
||||
val keys = listOf("video", "audio-0", "audio-1")
|
||||
|
||||
assertTrue(!isFullyOnDisk(dir, keys), "пустой каталог — не полный")
|
||||
|
||||
File(dir, "video.mkv").writeBytes(ByteArray(10))
|
||||
assertTrue(!isFullyOnDisk(dir, keys), "только видео — не полный")
|
||||
|
||||
File(dir, "audio-0.ogg").writeBytes(ByteArray(5))
|
||||
assertTrue(!isFullyOnDisk(dir, keys), "не все аудио — не полный")
|
||||
|
||||
File(dir, "audio-1.ogg").writeBytes(ByteArray(5))
|
||||
assertTrue(isFullyOnDisk(dir, keys), "все файлы — полный")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isFullyOnDiskRejectsPartFiles() {
|
||||
val base = newBaseDir()
|
||||
val dir = File(base, "i").apply { mkdirs() }
|
||||
File(dir, "video.mkv").writeBytes(ByteArray(10))
|
||||
File(dir, "audio-0.ogg").writeBytes(ByteArray(5))
|
||||
File(dir, "audio-1.ogg").writeBytes(ByteArray(5))
|
||||
File(dir, "audio-0.ogg.part").writeBytes(ByteArray(2))
|
||||
|
||||
assertTrue(!isFullyOnDisk(dir, listOf("video", "audio-0", "audio-1")), ".part не даёт полного")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun combinedPercentAveragesAcrossAllFiles() {
|
||||
val totals = mapOf("video" to 100L, "audio-0" to 50L, "audio-1" to 50L)
|
||||
val done = mapOf("video" to 100L, "audio-0" to 25L, "audio-1" to 0L)
|
||||
assertEquals(62, combinedPercent(totals, done)) // 125/200
|
||||
}
|
||||
|
||||
@Test
|
||||
fun diskPercentCountsPartsAndFinalFiles() {
|
||||
val base = newBaseDir()
|
||||
val dir = File(base, "i").apply { mkdirs() }
|
||||
File(dir, "video.mkv").writeBytes(ByteArray(100)) // видео полностью
|
||||
File(dir, "audio-0.ogg.part").writeBytes(ByteArray(50)) // аудио докачивается
|
||||
val sizes = mapOf("video" to 100L, "audio-0" to 100L, "audio-1" to 100L)
|
||||
|
||||
assertEquals(50, diskPercent(dir, listOf("video", "audio-0", "audio-1"), sizes)) // 150/300
|
||||
}
|
||||
|
||||
@Test
|
||||
fun filePercentFromPartAndFinal() {
|
||||
val base = newBaseDir()
|
||||
val dir = File(base, "i").apply { mkdirs() }
|
||||
|
||||
// ничего нет — процент посчитать нельзя
|
||||
assertNull(filePercent(dir, "video.mkv", emptyMap()))
|
||||
|
||||
// .part: процент от известного размера дорожки
|
||||
File(dir, "video.mkv.part").writeBytes(ByteArray(50))
|
||||
assertEquals(50, filePercent(dir, "video.mkv", mapOf("video" to 100L)))
|
||||
assertNull(filePercent(dir, "video.mkv", emptyMap()), "без размера дорожки процент не считаем")
|
||||
|
||||
// полный файл — 100% (даже если размера в статусе нет)
|
||||
File(dir, "video.mkv").writeBytes(ByteArray(100))
|
||||
assertEquals(100, filePercent(dir, "video.mkv", emptyMap()))
|
||||
|
||||
// аудио не начато
|
||||
assertNull(filePercent(dir, "audio-0.ogg", mapOf("audio-0" to 100L)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun scanWithExpectedFilesMarksPartialWithOverallPercent() {
|
||||
val base = newBaseDir()
|
||||
val dir = File(base, "i").apply { mkdirs() }
|
||||
File(dir, "video.mkv").writeBytes(ByteArray(100))
|
||||
File(dir, "audio-0.ogg.part").writeBytes(ByteArray(50))
|
||||
val saved = mapOf(
|
||||
"i" to DownloadStatus(
|
||||
phase = "downloading",
|
||||
expectedFiles = listOf("video", "audio-0", "audio-1"),
|
||||
fileSizes = mapOf("video" to 100L, "audio-0" to 100L, "audio-1" to 100L),
|
||||
),
|
||||
)
|
||||
|
||||
val result = scanDownloads(base, saved)
|
||||
|
||||
assertTrue(result.downloaded.isEmpty())
|
||||
assertEquals(DownloadPhase.DOWNLOADING, result.progress["i"]?.phase)
|
||||
assertEquals(50, result.progress["i"]?.percent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun scanWithExpectedFilesMarksFullyDownloadedOnlyWhenAllFiles() {
|
||||
val base = newBaseDir()
|
||||
val dir = File(base, "i").apply { mkdirs() }
|
||||
File(dir, "video.mkv").writeBytes(ByteArray(100))
|
||||
File(dir, "audio-0.ogg").writeBytes(ByteArray(50))
|
||||
val saved = mapOf(
|
||||
"i" to DownloadStatus(expectedFiles = listOf("video", "audio-0", "audio-1")),
|
||||
)
|
||||
|
||||
// нет audio-1 — не полный
|
||||
assertTrue(scanDownloads(base, saved).downloaded.isEmpty())
|
||||
|
||||
File(dir, "audio-1.ogg").writeBytes(ByteArray(50))
|
||||
val result = scanDownloads(base, saved)
|
||||
assertEquals(setOf("i"), result.downloaded)
|
||||
assertTrue(result.progress.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fileKeyForMapsPartNamesToTrackKeys() {
|
||||
assertEquals("video", fileKeyFor("video.mkv.part"))
|
||||
assertEquals("audio-0", fileKeyFor("audio-0.ogg.part"))
|
||||
assertEquals("audio-1", fileKeyFor("audio-1.ogg.part"))
|
||||
assertEquals("video.mkv.part", partFileName("video"))
|
||||
assertEquals("audio-1.ogg.part", partFileName("audio-1"))
|
||||
assertEquals("video.mkv", finalFileName("video"))
|
||||
assertEquals("audio-0.ogg", finalFileName("audio-0"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removeDownloadStateIsIdempotent() {
|
||||
val statuses = mutableMapOf(
|
||||
"i1" to DownloadStatus(phase = "downloading", percent = 40),
|
||||
"i2" to DownloadStatus(phase = "done", percent = 100),
|
||||
)
|
||||
val downloads = mutableMapOf(
|
||||
"i1" to DownloadProgress("i1", DownloadPhase.DOWNLOADING, 40, "видео"),
|
||||
"i2" to DownloadProgress("i2", DownloadPhase.DONE, 100, "готово"),
|
||||
)
|
||||
val downloadedIds = mutableSetOf("i1", "i2")
|
||||
|
||||
removeDownloadState(statuses, downloads, downloadedIds, "i1")
|
||||
|
||||
assertTrue("i1" !in statuses, "статус удалённого itemId не должен оставаться")
|
||||
assertTrue("i1" !in downloads, "прогресс удалённого itemId не должен оставаться")
|
||||
assertTrue("i1" !in downloadedIds)
|
||||
// чужие элементы не трогаем
|
||||
assertEquals("done", statuses["i2"]?.phase)
|
||||
assertTrue("i2" in downloads)
|
||||
|
||||
// повторный вызов (и даже третий) — идемпотентен, ничего не меняет
|
||||
val snapshot = Triple(statuses.toMap(), downloads.toMap(), downloadedIds.toSet())
|
||||
removeDownloadState(statuses, downloads, downloadedIds, "i1")
|
||||
removeDownloadState(statuses, downloads, downloadedIds, "i1")
|
||||
assertEquals(snapshot, Triple(statuses.toMap(), downloads.toMap(), downloadedIds.toSet()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteBumpsGenerationInvalidatingRunningDownload() {
|
||||
val generations = mutableMapOf("i1" to 0L)
|
||||
val running = 0L
|
||||
|
||||
assertTrue(isCurrentGeneration(generations, "i1", running), "до удаления качка актуальна")
|
||||
assertTrue(isCurrentGeneration(generations, "i2", 0L), "нет записи — поколение 0 актуально")
|
||||
|
||||
// delete() увеличивает поколение
|
||||
generations["i1"] = (generations["i1"] ?: 0L) + 1
|
||||
|
||||
assertTrue(
|
||||
!isCurrentGeneration(generations, "i1", running),
|
||||
"старая незавершённая корутина после delete должна «умереть», а не воскресить качку",
|
||||
)
|
||||
assertTrue(isCurrentGeneration(generations, "i1", 1L), "новая качка после delete актуальна")
|
||||
assertTrue(isCurrentGeneration(generations, "i2", 0L), "другие itemId не затронуты")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
package pw.binom.viewmate.phone
|
||||
|
||||
import pw.binom.viewmate.core.media.MirrorAudio
|
||||
import pw.binom.viewmate.core.media.MirrorFiles
|
||||
import pw.binom.viewmate.core.media.MirrorVideo
|
||||
import pw.binom.viewmate.core.phone.buildDownloadFiles
|
||||
import pw.binom.viewmate.core.phone.buildGlassesFileSpecs
|
||||
import pw.binom.viewmate.core.protocol.DownloadPhase
|
||||
import pw.binom.viewmate.core.protocol.DownloadProgress
|
||||
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 GlassesFilesTest {
|
||||
|
||||
private fun tempDir(): File = Files.createTempDirectory("glasses-files-test").toFile()
|
||||
|
||||
private fun mirrorFiles() = MirrorFiles(
|
||||
video = MirrorVideo(key = "v", url = "https://s3/mirror/video.mkv"),
|
||||
audios = listOf(
|
||||
MirrorAudio(index = 0, key = "a0", title = "Русский", url = "https://s3/mirror/audio-0.ogg"),
|
||||
MirrorAudio(index = 1, key = "a1", url = "https://s3/mirror/audio-1.ogg"),
|
||||
),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun buildDownloadFilesUsesPresignedUrlsAndNames() {
|
||||
val msg = buildDownloadFiles(
|
||||
itemId = "movie-1",
|
||||
title = "Interstellar",
|
||||
files = mirrorFiles(),
|
||||
sizes = emptyMap(),
|
||||
presign = { "$it?sig" },
|
||||
)
|
||||
|
||||
assertEquals("movie-1", msg.itemId)
|
||||
assertEquals("Interstellar", msg.title)
|
||||
assertEquals(3, msg.files.size)
|
||||
assertEquals("video.mkv", msg.files[0].name)
|
||||
assertEquals("https://s3/mirror/video.mkv?sig", msg.files[0].url)
|
||||
assertEquals("audio-0.ogg", msg.files[1].name)
|
||||
assertEquals("audio-1.ogg", msg.files[2].name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun buildDownloadFilesFillsKnownSizes() {
|
||||
val msg = buildDownloadFiles(
|
||||
itemId = "movie-1",
|
||||
title = "T",
|
||||
files = mirrorFiles(),
|
||||
sizes = mapOf("video" to 1_863_946_240L, "audio-0" to 68_157_440L),
|
||||
presign = { it },
|
||||
)
|
||||
|
||||
assertEquals(1_863_946_240L, msg.files[0].sizeBytes)
|
||||
assertEquals(68_157_440L, msg.files[1].sizeBytes)
|
||||
assertEquals(0L, msg.files[2].sizeBytes)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun buildGlassesFileSpecsPresignsEveryFile() {
|
||||
val specs = buildGlassesFileSpecs(
|
||||
files = mirrorFiles(),
|
||||
sizes = emptyMap(),
|
||||
presign = { "signed-$it" },
|
||||
)
|
||||
assertEquals(3, specs.size)
|
||||
assertTrue(specs.all { it.url.startsWith("signed-") })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun glassesFileStateNullIsNone() {
|
||||
assertEquals(FileDownloadState.NONE to 0, glassesFileState(null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun glassesFileStateMapsPhases() {
|
||||
val done = DownloadProgress("i", "video.mkv", 100, 10, 10, DownloadPhase.DONE)
|
||||
assertEquals(FileDownloadState.DONE to 100, glassesFileState(done))
|
||||
|
||||
val downloading = DownloadProgress("i", "video.mkv", 45, 450, 1000, DownloadPhase.DOWNLOADING)
|
||||
assertEquals(FileDownloadState.DOWNLOADING to 45, glassesFileState(downloading))
|
||||
|
||||
val failed = DownloadProgress("i", "audio-0.ogg", 0, 5, 10, DownloadPhase.FAILED)
|
||||
assertEquals(FileDownloadState.FAILED to 0, glassesFileState(failed))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun glassesFileStateClampsDownloadingPercent() {
|
||||
val p = DownloadProgress("i", "video.mkv", 150, 150, 100, DownloadPhase.DOWNLOADING)
|
||||
assertEquals(FileDownloadState.DOWNLOADING to 99, glassesFileState(p))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun phoneFileStateFinalExistsIsDone() {
|
||||
val dir = tempDir()
|
||||
val final = File(dir, "video.mkv").apply { writeBytes(ByteArray(10)) }
|
||||
val part = File(dir, "video.mkv.part")
|
||||
|
||||
assertEquals(FileDownloadState.DONE to 100, phoneFileState(final, part))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun phoneFileStatePartIsDownloading() {
|
||||
val dir = tempDir()
|
||||
val final = File(dir, "video.mkv")
|
||||
val part = File(dir, "video.mkv.part").apply { writeBytes(ByteArray(10)) }
|
||||
|
||||
assertEquals(FileDownloadState.DOWNLOADING to 0, phoneFileState(final, part))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun phoneFileStateNoneWhenNothing() {
|
||||
val dir = tempDir()
|
||||
assertEquals(FileDownloadState.NONE to 0, phoneFileState(File(dir, "video.mkv"), File(dir, "video.mkv.part")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun glassesOverallPercentEmptyIsNull() {
|
||||
assertNull(glassesOverallPercent(listOf("video.mkv", "audio-0.ogg"), emptyMap(), emptyMap()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun glassesOverallPercentAllDoneIsHundred() {
|
||||
val names = listOf("video.mkv", "audio-0.ogg")
|
||||
val perFile = mapOf(
|
||||
"video.mkv" to DownloadProgress("i", "video.mkv", 100, 100, 100, DownloadPhase.DONE),
|
||||
"audio-0.ogg" to DownloadProgress("i", "audio-0.ogg", 100, 50, 50, DownloadPhase.DONE),
|
||||
)
|
||||
assertEquals(100, glassesOverallPercent(names, emptyMap(), perFile))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun glassesOverallPercentUsesKnownSizes() {
|
||||
val names = listOf("video.mkv", "audio-0.ogg")
|
||||
val known = mapOf("video.mkv" to 100L, "audio-0.ogg" to 100L)
|
||||
val perFile = mapOf(
|
||||
"video.mkv" to DownloadProgress("i", "video.mkv", 100, 100, 100, DownloadPhase.DONE),
|
||||
"audio-0.ogg" to DownloadProgress("i", "audio-0.ogg", 25, 25, 100, DownloadPhase.DOWNLOADING),
|
||||
)
|
||||
assertEquals(62, glassesOverallPercent(names, known, perFile)) // 125/200
|
||||
}
|
||||
|
||||
@Test
|
||||
fun glassesOverallPercentFallsBackToStartedTotals() {
|
||||
val names = listOf("video.mkv", "audio-0.ogg")
|
||||
val perFile = mapOf(
|
||||
"video.mkv" to DownloadProgress("i", "video.mkv", 50, 50, 100, DownloadPhase.DOWNLOADING),
|
||||
)
|
||||
assertEquals(50, glassesOverallPercent(names, emptyMap(), perFile))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun glassesLivePercentRejectsEmpty() {
|
||||
assertNull(glassesLivePercent(emptyMap()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun glassesLivePercentAllDoneIsHundred() {
|
||||
val perFile = mapOf(
|
||||
"video.mkv" to DownloadProgress("i", "video.mkv", 100, 100, 100, DownloadPhase.DONE),
|
||||
)
|
||||
assertEquals(100, glassesLivePercent(perFile))
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,8 @@ import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import pw.binom.viewmate.core.GlassesMode
|
||||
import pw.binom.viewmate.core.phone.PhoneState
|
||||
import pw.binom.viewmate.core.protocol.DownloadPhase
|
||||
import pw.binom.viewmate.core.protocol.DownloadProgress
|
||||
import pw.binom.viewmate.core.protocol.Gesture
|
||||
import pw.binom.viewmate.core.protocol.GlassesStatus
|
||||
import pw.binom.viewmate.core.protocol.GlassesToHost
|
||||
@@ -112,6 +114,55 @@ class GlassesServerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun downloadProgressUpdatesHubPerFileMap() = testApplication {
|
||||
val hub = GlassesHub(PhoneState())
|
||||
application { glassesServerModule(hub) }
|
||||
val client = createClient { install(WebSockets) }
|
||||
|
||||
client.webSocket(GLASSES_WS_PATH) {
|
||||
send(json.encodeToString(GlassesToHost.serializer(), DownloadProgress("i1", "video.mkv", 45, 450, 1000, DownloadPhase.DOWNLOADING)))
|
||||
send(json.encodeToString(GlassesToHost.serializer(), DownloadProgress("i1", "audio-0.ogg", 100, 100, 100, DownloadPhase.DONE)))
|
||||
}
|
||||
|
||||
awaitTrue { hub.glassesDownloads.value["i1"]?.size == 2 }
|
||||
assertEquals(45, hub.glassesDownloads.value["i1"]?.get("video.mkv")?.percent)
|
||||
assertEquals(DownloadPhase.DONE, hub.glassesDownloads.value["i1"]?.get("audio-0.ogg")?.phase)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun extendedGlassesStatusUpdatesHubFields() = testApplication {
|
||||
val hub = GlassesHub(PhoneState())
|
||||
application { glassesServerModule(hub) }
|
||||
val client = createClient { install(WebSockets) }
|
||||
|
||||
client.webSocket(GLASSES_WS_PATH) {
|
||||
send(
|
||||
json.encodeToString(
|
||||
GlassesToHost.serializer(),
|
||||
GlassesStatus(
|
||||
batteryPercent = 60,
|
||||
storageUsedGb = 12.5,
|
||||
storageTotalGb = 32.0,
|
||||
storageTotalBytes = 34_359_738_368,
|
||||
storageFreeBytes = 20_971_520_000,
|
||||
mediaBytes = 1_863_946_240,
|
||||
downloadedItemIds = listOf("i1", "i2"),
|
||||
mediaDir = "/data/glasses/files/viewmate",
|
||||
appVersion = "0.2.0",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
awaitTrue { hub.downloadedItemIds.value.contains("i1") }
|
||||
assertEquals(34_359_738_368L, hub.storageTotalBytes.value)
|
||||
assertEquals(20_971_520_000L, hub.storageFreeBytes.value)
|
||||
assertEquals(1_863_946_240L, hub.mediaBytes.value)
|
||||
assertEquals("/data/glasses/files/viewmate", hub.mediaDir.value)
|
||||
assertEquals("0.2.0", hub.appVersion.value)
|
||||
}
|
||||
|
||||
private suspend fun awaitTrue(timeoutMs: Long = 10_000, condition: () -> Boolean) {
|
||||
val deadline = System.currentTimeMillis() + timeoutMs
|
||||
while (!condition()) {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package pw.binom.viewmate.phone.ui
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class ComponentsTest {
|
||||
|
||||
@Test
|
||||
fun bytesToSizeKilobytes() {
|
||||
assertEquals("512 КБ", bytesToSize(512 * 1024L))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bytesToSizeMegabytes() {
|
||||
assertEquals("65 МБ", bytesToSize(65 * 1024L * 1024L))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bytesToSizeGigabytes() {
|
||||
assertEquals("1.86 ГБ", bytesToSize(2_000_000_000L))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bytesToSizeUnknownIsDashDash() {
|
||||
assertEquals("--", bytesToSize(null))
|
||||
assertEquals("--", bytesToSize(0L))
|
||||
assertEquals("--", bytesToSize(-1L))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user