all: post-refactor cleanup

This commit is contained in:
Alexander Capehart 2023-01-29 17:45:51 -07:00
parent bfb1033ed7
commit f8d1a880d4
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
11 changed files with 103 additions and 97 deletions

View file

@ -95,7 +95,7 @@ dependencies {
implementation "androidx.preference:preference-ktx:1.2.0" implementation "androidx.preference:preference-ktx:1.2.0"
// Database // Database
def room_version = '2.4.3' def room_version = '2.5.0'
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version" implementation "androidx.room:room-ktx:$room_version"
@ -122,8 +122,8 @@ dependencies {
// Development // Development
debugImplementation "com.squareup.leakcanary:leakcanary-android:2.9.1" debugImplementation "com.squareup.leakcanary:leakcanary-android:2.9.1"
testImplementation "junit:junit:4.13.2" testImplementation "junit:junit:4.13.2"
androidTestImplementation 'androidx.test.ext:junit:1.1.4' androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
} }
spotless { spotless {

View file

@ -225,6 +225,7 @@ sealed interface Music : Item {
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
sealed interface MusicParent : Music { sealed interface MusicParent : Music {
/** The child [Song]s of this [MusicParent]. */
val songs: List<Song> val songs: List<Song>
} }
@ -337,10 +338,6 @@ interface Album : MusicParent {
/** /**
* An abstract artist. These are actually a combination of the artist and album artist tags from * An abstract artist. These are actually a combination of the artist and album artist tags from
* within the library, derived from [Song]s and [Album]s respectively. * within the library, derived from [Song]s and [Album]s respectively.
* @param raw The [Artist.Raw] to derive the member data from.
* @param songAlbums A list of the [Song]s and [Album]s that are a part of this [Artist], either
* through artist or album artist tags. Providing [Song]s to the artist is optional. These instances
* will be linked to this [Artist].
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
interface Artist : MusicParent { interface Artist : MusicParent {

View file

@ -401,7 +401,7 @@ class RealAlbum(val raw: Raw, override val songs: List<RealSong>) : Album {
val sortName: String?, val sortName: String?,
/** @see Album.releaseType */ /** @see Album.releaseType */
val releaseType: ReleaseType?, val releaseType: ReleaseType?,
/** @see Artist.Raw.name */ /** @see RealArtist.Raw.name */
val rawArtists: List<RealArtist.Raw> val rawArtists: List<RealArtist.Raw>
) { ) {
// Albums are grouped as follows: // Albums are grouped as follows:
@ -694,29 +694,6 @@ fun MessageDigest.update(n: Int?) {
} }
} }
/** Cached collator instance re-used with [makeCollationKey]. */
private val COLLATOR: Collator = Collator.getInstance().apply { strength = Collator.PRIMARY }
/**
* Provided implementation to create a [CollationKey] in the way described by [collationKey]. This
* should be used in all overrides of all [CollationKey].
* @param music The [Music] to create the [CollationKey] for.
* @return A [CollationKey] that follows the specification described by [collationKey].
*/
private fun makeCollationKey(music: Music): CollationKey? {
val sortName =
(music.rawSortName ?: music.rawName)?.run {
when {
length > 5 && startsWith("the ", ignoreCase = true) -> substring(4)
length > 4 && startsWith("an ", ignoreCase = true) -> substring(3)
length > 3 && startsWith("a ", ignoreCase = true) -> substring(2)
else -> this
}
}
return COLLATOR.getCollationKey(sortName)
}
/** /**
* Join a list of [Music]'s resolved names into a string in a localized manner, using * Join a list of [Music]'s resolved names into a string in a localized manner, using
* [R.string.fmt_list]. * [R.string.fmt_list].
@ -737,3 +714,26 @@ private fun resolveNames(context: Context, values: List<Music>): String {
} }
return joined return joined
} }
/** Cached collator instance re-used with [makeCollationKey]. */
private val COLLATOR: Collator = Collator.getInstance().apply { strength = Collator.PRIMARY }
/**
* Provided implementation to create a [CollationKey] in the way described by [Music.collationKey].
* This should be used in all overrides of all [CollationKey].
* @param music The [Music] to create the [CollationKey] for.
* @return A [CollationKey] that follows the specification described by [Music.collationKey].
*/
private fun makeCollationKey(music: Music): CollationKey? {
val sortName =
(music.rawSortName ?: music.rawName)?.run {
when {
length > 5 && startsWith("the ", ignoreCase = true) -> substring(4)
length > 4 && startsWith("an ", ignoreCase = true) -> substring(3)
length > 3 && startsWith("a ", ignoreCase = true) -> substring(2)
else -> this
}
}
return COLLATOR.getCollationKey(sortName)
}

View file

@ -202,9 +202,7 @@ private abstract class CacheDatabase : RoomDatabase() {
@Dao @Dao
private interface CacheDao { private interface CacheDao {
@Query("SELECT * FROM ${CachedSong.TABLE_NAME}") suspend fun readCache(): List<CachedSong> @Query("SELECT * FROM ${CachedSong.TABLE_NAME}") suspend fun readCache(): List<CachedSong>
@Query("DELETE FROM ${CachedSong.TABLE_NAME}") suspend fun nukeCache() @Query("DELETE FROM ${CachedSong.TABLE_NAME}") suspend fun nukeCache()
@Insert suspend fun insertCache(songs: List<CachedSong>) @Insert suspend fun insertCache(songs: List<CachedSong>)
} }
@ -216,49 +214,49 @@ private data class CachedSong(
* unstable and should only be used for accessing the audio file. * unstable and should only be used for accessing the audio file.
*/ */
@PrimaryKey var mediaStoreId: Long, @PrimaryKey var mediaStoreId: Long,
/** @see Song.dateAdded */ /** @see RealSong.Raw.dateAdded */
var dateAdded: Long, var dateAdded: Long,
/** The latest date the [Song]'s audio file was modified, as a unix epoch timestamp. */ /** The latest date the [Song]'s audio file was modified, as a unix epoch timestamp. */
var dateModified: Long, var dateModified: Long,
/** @see Song.size */ /** @see RealSong.Raw.size */
var size: Long? = null, var size: Long? = null,
/** @see Song.durationMs */ /** @see RealSong.Raw */
var durationMs: Long, var durationMs: Long,
/** @see Music.UID */ /** @see RealSong.Raw.musicBrainzId */
var musicBrainzId: String? = null, var musicBrainzId: String? = null,
/** @see Music.rawName */ /** @see RealSong.Raw.name */
var name: String, var name: String,
/** @see Music.rawSortName */ /** @see RealSong.Raw.sortName */
var sortName: String? = null, var sortName: String? = null,
/** @see Song.track */ /** @see RealSong.Raw.track */
var track: Int? = null, var track: Int? = null,
/** @see Disc.number */ /** @see RealSong.Raw.name */
var disc: Int? = null, var disc: Int? = null,
/** @See Disc.name */ /** @See RealSong.Raw.subtitle */
var subtitle: String? = null, var subtitle: String? = null,
/** @see Song.date */ /** @see RealSong.Raw.date */
var date: Date? = null, var date: Date? = null,
/** @see Album.Raw.musicBrainzId */ /** @see RealSong.Raw.albumMusicBrainzId */
var albumMusicBrainzId: String? = null, var albumMusicBrainzId: String? = null,
/** @see Album.Raw.name */ /** @see RealSong.Raw.albumName */
var albumName: String, var albumName: String,
/** @see Album.Raw.sortName */ /** @see RealSong.Raw.albumSortName */
var albumSortName: String? = null, var albumSortName: String? = null,
/** @see Album.Raw.releaseType */ /** @see RealSong.Raw.releaseTypes */
var releaseTypes: List<String> = listOf(), var releaseTypes: List<String> = listOf(),
/** @see Artist.Raw.musicBrainzId */ /** @see RealSong.Raw.artistMusicBrainzIds */
var artistMusicBrainzIds: List<String> = listOf(), var artistMusicBrainzIds: List<String> = listOf(),
/** @see Artist.Raw.name */ /** @see RealSong.Raw.artistNames */
var artistNames: List<String> = listOf(), var artistNames: List<String> = listOf(),
/** @see Artist.Raw.sortName */ /** @see RealSong.Raw.artistSortNames */
var artistSortNames: List<String> = listOf(), var artistSortNames: List<String> = listOf(),
/** @see Artist.Raw.musicBrainzId */ /** @see RealSong.Raw.albumArtistMusicBrainzIds */
var albumArtistMusicBrainzIds: List<String> = listOf(), var albumArtistMusicBrainzIds: List<String> = listOf(),
/** @see Artist.Raw.name */ /** @see RealSong.Raw.albumArtistNames */
var albumArtistNames: List<String> = listOf(), var albumArtistNames: List<String> = listOf(),
/** @see Artist.Raw.sortName */ /** @see RealSong.Raw.albumArtistSortNames */
var albumArtistSortNames: List<String> = listOf(), var albumArtistSortNames: List<String> = listOf(),
/** @see Genre.Raw.name */ /** @see RealSong.Raw.genreNames */
var genreNames: List<String> = listOf() var genreNames: List<String> = listOf()
) { ) {
fun copyToRaw(rawSong: RealSong.Raw): CachedSong { fun copyToRaw(rawSong: RealSong.Raw): CachedSong {

View file

@ -125,7 +125,7 @@ sealed class ReleaseType {
} }
/** /**
* A Mix-tape. These are usually [EP]-sized releases of music made to promote an [Artist] or a * A Mix-tape. These are usually [EP]-sized releases of music made to promote an Artist or a
* future release. * future release.
*/ */
object Mixtape : ReleaseType() { object Mixtape : ReleaseType() {
@ -141,7 +141,7 @@ sealed class ReleaseType {
/** A release consisting of a live performance */ /** A release consisting of a live performance */
LIVE, LIVE,
/** A release consisting of another [Artist]s remix of a prior performance. */ /** A release consisting of another Artists remix of a prior performance. */
REMIX REMIX
} }

View file

@ -86,8 +86,7 @@ interface Library {
} }
private class RealLibrary(rawSongs: List<RealSong.Raw>, settings: MusicSettings) : Library { private class RealLibrary(rawSongs: List<RealSong.Raw>, settings: MusicSettings) : Library {
override val songs = override val songs = buildSongs(rawSongs, settings)
Sort(Sort.Mode.ByName, true).songs(rawSongs.map { RealSong(it, settings) }.distinct())
override val albums = buildAlbums(songs) override val albums = buildAlbums(songs)
override val artists = buildArtists(songs, albums) override val artists = buildArtists(songs, albums)
override val genres = buildGenres(songs) override val genres = buildGenres(songs)
@ -124,6 +123,16 @@ private class RealLibrary(rawSongs: List<RealSong.Raw>, settings: MusicSettings)
songs.find { it.path.name == displayName && it.size == size } songs.find { it.path.name == displayName && it.size == size }
} }
/**
* Build a list [RealSong]s from the given [RealSong.Raw].
* @param rawSongs The [RealSong.Raw]s to build the [RealSong]s from.
* @param settings [MusicSettings] required to build [RealSong]s.
* @return A sorted list of [RealSong]s derived from the [RealSong.Raw] that should be suitable
* for grouping.
*/
private fun buildSongs(rawSongs: List<RealSong.Raw>, settings: MusicSettings) =
Sort(Sort.Mode.ByName, true).songs(rawSongs.map { RealSong(it, settings) }.distinct())
/** /**
* Build a list of [Album]s from the given [Song]s. * Build a list of [Album]s from the given [Song]s.
* @param songs The [Song]s to build [Album]s from. These will be linked with their respective * @param songs The [Song]s to build [Album]s from. These will be linked with their respective

View file

@ -96,7 +96,7 @@ fun List<String>.correctWhitespace() = mapNotNull { it.correctWhitespace() }
/** /**
* Attempt to parse a string by the user's separator preferences. * Attempt to parse a string by the user's separator preferences.
* @param settings [Settings] required to obtain user separator configuration. * @param settings [MusicSettings] required to obtain user separator configuration.
* @return A list of one or more [String]s that were split up by the user-defined separators. * @return A list of one or more [String]s that were split up by the user-defined separators.
*/ */
private fun String.maybeParseBySeparators(settings: MusicSettings): List<String> { private fun String.maybeParseBySeparators(settings: MusicSettings): List<String> {

View file

@ -186,8 +186,9 @@ class EditableQueue : Queue {
/** /**
* Add [Song]s to the top of the queue. Will start playback if nothing is playing. * Add [Song]s to the top of the queue. Will start playback if nothing is playing.
* @param songs The [Song]s to add. * @param songs The [Song]s to add.
* @return [ChangeResult.MAPPING] if added to an existing queue, or [ChangeResult.SONG] if there * @return [Queue.ChangeResult.MAPPING] if added to an existing queue, or
* was no prior playback and these enqueued [Song]s start new playback. * [Queue.ChangeResult.SONG] if there was no prior playback and these enqueued [Song]s start new
* playback.
*/ */
fun playNext(songs: List<Song>): Queue.ChangeResult { fun playNext(songs: List<Song>): Queue.ChangeResult {
if (orderedMapping.isEmpty()) { if (orderedMapping.isEmpty()) {

View file

@ -129,7 +129,7 @@ class WidgetComponent(private val context: Context) :
/** /**
* A condensed form of the playback state that is safe to use in AppWidgets. * A condensed form of the playback state that is safe to use in AppWidgets.
* @param song [PlaybackStateManager.song] * @param song [Queue.currentSong]
* @param cover A pre-loaded album cover [Bitmap] for [song]. * @param cover A pre-loaded album cover [Bitmap] for [song].
* @param isPlaying [PlaybackStateManager.playerState] * @param isPlaying [PlaybackStateManager.playerState]
* @param repeatMode [PlaybackStateManager.repeatMode] * @param repeatMode [PlaybackStateManager.repeatMode]

View file

@ -12,13 +12,14 @@
android:layout_height="match_parent"> android:layout_height="match_parent">
<TextView <TextView
style="@style/Widget.Auxio.TextView.Header"
android:id="@+id/dirs_mode_header" android:id="@+id/dirs_mode_header"
style="@style/Widget.Auxio.TextView.Header"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="@dimen/spacing_large" android:paddingStart="@dimen/spacing_large"
android:paddingEnd="@dimen/spacing_large" android:paddingEnd="@dimen/spacing_large"
android:text="@string/set_dirs_mode" /> android:text="@string/set_dirs_mode"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButtonToggleGroup <com.google.android.material.button.MaterialButtonToggleGroup

View file

@ -51,23 +51,23 @@ class MusicTest {
@Test @Test
fun albumRaw_equals_inconsistentCase() { fun albumRaw_equals_inconsistentCase() {
val a = val a =
Album.Raw( RealAlbum.Raw(
mediaStoreId = -1, mediaStoreId = -1,
musicBrainzId = null, musicBrainzId = null,
name = "Paraglow", name = "Paraglow",
sortName = null, sortName = null,
releaseType = null, releaseType = null,
rawArtists = rawArtists =
listOf(Artist.Raw(name = "Parannoul"), Artist.Raw(name = "Asian Glow"))) listOf(RealArtist.Raw(name = "Parannoul"), RealArtist.Raw(name = "Asian Glow")))
val b = val b =
Album.Raw( RealAlbum.Raw(
mediaStoreId = -1, mediaStoreId = -1,
musicBrainzId = null, musicBrainzId = null,
name = "paraglow", name = "paraglow",
sortName = null, sortName = null,
releaseType = null, releaseType = null,
rawArtists = rawArtists =
listOf(Artist.Raw(name = "Parannoul"), Artist.Raw(name = "Asian glow"))) listOf(RealArtist.Raw(name = "Parannoul"), RealArtist.Raw(name = "Asian glow")))
assertTrue(a == b) assertTrue(a == b)
assertTrue(a.hashCode() == b.hashCode()) assertTrue(a.hashCode() == b.hashCode())
} }
@ -75,21 +75,21 @@ class MusicTest {
@Test @Test
fun albumRaw_equals_withMbids() { fun albumRaw_equals_withMbids() {
val a = val a =
Album.Raw( RealAlbum.Raw(
mediaStoreId = -1, mediaStoreId = -1,
musicBrainzId = UUID.fromString("c7b245c9-8099-32ea-af95-893acedde2cf"), musicBrainzId = UUID.fromString("c7b245c9-8099-32ea-af95-893acedde2cf"),
name = "Weezer", name = "Weezer",
sortName = "Blue Album", sortName = "Blue Album",
releaseType = null, releaseType = null,
rawArtists = listOf(Artist.Raw(name = "Weezer"))) rawArtists = listOf(RealArtist.Raw(name = "Weezer")))
val b = val b =
Album.Raw( RealAlbum.Raw(
mediaStoreId = -1, mediaStoreId = -1,
musicBrainzId = UUID.fromString("923d5ba6-7eee-3bce-bcb2-c913b2bd69d4"), musicBrainzId = UUID.fromString("923d5ba6-7eee-3bce-bcb2-c913b2bd69d4"),
name = "Weezer", name = "Weezer",
sortName = "Green Album", sortName = "Green Album",
releaseType = null, releaseType = null,
rawArtists = listOf(Artist.Raw(name = "Weezer"))) rawArtists = listOf(RealArtist.Raw(name = "Weezer")))
assertTrue(a != b) assertTrue(a != b)
assertTrue(a.hashCode() != b.hashCode()) assertTrue(a.hashCode() != b.hashCode())
} }
@ -97,51 +97,51 @@ class MusicTest {
@Test @Test
fun albumRaw_equals_inconsistentMbids() { fun albumRaw_equals_inconsistentMbids() {
val a = val a =
Album.Raw( RealAlbum.Raw(
mediaStoreId = -1, mediaStoreId = -1,
musicBrainzId = UUID.fromString("c7b245c9-8099-32ea-af95-893acedde2cf"), musicBrainzId = UUID.fromString("c7b245c9-8099-32ea-af95-893acedde2cf"),
name = "Weezer", name = "Weezer",
sortName = "Blue Album", sortName = "Blue Album",
releaseType = null, releaseType = null,
rawArtists = listOf(Artist.Raw(name = "Weezer"))) rawArtists = listOf(RealArtist.Raw(name = "Weezer")))
val b = val b =
Album.Raw( RealAlbum.Raw(
mediaStoreId = -1, mediaStoreId = -1,
musicBrainzId = null, musicBrainzId = null,
name = "Weezer", name = "Weezer",
sortName = "Green Album", sortName = "Green Album",
releaseType = null, releaseType = null,
rawArtists = listOf(Artist.Raw(name = "Weezer"))) rawArtists = listOf(RealArtist.Raw(name = "Weezer")))
assertTrue(a != b) assertTrue(a != b)
assertTrue(a.hashCode() != b.hashCode()) assertTrue(a.hashCode() != b.hashCode())
} }
@Test @Test
fun albumRaw_equals_withArtists() { fun albumRaw_equals_withRealArtists() {
val a = val a =
Album.Raw( RealAlbum.Raw(
mediaStoreId = -1, mediaStoreId = -1,
musicBrainzId = null, musicBrainzId = null,
name = "Album", name = "Album",
sortName = null, sortName = null,
releaseType = null, releaseType = null,
rawArtists = listOf(Artist.Raw(name = "Artist A"))) rawArtists = listOf(RealArtist.Raw(name = "RealArtist A")))
val b = val b =
Album.Raw( RealAlbum.Raw(
mediaStoreId = -1, mediaStoreId = -1,
musicBrainzId = null, musicBrainzId = null,
name = "Album", name = "Album",
sortName = null, sortName = null,
releaseType = null, releaseType = null,
rawArtists = listOf(Artist.Raw(name = "Artist B"))) rawArtists = listOf(RealArtist.Raw(name = "RealArtist B")))
assertTrue(a != b) assertTrue(a != b)
assertTrue(a.hashCode() != b.hashCode()) assertTrue(a.hashCode() != b.hashCode())
} }
@Test @Test
fun artistRaw_equals_inconsistentCase() { fun artistRaw_equals_inconsistentCase() {
val a = Artist.Raw(musicBrainzId = null, name = "Parannoul") val a = RealArtist.Raw(musicBrainzId = null, name = "Parannoul")
val b = Artist.Raw(musicBrainzId = null, name = "parannoul") val b = RealArtist.Raw(musicBrainzId = null, name = "parannoul")
assertTrue(a == b) assertTrue(a == b)
assertTrue(a.hashCode() == b.hashCode()) assertTrue(a.hashCode() == b.hashCode())
} }
@ -149,11 +149,11 @@ class MusicTest {
@Test @Test
fun artistRaw_equals_withMbids() { fun artistRaw_equals_withMbids() {
val a = val a =
Artist.Raw( RealArtist.Raw(
musicBrainzId = UUID.fromString("677325ef-d850-44bb-8258-0d69bbc0b3f7"), musicBrainzId = UUID.fromString("677325ef-d850-44bb-8258-0d69bbc0b3f7"),
name = "Artist") name = "Artist")
val b = val b =
Artist.Raw( RealArtist.Raw(
musicBrainzId = UUID.fromString("6b625592-d88d-48c8-ac1a-c5b476d78bcc"), musicBrainzId = UUID.fromString("6b625592-d88d-48c8-ac1a-c5b476d78bcc"),
name = "Artist") name = "Artist")
assertTrue(a != b) assertTrue(a != b)
@ -163,50 +163,50 @@ class MusicTest {
@Test @Test
fun artistRaw_equals_inconsistentMbids() { fun artistRaw_equals_inconsistentMbids() {
val a = val a =
Artist.Raw( RealArtist.Raw(
musicBrainzId = UUID.fromString("677325ef-d850-44bb-8258-0d69bbc0b3f7"), musicBrainzId = UUID.fromString("677325ef-d850-44bb-8258-0d69bbc0b3f7"),
name = "Artist") name = "Artist")
val b = Artist.Raw(musicBrainzId = null, name = "Artist") val b = RealArtist.Raw(musicBrainzId = null, name = "Artist")
assertTrue(a != b) assertTrue(a != b)
assertTrue(a.hashCode() != b.hashCode()) assertTrue(a.hashCode() != b.hashCode())
} }
@Test @Test
fun artistRaw_equals_missingNames() { fun artistRaw_equals_missingNames() {
val a = Artist.Raw(name = null) val a = RealArtist.Raw(name = null)
val b = Artist.Raw(name = null) val b = RealArtist.Raw(name = null)
assertTrue(a == b) assertTrue(a == b)
assertTrue(a.hashCode() == b.hashCode()) assertTrue(a.hashCode() == b.hashCode())
} }
@Test @Test
fun artistRaw_equals_inconsistentNames() { fun artistRaw_equals_inconsistentNames() {
val a = Artist.Raw(name = null) val a = RealArtist.Raw(name = null)
val b = Artist.Raw(name = "Parannoul") val b = RealArtist.Raw(name = "Parannoul")
assertTrue(a != b) assertTrue(a != b)
assertTrue(a.hashCode() != b.hashCode()) assertTrue(a.hashCode() != b.hashCode())
} }
@Test @Test
fun genreRaw_equals_inconsistentCase() { fun genreRaw_equals_inconsistentCase() {
val a = Genre.Raw("Future Garage") val a = RealGenre.Raw("Future Garage")
val b = Genre.Raw("future garage") val b = RealGenre.Raw("future garage")
assertTrue(a == b) assertTrue(a == b)
assertTrue(a.hashCode() == b.hashCode()) assertTrue(a.hashCode() == b.hashCode())
} }
@Test @Test
fun genreRaw_equals_missingNames() { fun genreRaw_equals_missingNames() {
val a = Genre.Raw(name = null) val a = RealGenre.Raw(name = null)
val b = Genre.Raw(name = null) val b = RealGenre.Raw(name = null)
assertTrue(a == b) assertTrue(a == b)
assertTrue(a.hashCode() == b.hashCode()) assertTrue(a.hashCode() == b.hashCode())
} }
@Test @Test
fun genreRaw_equals_inconsistentNames() { fun genreRaw_equals_inconsistentNames() {
val a = Genre.Raw(name = null) val a = RealGenre.Raw(name = null)
val b = Genre.Raw(name = "Future Garage") val b = RealGenre.Raw(name = "Future Garage")
assertTrue(a != b) assertTrue(a != b)
assertTrue(a.hashCode() != b.hashCode()) assertTrue(a.hashCode() != b.hashCode())
} }