aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Hoisie <hoisie@google.com>2024-05-07 09:52:31 -0700
committerCopybara-Service <copybara-worker@google.com>2024-05-07 09:53:07 -0700
commit6aecbb4b15e7d33ae684c0e9b110230ec830e182 (patch)
treece7e5baf99698625fd6967c0f81d6343a1a88a78
parentccf8c6fd9013537e4ffd8432505d642b10baccc4 (diff)
downloadrobolectric-6aecbb4b15e7d33ae684c0e9b110230ec830e182.tar.gz
Add integration test for Kotlin Flow + Shadow Bluetooth APIs
This came up in a discussion about controlling coroutines in Robolectric tests. PiperOrigin-RevId: 631456525
-rw-r--r--integration_tests/kotlin/build.gradle1
-rw-r--r--integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/flow/BluetoothProvisioner.kt79
-rw-r--r--integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/flow/BluetoothProvisionerTest.kt83
3 files changed, 163 insertions, 0 deletions
diff --git a/integration_tests/kotlin/build.gradle b/integration_tests/kotlin/build.gradle
index c95d64f3f..37686d981 100644
--- a/integration_tests/kotlin/build.gradle
+++ b/integration_tests/kotlin/build.gradle
@@ -18,6 +18,7 @@ dependencies {
testCompileOnly AndroidSdk.MAX_SDK.coordinates
testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
testImplementation libs.kotlin.stdlib
+ testImplementation libs.kotlinx.coroutines.android
testImplementation libs.junit4
testImplementation libs.truth
testImplementation "androidx.test:core:$axtCoreVersion@aar"
diff --git a/integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/flow/BluetoothProvisioner.kt b/integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/flow/BluetoothProvisioner.kt
new file mode 100644
index 000000000..2094dd13b
--- /dev/null
+++ b/integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/flow/BluetoothProvisioner.kt
@@ -0,0 +1,79 @@
+package org.robolectric.integrationtests.kotlin.flow
+
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothGatt
+import android.bluetooth.BluetoothGattCallback
+import android.bluetooth.BluetoothGattService
+import android.bluetooth.BluetoothManager
+import android.bluetooth.BluetoothProfile
+import android.bluetooth.le.ScanCallback
+import android.bluetooth.le.ScanResult
+import android.content.Context
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.flow
+
+/** A class that invokes Android Bluetooth LE APIs. */
+class BluetoothProvisioner(applicationContext: Context) {
+
+ val context: Context
+
+ init {
+ context = applicationContext
+ }
+
+ fun startScan(): Flow<BluetoothDevice> = callbackFlow {
+ val scanCallback =
+ object : ScanCallback() {
+ override fun onScanResult(callbackType: Int, result: ScanResult?) {
+ if (result?.device != null) {
+ val unused = trySend(result.device)
+ }
+ }
+
+ override fun onScanFailed(errorCode: Int) {
+ cancel("BLE Scan Failed", null)
+ }
+ }
+ val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
+ val scanner = bluetoothManager.adapter.bluetoothLeScanner
+ scanner.startScan(scanCallback)
+ awaitClose { scanner.stopScan(scanCallback) }
+ }
+
+ fun connectToDevice(device: BluetoothDevice): Flow<BluetoothGatt> = callbackFlow {
+ val gattCallback =
+ object : BluetoothGattCallback() {
+ override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
+ if (newState == BluetoothProfile.STATE_CONNECTED) {
+ val unused = gatt!!.discoverServices()
+ } else {
+ cancel("Connect Failed", null)
+ }
+ }
+
+ override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
+ if (status == BluetoothGatt.GATT_SUCCESS) {
+ val unused = trySend(gatt!!)
+ } else {
+ cancel("Service discovery failed", null)
+ }
+ }
+ }
+
+ device.connectGatt(context, true, gattCallback)
+ awaitClose {}
+ }
+
+ fun scanAndConnect() =
+ flow<BluetoothGattService> {
+ val device = startScan().firstOrNull()
+ if (device != null) {
+ val gatt = connectToDevice(device).firstOrNull()
+ emit(gatt!!.services[0])
+ }
+ }
+}
diff --git a/integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/flow/BluetoothProvisionerTest.kt b/integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/flow/BluetoothProvisionerTest.kt
new file mode 100644
index 000000000..181b6a8b9
--- /dev/null
+++ b/integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/flow/BluetoothProvisionerTest.kt
@@ -0,0 +1,83 @@
+package org.robolectric.integrationtests.kotlin.flow
+
+import android.bluetooth.BluetoothGattService
+import android.bluetooth.BluetoothManager
+import android.bluetooth.le.ScanResult
+import android.content.Context
+import android.os.Build.VERSION_CODES.S
+import com.google.common.truth.Truth.assertThat
+import java.util.UUID
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.launch
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.RuntimeEnvironment
+import org.robolectric.android.util.concurrent.PausedExecutorService
+import org.robolectric.annotation.Config
+import org.robolectric.shadow.api.Shadow
+import org.robolectric.shadows.ShadowBluetoothDevice
+import org.robolectric.shadows.ShadowBluetoothGatt
+import org.robolectric.shadows.ShadowBluetoothLeScanner
+
+/**
+ * A test that uses a custom executor-backed coroutine dispatcher to control the execution of
+ * coroutines.
+ */
+@RunWith(RobolectricTestRunner::class)
+@Config(sdk = [S])
+class BluetoothProvisionerTest {
+
+ val BLUETOOTH_MAC = "00:11:22:33:AA:BB"
+
+ val context = RuntimeEnvironment.getApplication()
+
+ val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
+
+ fun newScanResult(): ScanResult {
+ val bluetoothDevice = bluetoothManager.adapter.getRemoteDevice(BLUETOOTH_MAC)
+ return ScanResult(bluetoothDevice, null, 0, 0)
+ }
+
+ @Test
+ fun testBluetoothProvisioner() {
+ val executor = PausedExecutorService()
+ val dispatcher = executor.asCoroutineDispatcher()
+ val scope = CoroutineScope(dispatcher)
+
+ scope.launch {
+ val gattService =
+ BluetoothProvisioner(RuntimeEnvironment.getApplication()).scanAndConnect().firstOrNull()
+ assertThat(gattService).isNotNull()
+ }
+
+ executor.runAll()
+
+ val scanner = bluetoothManager.adapter.bluetoothLeScanner
+ val shadowScanner = Shadow.extract<ShadowBluetoothLeScanner>(scanner)
+
+ val scanResult = newScanResult()
+ val bluetoothDevice = scanResult.device
+ shadowScanner.scanCallbacks.first().onScanResult(0, newScanResult())
+
+ executor.runAll()
+
+ val shadowDevice = Shadow.extract<ShadowBluetoothDevice>(bluetoothDevice)
+
+ val gatt = shadowDevice.bluetoothGatts.first()
+ val shadowGatt = Shadow.extract<ShadowBluetoothGatt>(gatt)
+
+ val service =
+ BluetoothGattService(
+ UUID.fromString("00000000-0000-0000-0000-0000000000A1"),
+ BluetoothGattService.SERVICE_TYPE_PRIMARY,
+ )
+
+ shadowGatt.addDiscoverableService(service)
+ shadowGatt.notifyConnection(BLUETOOTH_MAC)
+
+ executor.runAll()
+ }
+}