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: Load test cases durations from previous run and use for sharding #1998

Merged
merged 4 commits into from
Jun 2, 2021
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
1 change: 1 addition & 0 deletions corellium/cli/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ dependencies {
implementation(project(":corellium:domain"))
implementation(project(":corellium:adapter"))
implementation(project(":corellium:apk"))
implementation(project(":corellium:junit"))
implementation(Dependencies.JACKSON_KOTLIN)
implementation(Dependencies.JACKSON_YAML)
implementation(Dependencies.JACKSON_XML)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import flank.corellium.corelliumApi
import flank.corellium.domain.RunTestCorelliumAndroid
import flank.corellium.domain.RunTestCorelliumAndroid.Args
import flank.corellium.domain.invoke
import flank.junit.JUnit
import picocli.CommandLine

@CommandLine.Command(
Expand Down Expand Up @@ -101,6 +102,16 @@ class RunTestCorelliumAndroidCommand :
)
@set:JsonProperty("gpu-acceleration")
var gpuAcceleration: Boolean? by data

@set:CommandLine.Option(
names = ["--scan-previous-durations"],
description = [
"Scan the specified amount of JUnitReport.xml files to obtain test cases durations necessary for optimized sharding." +
"The `local-result-dir` is used for searching JUnit reports."
]
)
@set:JsonProperty("scan-previous-durations")
var scanPreviousDurations: Int? by data
}

@CommandLine.Mixin
Expand All @@ -118,6 +129,8 @@ class RunTestCorelliumAndroidCommand :

override val apk = Apk.Api()

override val junit = JUnit.Api()

override val args by lazy { createArgs() }

override fun run() = invoke()
Expand All @@ -131,6 +144,7 @@ private fun defaultConfig() = Config().apply {
localResultsDir = null
obfuscate = false
gpuAcceleration = true
scanPreviousDurations = 10
}

private fun RunTestCorelliumAndroidCommand.yamlConfig(): Config =
Expand All @@ -142,5 +156,6 @@ private fun RunTestCorelliumAndroidCommand.createArgs() = Args(
maxShardsCount = config.maxTestShards!!,
outputDir = config.localResultsDir ?: Args.DefaultOutputDir.new,
obfuscateDumpShards = config.obfuscate!!,
gpuAcceleration = config.gpuAcceleration!!
gpuAcceleration = config.gpuAcceleration!!,
scanPreviousDurations = config.scanPreviousDurations!!,
)
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class RunTestCorelliumAndroidCommandTest {
localResultsDir = "test_result_dir"
obfuscate = true
gpuAcceleration = false
scanPreviousDurations = 123
}

