[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>")
+ }
+}