Files
stemify/android/app/src/test/java/com/stemroller/mobile/MediaCacheTest.kt
T
ClericalErrorandClaude Opus 5.5 1845f03c41 StemRoller: desktop stem separator, Android stems player, and server daemons
- stemroller/: PySide6/QML desktop app and player for separating music into stems
- android/: Plex music player that plays per-stem mixes, with Android Auto support,
  a graphic + parametric (Equalizer APO-style) EQ, and offline downloads
- server/: stemserve (serves stems to the app) and stemd (separates the library on
  the server's GPU, with "Stem Now" priority requests)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:17:17 -07:00

114 lines
4.3 KiB
Kotlin

package com.stemroller.mobile
import com.stemroller.mobile.cache.MediaCache
import okhttp3.OkHttpClient
import org.junit.After
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Test
import java.io.File
import java.io.RandomAccessFile
import java.net.URI
import java.net.URLEncoder
import java.nio.file.Files
import java.util.Random
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
/**
* Streams a real stem from a locally running stemserve (server/stemserve.py) through the
* block cache and checks every byte against the file on the share.
*/
class MediaCacheTest {
private val stems = File("/var/mnt/unraid/Music/Stems")
private lateinit var dir: File
private lateinit var src: File
private lateinit var url: String
private val http = OkHttpClient.Builder().readTimeout(60, TimeUnit.SECONDS).build()
@Before fun setUp() {
val up = runCatching { URI("http://localhost:8765/api/info").toURL().readText() }.isSuccess
assumeTrue("stemserve not running on :8765", up && stems.isDirectory)
src = stems.walkTopDown().filter { it.name.endsWith(" - vocals.flac") }.minBy { it.length() }
val rel = src.relativeTo(stems).invariantSeparatorsPath.split('/')
.joinToString("/") { URLEncoder.encode(it, "UTF-8").replace("+", "%20") }
url = "http://localhost:8765/file/$rel"
dir = Files.createTempDirectory("mediacache").toFile()
}
@After fun tearDown() {
if (::dir.isInitialized) dir.deleteRecursively()
}
private fun expected(pos: Long, len: Int): ByteArray = RandomAccessFile(src, "r").use { f ->
f.seek(pos)
ByteArray(len).also { f.readFully(it) }
}
@Test fun randomReadsMatchTheFile() {
val cache = MediaCache(dir, http) { Long.MAX_VALUE }
val res = cache.acquire("test|vocals") { url }
assertEquals(src.length(), res.size())
val rnd = Random(42)
val pool = Executors.newFixedThreadPool(4)
val jobs = (0 until 64).map {
val len = 1 + rnd.nextInt(300_000)
val pos = (rnd.nextDouble() * (src.length() - len)).toLong()
pool.submit<Unit> {
val buf = ByteArray(len)
var got = 0
while (got < len) {
val n = res.readAt(pos + got, buf, got, len - got)
check(n > 0)
got += n
}
assertArrayEquals("at $pos+$len", expected(pos, len), buf)
}
}
jobs.forEach { it.get(60, TimeUnit.SECONDS) }
pool.shutdown()
// Past EOF
assertEquals(-1, res.readAt(src.length(), ByteArray(10), 0, 10))
cache.release(res)
}
@Test fun downloadPersistsAndSurvivesReopen() {
val cache = MediaCache(dir, http) { Long.MAX_VALUE }
val res = cache.acquire("dl|vocals") { url }
res.pinned = true
res.downloadAll({ _, _ -> }, { false })
assertTrue(res.isComplete)
cache.release(res)
assertTrue(cache.isComplete("dl|vocals"))
// A fresh cache over the same directory serves it without any network.
val offline = MediaCache(dir, http) { Long.MAX_VALUE }
val again = offline.acquire("dl|vocals") { "http://127.0.0.1:9/unreachable" }
val buf = ByteArray(4096)
val tail = src.length() - 4096
assertEquals(4096, again.readAt(tail, buf, 0, 4096))
assertArrayEquals(expected(tail, 4096), buf)
offline.release(again)
}
@Test fun evictionSparesPinnedAndInUse() {
val cache = MediaCache(dir, http) { 1L }
val pinned = cache.acquire("pinned") { url }
pinned.pinned = true
pinned.downloadAll({ _, _ -> }, { false })
cache.release(pinned)
val loose = cache.acquire("loose") { url }
loose.downloadAll({ _, _ -> }, { false })
cache.release(loose)
// Trimming runs on the cache's own thread; give it a moment.
val deadline = System.currentTimeMillis() + 5000
while (cache.isComplete("loose") && System.currentTimeMillis() < deadline) Thread.sleep(50)
assertFalse(cache.isComplete("loose"))
assertTrue(cache.isComplete("pinned"))
}
}