Skip to content

Commit

Permalink
Add implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
jan-goral committed Jul 20, 2021
1 parent a033c92 commit 05d5f58
Show file tree
Hide file tree
Showing 14 changed files with 333 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import flank.corellium.api.CorelliumApi
import flank.corellium.domain.RunTestCorelliumAndroid.Args.DefaultOutputDir
import flank.corellium.domain.RunTestCorelliumAndroid.Args.DefaultOutputDir.new
import flank.corellium.domain.run.test.android.step.authorize
import flank.corellium.domain.run.test.android.step.availableDevices
import flank.corellium.domain.run.test.android.step.createOutputDir
import flank.corellium.domain.run.test.android.step.dispatchShards
import flank.corellium.domain.run.test.android.step.dispatchTests
import flank.corellium.domain.run.test.android.step.dumpShards
import flank.corellium.domain.run.test.android.step.executeTests
import flank.corellium.domain.run.test.android.step.finish
Expand All @@ -25,7 +28,9 @@ import flank.exection.parallel.type
import flank.instrument.log.Instrument
import flank.junit.JUnit
import flank.log.Event
import flank.shard.InstanceShard
import flank.shard.Shard
import kotlinx.coroutines.channels.Channel
import java.io.File
import java.lang.System.currentTimeMillis
import java.text.SimpleDateFormat
Expand Down Expand Up @@ -127,6 +132,8 @@ object RunTestCorelliumAndroid {
val junit by !type<JUnit.Api>()
val args by !Args

val dispatch by -DispatchShards
val devices by -AvailableDevices
val testCases: Map<String, List<String>> by -ParseTestCases
val previousDurations: Map<String, Long> by -LoadPreviousDurations
val shards: List<List<Shard.App>> by -PrepareShards
Expand All @@ -152,7 +159,9 @@ object RunTestCorelliumAndroid {
object ParseApkInfo : Parallel.Type<Info>
object OutputDir : Parallel.Type<Unit>
object DumpShards : Parallel.Type<Unit>
object DispatchShards : Parallel.Type<Channel<InstanceShard>>
object Authorize : Parallel.Type<Unit>
object AvailableDevices : Parallel.Type<Channel<Device.Instance>>
object InvokeDevices : Parallel.Type<List<String>> {
object Status : Event.Type<AndroidInstance.Event>
}
Expand All @@ -174,6 +183,10 @@ object RunTestCorelliumAndroid {
) : Event.Data
}

object HandleResults : Parallel.Type<Unit> {
object Init : Parallel.Type<Channel<ExecuteTests.Status>>
}

object CleanUp : Parallel.Type<Unit>
object GenerateReport : Parallel.Type<Unit>
object CompleteTests : Parallel.Type<Unit>
Expand All @@ -190,10 +203,46 @@ object RunTestCorelliumAndroid {
object Created : Event.Type<File>
object AlreadyExist : Event.Type<File>

// Nested

/**
* Nested scope that represents shard execution on single device
*/
object Device {

internal class Context : Parallel.Context() {
val api by !type<CorelliumApi>()
val args by !Args
val info by !ParseApkInfo
val shard by !Shard
val instance by !Instance
}

internal val context = Parallel.Function(::Context)

object Shard : Parallel.Type<InstanceShard>

data class Instance(
val id: String,
val apks: Set<String> = emptySet()
) {
companion object : Parallel.Type<Instance>
}

internal val execute by lazy {
setOf(
flank.corellium.domain.run.test.android.device.task.installApks,
flank.corellium.domain.run.test.android.device.task.executeTests,
)
}
}

// Execution tasks

// Evaluate lazy to avoid strange NullPointerException.
val execute by lazy {
val execute get() = executeV2

private val executeV1 by lazy {
setOf(
context.validate,
authorize,
Expand All @@ -210,4 +259,25 @@ object RunTestCorelliumAndroid {
prepareShards,
)
}

private val executeV2 by lazy {
setOf(
context.validate,
authorize,
createOutputDir,
dispatchShards,
dispatchTests,
dumpShards,
finish,
generateReport,
installApks,
dispatchTests,
availableDevices,
invokeDevices,
loadPreviousDurations,
parseApksInfo,
parseTestCasesFromApks,
prepareShards,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package flank.corellium.domain.run.test.android.device.task

import flank.corellium.api.AndroidTestPlan
import flank.corellium.domain.RunTestCorelliumAndroid
import flank.corellium.domain.RunTestCorelliumAndroid.Authorize
import flank.corellium.domain.RunTestCorelliumAndroid.Device
import flank.corellium.domain.RunTestCorelliumAndroid.ExecuteTests
import flank.corellium.domain.RunTestCorelliumAndroid.InstallApks
import flank.corellium.domain.RunTestCorelliumAndroid.ParseApkInfo
import flank.exection.parallel.from
import flank.exection.parallel.using
import flank.instrument.command.formatAmInstrumentCommand
import flank.instrument.log.Instrument
import flank.instrument.log.parseAdbInstrumentLog
import flank.log.Output
import flank.shard.Shard
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.take
import java.io.File

/**
* The step is executing tests on previously invoked devices, and returning the test results.
*
* The side effect is console logs from `am instrument` saved inside [RunTestCorelliumAndroid.ExecuteTests.ADB_LOG] output subdirectory.
*
* The optional parsing errors are sent through [RunTestCorelliumAndroid.Context.out].
*/
internal val executeTests = ExecuteTests from setOf(
ParseApkInfo,
Authorize,
InstallApks,
) using Device.context {
val outputDir = File(args.outputDir, ExecuteTests.ADB_LOG).apply { mkdir() }
val testPlan: AndroidTestPlan.Config = prepareTestPlan()
coroutineScope {
ExecuteTests.Plan(testPlan).out()
api.executeTest(testPlan).map { (id, flow) ->
async { flow.collectResults(id, shard.size, outputDir, out) }
}.awaitAll()
}
}

/**
* Prepare [AndroidTestPlan.Config] for test execution.
* It is just mapping and formatting the data collected in state.
*/
private fun Device.Context.prepareTestPlan(): AndroidTestPlan.Config =
AndroidTestPlan.Config(
mapOf(
instance.id to shard.flatMap { app: Shard.App ->
app.tests.map { test ->
formatAmInstrumentCommand(
packageName = info.packageNames.getValue(test.name),
testRunner = info.testRunners.getValue(test.name),
testCases = test.cases.map { case -> "class " + case.name }
)
}
}
)
)

private suspend fun Flow<String>.collectResults(
id: String,
expectedResults: Int,
dir: File,
out: Output,
): List<Instrument> {
var read = 0
var parsed = 0
val file = dir.resolve(id)
val results = mutableListOf<Instrument>()
this.onEach { file.appendText(it + "\n") }
.buffer(100)
.flowOn(Dispatchers.IO)
.onEach { ++read }
.parseAdbInstrumentLog()
.onEach { parsed = read }
.onEach { result -> results += result }
.onEach { result -> ExecuteTests.Status(id, result).out() }
.catch { cause -> ExecuteTests.Error(id, cause, file.path, ++parsed..read).out() }
.filterIsInstance<Instrument.Result>()
.take(expectedResults)
.collect()
return results
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package flank.corellium.domain.run.test.android.device.task

import flank.corellium.api.AndroidApps
import flank.corellium.domain.RunTestCorelliumAndroid.Authorize
import flank.corellium.domain.RunTestCorelliumAndroid.Device
import flank.corellium.domain.RunTestCorelliumAndroid.InstallApks
import flank.exection.parallel.from
import flank.exection.parallel.using
import kotlinx.coroutines.flow.collect

/**
* The step is installing required software on android instances.
*/
internal val installApks = InstallApks from setOf(
Authorize
) using Device.context {
val apks = prepareApkToInstall()
api.installAndroidApps(listOf(apks)).collect { event ->
InstallApks.Status(event).out()
}
// If tests will be executed too fast just after the
// app installed, the instrumentation will fail
// delay(4_000) TODO: for verification
}

private fun Device.Context.prepareApkToInstall() =
AndroidApps(
instanceId = instance.id,
paths = shard.flatMap { app -> app.tests.map { test -> test.name } + app.name }
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package flank.corellium.domain.run.test.android.step

import flank.corellium.domain.RunTestCorelliumAndroid
import flank.corellium.domain.RunTestCorelliumAndroid.AvailableDevices
import flank.exection.parallel.from
import flank.exection.parallel.using
import kotlinx.coroutines.channels.Channel

/**
* Prepare channel for providing devices ready to use.
*/
val availableDevices = AvailableDevices from setOf(
RunTestCorelliumAndroid.PrepareShards
) using RunTestCorelliumAndroid.context {
Channel(shards.size)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package flank.corellium.domain.run.test.android.step

import flank.corellium.domain.RunTestCorelliumAndroid
import flank.corellium.domain.RunTestCorelliumAndroid.DispatchShards
import flank.exection.parallel.from
import flank.exection.parallel.using
import flank.shard.InstanceShard
import kotlinx.coroutines.channels.Channel

/**
* Prepare channel for dispatching shards and fill the buffer with initial elements.
*/
val dispatchShards = DispatchShards from setOf(
RunTestCorelliumAndroid.PrepareShards
) using RunTestCorelliumAndroid.context {
Channel<InstanceShard>(shards.size).apply {
shards.forEach { shard -> send(shard) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package flank.corellium.domain.run.test.android.step

import flank.corellium.domain.RunTestCorelliumAndroid.Authorize
import flank.corellium.domain.RunTestCorelliumAndroid.AvailableDevices
import flank.corellium.domain.RunTestCorelliumAndroid.Device
import flank.corellium.domain.RunTestCorelliumAndroid.DispatchShards
import flank.corellium.domain.RunTestCorelliumAndroid.ExecuteTests
import flank.corellium.domain.RunTestCorelliumAndroid.ParseApkInfo
import flank.corellium.domain.RunTestCorelliumAndroid.context
import flank.exection.parallel.ParallelState
import flank.exection.parallel.from
import flank.exection.parallel.invoke
import flank.exection.parallel.select
import flank.exection.parallel.using
import flank.exection.parallel.verify
import flank.shard.InstanceShard
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.last
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch

/**
* Dispatch each test shard execution into first best matching device.
*/
val dispatchTests = ExecuteTests from setOf(
ParseApkInfo,
Authorize,
AvailableDevices,
DispatchShards,
) using context { state ->
coroutineScope {
var count = 0
channelFlow {
for (shard in dispatch) {
val index = ++count
val instance = devices.maxBy { instance ->
(shard.apks intersect instance.apks).size
}
val seed = mapOf(
Device.Shard to shard,
Device.Instance to instance,
)
launch {
val result = Device
.execute(state + seed)
.last()
.verify()
.select(ExecuteTests)
send(result)
devices.send(instance)
if (index == count)
dispatch.close()
}
}
}.toList().reduce { accumulator, value ->
accumulator + value
}
}
}

/**
* Flatten into the list of apk.
*/
private val InstanceShard.apks: List<String>
get() = flatMap { app -> app.tests.map { test -> test.name } + app.name }

private suspend fun <T> Channel<T>.maxBy(select: (T) -> Int): T =
mutableListOf<T>().apply {
add(receive())
while (!isEmpty) add(receive())
sortByDescending(select)
drop(1).forEach { send(it) }
}.first()
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package flank.corellium.domain.run.test.android.step
import flank.corellium.domain.RunTestCorelliumAndroid
import flank.corellium.domain.RunTestCorelliumAndroid.ExecuteTests
import flank.corellium.domain.RunTestCorelliumAndroid.GenerateReport
import flank.corellium.domain.RunTestCorelliumAndroid.InvokeDevices
import flank.corellium.domain.RunTestCorelliumAndroid.OutputDir
import flank.corellium.domain.RunTestCorelliumAndroid.context
import flank.exection.parallel.from
Expand All @@ -18,6 +19,7 @@ import java.io.File
* Generated JUnit report is saved as formatted xml file
*/
internal val generateReport = GenerateReport from setOf(
InvokeDevices,
ExecuteTests,
OutputDir
) using context {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package flank.corellium.domain.run.test.android.step

import flank.corellium.domain.RunTestCorelliumAndroid.HandleResults
import flank.exection.parallel.from
import flank.exection.parallel.using

val handleResults = HandleResults from setOf(
HandleResults.Init
) using {
TODO()
}
Loading

0 comments on commit 05d5f58

Please sign in to comment.