/**
Expand All @@ -59,6 +60,7 @@ class RunTestCorelliumAndroidCommandTest {
"--local-result-dir=$localResultsDir",
"--obfuscate=$obfuscate",
"--gpu-acceleration=$gpuAcceleration",
"--scan-previous-durations=$scanPreviousDurations",
)
}

Expand All @@ -78,6 +80,7 @@ max-test-shards: $maxTestShards
local-result-dir: $localResultsDir
obfuscate: $obfuscate
gpu-acceleration: $gpuAcceleration
scan-previous-durations: $scanPreviousDurations
""".trimIndent()
}

Expand Down Expand Up @@ -141,6 +144,7 @@ gpu-acceleration: $gpuAcceleration
outputDir = localResultsDir!!,
obfuscateDumpShards = obfuscate!!,
gpuAcceleration = gpuAcceleration!!,
scanPreviousDurations = scanPreviousDurations!!,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ import flank.corellium.domain.run.test.android.step.finish
import flank.corellium.domain.run.test.android.step.generateReport
import flank.corellium.domain.run.test.android.step.installApks
import flank.corellium.domain.run.test.android.step.invokeDevices
import flank.corellium.domain.run.test.android.step.loadPreviousDurations
import flank.corellium.domain.run.test.android.step.parseApksInfo
import flank.corellium.domain.run.test.android.step.parseTestCasesFromApks
import flank.corellium.domain.run.test.android.step.prepareShards
import flank.corellium.domain.util.CreateTransformation
import flank.corellium.domain.util.execute
import flank.corellium.shard.Shard
import flank.instrument.log.Instrument
import flank.junit.JUnit
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking
import java.lang.System.currentTimeMillis
Expand All @@ -38,6 +40,7 @@ object RunTestCorelliumAndroid {
interface Context {
val api: CorelliumApi
val apk: Apk.Api
val junit: JUnit.Api
val args: Args
}

Expand All @@ -50,6 +53,7 @@ object RunTestCorelliumAndroid {
* @param obfuscateDumpShards Obfuscate the test names in shards before dumping to file.
* @param outputDir Set output dir. Default value is [DefaultOutputDir.new]
* @param gpuAcceleration Enable gpu acceleration for newly created virtual devices.
* @param scanPreviousDurations Scan the specified amount of JUnitReport.xml files to obtain test cases durations necessary for optimized sharding. The [outputDir] is used for searching JUnit reports.
*/
data class Args(
val credentials: Authorization.Credentials,
Expand All @@ -58,16 +62,17 @@ object RunTestCorelliumAndroid {
val obfuscateDumpShards: Boolean = false,
val outputDir: String = DefaultOutputDir.new,
val gpuAcceleration: Boolean = true,
val scanPreviousDurations: Int = 10,
) {
/**
* Default output directory scheme.
*
* @property new Directory name in format: `results/corellium/android/yyyy-MM-dd_HH-mm-ss-SSS`.
*/
object DefaultOutputDir {
private const val PATH = "results/corellium/android/"
internal const val ROOT = "results/corellium/android/"
private val date = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss-SSS")
val new get() = PATH + date.format(currentTimeMillis())
val new get() = ROOT + date.format(currentTimeMillis())
}

/**
Expand Down Expand Up @@ -102,20 +107,26 @@ object RunTestCorelliumAndroid {
* For convenience the properties are sorted in order equal to its initialization.
*
* @param testCases key - path to the test apk, value - list of test method names.
* @param previousDurations key - test case name, value - calculated previous duration.
* @param shards each item is representing list of apps to run on another device instance.
* @param ids the ids of corellium device instances.
* @param packageNames key - path to the test apk, value - package name.
* @param testRunners key - path to the test apk, value - fully qualified test runner name.
*/
internal data class State(
val testCases: Map<String, List<String>> = emptyMap(),
val previousDurations: Map<String, Long> = defaultPreviousDurations,
val shards: List<List<Shard.App>> = emptyList(),
val ids: List<String> = emptyList(),
val packageNames: Map<String, String> = emptyMap(),
val testRunners: Map<String, String> = emptyMap(),
val testResult: List<List<Instrument>> = emptyList(),
)

private const val DEFAULT_TEST_CASE_DURATION = 120L

private val defaultPreviousDurations = emptyMap<String, Long>().withDefault { DEFAULT_TEST_CASE_DURATION }

/**
* The reference to the step factory.
* Invoke it to generate new execution step.
Expand All @@ -127,6 +138,7 @@ operator fun Context.invoke(): Unit = runBlocking {
State() execute flowOf(
authorize(),
parseTestCasesFromApks(),
loadPreviousDurations(),
prepareShards(),
createOutputDir(),
dumpShards(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import java.io.File
*/
internal fun RunTestCorelliumAndroid.Context.generateReport() = RunTestCorelliumAndroid.step {
println("* Generating report")
val file = File(args.outputDir, JUNIT_REPORT_FILENAME)
val file = File(args.outputDir, JUnit.REPORT_FILE_NAME)
testResult
.prepareInputForJUnit()
.generateJUnitReport()
Expand All @@ -27,8 +27,6 @@ internal fun RunTestCorelliumAndroid.Context.generateReport() = RunTestCorellium
this
}

private const val JUNIT_REPORT_FILENAME = "JUnitReport.xml"

/**
* Simple mapper, no logical operations or API calls,
* just converting one structure to another.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package flank.corellium.domain.run.test.android.step

import flank.corellium.domain.RunTestCorelliumAndroid
import flank.corellium.domain.RunTestCorelliumAndroid.Args.DefaultOutputDir
import flank.junit.calculateTestCaseDurations

/**
* The step is searching result directory for JUnitReport.xml.
* Collected reports are used for calculating test cases durations.
*
* require:
* * [RunTestCorelliumAndroid.Context.parseTestCasesFromApks]
*
* updates:
* * [RunTestCorelliumAndroid.State.previousDurations]
*/
internal fun RunTestCorelliumAndroid.Context.loadPreviousDurations() = RunTestCorelliumAndroid.step {
println("* Obtaining previous test cases durations")

val directoryToScan: String =
if (args.outputDir.startsWith(DefaultOutputDir.ROOT)) DefaultOutputDir.ROOT
else args.outputDir

copy(
previousDurations = junit.parseTestResults(directoryToScan)
.take(args.scanPreviousDurations).toList()
.apply { println("Searching in $size JUnitReport.xml files...") }
.flatten()
.calculateTestCaseDurations()
.withDefault { previousDurations.getValue(it) }
.also { durations -> printStats(durations.keys) }
)
}

private fun RunTestCorelliumAndroid.State.printStats(obtainedDurations: Set<String>) {
val testCasesNames = testCases.flatMap { (_, cases) -> cases }.toSet()
val unknown = (obtainedDurations - testCasesNames).size
val matching = obtainedDurations.size - unknown
val required = testCasesNames.size

println("For $required test cases, found $matching matching and $unknown unknown")
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ internal fun RunTestCorelliumAndroid.Context.prepareShards() = RunTestCorelliumA
apps = prepareDataForSharding(
apks = args.apks,
testCases = testCases,
durations = previousDurations,
),
maxCount = args.maxShardsCount
)
Expand All @@ -32,7 +33,8 @@ internal fun RunTestCorelliumAndroid.Context.prepareShards() = RunTestCorelliumA
*/
private fun prepareDataForSharding(
apks: List<RunTestCorelliumAndroid.Args.Apk.App>,
testCases: Map<String, List<String>>
testCases: Map<String, List<String>>,
durations: Map<String, Long>,
): List<Shard.App> =
apks.map { app ->
Shard.App(
Expand All @@ -42,7 +44,12 @@ private fun prepareDataForSharding(
name = test.path,
cases = testCases
.getValue(test.path)
.map(Shard.Test::Case)
.map { name ->
Shard.Test.Case(
name = name,
duration = durations.getValue(name)
)
}
)
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package flank.corellium.domain

import flank.apk.Apk
import flank.corellium.corelliumApi
import flank.junit.JUnit

object RunTestAndroidCorelliumExample : RunTestCorelliumAndroid.Context {
override val api = corelliumApi("Default Project")
override val apk = Apk.Api()
override val junit = JUnit.Api()
override val args = RunTestCorelliumAndroid.Args(
credentials = loadedCredentials,
apks = fewTestArtifactsApks(APK_PATH_MAIN),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package flank.corellium.domain
import flank.apk.Apk
import flank.corellium.api.CorelliumApi
import flank.corellium.domain.RunTestCorelliumAndroid.Args
import flank.junit.JUnit
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.map
import org.junit.After
Expand Down Expand Up @@ -84,6 +85,8 @@ class RunTestAndroidCorelliumTestMockApiAndroid : RunTestCorelliumAndroid.Contex
},
)

override val junit = JUnit.Api()

@Test
fun test(): Unit = invoke()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package flank.corellium.domain

import flank.apk.Apk
import flank.corellium.api.CorelliumApi
import flank.junit.JUnit
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.map
import org.junit.After
Expand Down Expand Up @@ -55,6 +56,8 @@ class RunTestAndroidCorelliumTestParsingAndroid : RunTestCorelliumAndroid.Contex
},
)

override val junit = JUnit.Api()

override val apk = Apk.Api()

@Test
Expand Down
Loading