[Kotlin] Add a unit test which reviews all constants declared in Kotlin classes
by testing that the value of each named constant, matches the named
class member of the container class.

Change-Id: I59631951db3da2727af826455d7534975e1647c2
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/197236
Auto-Submit: Alex Benton <bentonian@google.com>
Reviewed-by: Sonakshi Saxena <nexa@google.com>
Commit-Queue: Sonakshi Saxena <nexa@google.com>
Reviewed-by: Alex Benton <bentonian@google.com>
diff --git a/tools/android/webgpu/build.gradle b/tools/android/webgpu/build.gradle
index ae7f117..fe8464b 100644
--- a/tools/android/webgpu/build.gradle
+++ b/tools/android/webgpu/build.gradle
@@ -77,14 +77,16 @@
     implementation 'androidx.core:core-ktx:1.13.1'
 
     testImplementation 'junit:junit:4.13.2'
+    testImplementation 'org.reflections:reflections:0.10.2'
+    testImplementation("org.jetbrains.kotlin:kotlin-reflect")
     testImplementation("org.jetbrains.kotlin:kotlin-test")
     testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
     testImplementation("org.mockito:mockito-core:5.12.0")
     testImplementation("org.mockito.kotlin:mockito-kotlin:5.3.1")
 
-    androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5'
-    androidTestImplementation 'androidx.test:runner:1.5.2'
-    androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.0'
+    androidTestImplementation 'androidx.test.ext:junit-ktx:1.2.1'
+    androidTestImplementation 'androidx.test:runner:1.6.1'
+    androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
 }
 
 project.afterEvaluate {
diff --git a/tools/android/webgpu/src/test/java/android/dawn/MappedNamedConstantsTest.kt b/tools/android/webgpu/src/test/java/android/dawn/MappedNamedConstantsTest.kt
new file mode 100644
index 0000000..95d6a94
--- /dev/null
+++ b/tools/android/webgpu/src/test/java/android/dawn/MappedNamedConstantsTest.kt
@@ -0,0 +1,147 @@
+package android.dawn
+
+import kotlin.reflect.KCallable
+import kotlin.reflect.KClass
+import kotlin.reflect.KProperty1
+import kotlin.reflect.full.companionObjectInstance
+import kotlin.reflect.full.primaryConstructor
+import kotlin.test.assertContains
+import kotlin.test.assertEquals
+import kotlin.test.fail
+import org.junit.Test
+import org.reflections.Reflections
+import org.reflections.scanners.Scanners
+
+class MappedNamedConstantsTest {
+
+    val TYPES_WITH_MAPPED_NAMED_CONSTANTS = arrayOf(
+        AdapterType::class,
+        AddressMode::class,
+        BackendType::class,
+        BlendFactor::class,
+        BlendOperation::class,
+        BufferBindingType::class,
+        BufferMapAsyncStatus::class,
+        BufferMapState::class,
+        BufferUsage::class,
+        CallbackMode::class,
+        ColorWriteMask::class,
+        CompareFunction::class,
+        CompilationInfoRequestStatus::class,
+        CompilationMessageType::class,
+        CompositeAlphaMode::class,
+        CreatePipelineAsyncStatus::class,
+        CullMode::class,
+        DeviceLostReason::class,
+        ErrorFilter::class,
+        ErrorType::class,
+        FeatureName::class,
+        FilterMode::class,
+        FrontFace::class,
+        IndexFormat::class,
+        LoadOp::class,
+        MapAsyncStatus::class,
+        MapMode::class,
+        MipmapFilterMode::class,
+        PopErrorScopeStatus::class,
+        PowerPreference::class,
+        PresentMode::class,
+        PrimitiveTopology::class,
+        QueryType::class,
+        QueueWorkDoneStatus::class,
+        RequestAdapterStatus::class,
+        RequestDeviceStatus::class,
+        SamplerBindingType::class,
+        ShaderStage::class,
+        Status::class,
+        StencilOperation::class,
+        StorageTextureAccess::class,
+        StoreOp::class,
+        SType::class,
+        SurfaceGetCurrentTextureStatus::class,
+        TextureAspect::class,
+        TextureDimension::class,
+        TextureFormat::class,
+        TextureSampleType::class,
+        TextureUsage::class,
+        TextureViewDimension::class,
+        VertexFormat::class,
+        VertexStepMode::class,
+        WaitStatus::class,
+        WGSLFeatureName::class
+    )
+
+    /**
+     * Test that the actual classes in our generated Kotlin are all tested in this unit test,
+     * and that there are no unexpected new classes.
+     *
+     * It's worth hardcoding the list above (rather than just listing the target classes to
+     * inspect dynamically) because this way, if the number of classes found dynamically were
+     * to suddenly drop to zero (as in, the code changed and the test filter dropped all the
+     * classes) then we'd know the test was no longer testing the correct thing. Similarly,
+     * if the number of classes found dynamically were to suddenly increase, then we'd know
+     * that the test was no longer testing the correct thing.
+     */
+    @Test
+    fun testPackageClassesMatchTestTargets() {
+        val dawnClasses = Reflections("android.dawn").getAll(Scanners.TypesAnnotated)
+        val actual = dawnClasses.filter { clazz ->
+            isAndroidDawn(clazz) && hasCompanionObjectWithNames(clazz)
+        }.map { it.removePrefix("android.dawn.") }
+
+        val expected = TYPES_WITH_MAPPED_NAMED_CONSTANTS.mapNotNull { it.simpleName }
+
+        // Test that the two lists match, throw a useful failure if they don't
+        actual.forEach { className -> assertContains(expected, className) }
+        expected.forEach { className -> assertContains(actual, className) }
+    }
+
+    /**
+     * Test that every class listed above (a) has a names field, and (b) each name is
+     * mapped to the correct string and instance value.
+     */
+    @Test
+    fun testMappedConstantsNamesAreCorrect() {
+        for (clazz in TYPES_WITH_MAPPED_NAMED_CONSTANTS) {
+            val companionObject = getCompanionObjectOrFail(clazz)
+            val namesMap = getNamesMapOrFail(companionObject, clazz)
+            val companionConstants = getCompanionConstants(companionObject)
+
+            for ((key, constantName) in namesMap) {
+                val constantProperty = companionConstants[constantName]
+                val actual = (constantProperty as KProperty1<*, *>).getter.call(companionObject)
+                val expected = clazz.primaryConstructor?.call(key)
+                assertEquals(expected, actual)
+            }
+        }
+    }
+
+    private fun isAndroidDawn(clazz: String): Boolean {
+        return clazz.startsWith("android.dawn") && clazz.count { it == '.' } == 2
+    }
+
+    private fun hasCompanionObjectWithNames(clazz: String): Boolean {
+        val kClass = Class.forName(clazz).kotlin
+        val companionObject = kClass.companionObjectInstance
+        return companionObject != null && companionObject::class.members.any { it.name == "names" }
+    }
+
+    private fun getCompanionConstants(companionObject: Any): Map<String, KCallable<*>> {
+        return companionObject::class.members.filter { it.isFinal && it is KProperty1<*, *> }
+            .associateBy { it.name }
+    }
+
+    private fun getCompanionObjectOrFail(clazz: KClass<out Any>): Any {
+        return clazz.companionObjectInstance
+            ?: fail("No companion object found in ${clazz.simpleName}")
+    }
+
+    private fun getNamesMapOrFail(companionObject: Any, clazz: KClass<out Any>): Map<Int, String> {
+        val namesProperty = companionObject::class.members.find { it.name == "names" }
+            ?: fail("Property 'names' not found in companion object of ${clazz.simpleName}")
+
+        @Suppress("UNCHECKED_CAST")
+        return namesProperty.call(companionObject) as? Map<Int, String>
+            ?: fail("Property 'names' is not of type Map<Int, String>")
+    }
+}