diff --git a/corellium/adapter/build.gradle.kts b/corellium/adapter/build.gradle.kts new file mode 100644 index 0000000000..2f642fd33d --- /dev/null +++ b/corellium/adapter/build.gradle.kts @@ -0,0 +1,19 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin(Plugins.Kotlin.PLUGIN_JVM) +} + +repositories { + mavenCentral() + maven(url = "https://kotlin.bintray.com/kotlinx") +} + +tasks.withType { kotlinOptions.jvmTarget = "1.8" } + +dependencies { + implementation(project(":corellium:api")) + implementation(project(":corellium:client")) + implementation(Dependencies.KOTLIN_COROUTINES_CORE) + testImplementation(Dependencies.JUNIT) +} diff --git a/corellium/adapter/src/main/kotlin/flank/corellium/Api.kt b/corellium/adapter/src/main/kotlin/flank/corellium/Api.kt new file mode 100644 index 0000000000..502d61a394 --- /dev/null +++ b/corellium/adapter/src/main/kotlin/flank/corellium/Api.kt @@ -0,0 +1,18 @@ +package flank.corellium + +import flank.corellium.adapter.ExecuteAndroidTestPlan +import flank.corellium.adapter.InstallAndroidApps +import flank.corellium.adapter.InvokeAndroidDevices +import flank.corellium.adapter.RequestAuthorization +import flank.corellium.api.CorelliumApi + +fun corelliumApi( + projectName: String +) = CorelliumApi( + authorize = RequestAuthorization, + installAndroidApps = InstallAndroidApps( + projectName = projectName + ), + invokeAndroidDevices = InvokeAndroidDevices, + executeTest = ExecuteAndroidTestPlan +) diff --git a/corellium/adapter/src/main/kotlin/flank/corellium/adapter/ClientReference.kt b/corellium/adapter/src/main/kotlin/flank/corellium/adapter/ClientReference.kt new file mode 100644 index 0000000000..47438f8a89 --- /dev/null +++ b/corellium/adapter/src/main/kotlin/flank/corellium/adapter/ClientReference.kt @@ -0,0 +1,11 @@ +package flank.corellium.adapter + +import flank.corellium.client.core.Corellium + +internal val corellium: Corellium + get() = requireNotNull(corelliumRef) { + "Corellium is not initialized, try to call connectCorellium at first." + } + +// It's totally ok to keep corellium as singleton since we don't need handle more than one connection for single run. +internal var corelliumRef: Corellium? = null diff --git a/corellium/adapter/src/main/kotlin/flank/corellium/adapter/ExecuteAndroidTestPlan.kt b/corellium/adapter/src/main/kotlin/flank/corellium/adapter/ExecuteAndroidTestPlan.kt new file mode 100644 index 0000000000..2a1c255950 --- /dev/null +++ b/corellium/adapter/src/main/kotlin/flank/corellium/adapter/ExecuteAndroidTestPlan.kt @@ -0,0 +1,57 @@ +package flank.corellium.adapter + +import flank.corellium.api.AndroidTestPlan +import flank.corellium.client.console.clear +import flank.corellium.client.console.flowLogs +import flank.corellium.client.console.sendCommand +import flank.corellium.client.console.waitForIdle +import flank.corellium.client.core.connectConsole +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch + +object ExecuteAndroidTestPlan : AndroidTestPlan.Execute { + override suspend fun AndroidTestPlan.Config.invoke(): Flow = + coroutineScope { + channelFlow { + instances.map { (instanceId, shards: List) -> + println("Getting console $instanceId") + launch { + corellium.connectConsole(instanceId).run { + clear() + launch { + shards.forEach { shard -> + val command = shard.prepareRunCommand() + println("Sending command: $command") + sendCommand(command) + } + } + launch { + flowLogs().collect { + channel.send(it) + } + } + waitForIdle(10_000) + } + } + }.joinAll() + } + } +} + +private fun AndroidTestPlan.Shard.prepareRunCommand(): String { + val base = "am instrument -r -w " + + val testCases = testCases + // group test cases by filter type - [class, package] + .map { it.split(" ") }.groupBy({ it.first() }, { it.last() }).toList() + // build test cases string + .joinToString("") { (type, tests) -> "-e $type ${tests.joinToString(",")} " } + + val runner = "$packageName/$testRunner" + + return base + testCases + runner +} diff --git a/corellium/adapter/src/main/kotlin/flank/corellium/adapter/InstallAndroidApps.kt b/corellium/adapter/src/main/kotlin/flank/corellium/adapter/InstallAndroidApps.kt new file mode 100644 index 0000000000..330d91241b --- /dev/null +++ b/corellium/adapter/src/main/kotlin/flank/corellium/adapter/InstallAndroidApps.kt @@ -0,0 +1,37 @@ +package flank.corellium.adapter + +import flank.corellium.api.AndroidApps +import flank.corellium.client.agent.uploadFile +import flank.corellium.client.console.sendCommand +import flank.corellium.client.core.connectAgent +import flank.corellium.client.core.connectConsole +import flank.corellium.client.core.getAllProjects +import flank.corellium.client.core.getProjectInstancesList +import java.io.File + +private const val PATH_TO_UPLOAD = "/sdcard" + +class InstallAndroidApps( + private val projectName: String +) : AndroidApps.Install { + + override suspend fun List.invoke() { + val corellium = corellium + val projectId = corellium.getAllProjects().first { it.name == projectName }.id + val instances = corellium.getProjectInstancesList(projectId).associateBy { it.id } + + forEach { apps -> + val instance = instances.getValue(apps.instanceId) + val agent = corellium.connectAgent(instance.agent!!.info) + val console = corellium.connectConsole(instance.id) + + apps.paths.forEach { localPath -> + val file = File(localPath) + val remotePath = "$PATH_TO_UPLOAD/${file.name}" + + agent.uploadFile(remotePath, file.readBytes()) + console.sendCommand("pm install $remotePath") + } + } + } +} diff --git a/corellium/adapter/src/main/kotlin/flank/corellium/adapter/InvokeAndroidDevices.kt b/corellium/adapter/src/main/kotlin/flank/corellium/adapter/InvokeAndroidDevices.kt new file mode 100644 index 0000000000..d25956f9b2 --- /dev/null +++ b/corellium/adapter/src/main/kotlin/flank/corellium/adapter/InvokeAndroidDevices.kt @@ -0,0 +1,8 @@ +package flank.corellium.adapter + +import flank.corellium.api.AndroidInstance +import flank.corellium.api.AndroidInstance.Config + +object InvokeAndroidDevices : AndroidInstance.Invoke { + override suspend fun Config.invoke(): List = TODO() // https://github.com/flank/flank/issues/1837 +} diff --git a/corellium/adapter/src/main/kotlin/flank/corellium/adapter/RequestAuthorization.kt b/corellium/adapter/src/main/kotlin/flank/corellium/adapter/RequestAuthorization.kt new file mode 100644 index 0000000000..74e3014478 --- /dev/null +++ b/corellium/adapter/src/main/kotlin/flank/corellium/adapter/RequestAuthorization.kt @@ -0,0 +1,14 @@ +package flank.corellium.adapter + +import flank.corellium.api.Authorization +import flank.corellium.client.core.connectCorellium + +object RequestAuthorization : Authorization.Request { + override suspend fun Authorization.Credentials.invoke() { + corelliumRef = connectCorellium( + api = host, + username = username, + password = password + ) + } +} diff --git a/corellium/adapter/src/test/kotlin/flank/corellium/adapter/AdaptersExample.kt b/corellium/adapter/src/test/kotlin/flank/corellium/adapter/AdaptersExample.kt new file mode 100644 index 0000000000..e9eff73c7b --- /dev/null +++ b/corellium/adapter/src/test/kotlin/flank/corellium/adapter/AdaptersExample.kt @@ -0,0 +1,74 @@ +package flank.corellium.adapter + +import flank.corellium.api.AndroidTestPlan +import flank.corellium.api.Authorization +import flank.corellium.api.invoke +import flank.corellium.client.core.getAllProjects +import flank.corellium.client.core.getProjectInstancesList +import flank.corellium.corelliumApi +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.runBlocking + +private const val PROJECT_NAME = "Default Project" + +// The credentials are not provided along with the code. +// If you need to execute example you have to deliver credentials on your own. +private val credentials = Authorization.Credentials( + host = TODO(), + username = TODO(), + password = TODO(), +) + +private val androidTestPlanConfig = AndroidTestPlan.Config( + instances = mapOf( + "d8ae09fe-a60a-480a-968f-1a30d77a0e11" to listOf( + AndroidTestPlan.Shard( + packageName = "com.example.test_app.test", + testRunner = "androidx.test.runner.AndroidJUnitRunner", + testCases = listOf( + "class com.example.test_app.InstrumentedTest#test0", + "class com.example.test_app.InstrumentedTest#test1", + ) + ), + AndroidTestPlan.Shard( + packageName = "com.example.test_app.test", + testRunner = "androidx.test.runner.AndroidJUnitRunner", + testCases = listOf( + "class com.example.test_app.InstrumentedTest#test2", + ) + ), + ), + "b7a305e5-b199-4ed6-9ba7-4c9b83c6762e" to listOf( + AndroidTestPlan.Shard( + packageName = "com.example.test_app.test", + testRunner = "androidx.test.runner.AndroidJUnitRunner", + testCases = listOf( + "class com.example.test_app.foo.FooInstrumentedTest#testFoo", + ) + ) + ) + ) +) + +fun main() { + val api = corelliumApi(PROJECT_NAME) + + runBlocking { + + println("* Authorizing") + api.authorize(credentials) + + corellium.run { + getProjectInstancesList(getAllProjects().first { it.name == PROJECT_NAME }.id) + }.forEach { + println(it) + } + + println("* Executing tests") + api.executeTest(androidTestPlanConfig).collect { line -> + print(line) + } + println() + println("* Finish") + } +} diff --git a/corellium/shard/README.md b/corellium/shard/README.md index 9948281ac7..7b96b66170 100644 --- a/corellium/shard/README.md +++ b/corellium/shard/README.md @@ -8,7 +8,7 @@ Depending on the provided test cases duration, the sharding algorithm will: ## Diagram -![sharding_class_diagram](http://www.plantuml.com/plantuml/proxy?cache=no&fmt=svg&src=https://raw.githubusercontent.com/Flank/flank/1801_Multi-module_sharding_algorithm/docs/corellium/sharding-class.puml) +![sharding_class_diagram](http://www.plantuml.com/plantuml/proxy?cache=no&fmt=svg&src=https://raw.githubusercontent.com/Flank/flank/master/docs/corellium/sharding-class.puml) ## Example diff --git a/settings.gradle.kts b/settings.gradle.kts index 607c8f6a79..0fa5038cf0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,6 +16,7 @@ include( ":corellium:log", ":corellium:api", ":corellium:shard", + ":corellium:adapter", ) plugins {