diff --git a/app/build.gradle b/app/build.gradle index e0725339f..5559d0b2b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -149,6 +149,8 @@ dependencies { debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12' testImplementation "junit:junit:4.13.2" testImplementation "io.mockk:mockk:1.13.7" + testImplementation "org.robolectric:robolectric:4.9" + testImplementation 'androidx.test:core-ktx:1.5.0' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' } diff --git a/app/src/androidTest/java/org/oxycblt/auxio/StubTest.kt b/app/src/androidTest/java/org/oxycblt/auxio/StubTest.kt deleted file mode 100644 index a0ba54a3d..000000000 --- a/app/src/androidTest/java/org/oxycblt/auxio/StubTest.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * StubTest.kt is part of Auxio. - * - * 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 . - */ - -package org.oxycblt.auxio - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Assert.* -import org.junit.Test -import org.junit.runner.RunWith - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class StubTest { - // TODO: Make tests - @Test - fun useAppContext() { - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("org.oxycblt.auxio.debug", appContext.packageName) - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt index d28547239..2e3e8a944 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt @@ -63,9 +63,9 @@ data class CachedSong( /** @see RawSong */ var durationMs: Long, /** @see RawSong.replayGainTrackAdjustment */ - val replayGainTrackAdjustment: Float?, + val replayGainTrackAdjustment: Float? = null, /** @see RawSong.replayGainAlbumAdjustment */ - val replayGainAlbumAdjustment: Float?, + val replayGainAlbumAdjustment: Float? = null, /** @see RawSong.musicBrainzId */ var musicBrainzId: String? = null, /** @see RawSong.name */ diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/Separators.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/Separators.kt index 989a7b128..8d2740e74 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/Separators.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/Separators.kt @@ -43,6 +43,7 @@ interface Separators { const val SLASH = '/' const val PLUS = '+' const val AND = '&' + /** * Creates a new instance from the **current state** of the given [MusicSettings]'s * user-defined separator configuration. diff --git a/app/src/test/java/org/oxycblt/auxio/music/cache/CacheRepositoryTest.kt b/app/src/test/java/org/oxycblt/auxio/music/cache/CacheRepositoryTest.kt new file mode 100644 index 000000000..9914dbe5f --- /dev/null +++ b/app/src/test/java/org/oxycblt/auxio/music/cache/CacheRepositoryTest.kt @@ -0,0 +1,266 @@ +/* + * Copyright (c) 2023 Auxio Project + * CacheRepositoryTest.kt is part of Auxio. + * + * 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 . + */ + +package org.oxycblt.auxio.music.cache + +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerifyAll +import io.mockk.coVerifySequence +import io.mockk.just +import io.mockk.mockk +import io.mockk.slot +import java.lang.IllegalStateException +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.oxycblt.auxio.music.device.RawSong +import org.oxycblt.auxio.music.info.Date + +class CacheRepositoryTest { + @Test + fun cache_read_noInvalidate() { + val dao = + mockk { + coEvery { readSongs() }.returnsMany(listOf(CACHED_SONG_A, CACHED_SONG_B)) + } + val cacheRepository = CacheRepositoryImpl(dao) + val cache = requireNotNull(runBlocking { cacheRepository.readCache() }) + coVerifyAll { dao.readSongs() } + assertFalse(cache.invalidated) + + val songA = RawSong(mediaStoreId = 0, dateAdded = 1, dateModified = 2) + assertTrue(cache.populate(songA)) + assertEquals(RAW_SONG_A, songA) + + assertFalse(cache.invalidated) + + val songB = RawSong(mediaStoreId = 9, dateAdded = 10, dateModified = 11) + assertTrue(cache.populate(songB)) + assertEquals(RAW_SONG_B, songB) + + assertFalse(cache.invalidated) + } + + @Test + fun cache_read_invalidate() { + val dao = + mockk { + coEvery { readSongs() }.returnsMany(listOf(CACHED_SONG_A, CACHED_SONG_B)) + } + val cacheRepository = CacheRepositoryImpl(dao) + val cache = requireNotNull(runBlocking { cacheRepository.readCache() }) + coVerifyAll { dao.readSongs() } + assertFalse(cache.invalidated) + + val nullStart = RawSong(mediaStoreId = 0, dateAdded = 0, dateModified = 0) + val nullEnd = RawSong(mediaStoreId = 0, dateAdded = 0, dateModified = 0) + assertFalse(cache.populate(nullStart)) + assertEquals(nullStart, nullEnd) + + assertTrue(cache.invalidated) + + val songB = RawSong(mediaStoreId = 9, dateAdded = 10, dateModified = 11) + assertTrue(cache.populate(songB)) + assertEquals(RAW_SONG_B, songB) + + assertTrue(cache.invalidated) + } + + @Test + fun cache_read_crashes() { + val dao = mockk { coEvery { readSongs() } throws IllegalStateException() } + val cacheRepository = CacheRepositoryImpl(dao) + assertEquals(null, runBlocking { cacheRepository.readCache() }) + coVerifyAll { dao.readSongs() } + } + + @Test + fun cache_write() { + var currentlyStoredSongs = listOf() + val insertSongsArg = slot>() + val dao = + mockk { + coEvery { nukeSongs() } answers { currentlyStoredSongs = listOf() } + + coEvery { insertSongs(capture(insertSongsArg)) } answers + { + currentlyStoredSongs = insertSongsArg.captured + } + } + + val cacheRepository = CacheRepositoryImpl(dao) + + val rawSongs = listOf(RAW_SONG_A, RAW_SONG_B) + runBlocking { cacheRepository.writeCache(rawSongs) } + + val cachedSongs = listOf(CACHED_SONG_A, CACHED_SONG_B) + coVerifySequence { + dao.nukeSongs() + dao.insertSongs(cachedSongs) + } + assertEquals(cachedSongs, currentlyStoredSongs) + } + + @Test + fun cache_write_nukeCrashes() { + val dao = + mockk { + coEvery { nukeSongs() } throws IllegalStateException() + coEvery { insertSongs(listOf()) } just Runs + } + val cacheRepository = CacheRepositoryImpl(dao) + runBlocking { cacheRepository.writeCache(listOf()) } + coVerifyAll { dao.nukeSongs() } + } + + @Test + fun cache_write_insertCrashes() { + val dao = + mockk { + coEvery { nukeSongs() } just Runs + coEvery { insertSongs(listOf()) } throws IllegalStateException() + } + val cacheRepository = CacheRepositoryImpl(dao) + runBlocking { cacheRepository.writeCache(listOf()) } + coVerifySequence { + dao.nukeSongs() + dao.insertSongs(listOf()) + } + } + + private companion object { + val CACHED_SONG_A = + CachedSong( + mediaStoreId = 0, + dateAdded = 1, + dateModified = 2, + size = 3, + durationMs = 4, + replayGainTrackAdjustment = 5.5f, + replayGainAlbumAdjustment = 6.6f, + musicBrainzId = "Song MBID A", + name = "Song Name A", + sortName = "Song Sort Name A", + track = 7, + disc = 8, + subtitle = "Subtitle A", + date = Date.from("2020-10-10"), + albumMusicBrainzId = "Album MBID A", + albumName = "Album Name A", + albumSortName = "Album Sort Name A", + releaseTypes = listOf("Release Type A"), + artistMusicBrainzIds = listOf("Artist MBID A"), + artistNames = listOf("Artist Name A"), + artistSortNames = listOf("Artist Sort Name A"), + albumArtistMusicBrainzIds = listOf("Album Artist MBID A"), + albumArtistNames = listOf("Album Artist Name A"), + albumArtistSortNames = listOf("Album Artist Sort Name A"), + genreNames = listOf("Genre Name A"), + ) + + val RAW_SONG_A = + RawSong( + mediaStoreId = 0, + dateAdded = 1, + dateModified = 2, + size = 3, + durationMs = 4, + replayGainTrackAdjustment = 5.5f, + replayGainAlbumAdjustment = 6.6f, + musicBrainzId = "Song MBID A", + name = "Song Name A", + sortName = "Song Sort Name A", + track = 7, + disc = 8, + subtitle = "Subtitle A", + date = Date.from("2020-10-10"), + albumMusicBrainzId = "Album MBID A", + albumName = "Album Name A", + albumSortName = "Album Sort Name A", + releaseTypes = listOf("Release Type A"), + artistMusicBrainzIds = listOf("Artist MBID A"), + artistNames = listOf("Artist Name A"), + artistSortNames = listOf("Artist Sort Name A"), + albumArtistMusicBrainzIds = listOf("Album Artist MBID A"), + albumArtistNames = listOf("Album Artist Name A"), + albumArtistSortNames = listOf("Album Artist Sort Name A"), + genreNames = listOf("Genre Name A"), + ) + + val CACHED_SONG_B = + CachedSong( + mediaStoreId = 9, + dateAdded = 10, + dateModified = 11, + size = 12, + durationMs = 13, + replayGainTrackAdjustment = 14.14f, + replayGainAlbumAdjustment = 15.15f, + musicBrainzId = "Song MBID B", + name = "Song Name B", + sortName = "Song Sort Name B", + track = 16, + disc = 17, + subtitle = "Subtitle B", + date = Date.from("2021-11-11"), + albumMusicBrainzId = "Album MBID B", + albumName = "Album Name B", + albumSortName = "Album Sort Name B", + releaseTypes = listOf("Release Type B"), + artistMusicBrainzIds = listOf("Artist MBID B"), + artistNames = listOf("Artist Name B"), + artistSortNames = listOf("Artist Sort Name B"), + albumArtistMusicBrainzIds = listOf("Album Artist MBID B"), + albumArtistNames = listOf("Album Artist Name B"), + albumArtistSortNames = listOf("Album Artist Sort Name B"), + genreNames = listOf("Genre Name B"), + ) + + val RAW_SONG_B = + RawSong( + mediaStoreId = 9, + dateAdded = 10, + dateModified = 11, + size = 12, + durationMs = 13, + replayGainTrackAdjustment = 14.14f, + replayGainAlbumAdjustment = 15.15f, + musicBrainzId = "Song MBID B", + name = "Song Name B", + sortName = "Song Sort Name B", + track = 16, + disc = 17, + subtitle = "Subtitle B", + date = Date.from("2021-11-11"), + albumMusicBrainzId = "Album MBID B", + albumName = "Album Name B", + albumSortName = "Album Sort Name B", + releaseTypes = listOf("Release Type B"), + artistMusicBrainzIds = listOf("Artist MBID B"), + artistNames = listOf("Artist Name B"), + artistSortNames = listOf("Artist Sort Name B"), + albumArtistMusicBrainzIds = listOf("Album Artist MBID B"), + albumArtistNames = listOf("Album Artist Name B"), + albumArtistSortNames = listOf("Album Artist Sort Name B"), + genreNames = listOf("Genre Name B"), + ) + } +}