diff options
author | Michael Hoisie <hoisie@google.com> | 2024-05-07 09:52:31 -0700 |
---|---|---|
committer | Copybara-Service <copybara-worker@google.com> | 2024-05-07 09:53:07 -0700 |
commit | 6aecbb4b15e7d33ae684c0e9b110230ec830e182 (patch) | |
tree | ce7e5baf99698625fd6967c0f81d6343a1a88a78 | |
parent | ccf8c6fd9013537e4ffd8432505d642b10baccc4 (diff) | |
download | robolectric-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
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() + } +} |