-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add integration test for Kotlin Flow + Shadow Bluetooth APIs
This came up in a discussion about controlling coroutines in Robolectric tests. PiperOrigin-RevId: 601792897
- Loading branch information
Showing
3 changed files
with
163 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
79 changes: 79 additions & 0 deletions
79
...tlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/flow/BluetoothProvisioner.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]) | ||
} | ||
} | ||
} |
83 changes: 83 additions & 0 deletions
83
.../src/test/kotlin/org/robolectric/integrationtests/kotlin/flow/BluetoothProvisionerTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
} | ||
} |