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

Commit f78b3b0

Browse files
authored
Added parsing of test runner and test package from apk-s. (#119)
1 parent c9a15aa commit f78b3b0

File tree

11 files changed

+221
-70
lines changed

11 files changed

+221
-70
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ build/
88
/captures
99
/gradle/local.properties
1010
/docs
11+
/composer-output*
1112

1213
# Share code style.
1314
!.idea/codeStyleSettings.xml

README.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,11 @@ Composer shipped as jar, to run it you need JVM 1.8+: `java -jar composer-latest
6767
* Path to application apk that needs to be tested.
6868
* `--test-apk`
6969
* Path to apk with tests.
70-
* `--test-package`
71-
* Android package name of the test apk (Could be parsed from `--test-apk`, PR welcome).
72-
* `--test-runner`
73-
* Full qualified name of test runner class you're using (Could be parsed from `--test-apk`, PR welcome).
7470

7571
##### Optional
7672

73+
* `--test-runner`
74+
* Fully qualified name of test runner class you're using. Parsed from `--test-apk`, if not specified manually.
7775
* `--help, -help, help, -h`
7876
* Print help and exit.
7977
* `--shard`
@@ -85,7 +83,7 @@ Composer shipped as jar, to run it you need JVM 1.8+: `java -jar composer-latest
8583
* `--verbose-output`
8684
* Either `true` or `false` to enable/disable verbose output for Composer. `false` by default.
8785
* `--keep-output-on-exit`
88-
* Keep output on exit. False by default.
86+
* Either `true` or `false` to keep/clean output on exit. `false` by default.".
8987
* `--devices`
9088
* 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`.
9189
* `--device-pattern`
@@ -96,11 +94,18 @@ Composer shipped as jar, to run it you need JVM 1.8+: `java -jar composer-latest
9694

9795
##### Example
9896

97+
Simplest :
98+
```console
99+
java -jar composer-latest-version.jar \
100+
--apk app/build/outputs/apk/example-debug.apk \
101+
--test-apk app/build/outputs/apk/example-debug-androidTest.apk
102+
```
103+
104+
With arguments :
99105
```console
100106
java -jar composer-latest-version.jar \
101107
--apk app/build/outputs/apk/example-debug.apk \
102108
--test-apk app/build/outputs/apk/example-debug-androidTest.apk \
103-
--test-package com.example.test \
104109
--test-runner com.example.test.ExampleTestRunner \
105110
--output-directory artifacts/composer-output \
106111
--instrumentation-arguments key1 value1 key2 value2 \

ci/docker/Dockerfile

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,29 @@ RUN apt-get update && \
66
apt-get --assume-yes install git curl sudo && \
77
curl -sL https://deb.nodesource.com/setup_7.x | bash - && apt-get install --assume-yes nodejs
88

9+
# `aapt` Android SDK build-tool is needed
10+
# v26.0.1, https://issuetracker.google.com/issues/64292349
11+
ENV ANDROID_SDK_URL "https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip"
12+
ENV ANDROID_SDK_FILE_NAME "android-sdk.zip"
13+
14+
ENV ANDROID_HOME /opt/android-sdk-linux
15+
ENV PATH ${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/tools/bin:${ANDROID_HOME}/platform-tools
16+
17+
RUN \
18+
mkdir -p $ANDROID_HOME && \
19+
curl $ANDROID_SDK_URL --progress-bar --location --output $ANDROID_SDK_FILE_NAME && \
20+
unzip $ANDROID_SDK_FILE_NAME -d $ANDROID_HOME && \
21+
rm $ANDROID_SDK_FILE_NAME
22+
23+
# Download required parts of Android SDK (separate from Android SDK layer).
24+
25+
ENV ANDROID_SDK_COMPONENTS_REVISION 2017-10-25-15-22
26+
ENV ANDROID_SDK_INSTALL_COMPONENT "echo \"y\" | \"$ANDROID_HOME\"/tools/bin/sdkmanager --verbose"
27+
28+
RUN \
29+
echo "Android SDK packages revision $ANDROID_SDK_COMPONENTS_REVISION" && \
30+
eval $ANDROID_SDK_INSTALL_COMPONENT '"build-tools;27.0.3"'
31+
932
# Entrypoint script will allow us run as non-root in the container.
1033
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
1134
RUN chmod +x /usr/local/bin/entrypoint.sh
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package com.gojuno.composer
2+
3+
import com.gojuno.commander.android.aapt
4+
import com.gojuno.commander.os.Notification
5+
import com.gojuno.commander.os.process
6+
7+
sealed class TestPackage {
8+
data class Valid(val value: String) : TestPackage()
9+
data class ParseError(val error: String) : TestPackage()
10+
}
11+
12+
sealed class TestRunner {
13+
data class Valid(val value: String) : TestRunner()
14+
data class ParseError(val error: String) : TestRunner()
15+
}
16+
17+
fun parseTestPackage(testApkPath: String): TestPackage =
18+
process(
19+
commandAndArgs = listOf(
20+
aapt, "dump", "badging", testApkPath
21+
),
22+
unbufferedOutput = true
23+
)
24+
.ofType(Notification.Exit::class.java)
25+
.map { (output) ->
26+
output
27+
.readText()
28+
.split(System.lineSeparator())
29+
// output format `package: name='$testPackage' versionCode='' versionName='' platformBuildVersionName='xxx'`
30+
.firstOrNull { it.contains("package") }
31+
?.split(" ")
32+
?.firstOrNull { it.startsWith("name=") }
33+
?.split("'")
34+
?.getOrNull(1)
35+
?.let(TestPackage::Valid)
36+
?: TestPackage.ParseError("Cannot parse test package from `aapt dump badging \$APK` output.")
37+
}
38+
.toSingle()
39+
.toBlocking()
40+
.value()
41+
42+
fun parseTestRunner(testApkPath: String): TestRunner =
43+
process(
44+
commandAndArgs = listOf(
45+
aapt, "dump", "xmltree", testApkPath, "AndroidManifest.xml"
46+
),
47+
unbufferedOutput = true
48+
)
49+
.ofType(Notification.Exit::class.java)
50+
.map { (output) ->
51+
output
52+
.readText()
53+
.split(System.lineSeparator())
54+
.dropWhile { !it.contains("instrumentation") }
55+
.firstOrNull { it.contains("android:name") }
56+
// output format : `A: android:name(0x01010003)="$testRunner" (Raw: "$testRunner")`
57+
?.split("\"")
58+
?.getOrNull(1)
59+
?.let(TestRunner::Valid)
60+
?: TestRunner.ParseError("Cannot parse test runner from `aapt dump xmltree \$TEST_APK AndroidManifest.xml` output.")
61+
}
62+
.toSingle()
63+
.toBlocking()
64+
.value()

composer/src/main/kotlin/com/gojuno/composer/Args.kt

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,24 @@ data class Args(
99
@Parameter(
1010
names = arrayOf("--apk"),
1111
required = true,
12-
description = "Path to application apk that needs to be tested",
12+
description = "Path to application apk that needs to be tested.",
1313
order = 0
1414
)
1515
var appApkPath: String = "",
1616

1717
@Parameter(
1818
names = arrayOf("--test-apk"),
1919
required = true,
20-
description = "Path to apk with tests",
20+
description = "Path to apk with tests.",
2121
order = 1
2222
)
2323
var testApkPath: String = "",
2424

25-
@Parameter(
26-
names = arrayOf("--test-package"),
27-
required = true,
28-
description = "Android package name of the test apk.",
29-
order = 2
30-
)
31-
var testPackage: String = "",
32-
3325
@Parameter(
3426
names = arrayOf("--test-runner"),
35-
required = true,
36-
description = "Full qualified name of test runner class you're using.",
37-
order = 3
27+
required = false,
28+
description = "Fully qualified name of test runner class you're using. Will be parsed from test APK if not specified excplicitly.",
29+
order = 2
3830
)
3931
var testRunner: String = "",
4032

@@ -43,15 +35,15 @@ data class Args(
4335
required = false,
4436
arity = 1,
4537
description = "Either `true` or `false` to enable/disable test sharding which runs tests in parallel on available devices/emulators. `true` by default.",
46-
order = 4
38+
order = 3
4739
)
4840
var shard: Boolean = true,
4941

5042
@Parameter(
5143
names = arrayOf("--output-directory"),
5244
required = false,
5345
description = "Either relative or absolute path to directory for output: reports, files from devices and so on. `composer-output` by default.",
54-
order = 5
46+
order = 4
5547
)
5648
var outputDirectory: String = "composer-output",
5749

@@ -61,7 +53,7 @@ data class Args(
6153
variableArity = true,
6254
description = "Key-value pairs to pass to Instrumentation Runner. Usage example: `--instrumentation-arguments myKey1 myValue1 myKey2 myValue2`.",
6355
listConverter = InstrumentationArgumentsConverter::class,
64-
order = 6
56+
order = 5
6557
)
6658
var instrumentationArguments: List<String> = listOf(),
6759

@@ -70,15 +62,15 @@ data class Args(
7062
required = false,
7163
arity = 1,
7264
description = "Either `true` or `false` to enable/disable verbose output for Composer. `false` by default.",
73-
order = 7
65+
order = 6
7466
)
7567
var verboseOutput: Boolean = false,
7668

7769
@Parameter(
7870
names = arrayOf("--keep-output-on-exit"),
7971
required = false,
80-
description = "Keep output on exit. False by default.",
81-
order = 8
72+
description = "Either `true` or `false` to keep/clean output on exit. `false` by default.",
73+
order = 7
8274
)
8375
var keepOutputOnExit: Boolean = false,
8476

@@ -87,29 +79,29 @@ data class Args(
8779
required = false,
8880
variableArity = true,
8981
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`.",
90-
order = 9
82+
order = 8
9183
)
9284
var devices: List<String> = emptyList(),
9385

