metadata: harden the screenshot generator and changelog test

Only cache a cover after a successful response and drop the file if decoding
fails, so a transient error cannot poison the cache permanently. Fail with a
clear message instead of a bare NoSuchElementException when a demo album has
no songs or the artist list is empty, and normalize line endings before the
changelog length check.
This commit is contained in:
austinried
2026-10-04 00:30:08 +00:00
parent e34363c1ac
commit cdba272f98
2 changed files with 26 additions and 14 deletions
@@ -18,7 +18,7 @@ class ChangelogTest {
.filter { it.isFile && it.extension == "txt" }
assertTrue("no changelog files found under metadata/", files.isNotEmpty())
files.forEach { file ->
val text = file.readText()
val text = file.readText().replace("\r\n", "\n")
val length = text.codePointCount(0, text.length)
assertTrue(
"metadata changelogs must be at most $LIMIT characters; ${file.name} is $length",
@@ -77,7 +77,8 @@ import java.io.File
/**
* Records the store screenshots from a real sync of the public Navidrome demo library. It is not run
* by `:*UnitTest` or the integration suite; run it explicitly with `gradle :app:demoScreenshots`.
* The synced database and the downloaded cover art live only in memory and are never written down.
* The synced database is rebuilt each run; downloaded cover art is cached under the system temp dir
* so reruns do not re-fetch it.
*/
@OptIn(DelicateCoilApi::class)
@RunWith(AndroidJUnit4::class)
@@ -378,17 +379,20 @@ class DemoScreenshotTest {
val allArtists = dao.artistIds(1).mapNotNull { dao.artist(1, it).first() }
val playlists = dao.playlistIds(1).mapNotNull { dao.playlist(1, it).first() }
suspend fun songsOf(album: Album) =
dao.songsByAlbum(1, album.id).first().ifEmpty { error("demo album '${album.name}' has no songs") }
val nowPlayingAlbum = allAlbums.firstOrNull { it.name == "Chillhop Essentials - Winter 2016" } ?: allAlbums.first()
val nowPlayingSongs = dao.songsByAlbum(1, nowPlayingAlbum.id).first()
val nowPlayingSongs = songsOf(nowPlayingAlbum)
val nowPlayingSong = nowPlayingSongs.getOrNull(1) ?: nowPlayingSongs.first()
val album = allAlbums.firstOrNull { it.name == "My latin way" } ?: allAlbums.first()
val albumSongs = dao.songsByAlbum(1, album.id).first()
val albumSongs = songsOf(album)
val homePlayingAlbum = allAlbums.firstOrNull { it.name == "Shaking The Habitual" } ?: allAlbums.last()
val homePlayingSong = dao.songsByAlbum(1, homePlayingAlbum.id).first().first()
val homePlayingSong = songsOf(homePlayingAlbum).first()
val artist =
allArtists.firstOrNull { it.name == "Ugress" }
?: allArtists.maxByOrNull { it.albumCount }
?: allArtists.first()
?: error("demo library has no artists")
val artistAlbums = dao.albumsForArtist(1, artist.id).first()
val coverArtIds =
@@ -492,26 +496,34 @@ class DemoScreenshotTest {
id: String,
thumbnail: Boolean,
coverDir: File,
): Bitmap? =
try {
val url = source.coverArtUri(id, thumbnail) ?: return null
val file = File(coverDir, "${url.hashCode().toUInt().toString(16)}.img")
): Bitmap? {
val url = source.coverArtUri(id, thumbnail) ?: return null
val file = File(coverDir, "${url.hashCode().toUInt().toString(16)}.img")
return try {
if (!file.exists()) {
http.newCall(Request.Builder().url(url).build()).execute().use { response ->
check(response.isSuccessful) { "cover art HTTP ${response.code}" }
response.body.byteStream().use { input ->
file.outputStream().use { output -> input.copyTo(output) }
}
}
}
val bytes = file.readBytes()
if (thumbnail) {
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
} else {
decodeDownsampled(bytes, MAX_ART_DIM)
val bitmap =
if (thumbnail) {
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
} else {
decodeDownsampled(bytes, MAX_ART_DIM)
}
bitmap ?: run {
file.delete()
null
}
} catch (_: Exception) {
file.delete()
null
}
}
private fun decodeDownsampled(
bytes: ByteArray,