Make test with service working

This commit is contained in:
Sylwester Zieliński
2023-06-23 10:55:26 +02:00
parent dfd307a698
commit 2870ef109e
21 changed files with 682 additions and 86 deletions

View File

@@ -29,37 +29,32 @@
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
package no.nordicsemi.android.uart package no.nordicsemi.android.nrftoolbox
import android.content.Context import android.app.Application
import androidx.room.Room import dagger.hilt.android.HiltAndroidApp
import dagger.Module import no.nordicsemi.android.analytics.AppAnalytics
import dagger.Provides import no.nordicsemi.android.analytics.AppOpenEvent
import dagger.hilt.InstallIn import no.nordicsemi.android.gls.UartServer
import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject
import dagger.hilt.components.SingletonComponent
import no.nordicsemi.android.uart.db.ConfigurationsDao
import no.nordicsemi.android.uart.db.ConfigurationsDatabase
import no.nordicsemi.android.uart.db.MIGRATION_1_2
import javax.inject.Singleton
@Module @HiltAndroidApp
@InstallIn(SingletonComponent::class) class NrfToolboxApplication : Application() {
class HiltModule {
@Provides @Inject
@Singleton lateinit var analytics: AppAnalytics
internal fun provideDB(@ApplicationContext context: Context): ConfigurationsDatabase {
return Room.databaseBuilder( @Inject
context, lateinit var glsServer: UartServer
ConfigurationsDatabase::class.java, "toolbox_uart.db"
).addMigrations(MIGRATION_1_2).build() @Inject
lateinit var uartServer: UartServer
override fun onCreate() {
super.onCreate()
analytics.logEvent(AppOpenEvent)
uartServer.start(this)
} }
@Provides
@Singleton
internal fun provideDao(db: ConfigurationsDatabase): ConfigurationsDao {
return db.dao()
}
} }

View File

@@ -35,7 +35,6 @@ import android.app.Application
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import no.nordicsemi.android.analytics.AppAnalytics import no.nordicsemi.android.analytics.AppAnalytics
import no.nordicsemi.android.analytics.AppOpenEvent import no.nordicsemi.android.analytics.AppOpenEvent
import no.nordicsemi.android.gls.GlsServer
import javax.inject.Inject import javax.inject.Inject
@HiltAndroidApp @HiltAndroidApp
@@ -44,14 +43,9 @@ class NrfToolboxApplication : Application() {
@Inject @Inject
lateinit var analytics: AppAnalytics lateinit var analytics: AppAnalytics
@Inject
lateinit var glsServer: GlsServer
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
analytics.logEvent(AppOpenEvent) analytics.logEvent(AppOpenEvent)
glsServer.start(this)
} }
} }

View File

@@ -31,41 +31,11 @@
package no.nordicsemi.android.service package no.nordicsemi.android.service
import android.bluetooth.BluetoothDevice
import android.content.Context
import android.content.Intent
import dagger.hilt.android.qualifiers.ApplicationContext
import no.nordicsemi.android.kotlin.ble.core.ServerDevice import no.nordicsemi.android.kotlin.ble.core.ServerDevice
import javax.inject.Inject
const val DEVICE_DATA = "device-data" const val DEVICE_DATA = "device-data"
class ServiceManager @Inject constructor( interface ServiceManager {
@ApplicationContext
private val context: Context
) {
fun <T> startService(service: Class<T>, device: ServerDevice) { fun <T> startService(service: Class<T>, device: ServerDevice)
val intent = Intent(context, service).apply {
putExtra(DEVICE_DATA, device)
}
context.startService(intent)
}
fun <T> startService(service: Class<T>, device: BluetoothDevice) {
val intent = Intent(context, service).apply {
putExtra(DEVICE_DATA, device)
}
context.startService(intent)
}
fun <T> startService(service: Class<T>) {
val intent = Intent(context, service)
context.startService(intent)
}
fun <T> stopService(service: Class<T>) {
val intent = Intent(context, service)
context.stopService(intent)
}
} }

View File

@@ -0,0 +1,21 @@
package no.nordicsemi.android.service
import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
class ServiceManagerHiltModule {
@Provides
fun createServiceManager(
@ApplicationContext
context: Context,
): ServiceManager {
return ServiceManagerImpl(context)
}
}

