Skip to content

Commit

Permalink
Add implementation of InvokeAndroidDevices
Browse files Browse the repository at this point in the history
Fix InstallAndroidApps
Update Example.kt
  • Loading branch information
jan-goral committed Apr 29, 2021
1 parent 20c2300 commit 372ac24
Show file tree
Hide file tree
Showing 15 changed files with 353 additions and 124 deletions.
1 change: 1 addition & 0 deletions corellium/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**/*.properties
1 change: 1 addition & 0 deletions corellium/adapter/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ plugins {
}

repositories {
jcenter()
mavenCentral()
maven(url = "https://kotlin.bintray.com/kotlinx")
}
Expand Down
4 changes: 3 additions & 1 deletion corellium/adapter/src/main/kotlin/flank/corellium/Api.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ fun corelliumApi(
installAndroidApps = InstallAndroidApps(
projectName = projectName
),
invokeAndroidDevices = InvokeAndroidDevices,
invokeAndroidDevices = InvokeAndroidDevices(
projectName = projectName
),
executeTest = ExecuteAndroidTestPlan
)
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,7 @@ object ExecuteAndroidTestPlan : AndroidTestPlan.Execute {
sendCommand(command)
}
}
launch {
flowLogs().collect {
channel.send(it)
}
}
launch { flowLogs().collect(channel::send) }
waitForIdle(10_000)
}
}
Expand All @@ -43,15 +39,18 @@ object ExecuteAndroidTestPlan : AndroidTestPlan.Execute {
}

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 testCases = testCases // example: listOf("class foo.Bar#baz")
.map { it.split(" ") } // example: listOf(listOf("class", "foo.Bar#baz"))
.groupBy({ it.first() }, { it.last() }) // example: first => "class", last => "foo.Bar#baz"
.toList().joinToString("") { (type, tests: List<String>) ->
"-e $type ${tests.joinToString(",")} " // example: "-e class foo.Bar#baz"
} // example: "-e class foo.Bar#baz1,foo.Bar#baz2 -e package foo.test "

val runner = "$packageName/$testRunner"

return base + testCases + runner
// example: "am instrument -r -w -e class foo.Bar#baz foo.test/androidx.test.runner.AndroidJUnitRunner"
return AM_INSTRUMENT + testCases + runner
}

private const val AM_INSTRUMENT = "am instrument -r -w "
Original file line number Diff line number Diff line change
@@ -1,37 +1,62 @@
package flank.corellium.adapter

import flank.corellium.api.AndroidApps
import flank.corellium.client.agent.disconnect
import flank.corellium.client.agent.uploadFile
import flank.corellium.client.console.close
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
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flowOf

private const val PATH_TO_UPLOAD = "/sdcard"

class InstallAndroidApps(
private val projectName: String
) : AndroidApps.Install {

override suspend fun List<AndroidApps>.invoke() {
val corellium = corellium
val projectId = corellium.getAllProjects().first { it.name == projectName }.id
val instances = corellium.getProjectInstancesList(projectId).associateBy { it.id }
override suspend fun List<AndroidApps>.invoke(): Unit = corellium.run {
val projectId = getAllProjects().first { it.name == projectName }.id
val instances = 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)

println("Connecting agent for ${apps.instanceId}")
val agentInfo = requireNotNull(instance.agent?.info) {
"Cannot connect to the agent, no agent info for instance ${instance.name} with id: ${instance.id}"
}
val agent = connectAgent(agentInfo)

println("Connecting console for ${apps.instanceId}")
val console = connectConsole(instance.id)

// Disable system logging
flowOf("su", "dmesg -n 1", "exit").collect(console::sendCommand)

apps.paths.forEach { localPath ->
val file = File(localPath)
val remotePath = "$PATH_TO_UPLOAD/${file.name}"

println("Uploading apk $localPath")
agent.uploadFile(remotePath, file.readBytes())
console.sendCommand("pm install $remotePath")

println("Installing apk $localPath")
console.sendCommand(
// Current solution is enough for the MVP.
// Fixme: Find better solution for recognizing test apk.
if (localPath.endsWith("androidTest.apk"))
"pm install -t $remotePath" else
"pm install $remotePath"
)
}

console.close()
agent.disconnect()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,136 @@ package flank.corellium.adapter

import flank.corellium.api.AndroidInstance
import flank.corellium.api.AndroidInstance.Config
import flank.corellium.client.core.createNewInstance
import flank.corellium.client.core.getAllProjects
import flank.corellium.client.core.getProjectInstancesList
import flank.corellium.client.core.startInstance
import flank.corellium.client.core.waitUntilInstanceIsReady
import flank.corellium.client.data.Instance
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch

object InvokeAndroidDevices : AndroidInstance.Invoke {
override suspend fun Config.invoke(): List<String> = TODO() // https://github.com/flank/flank/issues/1837
private const val FLANK_INSTANCE_NAME_PREFIX = "flank-android-"
private const val FLAVOUR = "ranchu"
private const val OS = "11.0.0"
private const val SCREEN = "720x1280:280"

class InvokeAndroidDevices(
private val projectName: String,
) : AndroidInstance.Invoke {
override suspend fun Config.invoke(): List<String> = coroutineScope {
val projectId = getProjectId(projectName)
val instances = getCreatedInstances(projectId, amount)
startNotRunningInstances(instances)

val ids = instances.map(Instance::id) + let {
// When existing instances size match required amount
// there is not needs for creating more instances.
if (instances.size == amount) emptyList()
// Otherwise is required to create some additional instances
else createInstances(
projectId = projectId,
indexes = calculateAdditionalIndexes(
current = instances,
requiredAmount = amount
)
)
}

waitForInstances(ids)
ids
}
}

// Important!!!
// Try to keep the private methods not dependent on each other.
// Otherwise the implementation will go complicated.

/**
* @return The project id for given project name.
*/
private suspend fun getProjectId(name: String) =
corellium.getAllProjects().first { it.name == name }.id

