music: add more tests

Add tests for MusicViewModel, MusicMode, and MusicRepository.

Going further requires me to really learn instrumentation tests.
This commit is contained in:
Alexander Capehart 2023-02-20 15:44:32 -07:00
parent f9857355bb
commit 2d9a5ad5cd
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
12 changed files with 493 additions and 16 deletions

View file

@ -91,7 +91,7 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
val formattedNumber = getString(R.string.fmt_number, it.number)
val zipped =
if (it.name != null) {
getString(R.string.fmt_zipped_names, it.name, formattedNumber)
getString(R.string.fmt_zipped_names, formattedNumber, it.name)
} else {
formattedNumber
}

View file

@ -42,6 +42,7 @@ constructor(
private val musicRepository: MusicRepository,
private val musicSettings: MusicSettings
) : ViewModel(), MusicRepository.Listener, HomeSettings.Listener {
private val _songsList = MutableStateFlow(listOf<Song>())
/** A list of [Song]s, sorted by the preferred [Sort], to be shown in the home view. */
val songsList: StateFlow<List<Song>>

View file

@ -347,9 +347,21 @@ interface Genre : MusicParent {
val durationMs: Long
}
/**
* Run [Music.resolveName] on each instance in the given list and concatenate them into a [String]
* in a localized manner.
* @param context [Context] required
* @return A concatenated string.
*/
fun <T : Music> List<T>.resolveNames(context: Context) =
concatLocalized(context) { it.resolveName(context) }
/**
* Returns if [Music.rawName] matches for each item in a list. Useful for scenarios where the
* display information of an item must be compared without a context.
* @param other The list of items to compare to.
* @return True if they are the same (by [Music.rawName]), false otherwise.
*/
fun <T : Music> List<T>.areRawNamesTheSame(other: List<T>): Boolean {
for (i in 0 until max(size, other.size)) {
val a = getOrNull(i) ?: return false

View file

@ -0,0 +1,144 @@
/*
* Copyright (c) 2023 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music
import android.content.Context
import android.net.Uri
import java.text.CollationKey
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.metadata.Date
import org.oxycblt.auxio.music.metadata.Disc
import org.oxycblt.auxio.music.metadata.ReleaseType
import org.oxycblt.auxio.music.storage.MimeType
import org.oxycblt.auxio.music.storage.Path
open class FakeSong : Song {
override val rawName: String?
get() = throw NotImplementedError()
override val rawSortName: String?
get() = throw NotImplementedError()
override val date: Date?
get() = throw NotImplementedError()
override val dateAdded: Long
get() = throw NotImplementedError()
override val disc: Disc?
get() = throw NotImplementedError()
override val genres: List<Genre>
get() = throw NotImplementedError()
override val mimeType: MimeType
get() = throw NotImplementedError()
override val track: Int?
get() = throw NotImplementedError()
override val path: Path
get() = throw NotImplementedError()
override val size: Long
get() = throw NotImplementedError()
override val uri: Uri
get() = throw NotImplementedError()
override val album: Album
get() = throw NotImplementedError()
override val artists: List<Artist>
get() = throw NotImplementedError()
override val collationKey: CollationKey?
get() = throw NotImplementedError()
override val durationMs: Long
get() = throw NotImplementedError()
override val uid: Music.UID
get() = throw NotImplementedError()
override fun resolveName(context: Context): String {
throw NotImplementedError()
}
}
open class FakeAlbum : Album {
override val rawName: String?
get() = throw NotImplementedError()
override val rawSortName: String?
get() = throw NotImplementedError()
override val coverUri: Uri
get() = throw NotImplementedError()
override val dateAdded: Long
get() = throw NotImplementedError()
override val dates: Date.Range?
get() = throw NotImplementedError()
override val releaseType: ReleaseType
get() = throw NotImplementedError()
override val artists: List<Artist>
get() = throw NotImplementedError()
override val collationKey: CollationKey?
get() = throw NotImplementedError()
override val durationMs: Long
get() = throw NotImplementedError()
override val songs: List<Song>
get() = throw NotImplementedError()
override val uid: Music.UID
get() = throw NotImplementedError()
override fun resolveName(context: Context): String {
throw NotImplementedError()
}
}
open class FakeArtist : Artist {
override val rawName: String?
get() = throw NotImplementedError()
override val rawSortName: String?
get() = throw NotImplementedError()
override val albums: List<Album>
get() = throw NotImplementedError()
override val genres: List<Genre>
get() = throw NotImplementedError()
override val isCollaborator: Boolean
get() = throw NotImplementedError()
override val collationKey: CollationKey?
get() = throw NotImplementedError()
override val durationMs: Long
get() = throw NotImplementedError()
override val songs: List<Song>
get() = throw NotImplementedError()
override val uid: Music.UID
get() = throw NotImplementedError()
override fun resolveName(context: Context): String {
throw NotImplementedError()
}
}
open class FakeGenre : Genre {
override val rawName: String?
get() = throw NotImplementedError()
override val rawSortName: String?
get() = throw NotImplementedError()
override val albums: List<Album>
get() = throw NotImplementedError()
override val artists: List<Artist>
get() = throw NotImplementedError()
override val collationKey: CollationKey?
get() = throw NotImplementedError()
override val durationMs: Long
get() = throw NotImplementedError()
override val songs: List<Song>
get() = throw NotImplementedError()
override val uid: Music.UID
get() = throw NotImplementedError()
override fun resolveName(context: Context): String {
throw NotImplementedError()
}
}

View file

@ -20,7 +20,7 @@ package org.oxycblt.auxio.music
import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.music.storage.MusicDirectories
interface FakeMusicSettings : MusicSettings {
open class FakeMusicSettings : MusicSettings {
override fun registerListener(listener: MusicSettings.Listener) = throw NotImplementedError()
override fun unregisterListener(listener: MusicSettings.Listener) = throw NotImplementedError()
override var musicDirs: MusicDirectories

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2023 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music
import org.junit.Assert.assertEquals
import org.junit.Test
class MusicModeTest {
@Test
fun intCode() {
assertEquals(MusicMode.SONGS, MusicMode.fromIntCode(MusicMode.SONGS.intCode))
assertEquals(MusicMode.ALBUMS, MusicMode.fromIntCode(MusicMode.ALBUMS.intCode))
assertEquals(MusicMode.ARTISTS, MusicMode.fromIntCode(MusicMode.ARTISTS.intCode))
assertEquals(MusicMode.GENRES, MusicMode.fromIntCode(MusicMode.GENRES.intCode))
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2023 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music
import org.junit.Assert.assertEquals
import org.junit.Test
import org.oxycblt.auxio.music.model.FakeLibrary
import org.oxycblt.auxio.music.model.Library
class MusicRepositoryTest {
@Test
fun listeners() {
val listener = TestListener()
val impl =
MusicRepositoryImpl().apply {
library = null
addListener(listener)
}
impl.library = TestLibrary(0)
assertEquals(listOf(null, TestLibrary(0)), listener.updates)
val listener2 = TestListener()
impl.addListener(listener2)
impl.library = TestLibrary(1)
assertEquals(listOf(TestLibrary(0), TestLibrary(1)), listener2.updates)
}
private class TestListener : MusicRepository.Listener {
val updates = mutableListOf<Library?>()
override fun onLibraryChanged(library: Library?) {
updates.add(library)
}
}
private data class TestLibrary(private val id: Int) : FakeLibrary()
}

View file

@ -0,0 +1,108 @@
/*
* Copyright (c) 2023 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.oxycblt.auxio.music.model.FakeLibrary
import org.oxycblt.auxio.music.system.FakeIndexer
import org.oxycblt.auxio.music.system.Indexer
import org.oxycblt.auxio.util.forceClear
class MusicViewModelTest {
@Test
fun indexerState() {
val indexer =
TestIndexer().apply { state = Indexer.State.Indexing(Indexer.Indexing.Indeterminate) }
val musicViewModel = MusicViewModel(indexer)
assertTrue(indexer.listener is MusicViewModel)
assertEquals(
Indexer.Indexing.Indeterminate,
(musicViewModel.indexerState.value as Indexer.State.Indexing).indexing)
indexer.state = null
assertEquals(null, musicViewModel.indexerState.value)
musicViewModel.forceClear()
assertTrue(indexer.listener == null)
}
@Test
fun statistics() {
val indexer =
TestIndexer().apply { state = Indexer.State.Complete(Result.success(TestLibrary())) }
val musicViewModel = MusicViewModel(indexer)
assertEquals(
MusicViewModel.Statistics(
2,
3,
4,
1,
161616 * 2,
),
musicViewModel.statistics.value)
}
@Test
fun requests() {
val indexer = TestIndexer()
val musicViewModel = MusicViewModel(indexer)
musicViewModel.refresh()
musicViewModel.rescan()
assertEquals(listOf(true, false), indexer.requests)
}
private class TestIndexer : FakeIndexer() {
var listener: Indexer.Listener? = null
var state: Indexer.State? = null
set(value) {
field = value
listener?.onIndexerStateChanged(value)
}
val requests = mutableListOf<Boolean>()
override fun registerListener(listener: Indexer.Listener) {
this.listener = listener
listener.onIndexerStateChanged(state)
}
override fun unregisterListener(listener: Indexer.Listener) {
this.listener = null
}
override fun requestReindex(withCache: Boolean) {
requests.add(withCache)
}
}
private class TestLibrary : FakeLibrary() {
override val songs: List<Song>
get() = listOf(TestSong(), TestSong())
override val albums: List<Album>
get() = listOf(FakeAlbum(), FakeAlbum(), FakeAlbum())
override val artists: List<Artist>
get() = listOf(FakeArtist(), FakeArtist(), FakeArtist(), FakeArtist())
override val genres: List<Genre>
get() = listOf(FakeGenre())
}
private class TestSong : FakeSong() {
override val durationMs: Long
get() = 161616
}
}

View file

@ -24,22 +24,20 @@ import org.oxycblt.auxio.music.FakeMusicSettings
class TagUtilTest {
@Test
fun parseMultiValue_single() {
assertEquals(
listOf("a", "b", "c"), listOf("a,b,c").parseMultiValue(SeparatorMusicSettings(",")))
assertEquals(listOf("a", "b", "c"), listOf("a,b,c").parseMultiValue(TestMusicSettings(",")))
}
@Test
fun parseMultiValue_many() {
assertEquals(
listOf("a", "b", "c"),
listOf("a", "b", "c").parseMultiValue(SeparatorMusicSettings(",")))
listOf("a", "b", "c"), listOf("a", "b", "c").parseMultiValue(TestMusicSettings(",")))
}
@Test
fun parseMultiValue_several() {
assertEquals(
listOf("a", "b", "c", "d", "e", "f"),
listOf("a,b;c/d+e&f").parseMultiValue(SeparatorMusicSettings(",;/+&")))
listOf("a,b;c/d+e&f").parseMultiValue(TestMusicSettings(",;/+&")))
}
@Test
@ -132,43 +130,41 @@ class TagUtilTest {
fun parseId3v2Genre_multi() {
assertEquals(
listOf("Post-Rock", "Shoegaze", "Glitch"),
listOf("Post-Rock", "Shoegaze", "Glitch")
.parseId3GenreNames(SeparatorMusicSettings(",")))
listOf("Post-Rock", "Shoegaze", "Glitch").parseId3GenreNames(TestMusicSettings(",")))
}
@Test
fun parseId3v2Genre_multiId3v1() {
assertEquals(
listOf("Post-Rock", "Shoegaze", "Glitch"),
listOf("176", "178", "Glitch").parseId3GenreNames(SeparatorMusicSettings(",")))
listOf("176", "178", "Glitch").parseId3GenreNames(TestMusicSettings(",")))
}
@Test
fun parseId3v2Genre_wackId3() {
assertEquals(listOf("2941"), listOf("2941").parseId3GenreNames(SeparatorMusicSettings(",")))
assertEquals(listOf("2941"), listOf("2941").parseId3GenreNames(TestMusicSettings(",")))
}
@Test
fun parseId3v2Genre_singleId3v23() {
assertEquals(
listOf("Post-Rock", "Shoegaze", "Remix", "Cover", "Glitch"),
listOf("(176)(178)(RX)(CR)Glitch").parseId3GenreNames(SeparatorMusicSettings(",")))
listOf("(176)(178)(RX)(CR)Glitch").parseId3GenreNames(TestMusicSettings(",")))
}
@Test
fun parseId3v2Genre_singleSeparated() {
assertEquals(
listOf("Post-Rock", "Shoegaze", "Glitch"),
listOf("Post-Rock, Shoegaze, Glitch").parseId3GenreNames(SeparatorMusicSettings(",")))
listOf("Post-Rock, Shoegaze, Glitch").parseId3GenreNames(TestMusicSettings(",")))
}
@Test
fun parsId3v2Genre_singleId3v1() {
assertEquals(
listOf("Post-Rock"), listOf("176").parseId3GenreNames(SeparatorMusicSettings(",")))
assertEquals(listOf("Post-Rock"), listOf("176").parseId3GenreNames(TestMusicSettings(",")))
}
class SeparatorMusicSettings(private val separators: String) : FakeMusicSettings {
class TestMusicSettings(private val separators: String) : FakeMusicSettings() {
override var multiValueSeparators: String
get() = separators
set(_) = throw NotImplementedError()

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2023 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.model
import android.content.Context
import android.net.Uri
import org.oxycblt.auxio.music.*
open class FakeLibrary : Library {
override val songs: List<Song>
get() = throw NotImplementedError()
override val albums: List<Album>
get() = throw NotImplementedError()
override val artists: List<Artist>
get() = throw NotImplementedError()
override val genres: List<Genre>
get() = throw NotImplementedError()
override fun <T : Music> find(uid: Music.UID): T? {
throw NotImplementedError()
}
override fun findSongForUri(context: Context, uri: Uri): Song? {
throw NotImplementedError()
}
override fun <T : MusicParent> sanitize(parent: T): T? {
throw NotImplementedError()
}
override fun sanitize(song: Song): Song? {
throw NotImplementedError()
}
}

View file

@ -0,0 +1,57 @@
/*
* Copyright (c) 2023 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.system
import android.content.Context
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
open class FakeIndexer : Indexer {
override val isIndeterminate: Boolean
get() = throw NotImplementedError()
override val isIndexing: Boolean
get() = throw NotImplementedError()
override fun index(context: Context, withCache: Boolean, scope: CoroutineScope): Job {
throw NotImplementedError()
}
override fun registerController(controller: Indexer.Controller) {
throw NotImplementedError()
}
override fun unregisterController(controller: Indexer.Controller) {
throw NotImplementedError()
}
override fun registerListener(listener: Indexer.Listener) {
throw NotImplementedError()
}
override fun unregisterListener(listener: Indexer.Listener) {
throw NotImplementedError()
}
override fun requestReindex(withCache: Boolean) {
throw NotImplementedError()
}
override fun reset() {
throw NotImplementedError()
}
}

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2023 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.util
import androidx.lifecycle.ViewModel
private val VM_CLEAR_METHOD =
ViewModel::class.java.getDeclaredMethod("clear").apply { isAccessible = true }
fun ViewModel.forceClear() {
VM_CLEAR_METHOD.invoke(this)
}