Integration tests for Adapter.kt

Bug: b/444152925
Change-Id: I526fdf4b46f3620e5a9889d3dd0ed5aac5510950
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/266514
Commit-Queue: Tarun Saini <sainitarun@google.com>
Reviewed-by: Jim Blackler <jimblackler@google.com>
diff --git a/tools/android/webgpu/src/androidTest/java/androidx/webgpu/AdapterTest.kt b/tools/android/webgpu/src/androidTest/java/androidx/webgpu/AdapterTest.kt
index f08e3d3..74dcbf2 100644
--- a/tools/android/webgpu/src/androidTest/java/androidx/webgpu/AdapterTest.kt
+++ b/tools/android/webgpu/src/androidTest/java/androidx/webgpu/AdapterTest.kt
@@ -1,29 +1,206 @@
-package androidx.webgpu
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import androidx.webgpu.helper.Util
-import kotlinx.coroutines.runBlocking
-import org.junit.Assert.assertEquals
+import org.junit.Assert.assertThrows
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assume.assumeTrue
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 class AdapterTest {
-    @Test
-    fun adaptorTest() {
-        Util  // Hack to force library initialization.
+  private lateinit var instance: GPUInstance
+  private lateinit var adapter: GPUAdapter
 
-        val instance = createInstance()
-        runBlocking {
-            val adapter = instance.requestAdapter()
+  @Before
+  fun setup() = runBlocking {
+    initLibrary()
+    instance = createInstance()
+    adapter = instance.requestAdapter()
+  }
 
-            val adapterInfo = adapter.getInfo()
+  @After
+  fun teardown() {
+    adapter.close()
+    instance.close()
+  }
 
-            assertEquals("The backend type should be Vulkan",
-                BackendType.Vulkan, adapterInfo.backendType
-            )
-        }
+  /**
+   * Helper map for default limit values as defined by the WebGPU spec.
+   * A full implementation would have all limits.
+   */
+  companion object {
+    private val kDefaultLimits = mapOf(
+      "maxTextureDimension2D" to 8192,
+      "maxBindGroups" to 4,
+      "minUniformBufferOffsetAlignment" to 256
+    )
+  }
+
+  @Test
+  fun adapterBackendTest() {
+    val adapterInfo = adapter.getInfo()
+    assertEquals(
+      "The backend type should be Vulkan",
+      BackendType.Vulkan, adapterInfo.backendType
+    )
+  }
+
+  private suspend fun requestTestDevice(
+    featureToTest: IntArray = intArrayOf(),
+    limits: Limits = Limits(),
+  ): GPUDevice {
+    val descriptor = DeviceDescriptor(
+      requiredFeatures = featureToTest,
+      requiredLimits = limits,
+      deviceLostCallback = DeviceLostCallback { device, reason, message ->
+        throw DeviceLostException(device, reason, message)
+      },
+      deviceLostCallbackExecutor = Executor(Runnable::run),
+      uncapturedErrorCallback = UncapturedErrorCallback { _, type, message ->
+        throw RuntimeException("Uncaptured error: $type, $message")
+      },
+      uncapturedErrorCallbackExecutor = Executor(Runnable::run),
+    )
+    return adapter.requestDevice(descriptor)
+  }
+
+  /**
+   * Verifies that requesting a device with default parameters returns a device
+   * with Core features enabled.
+   */
+  @Test
+  fun requestDeviceWithDefaultParametersEnablesCoreFeatures() {
+    val device = runBlocking { adapter.requestDevice() }
+    try {
+      val features = device.getFeatures().features
+      assertArrayEquals(intArrayOf(FeatureName.CoreFeaturesAndLimits), features)
+    } finally {
+      device.destroy()
     }
+  }
+
+  /**
+   * Verifies that requesting a device with default parameters returns a device
+   * with the correct, specification-defined default limits.
+   */
+  @Test
+  fun requestDeviceWithDefaultParametersReturnsDefaultLimits() {
+    val device = runBlocking { adapter.requestDevice() }
+    try {
+      val limits = device.getLimits()
+      assertEquals(
+        "maxTextureDimension2D should be the default value",
+        kDefaultLimits["maxTextureDimension2D"],
+        limits.maxTextureDimension2D
+      )
+      assertEquals(
+        "maxBindGroups should be the default value",
+        kDefaultLimits["maxBindGroups"],
+        limits.maxBindGroups
+      )
+    } finally {
+      device.destroy()
+    }
+  }
+
+  /**
+   * Tests that an adapter can only grant a device once. Subsequent requests fail.
+   *
+   * CTS Test: requestAdapter_stale
+   * @see <a href="https://github.com/gpuweb/cts/blob/main/src/webgpu/api/operation/adapter/requestAdapter.spec.ts">WebGPU CTS Test</a>
+   */
+  @Test
+  fun requestDeviceFailsAfterSuccessfulRequest() {
+    val device = runBlocking { adapter.requestDevice() }
+    device.destroy()
+    assertThrows(
+      "Adapter should be consumed after one device request", WebGpuException::class.java
+    ) {
+      runBlocking {
+        val secondDeviceStatus = runBlocking { adapter.requestDevice() }
+      }
+    }
+  }
+
+  /**
+   * Tests requesting a device with a feature that the adapter supports.
+   *
+   * CTSTest: features,known
+   * @see <a href="https://github.com/gpuweb/cts/blob/main/src/webgpu/api/operation/adapter/requestAdapter.spec.ts">WebGPU CTS Test</a>
+   */
+  @Test
+  fun requestDeviceWithSupportedFeatureSucceeds() {
+    val featureToTest = FeatureName.CoreFeaturesAndLimits
+    val device = runBlocking { requestTestDevice(intArrayOf(featureToTest)) }
+    val deviceFeatures = device.getFeatures()
+    assert(deviceFeatures.features.contains(featureToTest)) {
+      "Device should have the requested feature: $featureToTest"
+    }
+    runCatching {
+      device.destroy()
+    }
+  }
+
+  /**
+   * Tests that requesting a limit better than what the adapter supports fails.
+   *
+   * CTSTest: 'limit,better_than_supported'
+   * @see <a href="https://github.com/gpuweb/cts/blob/main/src/webgpu/api/operation/adapter/requestAdapter.spec.ts">WebGPU CTS Test</a>
+   */
+  @Test
+  fun requestDeviceWithBetterThanSupportedLimitFails() {
+    val adapterLimits = adapter.getLimits()
+    val betterLimit = adapterLimits.maxBindGroups + 1
+    assertThrows("Requesting a better limit should fail", DeviceLostException::class.java) {
+      runBlocking {
+        requestTestDevice(limits = Limits(maxBindGroups = betterLimit))
+      }
+    }
+  }
+
+  /**
+   * Tests that requesting a worse limit than the spec default gets clamped to the default.
+   *
+   * CTSTest: 'limit,worse_than_default'
+   * @see <a href="https://github.com/gpuweb/cts/blob/main/src/webgpu/api/operation/adapter/requestAdapter.spec.ts">WebGPU CTS Test</a>
+   */
+  @Test
+  fun requestDeviceWithWorseThanDefaultLimitClamps() {
+    runBlocking {
+      val worseLimit = kDefaultLimits.getValue("maxBindGroups") - 1
+      assert(worseLimit > 0) // Ensure the value is still valid, just worse.
+      val device = runBlocking { requestTestDevice(limits = Limits(maxBindGroups = worseLimit)) }
+      val deviceLimits = device.getLimits()
+      assertEquals(
+        "Device limit should be clamped to the default",
+        kDefaultLimits.getValue("maxBindGroups"),
+        deviceLimits.maxBindGroups
+      )
+      runCatching { device.destroy() }
+    }
+  }
+
+  /**
+   * Tests that requesting a limit with a value that is not a power of two fails.
+   *
+   * CTSTest: 'limit,out_of_range'
+   * @see <a href="https://github.com/gpuweb/cts/blob/main/src/webgpu/api/operation/adapter/requestAdapter.spec.ts">WebGPU CTS Test</a>
+   *
+   */
+  @Test
+  fun requestDeviceWithInvalidAlignmentLimitFails() {
+    // minUniformBufferOffsetAlignment must be a power of 2. 255 is not.
+    val invalidAlignment = 255
+    assertThrows(
+      "Alignment limit not a power of 2 should fail", DeviceLostException::class.java
+    ) {
+      runBlocking {
+        requestTestDevice(
+          limits = Limits(
+            minUniformBufferOffsetAlignment = invalidAlignment
+          )
+        )
+      }
+    }
+  }
 }