Skip to content
This repository has been archived by the owner on Dec 7, 2019. It is now read-only.

Commit

Permalink
Reworked args parsing, fixed issue with instrumentation args separate…
Browse files Browse the repository at this point in the history
…d by commas. (#104)
  • Loading branch information
yunikkk authored Oct 23, 2017
1 parent 0bace71 commit bd6c738
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 157 deletions.
230 changes: 99 additions & 131 deletions composer/src/main/kotlin/com/gojuno/composer/Args.kt
Original file line number Diff line number Diff line change
@@ -1,150 +1,118 @@
package com.gojuno.janulator
package com.gojuno.composer

import com.beust.jcommander.IStringConverter
import com.beust.jcommander.JCommander
import com.beust.jcommander.Parameter
import com.gojuno.composer.Exit
import com.gojuno.composer.exit

data class Args(
val appApkPath: String,
val testApkPath: String,
val testPackage: String,
val testRunner: String,
val shard: Boolean,
val outputDirectory: String,
val instrumentationArguments: List<Pair<String, String>>,
val verboseOutput: Boolean,
val devices: List<String>,
val devicePattern: String
@Parameter(
names = arrayOf("--apk"),
required = true,
description = "Path to application apk that needs to be tested",
order = 0
)
var appApkPath: String = "",

@Parameter(
names = arrayOf("--test-apk"),
required = true,
description = "Path to apk with tests",
order = 1
)
var testApkPath: String = "",

@Parameter(
names = arrayOf("--test-package"),
required = true,
description = "Android package name of the test apk.",
order = 2
)
var testPackage: String = "",

@Parameter(
names = arrayOf("--test-runner"),
required = true,
description = "Full qualified name of test runner class you're using.",
order = 3
)
var testRunner: String = "",

@Parameter(
names = arrayOf("--shard"),
required = false,
arity = 1,
description = "Either `true` or `false` to enable/disable test sharding which runs tests in parallel on available devices/emulators. `true` by default.",
order = 4
)
var shard: Boolean = true,

@Parameter(
names = arrayOf("--output-directory"),
required = false,
description = "Either relative or absolute path to directory for output: reports, files from devices and so on. `composer-output` by default.",
order = 5
)
var outputDirectory: String = "composer-output",

@Parameter(
names = arrayOf("--instrumentation-arguments"),
required = false,
variableArity = true,
description = "Key-value pairs to pass to Instrumentation Runner. Usage example: `--instrumentation-arguments myKey1 myValue1 myKey2 myValue2`.",
listConverter = InstrumentationArgumentsConverter::class,
order = 6
)
var instrumentationArguments: List<String> = listOf(),

@Parameter(
names = arrayOf("--verbose-output"),
required = false,
arity = 1,
description = "Either `true` or `false` to enable/disable verbose output for Swarmer. `false` by default.",
order = 7
)
var verboseOutput: Boolean = false,

@Parameter(
names = arrayOf("--devices"),
required = false,
variableArity = true,
description = "Connected devices/emulators that will be used to run tests against. If not passed — tests will run on all connected devices/emulators. Specifying both `--devices` and `--device-pattern` will result in an error. Usage example: `--devices emulator-5554 emulator-5556`.",
order = 8
)
var devices: List<String> = emptyList(),

@Parameter(
names = arrayOf("--device-pattern"),
required = false,
description = "Connected devices/emulators that will be used to run tests against. If not passed — tests will run on all connected devices/emulators. Specifying both `--device-pattern` and `--devices` will result in an error. Usage example: `--device-pattern \"somePatterns\"`.",
order = 9
)
var devicePattern: String = ""
)

// No way to share array both for runtime and annotation without reflection.
private val PARAMETER_HELP_NAMES = setOf("--help", "-help", "help", "-h")

private class JCommanderArgs {

@Parameter(
names = arrayOf("--help", "-help", "help", "-h"),
help = true,
description = "Print help and exit."
)
var help: Boolean? = null

@Parameter(
names = arrayOf("--apk"),
required = true,
description = "Path to application apk that needs to be tested"
)
lateinit var appApkPath: String

@Parameter(
names = arrayOf("--test-apk"),
required = true,
description = "Path to apk with tests"
)
lateinit var testApkPath: String

@Parameter(
names = arrayOf("--test-package"),
required = true,
description = "Android package name of the test apk."
)
lateinit var testPackage: String

@Parameter(
names = arrayOf("--test-runner"),
required = true,
description = "Full qualified name of test runner class you're using."
)
lateinit var testRunner: String

@Parameter(
names = arrayOf("--shard"),
required = false,
arity = 1,
description = "Either `true` or `false` to enable/disable test sharding which runs tests in parallel on available devices/emulators. `true` by default."
)
var shard: Boolean? = null

@Parameter(
names = arrayOf("--output-directory"),
required = false,
description = "Either relative or absolute path to directory for output: reports, files from devices and so on. `composer-output` by default."
)
var outputDirectory: String? = null

@Parameter(
names = arrayOf("--instrumentation-arguments"),
required = false,
variableArity = true,
description = "Key-value pairs to pass to Instrumentation Runner. Usage example: `--instrumentation-arguments myKey1 myValue1 myKey2 myValue2`."
)
var instrumentationArguments: List<String>? = null

@Parameter(
names = arrayOf("--verbose-output"),
required = false,
arity = 1,
description = "Either `true` or `false` to enable/disable verbose output for Swarmer. `false` by default."
)
var verboseOutput: Boolean? = null

@Parameter(
names = arrayOf("--devices"),
required = false,
variableArity = true,
description = "Connected devices/emulators that will be used to run tests against. If not passed — tests will run on all connected devices/emulators. Specifying both `--devices` and `--device-pattern` will result in an error. Usage example: `--devices emulator-5554 emulator-5556`."
)
var devices: List<String>? = null

@Parameter(
names = arrayOf("--device-pattern"),
required = false,
description = "Connected devices/emulators that will be used to run tests against. If not passed — tests will run on all connected devices/emulators. Specifying both `--device-pattern` and `--devices` will result in an error. Usage example: `--device-pattern \"somePatterns\"`."
)
var devicePattern: String? = null
}
val PARAMETER_HELP_NAMES = setOf("--help", "-help", "help", "-h")

private fun validateArguments(args: Args) {
if(!args.devicePattern.isEmpty() && !args.devices.isEmpty()) {
if (!args.devicePattern.isEmpty() && !args.devices.isEmpty()) {
throw IllegalArgumentException("Specifying both --devices and --device-pattern is prohibited.")
}
}

fun parseArgs(rawArgs: Array<String>): Args {
fun parseArgs(rawArgs: Array<String>) = Args().also { args ->
if (PARAMETER_HELP_NAMES.firstOrNull { rawArgs.contains(it) } != null) {
JCommander(JCommanderArgs()).usage()
JCommander(args).usage()
exit(Exit.Ok)
}

val jCommanderArgs = JCommanderArgs()
val jCommander = JCommander.newBuilder().addObject(jCommanderArgs).build()
jCommander.parse(*rawArgs)

return jCommanderArgs.let {
Args(
appApkPath = jCommanderArgs.appApkPath,
testApkPath = jCommanderArgs.testApkPath,
testPackage = jCommanderArgs.testPackage,
testRunner = jCommanderArgs.testRunner,
shard = jCommanderArgs.shard ?: true,
outputDirectory = jCommanderArgs.outputDirectory ?: "composer-output",
instrumentationArguments = mutableListOf<Pair<String, String>>().apply {
var pairIndex = 0

jCommanderArgs.instrumentationArguments?.forEachIndexed { index, arg ->
when (index % 2 == 0) {
true -> this += arg to ""
false -> {
this[pairIndex] = this[pairIndex].copy(second = arg)
pairIndex += 1
}
}
}
},
verboseOutput = jCommanderArgs.verboseOutput ?: false,
devices = jCommanderArgs.devices ?: emptyList(),
devicePattern = jCommanderArgs.devicePattern ?: ""
)
}.apply { validateArguments(this) }
JCommander.newBuilder()
.addObject(args)
.build()
.parse(*rawArgs)
validateArguments(args)
}

private class InstrumentationArgumentsConverter : IStringConverter<List<String>> {
override fun convert(argument: String): List<String> = listOf(argument)
}
44 changes: 33 additions & 11 deletions composer/src/main/kotlin/com/gojuno/composer/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import com.gojuno.commander.android.installApk
import com.gojuno.commander.os.log
import com.gojuno.commander.os.nanosToHumanReadableTime
import com.gojuno.composer.html.writeHtmlReport
import com.gojuno.janulator.parseArgs
import com.google.gson.Gson
import rx.Observable
import rx.schedulers.Schedulers
Expand Down Expand Up @@ -41,7 +40,7 @@ fun main(rawArgs: Array<String>) {
.map { devices ->
when (args.devicePattern.isEmpty()) {
true -> devices
false -> Regex(args.devicePattern).let { regex -> devices.filter { regex.matches(it.id) }}
false -> Regex(args.devicePattern).let { regex -> devices.filter { regex.matches(it.id) } }
}
}
.map {
Expand Down Expand Up @@ -69,21 +68,19 @@ fun main(rawArgs: Array<String>) {
.subscribeOn(Schedulers.io())
.toList()
.flatMap {
val shardOptions = when {
args.shard && connectedAdbDevices.size > 1 -> listOf(
"numShards" to "${connectedAdbDevices.size}",
"shardIndex" to "$index"
)

else -> emptyList()
}
val instrumentationArguments =
buildShardArguments(
shardingOn = args.shard,
shardIndex = index,
devices = connectedAdbDevices.size
) + args.instrumentationArguments.pairArguments()

device
.runTests(
// TODO parse package name and runner class from test apk.
testPackageName = args.testPackage,
testRunnerClass = args.testRunner,
instrumentationArguments = shardOptions + args.instrumentationArguments,
instrumentationArguments = instrumentationArguments.formatInstrumentationArguments(),
outputDir = File(args.outputDirectory),
verboseOutput = args.verboseOutput
)
Expand Down Expand Up @@ -150,6 +147,31 @@ fun main(rawArgs: Array<String>) {
}
}

private fun List<String>.pairArguments(): List<Pair<String, String>> =
foldIndexed(mutableListOf()) { index, accumulator, value ->
accumulator.apply {
if (index % 2 == 0) {
add(value to "")
} else {
set(lastIndex, last().first to value)
}
}
}

private fun buildShardArguments(shardingOn: Boolean, shardIndex: Int, devices: Int): List<Pair<String, String>> = when {
shardingOn && devices > 1 -> listOf(
"numShards" to "$devices",
"shardIndex" to "$shardIndex"
)

else -> emptyList()
}

private fun List<Pair<String, String>>.formatInstrumentationArguments(): String = when (isEmpty()) {
true -> ""
false -> " " + joinToString(separator = " ") { "-e ${it.first} ${it.second}" }
}

data class Suite(
val testPackage: String,
val devices: List<Device>,
Expand Down
9 changes: 2 additions & 7 deletions composer/src/main/kotlin/com/gojuno/composer/TestRun.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ data class AdbDeviceTest(
fun AdbDevice.runTests(
testPackageName: String,
testRunnerClass: String,
instrumentationArguments: List<Pair<String, String>>,
instrumentationArguments: String,
outputDir: File,
verboseOutput: Boolean
): Single<AdbDeviceTestRun> {
Expand All @@ -54,7 +54,7 @@ fun AdbDevice.runTests(
commandAndArgs = listOf(
adb,
"-s", adbDevice.id,
"shell", "am instrument -w -r${instrumentationArguments.formatInstrumentationOptions()} $testPackageName/$testRunnerClass"
"shell", "am instrument -w -r $instrumentationArguments $testPackageName/$testRunnerClass"
),
timeout = null,
redirectOutputTo = instrumentationOutputFile
Expand Down Expand Up @@ -147,11 +147,6 @@ fun AdbDevice.runTests(
.toSingle()
}

private fun List<Pair<String, String>>.formatInstrumentationOptions(): String = when (isEmpty()) {
true -> ""
false -> " " + joinToString(separator = " ") { "-e ${it.first} ${it.second}" }
}

data class PulledFiles(
val files: List<File>,
val screenshots: List<File>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ fun writeHtmlReport(gson: Gson, suites: List<Suite>, outputDir: File, date: Date
)
)


val date = SimpleDateFormat("HH:mm:ss z, MMM d yyyy").apply { timeZone = TimeZone.getTimeZone("UTC") }.format(date)
val formattedDate = SimpleDateFormat("HH:mm:ss z, MMM d yyyy").apply { timeZone = TimeZone.getTimeZone("UTC") }.format(date)

val appJs = File(outputDir, "app.min.js")
inputStreamFromResources("html-report/app.min.js").copyTo(appJs.outputStream())
Expand All @@ -48,7 +47,7 @@ fun writeHtmlReport(gson: Gson, suites: List<Suite>, outputDir: File, date: Date
indexHtmlFile.writeText(indexHtml
.replace("\${relative_path}", indexHtmlFile.relativePathToHtmlDir())
.replace("\${data_json}", "window.mainData = $htmlIndexJson")
.replace("\${date}", date)
.replace("\${date}", formattedDate)
.replace("\${log}", "")
)

Expand All @@ -61,7 +60,7 @@ fun writeHtmlReport(gson: Gson, suites: List<Suite>, outputDir: File, date: Date
suiteHtmlFile.writeText(indexHtml
.replace("\${relative_path}", suiteHtmlFile.relativePathToHtmlDir())
.replace("\${data_json}", "window.suite = $suiteJson")
.replace("\${date}", date)
.replace("\${date}", formattedDate)
.replace("\${log}", "")
)

Expand All @@ -76,7 +75,7 @@ fun writeHtmlReport(gson: Gson, suites: List<Suite>, outputDir: File, date: Date
testHtmlFile.writeText(indexHtml
.replace("\${relative_path}", testHtmlFile.relativePathToHtmlDir())
.replace("\${data_json}", "window.test = $testJson")
.replace("\${date}", date)
.replace("\${date}", formattedDate)
.replace("\${log}", generateLogcatHtml(test.logcat))
)
}
Expand Down
Loading

0 comments on commit bd6c738

Please sign in to comment.