From e2e062b80da9c180bb9f0e1919010d39dfa89c65 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Fri, 28 Feb 2020 13:01:30 -0800 Subject: [PATCH] Stats collector; kotlin; mcumgr test mock framework (#65) --- build.gradle | 4 +- gradle/wrapper/gradle-wrapper.properties | 4 +- mcumgr-ble/gradle.properties | 2 +- mcumgr-core/build.gradle | 12 +- .../managers/meta/StatisticsCollector.kt | 146 +++++++++++++++ .../java/io/runtime/mcumgr/util/CBOR.java | 14 ++ .../io/runtime/mcumgr/McuMgrImageTest.java | 44 ----- .../java/io/runtime/mcumgr/McuMgrImageTest.kt | 35 ++++ .../runtime/mcumgr/StatisticsCollectorTest.kt | 171 ++++++++++++++++++ .../io/runtime/mcumgr/mock/McuMgrGroup.kt | 14 ++ .../io/runtime/mcumgr/mock/McuMgrHandler.kt | 17 ++ .../io/runtime/mcumgr/mock/McuMgrOperation.kt | 9 + .../runtime/mcumgr/mock/MockMcuMgrResponse.kt | 69 +++++++ .../mcumgr/mock/MockMcuMgrTransport.kt | 77 ++++++++ .../mcumgr/mock/handlers/MockStatsHandler.kt | 128 +++++++++++++ sample/build.gradle | 28 +-- 16 files changed, 709 insertions(+), 65 deletions(-) create mode 100644 mcumgr-core/src/main/java/io/runtime/mcumgr/managers/meta/StatisticsCollector.kt delete mode 100644 mcumgr-core/src/test/java/io/runtime/mcumgr/McuMgrImageTest.java create mode 100644 mcumgr-core/src/test/java/io/runtime/mcumgr/McuMgrImageTest.kt create mode 100644 mcumgr-core/src/test/java/io/runtime/mcumgr/StatisticsCollectorTest.kt create mode 100644 mcumgr-core/src/test/java/io/runtime/mcumgr/mock/McuMgrGroup.kt create mode 100644 mcumgr-core/src/test/java/io/runtime/mcumgr/mock/McuMgrHandler.kt create mode 100644 mcumgr-core/src/test/java/io/runtime/mcumgr/mock/McuMgrOperation.kt create mode 100644 mcumgr-core/src/test/java/io/runtime/mcumgr/mock/MockMcuMgrResponse.kt create mode 100644 mcumgr-core/src/test/java/io/runtime/mcumgr/mock/MockMcuMgrTransport.kt create mode 100644 mcumgr-core/src/test/java/io/runtime/mcumgr/mock/handlers/MockStatsHandler.kt diff --git a/build.gradle b/build.gradle index ade2055b..2a6a56ff 100644 --- a/build.gradle +++ b/build.gradle @@ -6,12 +6,14 @@ */ buildscript { + ext.kotlin_version = '1.3.61' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.2.1' + classpath 'com.android.tools.build:gradle:3.5.3' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.dicedmelon.gradle:jacoco-android:0.1.4' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 45ae433a..f5086104 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Oct 01 11:43:39 CEST 2018 +#Fri Feb 21 15:26:59 PST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/mcumgr-ble/gradle.properties b/mcumgr-ble/gradle.properties index 31d1cd56..3b7b8dc2 100644 --- a/mcumgr-ble/gradle.properties +++ b/mcumgr-ble/gradle.properties @@ -1,3 +1,3 @@ POM_ARTIFACT_ID=mcumgr-ble POM_NAME=McuManager Ble -POM_PACKAGING=aar \ No newline at end of file +POM_PACKAGING=aar diff --git a/mcumgr-core/build.gradle b/mcumgr-core/build.gradle index 4fa4d03a..dc6847ba 100644 --- a/mcumgr-core/build.gradle +++ b/mcumgr-core/build.gradle @@ -6,6 +6,7 @@ */ apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' apply from: rootProject.file('gradle/jacoco-android.gradle') android { @@ -26,6 +27,10 @@ android { dependencies { + // Kotlin + implementation "androidx.core:core-ktx:1.2.0" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + // Annotations implementation 'org.jetbrains:annotations:16.0.1' @@ -33,12 +38,13 @@ dependencies { implementation 'org.slf4j:slf4j-api:1.7.25' // Import CBOR parser - implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.9.6' - implementation 'com.fasterxml.jackson.core:jackson-core:2.9.6' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.6' + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.9.10' + implementation 'com.fasterxml.jackson.core:jackson-core:2.9.10' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.10' // Test testImplementation 'junit:junit:4.12' + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2" } apply from: rootProject.file('gradle/gradle-mvn-push.gradle') diff --git a/mcumgr-core/src/main/java/io/runtime/mcumgr/managers/meta/StatisticsCollector.kt b/mcumgr-core/src/main/java/io/runtime/mcumgr/managers/meta/StatisticsCollector.kt new file mode 100644 index 00000000..c090a18c --- /dev/null +++ b/mcumgr-core/src/main/java/io/runtime/mcumgr/managers/meta/StatisticsCollector.kt @@ -0,0 +1,146 @@ +package io.runtime.mcumgr.managers.meta + +import io.runtime.mcumgr.McuMgrCallback +import io.runtime.mcumgr.exception.McuMgrErrorException +import io.runtime.mcumgr.exception.McuMgrException +import io.runtime.mcumgr.managers.StatsManager +import io.runtime.mcumgr.response.stat.McuMgrStatListResponse +import io.runtime.mcumgr.response.stat.McuMgrStatResponse +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Result of statistic collections. + */ +sealed class StatCollectionResult { + data class Success(val statistics: Map>): StatCollectionResult() + data class Cancelled(val statistics: Map>): StatCollectionResult() + data class Failure(val throwable: Throwable): StatCollectionResult() +} + +/** + * Callback for statistic collections. + */ +typealias StatCollectionCallback = (StatCollectionResult) -> Unit + +/** + * Non-blocking cancellable interface for cancelling an ongoing task. + */ +interface Cancellable { + fun cancel() +} + +/** + * Collects stats from a device. + */ +class StatisticsCollector(private val statsManager: StatsManager) { + + /** + * Collect stats from a single group by name. + */ + fun collect(groupName: String, callback: StatCollectionCallback): Cancellable { + return StatCollection(statsManager, callback).start(listOf(groupName)) + } + + /** + * Collect from a list of statistic group names. + */ + fun collectGroups(groupNames: List, callback: StatCollectionCallback): Cancellable { + return StatCollection(statsManager, callback).start(groupNames) + } + + /** + * List the stat group names from the device and collect each which intersects with the filter. + */ + fun collectAll(filter: Set? = null, callback: StatCollectionCallback): Cancellable { + val collection = StatCollection(statsManager, callback) + statsManager.list(object: McuMgrCallback { + + override fun onResponse(response: McuMgrStatListResponse) { + // Check for error response. + if (!response.isSuccess) { + callback(StatCollectionResult.Failure(McuMgrErrorException(response))) + return + } + // Filter statistic group names. + val groupNames = filter?.intersect(response.stat_list.toSet())?.toList() + ?: response.stat_list.toList() + // Ensure group names in response. + if (groupNames.isEmpty()) { + callback(StatCollectionResult.Failure( + IllegalStateException("Statistic group list is empty.") + )) + return + } + // Start collection + collection.start(groupNames) + } + + override fun onError(error: McuMgrException) { + callback(StatCollectionResult.Failure(error)) + } + }) + return collection + } +} + +/** + * Manages a single statistics collection. + */ +private class StatCollection( + private val statsManager: StatsManager, + private val callback: StatCollectionCallback +): Cancellable { + + private val cancelled = AtomicBoolean(false) + private val started = AtomicBoolean(false) + private val result = mutableMapOf>() + + /** + * Start the stat collection for a given list of statistics groups. + * + * Start must only be called once per collection and must be provided at least one group to + * collect from. Otherwise this method will throw an error. + * + * @throws IllegalArgumentException If the stat collection has already been started. + */ + fun start(groupNames: List): Cancellable { + check(started.compareAndSet(false, true)) { "Cannot call start() twice." } + if (groupNames.isEmpty()) { + callback(StatCollectionResult.Failure(IllegalArgumentException("List of group names is empty."))) + return this + } + if (cancelled.get()) { + callback(StatCollectionResult.Cancelled(result)) + return this + } + collect(0, groupNames, callback) + return this + } + + private fun collect(index: Int, groupNames: List, callback: StatCollectionCallback) { + require(index in groupNames.indices) { "Index $index is out of range of groupList." } + statsManager.read(groupNames[index], object: McuMgrCallback { + + override fun onResponse(response: McuMgrStatResponse) { + if (!response.isSuccess) { + callback(StatCollectionResult.Failure(McuMgrErrorException(response))) + return + } + result[response.name] = response.fields + when { + index == groupNames.size - 1 -> callback(StatCollectionResult.Success(result)) + cancelled.get() -> callback(StatCollectionResult.Cancelled(result)) + else -> collect(index + 1, groupNames, callback) + } + } + + override fun onError(error: McuMgrException) { + callback(StatCollectionResult.Failure(error)) + } + }) + } + + override fun cancel() { + cancelled.set(true) + } +} diff --git a/mcumgr-core/src/main/java/io/runtime/mcumgr/util/CBOR.java b/mcumgr-core/src/main/java/io/runtime/mcumgr/util/CBOR.java index d4df0a1c..58f0fa9a 100644 --- a/mcumgr-core/src/main/java/io/runtime/mcumgr/util/CBOR.java +++ b/mcumgr-core/src/main/java/io/runtime/mcumgr/util/CBOR.java @@ -10,6 +10,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.cbor.CBORFactory; +import org.jetbrains.annotations.NotNull; + import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.HashMap; @@ -56,4 +58,16 @@ public static Map toObjectMap(byte[] data) throws IOException { ByteArrayInputStream inputStream = new ByteArrayInputStream(data); return mapper.readValue(inputStream, typeRef); } + + public static T getObject(@NotNull byte[] data, @NotNull String key, @NotNull Class type) throws IOException { + ObjectMapper mapper = new ObjectMapper(sFactory); + return mapper.convertValue(mapper.readTree(data).get(key), type); + } + + @NotNull + public static String getString(@NotNull byte[] data, @NotNull String key) throws IOException { + ObjectMapper mapper = new ObjectMapper(sFactory); + return mapper.readTree(data).get(key).asText(); + } + } diff --git a/mcumgr-core/src/test/java/io/runtime/mcumgr/McuMgrImageTest.java b/mcumgr-core/src/test/java/io/runtime/mcumgr/McuMgrImageTest.java deleted file mode 100644 index 3b6a7ee4..00000000 --- a/mcumgr-core/src/test/java/io/runtime/mcumgr/McuMgrImageTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package io.runtime.mcumgr; - -import org.junit.Test; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; - -import io.runtime.mcumgr.image.McuMgrImage; - -import static org.junit.Assert.assertNotNull; - -public class McuMgrImageTest { - - @Test - public void fromBytes_unprotectedTlvs_success() throws Exception { - ClassLoader classLoader = this.getClass().getClassLoader(); - assertNotNull(classLoader); - InputStream inputStream = classLoader.getResourceAsStream("slinky-no-prot-tlv.img"); - assertNotNull(inputStream); - byte[] imageData = toByteArray(inputStream); - McuMgrImage.fromBytes(imageData); - } - - @Test - public void fromBytes_protectedTlvs_success() throws Exception { - ClassLoader classLoader = this.getClass().getClassLoader(); - assertNotNull(classLoader); - InputStream inputStream = classLoader.getResourceAsStream("slinky-prot-tlv.img"); - assertNotNull(inputStream); - byte[] imageData = toByteArray(inputStream); - McuMgrImage.fromBytes(imageData); - } - - private static byte[] toByteArray(InputStream in) throws IOException { - ByteArrayOutputStream os = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - int len; - while ((len = in.read(buffer)) != -1) { - os.write(buffer, 0, len); - } - return os.toByteArray(); - } -} diff --git a/mcumgr-core/src/test/java/io/runtime/mcumgr/McuMgrImageTest.kt b/mcumgr-core/src/test/java/io/runtime/mcumgr/McuMgrImageTest.kt new file mode 100644 index 00000000..3a4aa25b --- /dev/null +++ b/mcumgr-core/src/test/java/io/runtime/mcumgr/McuMgrImageTest.kt @@ -0,0 +1,35 @@ +package io.runtime.mcumgr + +import io.runtime.mcumgr.image.McuMgrImage +import org.junit.Test +import java.io.ByteArrayOutputStream +import java.io.InputStream + +class McuMgrImageTest { + + @Test + fun `parse image without protected tlvs success`() { + val inputStream = this::class.java.classLoader?.getResourceAsStream("slinky-no-prot-tlv.img")!! + ?: throw IllegalStateException("input stream is null") + val imageData = toByteArray(inputStream) + McuMgrImage.fromBytes(imageData) + } + + @Test + fun `parse image with protected tlvs success`() { + val inputStream = this::class.java.classLoader?.getResourceAsStream("slinky-prot-tlv.img") + ?: throw IllegalStateException("input stream is null") + val imageData = toByteArray(inputStream) + McuMgrImage.fromBytes(imageData) + } + + private fun toByteArray(inputStream: InputStream): ByteArray { + val os = ByteArrayOutputStream() + val buffer = ByteArray(1024) + var len: Int + while (inputStream.read(buffer).also { len = it } != -1) { + os.write(buffer, 0, len) + } + return os.toByteArray() + } +} diff --git a/mcumgr-core/src/test/java/io/runtime/mcumgr/StatisticsCollectorTest.kt b/mcumgr-core/src/test/java/io/runtime/mcumgr/StatisticsCollectorTest.kt new file mode 100644 index 00000000..23fae697 --- /dev/null +++ b/mcumgr-core/src/test/java/io/runtime/mcumgr/StatisticsCollectorTest.kt @@ -0,0 +1,171 @@ +package io.runtime.mcumgr + +import io.runtime.mcumgr.exception.McuMgrErrorException +import io.runtime.mcumgr.managers.StatsManager +import io.runtime.mcumgr.managers.meta.StatCollectionResult +import io.runtime.mcumgr.managers.meta.StatisticsCollector +import io.runtime.mcumgr.mock.MockMcuMgrTransport +import io.runtime.mcumgr.mock.handlers.MockStatsHandler +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Test + +private const val GROUP1 = "group1" +private const val GROUP2 = "group2" +private const val GROUP3 = "group3" + +class StatisticsCollectorTest { + + private val group1Stats = mapOf( + GROUP1 to mapOf( + "stat1" to 1L, + "stat2" to 2L, + "stat3" to 3L, + "stat4" to 4L, + "stat5" to 5L + ) + ) + + private val group2Stats = mapOf( + GROUP2 to mapOf( + "stat1" to 1L, + "stat2" to 2L, + "stat3" to 3L, + "stat4" to 4L, + "stat5" to 5L + ) + ) + + private val group3Stats = mapOf( + GROUP3 to mapOf( + "stat1" to 1L, + "stat2" to 2L, + "stat3" to 3L, + "stat4" to 4L, + "stat5" to 5L + ) + ) + + private val allStats = group1Stats + group2Stats + group3Stats + + @Test + fun `collect all stats success`() = runBlocking { + val resultLock = Channel(Channel.CONFLATED) + val statsHandler = MockStatsHandler(allStats) + val statsManager = StatsManager(MockMcuMgrTransport(statsHandler)) + val statsCollector = StatisticsCollector(statsManager) + statsCollector.collectAll { result -> + resultLock.offer(result) + } + val result = resultLock.receive() + require(result is StatCollectionResult.Success) { + "Expected stat collection result success, was ${result::class.java.canonicalName}" + } + assertEquals(allStats, result.statistics) + } + + @Test + fun `collect group success`() = runBlocking { + val resultLock = Channel(Channel.CONFLATED) + val statsHandler = MockStatsHandler(allStats) + val statsManager = StatsManager(MockMcuMgrTransport(statsHandler)) + val statsCollector = StatisticsCollector(statsManager) + statsCollector.collect(GROUP1) { result -> + resultLock.offer(result) + } + val result = resultLock.receive() + require(result is StatCollectionResult.Success) { + "Expected stat collection result success, was ${result::class.java.canonicalName}" + } + assertEquals(group1Stats, result.statistics) + } + + @Test + fun `collect multiple groups success`() = runBlocking { + val resultLock = Channel(Channel.CONFLATED) + val statsHandler = MockStatsHandler(allStats) + val statsManager = StatsManager(MockMcuMgrTransport(statsHandler)) + val statsCollector = StatisticsCollector(statsManager) + statsCollector.collectGroups(listOf(GROUP1, GROUP2)) { result -> + resultLock.offer(result) + } + val result = resultLock.receive() + require(result is StatCollectionResult.Success) { + "Expected stat collection result success, was ${result::class.java.canonicalName}" + } + assertEquals(group1Stats + group2Stats, result.statistics) + } + + @Test + fun `collect all with filter success`() = runBlocking { + val resultLock = Channel(Channel.CONFLATED) + val statsHandler = MockStatsHandler(allStats) + val statsManager = StatsManager(MockMcuMgrTransport(statsHandler)) + val statsCollector = StatisticsCollector(statsManager) + statsCollector.collectAll(setOf(GROUP1)) { result -> + resultLock.offer(result) + } + val result = resultLock.receive() + require(result is StatCollectionResult.Success) { + "Expected stat collection result success, was ${result::class.java.canonicalName}" + } + assertEquals(group1Stats, result.statistics) + } + + @Test + fun `collect all with bad filter failure`() = runBlocking { + val resultLock = Channel(Channel.CONFLATED) + val statsHandler = MockStatsHandler(allStats) + val statsManager = StatsManager(MockMcuMgrTransport(statsHandler)) + val statsCollector = StatisticsCollector(statsManager) + statsCollector.collectAll(setOf("asdf")) { result -> + resultLock.offer(result) + } + val result = resultLock.receive() + require(result is StatCollectionResult.Failure) { + "Expected stat collection result success, was ${result::class.java.canonicalName}" + } + } + + @Test + fun `collect all cancel success`() = runBlocking { + val resultLock = Channel(Channel.CONFLATED) + val statsHandler = MockStatsHandler(allStats) + val statsManager = StatsManager(MockMcuMgrTransport(statsHandler)) + val statsCollector = StatisticsCollector(statsManager) + val cancellable = statsCollector.collectAll { result -> + resultLock.offer(result) + } + cancellable.cancel() + val result = resultLock.receive() + require(result is StatCollectionResult.Cancelled) { + "Expected stat collection result success, was ${result::class.java.canonicalName}" + } + } + + @Test + fun `collect all fail stat read`() = runBlocking { + val resultLock = Channel(Channel.CONFLATED) + val statsHandler = MockStatsHandler(allStats) + val statsManager = StatsManager(MockMcuMgrTransport(statsHandler)) + val statsCollector = StatisticsCollector(statsManager) + val cancellable = statsCollector.collect("asdf") { result -> + resultLock.offer(result) + } + cancellable.cancel() + val result = resultLock.receive() + require(result is StatCollectionResult.Failure) { + "Expected stat collection result success, was ${result::class.java.canonicalName}" + } + val throwable = result.throwable + require(throwable is McuMgrErrorException) { + "Expected McuMgrErrorException, was ${throwable::class.java.canonicalName}" + } + assertEquals(throwable.code, McuMgrErrorCode.IN_VALUE) + } +} + + + + diff --git a/mcumgr-core/src/test/java/io/runtime/mcumgr/mock/McuMgrGroup.kt b/mcumgr-core/src/test/java/io/runtime/mcumgr/mock/McuMgrGroup.kt new file mode 100644 index 00000000..4f519251 --- /dev/null +++ b/mcumgr-core/src/test/java/io/runtime/mcumgr/mock/McuMgrGroup.kt @@ -0,0 +1,14 @@ +package io.runtime.mcumgr.mock + +// TODO pull this out of tests in major version release +enum class McuMgrGroup(val value: Int) { + DEFAULT(0), + IMAGE(1), + STATS(2), + CONFIG(3), + LOGS(4), + CRASH(5), + SPLIT(6), + RUN(7), + FS(8) +} diff --git a/mcumgr-core/src/test/java/io/runtime/mcumgr/mock/McuMgrHandler.kt b/mcumgr-core/src/test/java/io/runtime/mcumgr/mock/McuMgrHandler.kt new file mode 100644 index 00000000..ffdc0dbc --- /dev/null +++ b/mcumgr-core/src/test/java/io/runtime/mcumgr/mock/McuMgrHandler.kt @@ -0,0 +1,17 @@ +package io.runtime.mcumgr.mock + +import io.runtime.mcumgr.McuMgrHeader +import io.runtime.mcumgr.response.McuMgrResponse + +interface McuMgrHandler { + fun handle( + header: McuMgrHeader, + payload: ByteArray, + responseType: Class + ): T +} + +interface OverrideHandler: McuMgrHandler { + val groupId: Int + val commandId: Int +} diff --git a/mcumgr-core/src/test/java/io/runtime/mcumgr/mock/McuMgrOperation.kt b/mcumgr-core/src/test/java/io/runtime/mcumgr/mock/McuMgrOperation.kt new file mode 100644 index 00000000..78bf9d0f --- /dev/null +++ b/mcumgr-core/src/test/java/io/runtime/mcumgr/mock/McuMgrOperation.kt @@ -0,0 +1,9 @@ +package io.runtime.mcumgr.mock + +// TODO pull this out of tests in major version release +enum class McuMgrOperation(val value: Int) { + READ(0), + READ_RESPONSE(1), + WRITE(2), + WRITE_RESPONSE(3) +} diff --git a/mcumgr-core/src/test/java/io/runtime/mcumgr/mock/MockMcuMgrResponse.kt b/mcumgr-core/src/test/java/io/runtime/mcumgr/mock/MockMcuMgrResponse.kt new file mode 100644 index 00000000..093e39b0 --- /dev/null +++ b/mcumgr-core/src/test/java/io/runtime/mcumgr/mock/MockMcuMgrResponse.kt @@ -0,0 +1,69 @@ +package io.runtime.mcumgr.mock + +import io.runtime.mcumgr.McuMgrErrorCode +import io.runtime.mcumgr.McuMgrHeader +import io.runtime.mcumgr.McuMgrScheme +import io.runtime.mcumgr.response.McuMgrResponse +import io.runtime.mcumgr.util.CBOR + +/** + * Build a mock error response. + */ +fun buildMockErrorResponse( + errorCode: McuMgrErrorCode, + responseHeader: McuMgrHeader, + responseType: Class, + codeClass: Int = 2, + codeDetail: Int = 5 +): T { + val responsePayload = CBOR.toBytes(McuMgrErrorResponse(errorCode)) + return McuMgrResponse.buildCoapResponse( + McuMgrScheme.COAP_BLE, + responsePayload, + responseHeader.toBytes(), + responsePayload, + codeClass, + codeDetail, + responseType + ) +} + +/** + * Build a mock response. + */ +fun buildMockResponse( + responseHeader: McuMgrHeader, + responsePayload: ByteArray, + responseType: Class, + codeClass: Int = 2, + codeDetail: Int = 5 +): T = McuMgrResponse.buildCoapResponse( + McuMgrScheme.COAP_BLE, + responsePayload, + responseHeader.toBytes(), + responsePayload, + codeClass, + codeDetail, + responseType +) + +/** + * Helper class for building an mcumgr error response. + */ +class McuMgrErrorResponse(errorCode: McuMgrErrorCode): McuMgrResponse() { + init { + rc = errorCode.value() + } +} + +/** + * Return a new mcumgr header with the operation converted to a response. + */ +fun McuMgrHeader.toResponse(): McuMgrHeader { + val newOp = when (op) { + McuMgrOperation.READ.value -> McuMgrOperation.READ_RESPONSE.value + McuMgrOperation.WRITE.value -> McuMgrOperation.WRITE_RESPONSE.value + else -> op + } + return McuMgrHeader(newOp, flags, len, groupId, sequenceNum, commandId) +} diff --git a/mcumgr-core/src/test/java/io/runtime/mcumgr/mock/MockMcuMgrTransport.kt b/mcumgr-core/src/test/java/io/runtime/mcumgr/mock/MockMcuMgrTransport.kt new file mode 100644 index 00000000..164757fe --- /dev/null +++ b/mcumgr-core/src/test/java/io/runtime/mcumgr/mock/MockMcuMgrTransport.kt @@ -0,0 +1,77 @@ +package io.runtime.mcumgr.mock + +import io.runtime.mcumgr.McuMgrCallback +import io.runtime.mcumgr.McuMgrErrorCode +import io.runtime.mcumgr.McuMgrHeader +import io.runtime.mcumgr.McuMgrScheme +import io.runtime.mcumgr.McuMgrTransport +import io.runtime.mcumgr.exception.McuMgrException +import io.runtime.mcumgr.mock.handlers.MockStatsHandler +import io.runtime.mcumgr.response.McuMgrResponse +import io.runtime.mcumgr.util.CBOR +import java.util.concurrent.Executor +import java.util.concurrent.Executors + +class MockMcuMgrTransport( + private val statsHandler: MockStatsHandler? = null, + private val handlerOverrides: List = listOf() +): McuMgrTransport { + + private val executor: Executor = Executors.newSingleThreadExecutor() + + override fun getScheme(): McuMgrScheme { + return McuMgrScheme.COAP_BLE + } + + override fun send(payload: ByteArray, responseType: Class): T { + val rawHeader = CBOR.getObject(payload, "_h", ByteArray::class.java) + val header = McuMgrHeader.fromBytes(rawHeader) + + // Check for handler overrides + handlerOverrides.firstOrNull { handler -> + handler.groupId == header.groupId && handler.commandId == header.commandId + }?.let { + return it.handle(header, payload, responseType) + } + + // Call defaults + return when (header.groupId) { + McuMgrGroup.STATS.value -> statsHandler?.handle(header, payload, responseType) ?: + buildMockErrorResponse(McuMgrErrorCode.NOT_SUPPORTED, header.toResponse(), responseType) + else -> buildMockErrorResponse(McuMgrErrorCode.NOT_SUPPORTED, header.toResponse(), responseType) + } + } + + override fun send( + payload: ByteArray, + responseType: Class, + callback: McuMgrCallback + ) { + executor.execute { + try { + callback.onResponse(send(payload, responseType)) + } catch (mme: McuMgrException) { + callback.onError(mme) + } catch (e: Exception) { + callback.onError(McuMgrException(e)) + } + } + } + + /* + * Unimplemented. + */ + override fun addObserver(observer: McuMgrTransport.ConnectionObserver) = + throw IllegalStateException("Not implemented.") + + override fun removeObserver(observer: McuMgrTransport.ConnectionObserver) = + throw IllegalStateException("Not implemented.") + + override fun release() = + throw IllegalStateException("Not implemented.") + + override fun connect(callback: McuMgrTransport.ConnectionCallback?) = + throw IllegalStateException("Not implemented.") +} + + diff --git a/mcumgr-core/src/test/java/io/runtime/mcumgr/mock/handlers/MockStatsHandler.kt b/mcumgr-core/src/test/java/io/runtime/mcumgr/mock/handlers/MockStatsHandler.kt new file mode 100644 index 00000000..28d04c73 --- /dev/null +++ b/mcumgr-core/src/test/java/io/runtime/mcumgr/mock/handlers/MockStatsHandler.kt @@ -0,0 +1,128 @@ +package io.runtime.mcumgr.mock.handlers + +import io.runtime.mcumgr.McuMgrErrorCode +import io.runtime.mcumgr.McuMgrHeader +import io.runtime.mcumgr.mock.McuMgrErrorResponse +import io.runtime.mcumgr.mock.McuMgrHandler +import io.runtime.mcumgr.mock.buildMockResponse +import io.runtime.mcumgr.mock.toResponse +import io.runtime.mcumgr.response.McuMgrResponse +import io.runtime.mcumgr.response.stat.McuMgrStatListResponse +import io.runtime.mcumgr.response.stat.McuMgrStatResponse +import io.runtime.mcumgr.util.CBOR + +enum class McuMgrStatsCommand(val value: Int) { + READ(0), + LIST (1) +} + +class MockStatsHandler( + private val stats: Map> = allStats +): McuMgrHandler { + + /** + * Default stats to query and return when + */ + companion object DefaultStats { + + const val GROUP1_NAME = "group1" + const val GROUP2_NAME = "group2" + const val GROUP3_NAME = "group3" + + const val stat1Name = "stat1" + const val stat2Name = "stat2" + const val stat3Name = "stat3" + const val stat4Name = "stat4" + const val stat5Name = "stat5" + + const val stat1Value = 1L + const val stat2Value = 2L + const val stat3Value = 3L + const val stat4Value = 4L + const val stat5Value = 5L + + val group1Stats = mapOf( + GROUP1_NAME to mapOf( + stat1Name to stat1Value, + stat2Name to stat2Value, + stat3Name to stat3Value, + stat4Name to stat4Value, + stat5Name to stat5Value + ) + ) + + val group2Stats = mapOf( + GROUP2_NAME to mapOf( + stat1Name to stat1Value, + stat2Name to stat2Value, + stat3Name to stat3Value, + stat4Name to stat4Value, + stat5Name to stat5Value + ) + ) + + val group3Stats = mapOf( + GROUP3_NAME to mapOf( + stat1Name to stat1Value, + stat2Name to stat2Value, + stat3Name to stat3Value, + stat4Name to stat4Value, + stat5Name to stat5Value + ) + ) + + private val allStats = group1Stats + group2Stats + group3Stats + } + + /** + * Handle a request for the stats group + */ + override fun handle( + header: McuMgrHeader, + payload: ByteArray, + responseType: Class + ): T { + return when (header.commandId) { + McuMgrStatsCommand.LIST.value -> handleStatsListRequest(header, responseType) + McuMgrStatsCommand.READ.value -> handleStatsReadRequest(header, payload, responseType) + else -> throw IllegalArgumentException("Unimplemented command with ID ${header.commandId}") + } + } + + /** + * Handle a stats list request. + */ + private fun handleStatsListRequest( + header: McuMgrHeader, + responseType: Class + ): T { + val response = McuMgrStatListResponse().apply { + stat_list = stats.keys.toTypedArray() + } + val responsePayload = CBOR.toBytes(response) + return buildMockResponse(header.toResponse(), responsePayload, responseType) + } + + /** + * Handle a stats read request. + */ + private fun handleStatsReadRequest( + header: McuMgrHeader, + payload: ByteArray, + responseType: Class + ): T { + val requestName = CBOR.getString(payload, "name") + val fields = stats[requestName] + val response = if (fields != null) { + McuMgrStatResponse().apply { + this.name = requestName + this.fields = fields + } + } else { + // If the stat group is not found, return an error code. + McuMgrErrorResponse(McuMgrErrorCode.IN_VALUE) + } + val responsePayload = CBOR.toBytes(response) + return buildMockResponse(header.toResponse(), responsePayload, responseType) + } +} diff --git a/sample/build.gradle b/sample/build.gradle index 5f4c6dcb..d04ec004 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -29,25 +29,25 @@ android { } dependencies { - implementation 'androidx.appcompat:appcompat:1.1.0-alpha01' - implementation 'androidx.recyclerview:recyclerview:1.1.0-alpha01' + implementation 'androidx.appcompat:appcompat:1.2.0-alpha02' + implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha01' implementation 'androidx.cardview:cardview:1.0.0' - implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2' - implementation 'com.google.android.material:material:1.1.0-alpha02' + implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4' + implementation 'com.google.android.material:material:1.2.0-alpha04' // Lifecycle extensions - implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' + implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' // Butter Knife - implementation 'com.jakewharton:butterknife:9.0.0-rc2' - annotationProcessor 'com.jakewharton:butterknife-compiler:9.0.0-rc2' + implementation 'com.jakewharton:butterknife:10.2.1' + annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.1' // Dagger 2 - implementation 'com.google.dagger:dagger:2.19' - implementation "com.google.dagger:dagger-android:2.19" - implementation "com.google.dagger:dagger-android-support:2.19" - annotationProcessor 'com.google.dagger:dagger-compiler:2.19' - annotationProcessor "com.google.dagger:dagger-android-processor:2.19" + implementation 'com.google.dagger:dagger:2.23' + implementation "com.google.dagger:dagger-android:2.23" + implementation "com.google.dagger:dagger-android-support:2.23" + annotationProcessor 'com.google.dagger:dagger-compiler:2.23' + annotationProcessor "com.google.dagger:dagger-android-processor:2.23" // Brings the new BluetoothLeScanner API to older platforms implementation 'no.nordicsemi.android.support.v18:scanner:1.1.0' @@ -60,7 +60,7 @@ dependencies { implementation project(':mcumgr-ble') // Test - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' - androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + androidTestImplementation 'androidx.test:runner:1.2.0' testImplementation 'junit:junit:4.12' }