Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add structural logging #2032

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,10 @@ import kotlinx.coroutines.launch

val executeAndroidTestPlan = AndroidTestPlan.Execute { config ->
config.instances.map { (instanceId, commands: List<String>) ->
channelFlow<String> {
println("Getting console $instanceId")
instanceId to channelFlow<String> {
corellium.connectConsole(instanceId).apply {
clear()
launch {
commands.forEach { string ->
println("Sending command: $string")
sendCommand(string)
}
}
launch { commands.forEach { string -> sendCommand(string) } }
launch { flowLogs().collect(channel::send) }
waitForIdle(10_000)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package flank.corellium.adapter

import flank.corellium.api.AndroidApps
import flank.corellium.api.AndroidApps.Event.Apk
import flank.corellium.api.AndroidApps.Event.Connecting
import flank.corellium.client.agent.disconnect
import flank.corellium.client.agent.uploadFile
import flank.corellium.client.console.close
Expand All @@ -9,6 +11,9 @@ 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 kotlinx.coroutines.Job
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
Expand All @@ -19,20 +24,30 @@ private const val PATH_TO_UPLOAD = "/sdcard"
fun installAndroidApps(
projectName: String
) = AndroidApps.Install { apps ->
channelFlow<AndroidApps.Event> {
install(projectName, apps).join()
}
}

private suspend fun SendChannel<AndroidApps.Event>.install(
projectName: String,
appsList: List<AndroidApps>
): Job =
corellium.launch {
val projectId = corellium.getAllProjects().first { it.name == projectName }.id
val instances = corellium.getProjectInstancesList(projectId).associateBy { it.id }

apps.forEach { apps ->
appsList.forEach { apps ->
val instance = instances.getValue(apps.instanceId)

println("Connecting agent for ${apps.instanceId}")
send(Connecting.Agent(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 = corellium.connectAgent(agentInfo)

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

// Disable system logging
Expand All @@ -42,10 +57,10 @@ fun installAndroidApps(
val file = File(localPath)
val remotePath = "$PATH_TO_UPLOAD/${file.name}"

println("Uploading apk $localPath")
send(Apk.Uploading(localPath))
agent.uploadFile(remotePath, file.readBytes())

println("Installing apk $localPath")
send(Apk.Installing(localPath))
console.sendCommand(
// Current solution is enough for the MVP.
// Fixme: Find better solution for recognizing test apk.
Expand All @@ -59,4 +74,3 @@ fun installAndroidApps(
agent.disconnect()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package flank.corellium.adapter

import flank.corellium.api.AndroidInstance
import flank.corellium.api.AndroidInstance.Event
import flank.corellium.client.core.createNewInstance
import flank.corellium.client.core.getAllProjects
import flank.corellium.client.core.getProjectInstancesList
Expand All @@ -22,7 +23,7 @@ private const val SCREEN = "720x1280:280"
fun invokeAndroidDevices(
projectName: String,
) = AndroidInstance.Invoke { config ->
channelFlow<String> {
channelFlow<Event> {
val projectId = getProjectId(projectName)
val instances = getCreatedInstances(projectId, config.amount)
startNotRunningInstances(instances)
Expand All @@ -42,14 +43,16 @@ fun invokeAndroidDevices(
)
}

waitForInstances(channel, ids)
waitForInstances(ids)
}
}

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

private typealias OutputChannel = SendChannel<Event>

/**
* @return The project id for given project name.
*/
Expand All @@ -60,39 +63,39 @@ private suspend fun getProjectId(name: String) =
* Get all instances that was already created for flank.
* @return [List] of [Instance] where [List.size] <= [amount]
*/
private suspend fun getCreatedInstances(
private suspend fun OutputChannel.getCreatedInstances(
projectId: String,
amount: Int
): List<Instance> = corellium
.also { println("Getting instances already created by flank.") }
.also { send(Event.GettingAlreadyCreated) }
.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") }
.apply { send(Event.Obtained(size)) }

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

/**
* Create new instances basing on given indexes.
*/
private suspend fun createInstances(
private suspend fun OutputChannel.createInstances(
projectId: String,
indexes: List<Int>,
gpuAcceleration: Boolean,
) = indexes
.apply { println("Creating additional ${indexes.size} instances. Connecting to the agents may take longer.") }
.apply { send(Event.Creating(size)) }
.map { index ->
corellium.createNewInstance(
Instance(
Expand Down Expand Up @@ -131,17 +134,15 @@ private val Instance.index get() = name.removePrefix(FLANK_INSTANCE_NAME_PREFIX)
/**
* Block the execution and wait until each instance change the status to "on".
*/
private suspend fun waitForInstances(
channel: SendChannel<String>,
private suspend fun OutputChannel.waitForInstances(
ids: List<String>
) = coroutineScope {
println("Wait until all instances are ready...")
send(Event.Waiting)
ids.map { id ->
launch {
corellium.waitUntilInstanceIsReady(id)
println("ready: $id")
channel.send(id)
send(Event.Ready(id))
}
}.joinAll()
println("All instances invoked and ready to use.")
send(Event.AllReady)
}
16 changes: 14 additions & 2 deletions corellium/api/src/main/kotlin/flank/corellium/api/AndroidApps.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package flank.corellium.api

import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow

/**
* [AndroidApps] represents a bunch of apk files related to the testing instance.
Expand All @@ -16,5 +16,17 @@ data class AndroidApps(
/**
* Install android apps on the specified instances.
*/
fun interface Install : (List<AndroidApps>) -> Job
fun interface Install : (List<AndroidApps>) -> Flow<Event>

sealed class Event {
object Connecting {
data class Agent(val instanceId: String) : Event()
data class Console(val instanceId: String) : Event()
}

object Apk {
data class Uploading(val path: String) : Event()
data class Installing(val path: String) : Event()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,16 @@ object AndroidInstance {
*
* @return List of invoked device ids.
*/
fun interface Invoke : (Config) -> Flow<String>
fun interface Invoke : (Config) -> Flow<Event>

sealed class Event {
object GettingAlreadyCreated : Event()
class Obtained(val size: Int) : Event()
class Starting(val size: Int) : Event()
class Started(val id: String, val name: String) : Event()
class Creating(val size: Int) : Event()
object Waiting : Event()
class Ready(val id: String) : Event()
object AllReady : Event()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ object AndroidTestPlan {
/**
* Execute tests on android instances using specified configuration.
*/
fun interface Execute : (Config) -> List<Flow<AmInstrumentOutputLine>>
fun interface Execute : (Config) -> List<Pair<InstanceId, Flow<AmInstrumentOutputLine>>>
}

private typealias InstanceId = String
Expand Down
6 changes: 3 additions & 3 deletions corellium/cli/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ tasks.withType<KotlinCompile> { kotlinOptions.jvmTarget = "1.8" }

dependencies {
implementation(Dependencies.KOTLIN_COROUTINES_CORE)
implementation(Dependencies.KOTLIN_REFLECT)
implementation(Dependencies.PICOCLI)
implementation(project(":corellium:api"))
implementation(project(":corellium:domain"))
implementation(project(":corellium:adapter"))
implementation(project(":tool:apk"))
implementation(project(":tool:junit"))
implementation(project(":tool:log:format"))
implementation(Dependencies.JACKSON_KOTLIN)
implementation(Dependencies.JACKSON_YAML)
implementation(Dependencies.JACKSON_XML)
testImplementation(Dependencies.JUNIT)
testImplementation(Dependencies.MOCKK)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
import flank.apk.Apk
import flank.corellium.api.AndroidApps
import flank.corellium.api.AndroidInstance
import flank.corellium.cli.RunTestCorelliumAndroidCommand.Config
import flank.corellium.cli.util.ConfigMap
import flank.corellium.cli.util.emptyConfigMap
Expand All @@ -12,8 +14,25 @@ import flank.corellium.cli.util.merge
import flank.corellium.corelliumApi
import flank.corellium.domain.RunTestCorelliumAndroid
import flank.corellium.domain.RunTestCorelliumAndroid.Args
import flank.corellium.domain.RunTestCorelliumAndroid.Authorize
import flank.corellium.domain.RunTestCorelliumAndroid.CleanUp
import flank.corellium.domain.RunTestCorelliumAndroid.CompleteTests
import flank.corellium.domain.RunTestCorelliumAndroid.DumpShards
import flank.corellium.domain.RunTestCorelliumAndroid.ExecuteTests
import flank.corellium.domain.RunTestCorelliumAndroid.GenerateReport
import flank.corellium.domain.RunTestCorelliumAndroid.InstallApks
import flank.corellium.domain.RunTestCorelliumAndroid.InvokeDevices
import flank.corellium.domain.RunTestCorelliumAndroid.LoadPreviousDurations
import flank.corellium.domain.RunTestCorelliumAndroid.OutputDir
import flank.corellium.domain.RunTestCorelliumAndroid.ParseApkInfo
import flank.corellium.domain.RunTestCorelliumAndroid.ParseTestCases
import flank.corellium.domain.RunTestCorelliumAndroid.PrepareShards
import flank.corellium.domain.invoke
import flank.instrument.log.Instrument
import flank.junit.JUnit
import flank.log.Event.Start
import flank.log.buildFormatter
import flank.log.output
import picocli.CommandLine

@CommandLine.Command(
Expand Down Expand Up @@ -134,12 +153,14 @@ class RunTestCorelliumAndroidCommand :

override val api by lazy { corelliumApi(config.project!!) }

override val apk = Apk.Api()
override val apk by lazy { Apk.Api() }

override val junit = JUnit.Api()
override val junit by lazy { JUnit.Api() }

override val args by lazy { createArgs() }

override val out by lazy { format.output }

override fun run() = invoke()
}

Expand All @@ -166,3 +187,53 @@ private fun RunTestCorelliumAndroidCommand.createArgs() = Args(
gpuAcceleration = config.gpuAcceleration!!,
scanPreviousDurations = config.scanPreviousDurations!!,
)

internal val format = buildFormatter<String> {

Start(Authorize) { "* Authorizing" }
Start(CleanUp) { "* Cleaning instances" }
Start(OutputDir) { "* Preparing output directory" }
Start(DumpShards) { "* Dumping shards" }
Start(ExecuteTests) { "* Executing tests" }
Start(CompleteTests) { "* Finish" }
Start(GenerateReport) { "* Generating report" }
Start(InstallApks) { "* Installing apks" }
Start(InvokeDevices) { "* Invoking devices" }
Start(LoadPreviousDurations) { "* Obtaining previous test cases durations" }
Start(ParseApkInfo) { "* Parsing apk info" }
Start(ParseTestCases) { "* Parsing test cases" }
Start(PrepareShards) { "* Calculating shards" }

LoadPreviousDurations.Searching { "Searching in $this JUnitReport.xml files..." }
LoadPreviousDurations.Summary::class { "For $required test cases, found $matching matching and $unknown unknown" }
InstallApks.Status {
when (this) {
is AndroidApps.Event.Connecting.Agent -> "Connecting agent for $instanceId"
is AndroidApps.Event.Connecting.Console -> "Connecting console for $instanceId"
is AndroidApps.Event.Apk.Uploading -> "Uploading apk $path"
is AndroidApps.Event.Apk.Installing -> "Installing apk $path"
}
}
InvokeDevices.Status {
when (this) {
is AndroidInstance.Event.GettingAlreadyCreated -> "Getting instances already created by flank."
is AndroidInstance.Event.Obtained -> "Obtained $size already created devices"
is AndroidInstance.Event.Starting -> "Starting not running $size instances."
is AndroidInstance.Event.Started -> "$id - $name"
is AndroidInstance.Event.Creating -> "Creating additional $size instances. Connecting to the agents may take longer."
is AndroidInstance.Event.Waiting -> "Wait until all instances are ready..."
is AndroidInstance.Event.Ready -> "ready: $id"
is AndroidInstance.Event.AllReady -> "All instances invoked and ready to use."
}
}
ExecuteTests.Status::class {
when (val status = status) {
is Instrument.Status -> "$id: " + status.details.run { "$className#$testName" } + " - " + status.code
else -> null
}
}
RunTestCorelliumAndroid.Created { "Created $path" }
RunTestCorelliumAndroid.AlreadyExist { "Already exist $path" }

match { it as? String } to { this }
}
Loading