View File

@@ -0,0 +1,20 @@
package no.nordicsemi.android.service
import android.content.Context
import android.content.Intent
import dagger.hilt.android.qualifiers.ApplicationContext
import no.nordicsemi.android.kotlin.ble.core.ServerDevice
import javax.inject.Inject
class ServiceManagerImpl @Inject constructor(
@ApplicationContext
private val context: Context
): ServiceManager {
override fun <T> startService(service: Class<T>, device: ServerDevice) {
val intent = Intent(context, service).apply {
putExtra(DEVICE_DATA, device)
}
context.startService(intent)
}
}

View File

@@ -35,6 +35,10 @@ plugins {
android { android {
namespace = "no.nordicsemi.android.ui" namespace = "no.nordicsemi.android.ui"
testOptions {
unitTests.isIncludeAndroidResources = true
}
} }
dependencies { dependencies {

View File

@@ -6,12 +6,11 @@ import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import no.nordicsemi.android.common.logger.NordicBlekLogger import no.nordicsemi.android.common.logger.NordicBlekLogger
import no.nordicsemi.android.common.logger.BlekLogger
import no.nordicsemi.android.common.logger.BlekLoggerAndLauncher import no.nordicsemi.android.common.logger.BlekLoggerAndLauncher
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
class HiltModule { class NordicLoggerFactoryHiltModule {
@Provides @Provides
fun createLogger(): NordicLoggerFactory { fun createLogger(): NordicLoggerFactory {

View File

@@ -119,8 +119,6 @@ internal class GLSViewModel @Inject constructor(
private val highestSequenceNumber private val highestSequenceNumber
get() = state.value.glsServiceData.records.keys.maxByOrNull { it.sequenceNumber }?.sequenceNumber ?: -1 get() = state.value.glsServiceData.records.keys.maxByOrNull { it.sequenceNumber }?.sequenceNumber ?: -1
fun test() = 2
init { init {
navigationManager.navigateTo(ScannerDestinationId, ParcelUuid(GLS_SERVICE_UUID)) navigationManager.navigateTo(ScannerDestinationId, ParcelUuid(GLS_SERVICE_UUID))

View File

@@ -37,7 +37,6 @@ import no.nordicsemi.android.ui.view.NordicLoggerFactory
import no.nordicsemi.android.ui.view.StringConst import no.nordicsemi.android.ui.view.StringConst
import org.junit.After import org.junit.After
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@@ -121,11 +120,6 @@ internal class GLSViewModelTest {
every { NordicBlekLogger.create(any(), any(), any(), any()) } returns mockk() every { NordicBlekLogger.create(any(), any(), any(), any()) } returns mockk()
} }
@Test
fun addition_isCorrect() {
assertEquals(2, viewModel.test())
}
@Test @Test
fun `when connection fails return disconnected`() = runTest { fun `when connection fails return disconnected`() = runTest {
val disconnectedState = GattConnectionStateWithStatus( val disconnectedState = GattConnectionStateWithStatus(

View File

@@ -38,6 +38,10 @@ plugins {
android { android {
namespace = "no.nordicsemi.android.uart" namespace = "no.nordicsemi.android.uart"
testOptions {
unitTests.isIncludeAndroidResources = true
}
} }
wire { wire {
@@ -54,6 +58,8 @@ dependencies {
implementation(libs.nordic.blek.client) implementation(libs.nordic.blek.client)
implementation(libs.nordic.blek.profile) implementation(libs.nordic.blek.profile)
implementation(libs.nordic.blek.core) implementation(libs.nordic.blek.core)
implementation(libs.nordic.blek.server)
implementation(libs.nordic.blek.advertiser)
implementation(libs.room.runtime) implementation(libs.room.runtime)
implementation(libs.room.ktx) implementation(libs.room.ktx)
@@ -81,6 +87,21 @@ dependencies {
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(libs.androidx.lifecycle.service) implementation(libs.androidx.lifecycle.service)
// For Robolectric tests.
testImplementation("com.google.dagger:hilt-android-testing:2.44")
// ...with Kotlin.
kaptTest("com.google.dagger:hilt-android-compiler:2.46.1")
testImplementation("androidx.test:rules:1.5.0")
testImplementation(libs.junit4)
testImplementation(libs.test.mockk)
testImplementation(libs.androidx.test.ext)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.test.slf4j.simple)
testImplementation(libs.test.robolectric)
testImplementation(libs.kotlin.junit)
implementation("org.simpleframework:simple-xml:2.7.1") { implementation("org.simpleframework:simple-xml:2.7.1") {
exclude(group = "stax", module = "stax-api") exclude(group = "stax", module = "stax-api")
exclude(group = "xpp3", module = "xpp3") exclude(group = "xpp3", module = "xpp3")

View File

@@ -0,0 +1,184 @@
package no.nordicsemi.android.gls
import android.annotation.SuppressLint
import android.content.Context
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import no.nordicsemi.android.kotlin.ble.advertiser.BleAdvertiser
import no.nordicsemi.android.kotlin.ble.core.MockServerDevice
import no.nordicsemi.android.kotlin.ble.core.advertiser.BleAdvertiseConfig
import no.nordicsemi.android.kotlin.ble.core.data.BleGattPermission
import no.nordicsemi.android.kotlin.ble.core.data.BleGattProperty
import no.nordicsemi.android.kotlin.ble.profile.gls.RecordAccessControlPointInputParser
import no.nordicsemi.android.kotlin.ble.server.main.BleGattServer
import no.nordicsemi.android.kotlin.ble.server.main.service.BleGattServerServiceType
import no.nordicsemi.android.kotlin.ble.server.main.service.BleServerGattCharacteristic
import no.nordicsemi.android.kotlin.ble.server.main.service.BleServerGattCharacteristicConfig
import no.nordicsemi.android.kotlin.ble.server.main.service.BleServerGattServiceConfig
import no.nordicsemi.android.kotlin.ble.server.main.service.BluetoothGattServerConnection
import no.nordicsemi.android.uart.repository.BATTERY_LEVEL_CHARACTERISTIC_UUID
import no.nordicsemi.android.uart.repository.BATTERY_SERVICE_UUID
import no.nordicsemi.android.uart.repository.UART_RX_CHARACTERISTIC_UUID
import no.nordicsemi.android.uart.repository.UART_SERVICE_UUID
import no.nordicsemi.android.uart.repository.UART_TX_CHARACTERISTIC_UUID
import javax.inject.Inject
import javax.inject.Singleton
private const val STANDARD_DELAY = 1000L
@SuppressLint("MissingPermission")
@Singleton
class UartServer @Inject constructor(
private val scope: CoroutineScope
) {
lateinit var server: BleGattServer
lateinit var glsCharacteristic: BleServerGattCharacteristic
lateinit var glsContextCharacteristic: BleServerGattCharacteristic
lateinit var racpCharacteristic: BleServerGattCharacteristic
lateinit var batteryLevelCharacteristic: BleServerGattCharacteristic
private var lastRequest = byteArrayOf()
val YOUNGEST_RECORD = byteArrayOf(0x07, 0x00, 0x00, 0xDC.toByte(), 0x07, 0x01, 0x01, 0x0C, 0x1E, 0x05, 0x00, 0x00, 0x26, 0xD2.toByte(), 0x11)
val OLDEST_RECORD = byteArrayOf(0x07, 0x04, 0x00, 0xDC.toByte(), 0x07, 0x01, 0x01, 0x0C, 0x1E, 0x11, 0x00, 0x00, 0x82.toByte(), 0xD2.toByte(), 0x11)
val records = listOf(
YOUNGEST_RECORD,
byteArrayOf(0x07, 0x01, 0x00, 0xDC.toByte(), 0x07, 0x01, 0x01, 0x0C, 0x1E, 0x08, 0x00, 0x00, 0x3D, 0xD2.toByte(), 0x11),
byteArrayOf(0x07, 0x02, 0x00, 0xDC.toByte(), 0x07, 0x01, 0x01, 0x0C, 0x1E, 0x0B, 0x00, 0x00, 0x54, 0xD2.toByte(), 0x11),
byteArrayOf(0x07, 0x03, 0x00, 0xDC.toByte(), 0x07, 0x01, 0x01, 0x0C, 0x1E, 0x0E, 0x00, 0x00, 0x6B, 0xD2.toByte(), 0x11),
OLDEST_RECORD
)
val racp = byteArrayOf(0x06, 0x00, 0x01, 0x01)
fun start(
context: Context,
device: MockServerDevice = MockServerDevice(
name = "GLS Server",
address = "55:44:33:22:11"
),
) = scope.launch {
val rxCharacteristic = BleServerGattCharacteristicConfig(
UART_RX_CHARACTERISTIC_UUID,
listOf(BleGattProperty.PROPERTY_NOTIFY),
listOf()
)
val txCharacteristic = BleServerGattCharacteristicConfig(
UART_TX_CHARACTERISTIC_UUID,
listOf(BleGattProperty.PROPERTY_INDICATE, BleGattProperty.PROPERTY_WRITE),
listOf(BleGattPermission.PERMISSION_WRITE)
)
val uartService = BleServerGattServiceConfig(
UART_SERVICE_UUID,
BleGattServerServiceType.SERVICE_TYPE_PRIMARY,
listOf(rxCharacteristic, txCharacteristic)
)
val batteryLevelCharacteristic = BleServerGattCharacteristicConfig(
BATTERY_LEVEL_CHARACTERISTIC_UUID,
listOf(BleGattProperty.PROPERTY_READ, BleGattProperty.PROPERTY_NOTIFY),
listOf(BleGattPermission.PERMISSION_READ)
)
val batteryService = BleServerGattServiceConfig(
BATTERY_SERVICE_UUID,
BleGattServerServiceType.SERVICE_TYPE_PRIMARY,
listOf(batteryLevelCharacteristic)
)
server = BleGattServer.create(
context = context,
config = arrayOf(uartService, batteryService),
mock = device
)
val advertiser = BleAdvertiser.create(context)
advertiser.advertise(config = BleAdvertiseConfig(), mock = device).launchIn(scope)
launch {
server.connections
.mapNotNull { it.values.firstOrNull() }
.collect { setUpConnection(it) }
}
}
internal fun stopServer() {
server.stopServer()
}
private fun setUpConnection(connection: BluetoothGattServerConnection) {
// val glsService = connection.services.findService(GLS_SERVICE_UUID)!!
// glsCharacteristic = glsService.findCharacteristic(GLUCOSE_MEASUREMENT_CHARACTERISTIC)!!
// glsContextCharacteristic = glsService.findCharacteristic(GLUCOSE_MEASUREMENT_CONTEXT_CHARACTERISTIC)!!
// racpCharacteristic = glsService.findCharacteristic(RACP_CHARACTERISTIC)!!
val batteryService = connection.services.findService(BATTERY_SERVICE_UUID)!!
batteryLevelCharacteristic = batteryService.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)!!
// startGlsService(connection)
// startBatteryService(connection)
}
private fun startGlsService(connection: BluetoothGattServerConnection) {
racpCharacteristic.value
.filter { it.isNotEmpty() }
.onEach { lastRequest = it }
.launchIn(scope)
}
internal fun continueWithResponse() {
sendResponse(lastRequest)
}
private fun sendResponse(request: ByteArray) {
if (request.contentEquals(RecordAccessControlPointInputParser.reportNumberOfAllStoredRecords().value)) {
sendAll(glsCharacteristic)
racpCharacteristic.setValue(racp)
} else if (request.contentEquals(RecordAccessControlPointInputParser.reportLastStoredRecord().value)) {
sendLast(glsCharacteristic)
racpCharacteristic.setValue(racp)
} else if (request.contentEquals(RecordAccessControlPointInputParser.reportFirstStoredRecord().value)) {
sendFirst(glsCharacteristic)
racpCharacteristic.setValue(racp)
}
}
private fun sendFirst(characteristics: BleServerGattCharacteristic) {
characteristics.setValue(records.first())
}
private fun sendLast(characteristics: BleServerGattCharacteristic) {
characteristics.setValue(records.last())
}
private fun sendAll(characteristics: BleServerGattCharacteristic) = scope.launch {
records.forEach {
characteristics.setValue(it)
delay(100)
}
}
private fun startBatteryService(connection: BluetoothGattServerConnection) {
scope.launch {
repeat(100) {
batteryLevelCharacteristic.setValue(byteArrayOf(0x61))
delay(STANDARD_DELAY)
batteryLevelCharacteristic.setValue(byteArrayOf(0x60))
delay(STANDARD_DELAY)
batteryLevelCharacteristic.setValue(byteArrayOf(0x5F))
delay(STANDARD_DELAY)
}
}
}
}

View File

@@ -0,0 +1,20 @@
package no.nordicsemi.android.uart
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import no.nordicsemi.android.uart.db.ConfigurationsDao
import no.nordicsemi.android.uart.db.ConfigurationsDatabase
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class DaoHiltModule {
@Provides
@Singleton
internal fun provideDao(db: ConfigurationsDatabase): ConfigurationsDao {
return db.dao()
}
}

View File

@@ -0,0 +1,26 @@
package no.nordicsemi.android.uart
import android.content.Context
import androidx.room.Room
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import no.nordicsemi.android.uart.db.ConfigurationsDatabase
import no.nordicsemi.android.uart.db.MIGRATION_1_2
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class DbHiltModule {
@Provides
@Singleton
internal fun provideDB(@ApplicationContext context: Context): ConfigurationsDatabase {
return Room.databaseBuilder(
context,
ConfigurationsDatabase::class.java, "toolbox_uart.db"
).addMigrations(MIGRATION_1_2).build()
}
}

View File

@@ -39,7 +39,6 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import no.nordicsemi.android.common.core.simpleSharedFlow import no.nordicsemi.android.common.core.simpleSharedFlow
import no.nordicsemi.android.common.logger.BlekLoggerAndLauncher import no.nordicsemi.android.common.logger.BlekLoggerAndLauncher
import no.nordicsemi.android.common.logger.NordicBlekLogger
import no.nordicsemi.android.kotlin.ble.core.ServerDevice import no.nordicsemi.android.kotlin.ble.core.ServerDevice
import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus
@@ -52,6 +51,7 @@ import no.nordicsemi.android.uart.data.UARTRecord
import no.nordicsemi.android.uart.data.UARTRecordType import no.nordicsemi.android.uart.data.UARTRecordType
import no.nordicsemi.android.uart.data.UARTServiceData import no.nordicsemi.android.uart.data.UARTServiceData
import no.nordicsemi.android.uart.data.parseWithNewLineChar import no.nordicsemi.android.uart.data.parseWithNewLineChar
import no.nordicsemi.android.ui.view.NordicLoggerFactory
import no.nordicsemi.android.ui.view.StringConst import no.nordicsemi.android.ui.view.StringConst
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -62,7 +62,8 @@ class UARTRepository @Inject internal constructor(
private val context: Context, private val context: Context,
private val serviceManager: ServiceManager, private val serviceManager: ServiceManager,
private val configurationDataSource: ConfigurationDataSource, private val configurationDataSource: ConfigurationDataSource,
private val stringConst: StringConst private val stringConst: StringConst,
private val loggerFactory: NordicLoggerFactory
) { ) {
private var logger: BlekLoggerAndLauncher? = null private var logger: BlekLoggerAndLauncher? = null
@@ -97,7 +98,7 @@ class UARTRepository @Inject internal constructor(
private fun shouldClean() = !isOnScreen && !isServiceRunning private fun shouldClean() = !isOnScreen && !isServiceRunning
fun launch(device: ServerDevice) { fun launch(device: ServerDevice) {
logger = NordicBlekLogger.create(context, stringConst.APP_NAME, "UART", device.address) logger = loggerFactory.createNordicLogger(context, stringConst.APP_NAME, "UART", device.address)
_data.value = _data.value.copy(deviceName = device.name) _data.value = _data.value.copy(deviceName = device.name)
serviceManager.startService(UARTService::class.java, device) serviceManager.startService(UARTService::class.java, device)
} }

View File

@@ -56,11 +56,11 @@ import java.util.*
import javax.inject.Inject import javax.inject.Inject
val UART_SERVICE_UUID: UUID = UUID.fromString("6E400001-B5A3-F393-E0A9-E50E24DCCA9E") val UART_SERVICE_UUID: UUID = UUID.fromString("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
private val UART_RX_CHARACTERISTIC_UUID = UUID.fromString("6E400002-B5A3-F393-E0A9-E50E24DCCA9E") internal val UART_RX_CHARACTERISTIC_UUID = UUID.fromString("6E400002-B5A3-F393-E0A9-E50E24DCCA9E")
private val UART_TX_CHARACTERISTIC_UUID = UUID.fromString("6E400003-B5A3-F393-E0A9-E50E24DCCA9E") internal val UART_TX_CHARACTERISTIC_UUID = UUID.fromString("6E400003-B5A3-F393-E0A9-E50E24DCCA9E")
private val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb") internal val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb")
private val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb") internal val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb")
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
@AndroidEntryPoint @AndroidEntryPoint

View File

@@ -77,6 +77,7 @@ import no.nordicsemi.android.uart.view.OnRunMacro
import no.nordicsemi.android.uart.view.OpenLogger import no.nordicsemi.android.uart.view.OpenLogger
import no.nordicsemi.android.uart.view.UARTViewEvent import no.nordicsemi.android.uart.view.UARTViewEvent
import no.nordicsemi.android.uart.view.UARTViewState import no.nordicsemi.android.uart.view.UARTViewState
import no.nordicsemi.android.ui.view.NordicLoggerFactory
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@@ -84,7 +85,8 @@ internal class UARTViewModel @Inject constructor(
private val repository: UARTRepository, private val repository: UARTRepository,
private val navigationManager: Navigator, private val navigationManager: Navigator,
private val dataSource: UARTPersistentDataSource, private val dataSource: UARTPersistentDataSource,
private val analytics: AppAnalytics private val analytics: AppAnalytics,
private val loggerFactory: NordicLoggerFactory
) : ViewModel() { ) : ViewModel() {
private val _state = MutableStateFlow(UARTViewState()) private val _state = MutableStateFlow(UARTViewState())
@@ -126,7 +128,7 @@ internal class UARTViewModel @Inject constructor(
.launchIn(viewModelScope) .launchIn(viewModelScope)
} }
private fun handleResult(result: NavigationResult<ServerDevice>) { internal fun handleResult(result: NavigationResult<ServerDevice>) {
when (result) { when (result) {
is NavigationResult.Cancelled -> navigationManager.navigateUp() is NavigationResult.Cancelled -> navigationManager.navigateUp()
is NavigationResult.Success -> onDeviceSelected(result.value) is NavigationResult.Success -> onDeviceSelected(result.value)

View File

@@ -0,0 +1,40 @@
package no.nordicsemi.android.gls
import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn
import no.nordicsemi.android.common.logger.BlekLoggerAndLauncher
import no.nordicsemi.android.ui.view.NordicLoggerFactory
import no.nordicsemi.android.ui.view.NordicLoggerFactoryHiltModule
@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [NordicLoggerFactoryHiltModule::class]
)
class NordicLoggerFactoryTestModule {
@Provides
fun createLogger(): NordicLoggerFactory {
return object : NordicLoggerFactory {
override fun createNordicLogger(
context: Context,
profile: String?,
key: String,
name: String?,
): BlekLoggerAndLauncher {
return object : BlekLoggerAndLauncher {
override fun launch() {
}
override fun log(priority: Int, log: String) {
println(log)
}
}
}
}
}
}

View File

@@ -0,0 +1,57 @@
package no.nordicsemi.android.gls
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import dagger.Module
import dagger.Provides
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn
import no.nordicsemi.android.kotlin.ble.core.MockServerDevice
import no.nordicsemi.android.kotlin.ble.core.ServerDevice
import no.nordicsemi.android.service.DEVICE_DATA
import no.nordicsemi.android.service.ServiceManager
import no.nordicsemi.android.service.ServiceManagerHiltModule
import no.nordicsemi.android.uart.repository.UARTService
import org.robolectric.Robolectric
import org.robolectric.android.controller.ServiceController
import javax.inject.Singleton
@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [ServiceManagerHiltModule::class]
)
class ServiceManagerTestModule {
private val componentName = ComponentName("org.robolectric", UARTService::class.java.name)
@Provides
internal fun provideDevice(): MockServerDevice {
return MockServerDevice(
name = "GLS Server",
address = "55:44:33:22:11"
)
}
@Provides
internal fun provideServiceController(
@ApplicationContext context: Context,
device: MockServerDevice
): ServiceController<UARTService> {
return Robolectric.buildService(UARTService::class.java, Intent(context, UARTService::class.java).apply {
putExtra(DEVICE_DATA, device)
})
}
@Provides
@Singleton
internal fun provideServiceManager(controller: ServiceController<UARTService>): ServiceManager {
return object : ServiceManager {
override fun <T> startService(service: Class<T>, device: ServerDevice) {
controller.create().startCommand(3, 4).get()
}
}
}
}

View File

@@ -0,0 +1,29 @@
package no.nordicsemi.android.gls
import android.content.Context
import androidx.room.Room
import dagger.Module
import dagger.Provides
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn
import no.nordicsemi.android.uart.DbHiltModule
import no.nordicsemi.android.uart.db.ConfigurationsDatabase
import no.nordicsemi.android.uart.db.MIGRATION_1_2
import javax.inject.Singleton
@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [DbHiltModule::class]
)
class TestDbHiltModule {
@Provides
@Singleton
internal fun provideDB(@ApplicationContext context: Context): ConfigurationsDatabase {
return Room.inMemoryDatabaseBuilder(
context,
ConfigurationsDatabase::class.java
).addMigrations(MIGRATION_1_2).build()
}
}

View File

@@ -0,0 +1,13 @@
package no.nordicsemi.android.gls
import dagger.Module
import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn
//@Module
//@TestInstallIn(
// components = [SingletonComponent::class],
// replaces = [AnalyticsModule::class]
//)
//class TestHiltModule {
//}

View File

@@ -0,0 +1,208 @@
package no.nordicsemi.android.gls
import android.content.Context
import androidx.test.rule.ServiceTestRule
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication
import dagger.hilt.android.testing.UninstallModules
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.impl.annotations.RelaxedMockK
import io.mockk.junit4.MockKRule
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.mockkStatic
import io.mockk.spyk
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import no.nordicsemi.android.analytics.AppAnalytics
import no.nordicsemi.android.common.logger.NordicBlekLogger
import no.nordicsemi.android.common.navigation.NavigationResult
import no.nordicsemi.android.common.navigation.Navigator
import no.nordicsemi.android.common.navigation.di.NavigationModule
import no.nordicsemi.android.kotlin.ble.client.main.ClientScope
import no.nordicsemi.android.kotlin.ble.core.MockServerDevice
import no.nordicsemi.android.kotlin.ble.core.data.BleGattConnectionStatus
import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus
import no.nordicsemi.android.kotlin.ble.server.main.ServerScope
import no.nordicsemi.android.uart.data.UARTPersistentDataSource
import no.nordicsemi.android.uart.repository.UARTRepository
import no.nordicsemi.android.uart.view.DisconnectEvent
import no.nordicsemi.android.uart.viewmodel.UARTViewModel
import no.nordicsemi.android.ui.view.NordicLoggerFactory
import no.nordicsemi.android.ui.view.StringConst
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import javax.inject.Inject
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@HiltAndroidTest
@Config(application = HiltTestApplication::class)
@UninstallModules(NavigationModule::class)
@RunWith(RobolectricTestRunner::class)
internal class UARTViewModelTest {
@get:Rule
val mockkRule = MockKRule(this)
@get:Rule
val serviceRule = ServiceTestRule()
@get:Rule
var hiltRule = HiltAndroidRule(this)
@BindValue
@JvmField
val analyticsService: Navigator = mockk(relaxed = true)
@RelaxedMockK
lateinit var analytics: AppAnalytics
@MockK
lateinit var stringConst: StringConst
@RelaxedMockK
lateinit var context: Context
@RelaxedMockK
lateinit var logger: NordicBlekLogger
@Inject
lateinit var repository: UARTRepository
@Inject
lateinit var dataSource: UARTPersistentDataSource
lateinit var viewModel: UARTViewModel
lateinit var uartServer: UartServer
@Inject
lateinit var device: MockServerDevice
@Before
fun setUp() {
hiltRule.inject()
Dispatchers.setMain(UnconfinedTestDispatcher())
}
@After
fun release() {
Dispatchers.resetMain()
}
@Before
fun before() {
viewModel = UARTViewModel(repository, mockk(relaxed = true), dataSource, mockk(relaxed = true), object :
NordicLoggerFactory {
override fun createNordicLogger(
context: Context,
profile: String?,
key: String,
name: String?,
): NordicBlekLogger {
return logger
}
})
runBlocking {
mockkStatic("no.nordicsemi.android.kotlin.ble.client.main.ClientScopeKt")
every { ClientScope } returns CoroutineScope(UnconfinedTestDispatcher())
mockkStatic("no.nordicsemi.android.kotlin.ble.server.main.ServerScopeKt")
every { ServerScope } returns CoroutineScope(UnconfinedTestDispatcher())
every { stringConst.APP_NAME } returns "Test"
uartServer = UartServer(CoroutineScope(UnconfinedTestDispatcher()))
uartServer.start(spyk(), device)
}
}
@Before
fun prepareLogger() {
mockkObject(NordicBlekLogger.Companion)
every { NordicBlekLogger.create(any(), any(), any(), any()) } returns mockk()
}
@Test
fun `when connected should return state connected`() = runTest {
val connectedState = GattConnectionStateWithStatus(
GattConnectionState.STATE_CONNECTED,
BleGattConnectionStatus.SUCCESS
)
viewModel.handleResult(NavigationResult.Success(device))
advanceUntilIdle()
assertEquals(connectedState, viewModel.state.value.uartManagerState.connectionState)
}
@Test
fun `when disconnected should return state connected`() = runTest {
val disconnectedState = GattConnectionStateWithStatus(
GattConnectionState.STATE_DISCONNECTED,
BleGattConnectionStatus.SUCCESS
)
viewModel.handleResult(NavigationResult.Success(device))
viewModel.onEvent(DisconnectEvent)
advanceUntilIdle()
assertEquals(disconnectedState, viewModel.state.value.uartManagerState.connectionState)
}
//
// @Test
// fun `when request last record then change status and get 1 record`() = runTest {
// viewModel.handleResult(NavigationResult.Success(device))
// advanceUntilIdle() //Needed because of delay() in waitForBonding()
// assertEquals(RequestStatus.IDLE, viewModel.state.value.glsServiceData.requestStatus)
//
// viewModel.onEvent(OnWorkingModeSelected(WorkingMode.LAST))
// assertEquals(RequestStatus.PENDING, viewModel.state.value.glsServiceData.requestStatus)
//
// glsServer.continueWithResponse() //continue server breakpoint
//
// assertEquals(RequestStatus.SUCCESS, viewModel.state.value.glsServiceData.requestStatus)
// assertEquals(1, viewModel.state.value.glsServiceData.records.size)
//
// val parsedResponse = GlucoseMeasurementParser.parse(glsServer.OLDEST_RECORD)
// assertEquals(parsedResponse, viewModel.state.value.glsServiceData.records.keys.first())
// }
//
// @Test
// fun `when request all record then change status and get 5 records`() = runTest {
// viewModel.handleResult(NavigationResult.Success(device))
// advanceUntilIdle() //Needed because of delay() in waitForBonding()
// assertEquals(RequestStatus.IDLE, viewModel.state.value.glsServiceData.requestStatus)
//
// viewModel.onEvent(OnWorkingModeSelected(WorkingMode.ALL))
// assertEquals(RequestStatus.PENDING, viewModel.state.value.glsServiceData.requestStatus)
//
// glsServer.continueWithResponse() //continue server breakpoint
// advanceUntilIdle() //We have to use because of delay() in sendAll()
//
// assertEquals(RequestStatus.SUCCESS, viewModel.state.value.glsServiceData.requestStatus)
// assertEquals(5, viewModel.state.value.glsServiceData.records.size)
//
// val expectedRecords = glsServer.records.map { GlucoseMeasurementParser.parse(it) }
// assertContentEquals(expectedRecords, viewModel.state.value.glsServiceData.records.keys)
// }
}