9486
@Parameter(
9587
names = arrayOf("--device-pattern"),
9688
required = false,
9789
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\"`.",
98-
order = 10
90+
order = 9
9991
)
10092
var devicePattern: String = "",
10193

10294
@Parameter(
10395
names = arrayOf("--install-timeout"),
10496
required = false,
10597
description = "APK installation timeout in seconds. If not passed defaults to 120 seconds (2 minutes). Applicable to both test APK and APK under test.",
106-
order = 11
98+
order = 10
10799
)
108100
var installTimeoutSeconds: Int = TimeUnit.MINUTES.toSeconds(2).toInt()
109101
)
110102

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

114106
private fun validateArguments(args: Args) {
115107
if (!args.devicePattern.isEmpty() && !args.devices.isEmpty()) {
@@ -118,7 +110,7 @@ private fun validateArguments(args: Args) {
118110
}
119111

120112
fun parseArgs(rawArgs: Array<String>) = Args().also { args ->
121-
if (PARAMETER_HELP_NAMES.firstOrNull { rawArgs.contains(it) } != null) {
113+
if (PARAMETER_HELP_NAMES.any { rawArgs.contains(it) }) {
122114
JCommander(args).usage()
123115
exit(Exit.Ok)
124116
}

composer/src/main/kotlin/com/gojuno/composer/Instrumentation.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ private fun parseInstrumentationEntry(str: String): InstrumentationEntry =
104104

105105
// Reads stream in "tail -f" mode.
106106
fun readInstrumentationOutput(output: File): Observable<InstrumentationEntry> {
107-
data class result(val buffer: String = "", val readyForProcessing: Boolean = false)
107+
data class Result(val buffer: String = "", val readyForProcessing: Boolean = false)
108108

109109
return tail(output)
110110
.map(String::trim)
@@ -113,24 +113,24 @@ fun readInstrumentationOutput(output: File): Observable<InstrumentationEntry> {
113113
// `INSTRUMENTATION_CODE: <code>` is the last line printed by instrumentation, even if 0 tests were run.
114114
!it.startsWith("INSTRUMENTATION_CODE")
115115
}
116-
.scan(result()) { previousResult, newLine ->
116+
.scan(Result()) { previousResult, newLine ->
117117
val buffer = when (previousResult.readyForProcessing) {
118118
true -> newLine
119119
false -> "${previousResult.buffer}${System.lineSeparator()}$newLine"
120120
}
121121

122-
result(buffer = buffer, readyForProcessing = newLine.startsWith("INSTRUMENTATION_STATUS_CODE"))
122+
Result(buffer = buffer, readyForProcessing = newLine.startsWith("INSTRUMENTATION_STATUS_CODE"))
123123
}
124124
.filter { it.readyForProcessing }
125125
.map { it.buffer }
126126
.map(::parseInstrumentationEntry)
127127
}
128128

129129
fun Observable<InstrumentationEntry>.asTests(): Observable<InstrumentationTest> {
130-
data class result(val entries: List<InstrumentationEntry> = emptyList(), val tests: List<InstrumentationTest> = emptyList(), val totalTestsCount: Int = 0)
130+
data class Result(val entries: List<InstrumentationEntry> = emptyList(), val tests: List<InstrumentationTest> = emptyList(), val totalTestsCount: Int = 0)
131131

132132
return this
133-
.scan(result()) { previousResult, newEntry ->
133+
.scan(Result()) { previousResult, newEntry ->
134134
val entries = previousResult.entries + newEntry
135135
val tests: List<InstrumentationTest> = entries
136136
.mapIndexed { index, first ->
@@ -166,7 +166,7 @@ fun Observable<InstrumentationEntry>.asTests(): Observable<InstrumentationTest>
166166
)
167167
}
168168

169-
result(
169+
Result(
170170
entries = entries.filter { entry -> tests.firstOrNull { it.className == entry.clazz && it.testName == entry.test } == null },
171171
tests = tests,
172172
totalTestsCount = previousResult.totalTestsCount + tests.size

0 commit comments

Comments
 (0)