Create XEnumTypeElement for enum declarations

Enum classes are slightly different than other classes where they have
inner classes/properties that are actually enum constants.

Moreoever, KSP returns those constants as class declarations whereas
java AP returns them as fields.

To avoid possibly confusions, this CL adds a new XEnumTypeElement with a
field to receive enum constant names.
Subsequently, java implementation of XTypeElement was returning enum
constants as if they are fields, I've removed them from that list as
well.

As part of this change, I've removed XType.isEnum and instead added
XTypeElement.isEnum which can also do the auto cast to XEnumTypeElement.
Also refactored EnumColumnTypeAdapter to use the new APIs.

Note that there is an existing bug in KSP where it does not report enum
constants properly from java sources so for now, I've excluded that case
from tests: https://github.com/google/ksp/issues/234

I've copied the EnumColumnTypeAdapterTest to also run in room kotlin
test app so that we can ensure KSP is working propery (it was failing).

Bug: 160322705
Bug: 173236324
Test: XTypeElementTest, EnumColumnTypeAdapterTest.kt

Change-Id: Ie6d74cc5ccd93ed6be2687cdb382baf9dee16a0f
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XEnumTypeElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XEnumTypeElement.kt
new file mode 100644
index 0000000..5d35646
--- /dev/null
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XEnumTypeElement.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.compiler.processing
+
+import kotlin.contracts.contract
+
+/**
+ * Type elements that represent Enum declarations.
+ */
+interface XEnumTypeElement : XTypeElement {
+    val enumConstantNames: Set<String>
+}
+
+fun XTypeElement.isEnum(): Boolean {
+    contract {
+        returns(true) implies (this@isEnum is XEnumTypeElement)
+    }
+    return this is XEnumTypeElement
+}
\ No newline at end of file
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XType.kt
index 13d02ef..52388ed 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XType.kt
@@ -107,11 +107,6 @@
     fun isNone(): Boolean
 
     /**
-     * Returns true if this represented by an [Enum].
-     */
-    fun isEnum(): Boolean
-
-    /**
      * Returns `true` if this is the same raw type as [other]
      */
     fun isTypeOf(other: KClass<*>): Boolean
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/ElementExt.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/ElementExt.kt
index 8959ca0..d9bee51 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/ElementExt.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/ElementExt.kt
@@ -20,6 +20,7 @@
 import com.google.auto.common.MoreElements
 import com.google.auto.common.MoreTypes
 import javax.lang.model.element.Element
+import javax.lang.model.element.ElementKind
 import javax.lang.model.element.TypeElement
 import javax.lang.model.element.VariableElement
 import javax.lang.model.type.TypeKind
