Integration tests for Device.kt

Bug: b/444152925
Change-Id: I38473d5720e23ca4ae6ff1ca0e6fa60deaa20808
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/266534
Reviewed-by: Jim Blackler <jimblackler@google.com>
Commit-Queue: Tarun Saini <sainitarun@google.com>
diff --git a/tools/android/webgpu/src/androidTest/java/androidx/webgpu/DeviceTest.kt b/tools/android/webgpu/src/androidTest/java/androidx/webgpu/DeviceTest.kt
new file mode 100644
index 0000000..9444ea1
--- /dev/null
+++ b/tools/android/webgpu/src/androidTest/java/androidx/webgpu/DeviceTest.kt
@@ -0,0 +1,253 @@
+package androidx.webgpu
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.webgpu.helper.UncapturedErrorException
+import androidx.webgpu.helper.WebGpu
+import androidx.webgpu.helper.createWebGpu
+import junit.framework.TestCase.assertEquals
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.Assert.assertThrows
+import org.junit.Before
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class DeviceTest {
+  private lateinit var device: Device
+  private lateinit var webGpu: WebGpu
+
+  @Before
+  fun setup() = runBlocking {
+    webGpu = createWebGpu()
+    device = webGpu.device
+  }
+
+  @After
+  fun teardown() {
+    runCatching { device.destroy() }
+    webGpu.close()
+  }
+
+  @Test
+  @SmallTest
+  fun testHasFeature() {
+    runBlocking {
+      // This test ensures the API is callable.
+      device.hasFeature(FeatureName.TimestampQuery)
+    }
+  }
+
+  @Test
+  @SmallTest
+  fun testErrorScope() {
+    device.pushErrorScope(ErrorFilter.Validation)
+
+    // Intentionally create an invalid buffer to trigger a validation error.
+    // A buffer size must be a multiple of 4.
+    device.createBuffer(
+      BufferDescriptor(
+        size = 1, usage = BufferUsage.Vertex, mappedAtCreation = true
+      )
+    )
+
+    val error = runBlocking { device.popErrorScope() }
+    assertEquals(error.type, ErrorType.Validation)
+    assertEquals(error.status, PopErrorScopeStatus.Success)
+  }
+
+
+  @Test
+  @SmallTest
+  fun testCreateBuffer() {
+    val buffer = device.createBuffer(
+      BufferDescriptor(
+        size = 4, usage = BufferUsage.Vertex
+      )
+    )
+    assertEquals(buffer.usage, BufferUsage.Vertex)
+  }
+
+
+  @Test
+  @SmallTest
+  fun testCreateTexture() {
+    val texture = device.createTexture(
+      TextureDescriptor(
+        size = Extent3D(1, 1, 1),
+        format = TextureFormat.RGBA8Unorm,
+        usage = TextureUsage.TextureBinding
+      )
+    )
+    assertEquals(texture.usage, TextureUsage.TextureBinding)
+  }
+
+  @Test
+  @SmallTest
+  fun testCreateComputePipeline_withInvalidEntryPoint_throwsException() {
+    val shaderModule = device.createShaderModule(
+      ShaderModuleDescriptor(
+        shaderSourceWGSL = ShaderSourceWGSL(
+          code = "@compute @workgroup_size(1) fn main() {}"
+        )
+      )
+    )
+
+    assertThrows(UncapturedErrorException::class.java) {
+      device.createComputePipeline(
+        ComputePipelineDescriptor(
+          compute = ComputeState(
+            module = shaderModule, entryPoint = "non_existent_entry_point"
+          )
+        )
+      )
+    }
+  }
+
+  @Test
+  @SmallTest
+  fun testCreateShaderModule_withInvalidShader_throwsException() {
+    // This shader has a syntax error ("fu" instead of "fn")
+    val badShaderCode = "@compute @workgroup_size(1) fu main() {}"
+
+    // Creating the shader module itself should fail
+    assertThrows(UncapturedErrorException::class.java) {
+      device.createShaderModule(
+        ShaderModuleDescriptor(shaderSourceWGSL = ShaderSourceWGSL(code = badShaderCode))
+      )
+    }
+  }
+
+  /**
+   * Verifies that createRenderPipeline fails validation if the entry point is incorrect.
+   */
+  @Test
+  @SmallTest
+  fun testCreateRenderPipeline_withInvalidEntryPoint_failsValidation() {
+    val shaderModule = device.createShaderModule(
+      ShaderModuleDescriptor(
+        shaderSourceWGSL = ShaderSourceWGSL(
+          code = "@vertex fn main() -> @builtin(position) vec4<f32> { return vec4<f32>(0.0); }"
+        )
+      )
+    )
+
+    device.pushErrorScope(ErrorFilter.Validation)
+    device.createRenderPipeline(
+      RenderPipelineDescriptor(
+        vertex = VertexState(
+          module = shaderModule, entryPoint = "non_existent_entry_point" // Invalid
+        )
+      )
+    )
+    val error = runBlocking { device.popErrorScope() }
+    assertEquals(error.type, ErrorType.Validation)
+    assertEquals(error.status, PopErrorScopeStatus.Success)
+  }
+
+  @Test
+  @SmallTest
+  fun testCreateBindGroupLayout_withDuplicateBindings_failsValidation() {
+    device.pushErrorScope(ErrorFilter.Validation)
+    device.createBindGroupLayout(
+      BindGroupLayoutDescriptor(
+        entries = arrayOf(
+          BindGroupLayoutEntry(
+            binding = 0, // Duplicate
+            visibility = ShaderStage.Fragment,
+            buffer = BufferBindingLayout(type = BufferBindingType.Storage)
+          ), BindGroupLayoutEntry(
+            binding = 0, // Duplicate
+            visibility = ShaderStage.Fragment,
+            buffer = BufferBindingLayout(type = BufferBindingType.Storage)
+          )
+        )
+      )
+    )
+    val error = runBlocking { device.popErrorScope() }
+
+    assertEquals(error.type, ErrorType.Validation)
+    assertEquals(error.status, PopErrorScopeStatus.Success)
+  }
+
+  /**
+   * Verifies that createBindGroup fails validation if the buffer's usage is incorrect.
+   */
+  @Test
+  @SmallTest
+  fun testCreateBindGroup_withMismatchedBufferUsage_failsValidation() {
+    val layout = device.createBindGroupLayout(
+      BindGroupLayoutDescriptor(
+        entries = arrayOf(
+          BindGroupLayoutEntry(
+            binding = 0,
+            visibility = ShaderStage.Compute,
+            buffer = BufferBindingLayout(type = BufferBindingType.Uniform)
+          )
+        )
+      )
+    )
+
+    // Create a buffer WITHOUT the required `Uniform` usage.
+    val buffer = device.createBuffer(
+      BufferDescriptor(size = 16, usage = BufferUsage.CopySrc) // Invalid usage
+    )
+
+    device.pushErrorScope(ErrorFilter.Validation)
+    device.createBindGroup(
+      BindGroupDescriptor(
+        layout = layout, entries = arrayOf(
+          BindGroupEntry(binding = 0, buffer = buffer)
+        )
+      )
+    )
+    val error = runBlocking { device.popErrorScope() }
+    assertEquals(error.type, ErrorType.Validation)
+    assertEquals(error.status, PopErrorScopeStatus.Success)
+  }
+
+  @Test
+  @SmallTest
+  fun testCreateQuerySet_withInvalidCount_failsValidation() {
+    device.pushErrorScope(ErrorFilter.Validation)
+    device.createQuerySet(
+      QuerySetDescriptor(
+        type = QueryType.Occlusion, count = -1 // Invalid: count must be > 0.
+      )
+    )
+    val error = runBlocking { device.popErrorScope() }
+    assertEquals(error.type, ErrorType.Validation)
+    assertEquals(error.status, PopErrorScopeStatus.Success)
+  }
+
+  @Test
+  fun validationError_withoutActiveErrorScope_throwsUncapturedErrorException() {
+    val invalidDescriptor = QuerySetDescriptor(
+      type = QueryType.Occlusion,
+      count = -1 // Invalid parameter
+    )
+    assertThrows(UncapturedErrorException::class.java) {
+      device.createQuerySet(invalidDescriptor)
+    }
+  }
+
+  /**
+   * Verifies that createSampler fails validation with an invalid descriptor.
+   */
+  @Test
+  @SmallTest
+  fun testCreateSampler_withInvalidLodClamp_failsValidation() = runBlocking {
+    val invalidDescriptor = SamplerDescriptor(
+      lodMinClamp = 10.0f, lodMaxClamp = 1.0f // Invalid: min cannot be greater than max.
+    )
+
+    device.pushErrorScope(ErrorFilter.Validation)
+    device.createSampler(invalidDescriptor)
+    val error = device.popErrorScope()
+
+    assertEquals(error.type, ErrorType.Validation)
+    assertEquals(error.status, PopErrorScopeStatus.Success)
+  }
+}