/**
* Get all instances that was already created for flank.
* @return [List] of [Instance] where [List.size] <= [amount]
*/
private suspend fun getCreatedInstances(
projectId: String,
amount: Int
): List<Instance> = corellium
.also { println("Getting instances already created by flank.") }
.getProjectInstancesList(projectId)
.filter { it.name.startsWith(FLANK_INSTANCE_NAME_PREFIX) }
.filter { it.state !in Instance.State.unavailable }
.take(amount)
.apply { println("Obtained $size already created devices") }

/**
* Start all given instances with status different than "on".
*/
private suspend fun startNotRunningInstances(
instances: List<Instance>
): Unit = instances
.filter { it.state != "on" }
.apply { if (isNotEmpty()) println("Starting not running $size instances.") }
.forEach { instance ->
corellium.startInstance(instance.id)
println(instance)
}

/**
* Create new instances basing on given indexes.
*/
private suspend fun createInstances(
projectId: String,
indexes: List<Int>
) = indexes
.apply { println("Creating additional ${indexes.size} instances. Connecting to the agents may take longer.") }
.map { index ->
corellium.createNewInstance(
Instance(
project = projectId,
name = FLANK_INSTANCE_NAME_PREFIX + index,
flavor = FLAVOUR,
os = OS,
bootOptions = Instance.BootOptions(
screen = SCREEN
)
)
)
}

/**
* Calculate the list indexes for additional instances to create.
*/
private fun calculateAdditionalIndexes(
current: List<Instance>,
requiredAmount: Int,
): List<Int> =
(0 until requiredAmount).toSet() // Create set of possible indexes starting from 0
.minus(current.map(Instance::index)) // Subtract already created indexes
.sorted()
.take(requiredAmount - current.size) // Skip the excess indexes

/**
* The index encoded in instance name.
* This will work only on devices created by flank.
*/
private val Instance.index get() = name.removePrefix(FLANK_INSTANCE_NAME_PREFIX).toInt()

/**
* Block the execution and wait until each instance change the status to "on".
*/
private suspend fun waitForInstances(ids: List<String>): Unit = coroutineScope {
println("Wait until all instances are ready...")
ids.map { id ->
launch {
corellium.waitUntilInstanceIsReady(id)
println("ready: $id")
}
}.joinAll()
println("All instances invoked and ready to use.")
}

This file was deleted.

Loading

0 comments on commit 372ac24

Please sign in to comment.