@@ -39,6 +40,8 @@
 /**
  * Returns all fields including private fields (including private fields in super). Removes
  * duplicate fields if class has a field with the same name as the parent.
+ * Note that enum constants are not included in the list even thought they are fields in java.
+ * To access enum constants, use [JavacTypeElement.JavacEnumTypeElement].
  */
 internal fun TypeElement.getAllFieldsIncludingPrivateSupers(
     elementUtils: Elements
@@ -46,6 +49,7 @@
     val selection = ElementFilter
         .fieldsIn(elementUtils.getAllMembers(this))
         .filterIsInstance<VariableElement>()
+        .filterNot { it.kind == ElementKind.ENUM_CONSTANT }
         .toMutableSet()
     val selectionNames = selection.mapTo(mutableSetOf()) {
         it.simpleName
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt
index 8e6e544..2266ced 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt
@@ -46,8 +46,8 @@
             findElement = { qName ->
                 delegate.elementUtils.getTypeElement(qName)
             },
-            wrap = {
-                JavacTypeElement(this, it)
+            wrap = { typeElement ->
+                JavacTypeElement.create(this, typeElement)
             },
             getQName = {
                 it.qualifiedName.toString()
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacType.kt
index 13889f5..e220f6b 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacType.kt
@@ -24,7 +24,6 @@
 import androidx.room.compiler.processing.ksp.ERROR_TYPE_NAME
 import androidx.room.compiler.processing.safeTypeName
 import com.google.auto.common.MoreTypes
-import javax.lang.model.element.ElementKind
 import javax.lang.model.type.TypeKind
 import javax.lang.model.type.TypeMirror
 import kotlin.reflect.KClass
@@ -167,7 +166,4 @@
         // a boxed primitive to be marked as non-null.
         return copyWithNullability(XNullability.NONNULL)
     }
-
-    override fun isEnum() = typeMirror.kind == TypeKind.DECLARED &&
-        MoreTypes.asElement(typeMirror).kind == ElementKind.ENUM
 }
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
index 2395061..acaffb7 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
@@ -16,10 +16,12 @@
 
 package androidx.room.compiler.processing.javac
 
+import androidx.room.compiler.processing.XEnumTypeElement
 import androidx.room.compiler.processing.XFieldElement
 import androidx.room.compiler.processing.XHasModifiers
 import androidx.room.compiler.processing.XMethodElement
 import androidx.room.compiler.processing.XTypeElement
+import androidx.room.compiler.processing.javac.JavacTypeElement.JavacEnumTypeElement
 import androidx.room.compiler.processing.javac.kotlin.KotlinMetadataElement
 import com.google.auto.common.MoreElements
 import com.google.auto.common.MoreTypes
@@ -29,7 +31,7 @@
 import javax.lang.model.type.TypeKind
 import javax.lang.model.util.ElementFilter
 
-internal class JavacTypeElement(
+internal sealed class JavacTypeElement(
     env: JavacProcessingEnv,
     override val element: TypeElement
 ) : JavacElement(env, element), XTypeElement, XHasModifiers by JavacHasModifiers(element) {
@@ -142,4 +144,38 @@
     override val equalityItems: Array<out Any?> by lazy {
         arrayOf(element)
     }
+
+    class DefaultJavacTypeElement(
+        env: JavacProcessingEnv,
+        element: TypeElement
+    ) : JavacTypeElement(env, element)
+
+    class JavacEnumTypeElement(
+        env: JavacProcessingEnv,
+        element: TypeElement
+    ) : JavacTypeElement(env, element), XEnumTypeElement {
+        init {
+            check(element.kind == ElementKind.ENUM)
+        }
+
+        override val enumConstantNames: Set<String> by lazy {
+            element.enclosedElements.filter {
+                it.kind == ElementKind.ENUM_CONSTANT
+            }.mapTo(mutableSetOf()) {
+                it.simpleName.toString()
+            }
+        }
+    }
+
+    companion object {
+        fun create(
+            env: JavacProcessingEnv,
+            typeElement: TypeElement
+        ): JavacTypeElement {
+            return when (typeElement.kind) {
+                ElementKind.ENUM -> JavacEnumTypeElement(env, typeElement)
+                else -> DefaultJavacTypeElement(env, typeElement)
+            }
+        }
+    }
 }
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspProcessingEnv.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspProcessingEnv.kt
index fee52da..ba8ae4e 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspProcessingEnv.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspProcessingEnv.kt
@@ -57,8 +57,8 @@
                 // it is best to just not cache them
                 it.qualifiedName?.asString()
             },
-            wrap = {
-                KspTypeElement(this, it)
+            wrap = { classDeclaration ->
+                KspTypeElement.create(this, classDeclaration)
             }
         )
 
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
index dcdfca1..4284a63 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
@@ -21,7 +21,6 @@
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.tryBox
 import androidx.room.compiler.processing.tryUnbox
-import com.google.devtools.ksp.symbol.ClassKind
 import com.google.devtools.ksp.symbol.KSClassDeclaration
 import com.google.devtools.ksp.symbol.KSType
 import com.google.devtools.ksp.symbol.KSTypeReference
@@ -139,10 +138,6 @@
         return ksType.toString()
     }
 
-    override fun isEnum(): Boolean {
-        return (ksType.declaration as? KSClassDeclaration)?.classKind == ClassKind.ENUM_CLASS
-    }
-
     abstract override fun boxed(): KspType
 
     /**
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
index 6712e14..437d9a3 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
@@ -18,6 +18,7 @@
 
 import androidx.room.compiler.processing.XAnnotated
 import androidx.room.compiler.processing.XConstructorElement
+import androidx.room.compiler.processing.XEnumTypeElement
 import androidx.room.compiler.processing.XFieldElement
 import androidx.room.compiler.processing.XHasModifiers
 import androidx.room.compiler.processing.XMethodElement
@@ -40,7 +41,7 @@
 import com.google.devtools.ksp.symbol.Origin
 import com.squareup.javapoet.ClassName
 
-internal class KspTypeElement(
+internal sealed class KspTypeElement(
     env: KspProcessingEnv,
     override val declaration: KSClassDeclaration
 ) : KspElement(env, declaration),
@@ -342,4 +343,36 @@
     override fun toString(): String {
         return declaration.toString()
     }
+
+    private class DefaultKspTypeElement(
+        env: KspProcessingEnv,
+        declaration: KSClassDeclaration
+    ) : KspTypeElement(env, declaration)
+
+    private class KspEnumTypeElement(
+        env: KspProcessingEnv,
+        declaration: KSClassDeclaration
+    ) : KspTypeElement(env, declaration), XEnumTypeElement {
+        override val enumConstantNames: Set<String> by lazy {
+            // TODO this does not work for java sources
+            // https://github.com/google/ksp/issues/234
+            declaration.declarations.filter {
+                it is KSClassDeclaration && it.classKind == ClassKind.ENUM_ENTRY
+            }.mapTo(mutableSetOf()) {
+                it.simpleName.asString()
+            }
+        }
+    }
+
+    companion object {
+        fun create(
+            env: KspProcessingEnv,
+            ksClassDeclaration: KSClassDeclaration
+        ): KspTypeElement {
+            return when (ksClassDeclaration.classKind) {
+                ClassKind.ENUM_CLASS -> KspEnumTypeElement(env, ksClassDeclaration)
+                else -> DefaultKspTypeElement(env, ksClassDeclaration)
+            }
+        }
+    }
 }
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/ResolverExt.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/ResolverExt.kt
index 6e7b5bc..21ab8ac 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/ResolverExt.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/ResolverExt.kt
@@ -144,12 +144,8 @@
         return declaration.simpleName.asString()
     } catch (cannotFindDeclaration: IllegalStateException) {
         // workaround for https://github.com/google/ksp/issues/200
-        val name = declaration.simpleName.asString()
-        if (name.startsWith("get") or name.startsWith("set")) {
-            return name
-        }
-        // we don't know why it happened so we better throw
-        throw cannotFindDeclaration
+        // happens for setters getters as well as `values` method of Enum descriptor
+        return declaration.simpleName.asString()
     }
 }
 
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
index ddd18f3..7e46099 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
@@ -18,13 +18,14 @@
 
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.compiler.processing.util.compileFiles
 import androidx.room.compiler.processing.util.getAllFieldNames
 import androidx.room.compiler.processing.util.getField
 import androidx.room.compiler.processing.util.getMethod
 import androidx.room.compiler.processing.util.runKspTest
 import androidx.room.compiler.processing.util.runProcessorTest
-import com.google.common.truth.Truth
 import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
 import com.squareup.javapoet.ClassName
 import com.squareup.javapoet.ParameterizedTypeName
 import com.squareup.javapoet.TypeName
@@ -764,13 +765,84 @@
                 )
 
             subjects.forEach {
-                Truth.assertWithMessage(it)
+                assertWithMessage(it)
                     .that(invocation.processingEnv.requireTypeElement(it).findPrimaryConstructor())
                     .isNull()
             }
         }
     }
 
+    @Test
+    fun enumTypeElement() {
+        fun createSources(packageName: String) = listOf(
+            Source.kotlin(
+                "$packageName/KotlinEnum.kt",
+                """
+                package $packageName
+                enum class KotlinEnum(private val x:Int) {
+                    VAL1(1),
+                    VAL2(2);
+
+                    fun enumMethod():Unit {}
+                }
+                """.trimIndent()
+            ),
+            Source.java(
+                "$packageName.JavaEnum",
+                """
+                package $packageName;
+                public enum JavaEnum {
+                    VAL1(1),
+                    VAL2(2);
+
+                    private int x;
+
+                    JavaEnum(int x) {
+                        this.x = x;
+                    }
+                    void enumMethod() {}
+                }
+                """.trimIndent()
+            )
+        )
+        val classpath = compileFiles(
+            createSources("lib")
+        )
+        runProcessorTest(
+            sources = createSources("app"),
+            classpath = listOf(classpath)
+        ) { invocation ->
+            listOf(
+                "lib.KotlinEnum", "lib.JavaEnum",
+                "app.KotlinEnum", "app.JavaEnum"
+            ).forEach { qName ->
+                val typeElement = invocation.processingEnv.requireTypeElement(qName)
+                assertWithMessage("$qName is enum")
+                    .that(typeElement.isEnum())
+                    .isTrue()
+                assertWithMessage("$qName does not report enum constants in methods")
+                    .that(typeElement.getDeclaredMethods().map { it.name })
+                    .run {
+                        contains("enumMethod")
+                        containsNoneOf("VAL1", "VAL2")
+                    }
+                if (qName != "app.JavaEnum" || !invocation.isKsp) {
+                    // KSP does not properly return enum constants for java sources yet
+                    // https://github.com/google/ksp/issues/234
+                    assertWithMessage("$qName can return enum constants")
+                        .that((typeElement as XEnumTypeElement).enumConstantNames)
+                        .containsExactly("VAL1", "VAL2")
+                    assertWithMessage("$qName  does not report enum constants in fields")
+                        .that(typeElement.getAllFieldNames())
+                        .run {
+                            contains("x")
+                            containsNoneOf("VAL1", "VAL2")
+                        }
+                }
+            }
+        }
+    }
+
     /**
      * it is good to exclude methods coming from Object when testing as they differ between KSP
      * and KAPT but irrelevant for Room.
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt b/room/compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
index f12c2b1..0a3c658 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
@@ -18,6 +18,7 @@
 
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.isArray
+import androidx.room.compiler.processing.isEnum
 import androidx.room.ext.CommonTypeNames
 import androidx.room.ext.GuavaBaseTypeNames
 import androidx.room.ext.isEntityElement
@@ -346,8 +347,9 @@
     }
 
     private fun createEnumTypeAdapter(type: XType): ColumnTypeAdapter? {
-        if (type.isEnum()) {
-            return EnumColumnTypeAdapter(type)
+        val typeElement = type.typeElement ?: return null
+        if (typeElement.isEnum()) {
+            return EnumColumnTypeAdapter(typeElement)
         }
         return null
     }
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt
index 2f01519..879019b 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt
@@ -16,8 +16,7 @@
 
 package androidx.room.solver.types
 
-import androidx.room.compiler.processing.XFieldElement
-import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.XEnumTypeElement
 import androidx.room.ext.CommonTypeNames.ILLEGAL_ARG_EXCEPTION
 import androidx.room.ext.L
 import androidx.room.ext.N
@@ -28,15 +27,14 @@
 import androidx.room.writer.ClassWriter
 import com.squareup.javapoet.MethodSpec
 import com.squareup.javapoet.ParameterSpec
-import java.util.Locale
-import javax.lang.model.element.ElementKind
 import javax.lang.model.element.Modifier
 
 /**
  * Uses enum string representation.
  */
-class EnumColumnTypeAdapter(out: XType) :
-    ColumnTypeAdapter(out, TEXT) {
+class EnumColumnTypeAdapter(
+    private val enumTypeElement: XEnumTypeElement
+) : ColumnTypeAdapter(enumTypeElement.type, TEXT) {
     override fun readFromCursor(
         outVarName: String,
         cursorVarName: String,
@@ -92,8 +90,8 @@
                         beginControlFlow("if ($N == null)", param)
                         addStatement("return null")
                         nextControlFlow("switch ($N)", param)
-                        getEnumConstantElements().forEach { enumConstant ->
-                            addStatement("case $L: return $S", enumConstant.name, enumConstant.name)
+                        enumTypeElement.enumConstantNames.forEach { enumConstantName ->
+                            addStatement("case $L: return $S", enumConstantName, enumConstantName)
                         }
                         addStatement(
                             "default: throw new $T($S + $N)",
@@ -129,11 +127,10 @@
                         beginControlFlow("if ($N == null)", param)
                         addStatement("return null")
                         nextControlFlow("switch ($N)", param)
-                        getEnumConstantElements().forEach {
-                            enumConstant ->
+                        enumTypeElement.enumConstantNames.forEach { enumConstantName ->
                             addStatement(
                                 "case $S: return $T.$L",
-                                enumConstant.name, out.typeName, enumConstant.name
+                                enumConstantName, out.typeName, enumConstantName
                             )
                         }
                         addStatement(
@@ -147,14 +144,4 @@
                 }
             })
     }
-
-    private fun getEnumConstantElements(): List<XFieldElement> {
-        // TODO: Switch below logic to use`getDeclaredFields` when the
-        //  functionality is available in the XTypeElement API
-        val typeElementFields = out.typeElement!!.getAllFieldsIncludingPrivateSupers()
-        return typeElementFields.filter {
-            // TODO: (b/173236324) Add kind to the X abstraction API to avoid using kindName()
-            ElementKind.ENUM_CONSTANT.toString().toLowerCase(Locale.US) == it.kindName()
-        }
-    }
 }
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/EnumColumnTypeAdapterTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/EnumColumnTypeAdapterTest.kt
new file mode 100644
index 0000000..2911d71
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/EnumColumnTypeAdapterTest.kt
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.integration.kotlintestapp.test
+
+import android.content.Context
+import androidx.room.Dao
+import androidx.room.Database
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverter
+import androidx.room.TypeConverters
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class EnumColumnTypeAdapterTest {
+    private lateinit var db: EnumColumnTypeAdapterDatabase
+
+    @Entity
+    data class EntityWithEnum(
+        @PrimaryKey
+        val id: Long,
+        val fruit: Fruit
+    )
+
+    @Entity
+    data class EntityWithOneWayEnum(
+        @PrimaryKey
+        val id: Long,
+        val color: Color
+    )
+
+    @Entity
+    data class ComplexEntityWithEnum(
+        @PrimaryKey
+        val id: Long,
+        val season: Season
+    )
+
+    enum class Color {
+        RED, GREEN
+    }
+
+    enum class Fruit {
+        BANANA, STRAWBERRY, WILDBERRY
+    }
+
+    enum class Season(private val text: String) {
+        SUMMER("Sunny"), SPRING("Warm"), WINTER("Cold"), AUTUMN("Rainy");
+    }
+
+    @Dao
+    interface SampleDao {
+        @Query("INSERT INTO EntityWithEnum (id, fruit) VALUES (:id, :fruit)")
+        fun insert(id: Long, fruit: Fruit?): Long
+
+        @Query("SELECT * FROM EntityWithEnum WHERE id = :id")
+        fun getValueWithId(id: Long): EntityWithEnum
+    }
+
+    @Dao
+    interface SampleDaoWithOneWayConverter {
+        @Query("INSERT INTO EntityWithOneWayEnum (id, color) VALUES (:id, :colorInt)")
+        fun insert(id: Long, colorInt: Int): Long
+
+        @Query("SELECT * FROM EntityWithOneWayEnum WHERE id = :id")
+        fun getValueWithId(id: Long): EntityWithOneWayEnum
+    }
+
+    class ColorTypeConverter {
+        @TypeConverter
+        fun fromIntToColorEnum(colorInt: Int): Color {
+            return if (colorInt == 1) {
+                Color.RED
+            } else {
+                Color.GREEN
+            }
+        }
+    }
+
+    @Dao
+    interface SampleDaoWithComplexEnum {
+        @Query("INSERT INTO ComplexEntityWithEnum (id, season) VALUES (:id, :season)")
+        fun insertComplex(id: Long, season: Season?): Long
+
+        @Query("SELECT * FROM ComplexEntityWithEnum WHERE id = :id")
+        fun getComplexValueWithId(id: Long): ComplexEntityWithEnum
+    }
+
+    @Database(
+        entities = [
+            EntityWithEnum::class,
+            ComplexEntityWithEnum::class,
+            EntityWithOneWayEnum::class
+        ],
+        version = 1,
+        exportSchema = false
+    )
+    @TypeConverters(
+        ColorTypeConverter::class
+    )
+    abstract class EnumColumnTypeAdapterDatabase : RoomDatabase() {
+        abstract fun dao(): SampleDao
+        abstract fun oneWayDao(): SampleDaoWithOneWayConverter
+        abstract fun complexDao(): SampleDaoWithComplexEnum
+    }
+
+    @Before
+    fun initDb() {
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        db = Room.inMemoryDatabaseBuilder(
+            context,
+            EnumColumnTypeAdapterDatabase::class.java
+        ).build()
+    }
+
+    @After
+    fun teardown() {
+        db.close()
+    }
+
+    @Test
+    fun readAndWriteEnumToDatabase() {
+        db.dao().insert(1, Fruit.BANANA)
+        db.dao().insert(2, Fruit.STRAWBERRY)
+        assertThat(
+            db.dao().getValueWithId(1).fruit
+        ).isEqualTo(Fruit.BANANA)
+        assertThat(
+            db.dao().getValueWithId(2).fruit
+        ).isEqualTo(Fruit.STRAWBERRY)
+    }
+
+    @Test
+    fun writeOneWayEnumToDatabase() {
+        db.oneWayDao().insert(1, 1)
+        assertThat(
+            db.oneWayDao().getValueWithId(1).color
+        ).isEqualTo(
+            Color.RED
+        )
+    }
+
+    @Test
+    fun filterOutComplexEnumTest() {
+        db.complexDao().insertComplex(1, Season.AUTUMN)
+        assertThat(
+            db.complexDao().getComplexValueWithId(1).season
+        ).isEqualTo(
+            Season.AUTUMN
+        )
+    }
+}