├── .gitignore
├── .idea
├── gradle.xml
├── kotlinc.xml
├── migrations.xml
├── misc.xml
├── other.xml
├── vcs.xml
└── workspace.xml
├── README.md
├── androidplugin
├── build.gradle.kts
└── src
│ └── main
│ └── java
│ └── dev
│ └── oianmol
│ └── opentestlab
│ └── tasks
│ ├── AndroidTestDeviceFarmTask.kt
│ ├── DeviceFarmPlugin.kt
│ ├── OpenTestLabExtension.kt
│ ├── TestLabPullReportTask.kt
│ ├── getCurrentGitBranchName.kt
│ └── runCatchingCancellable.kt
├── build.gradle.kts
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── orchestrator-1.4.1.apk
├── protos
├── build.gradle.kts
└── src
│ └── main
│ └── proto
│ ├── device_farm.proto
│ ├── device_farm_service.proto
│ ├── report_management.proto
│ └── test_execution.proto
├── sampleandroidapp
├── .gitignore
├── .idea
│ ├── .gitignore
│ ├── androidTestResultsUserPreferences.xml
│ ├── compiler.xml
│ ├── deploymentTargetSelector.xml
│ ├── gradle.xml
│ ├── inspectionProfiles
│ │ └── Project_Default.xml
│ ├── kotlinc.xml
│ ├── migrations.xml
│ ├── misc.xml
│ ├── other.xml
│ └── vcs.xml
├── app
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ └── src
│ │ ├── androidTest
│ │ └── java
│ │ │ └── dev
│ │ │ └── oianmol
│ │ │ └── sampleandroidapp
│ │ │ ├── MainActivityTest.kt
│ │ │ ├── rules
│ │ │ ├── InstrumentationTestingUtils.kt
│ │ │ └── Logcat.kt
│ │ │ └── runner
│ │ │ ├── OpenAndroidTestRunListener.kt
│ │ │ ├── TestSuite.kt
│ │ │ └── TestSuiteXmlGen.kt
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ │ └── dev
│ │ │ │ └── oianmol
│ │ │ │ └── sampleandroidapp
│ │ │ │ ├── MainActivity.kt
│ │ │ │ └── ui
│ │ │ │ └── theme
│ │ │ │ ├── Color.kt
│ │ │ │ ├── Theme.kt
│ │ │ │ └── Type.kt
│ │ └── res
│ │ │ ├── drawable
│ │ │ ├── ic_launcher_background.xml
│ │ │ └── ic_launcher_foreground.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ │ ├── values
│ │ │ ├── colors.xml
│ │ │ ├── strings.xml
│ │ │ └── themes.xml
│ │ │ └── xml
│ │ │ ├── backup_rules.xml
│ │ │ └── data_extraction_rules.xml
│ │ └── test
│ │ └── java
│ │ └── dev
│ │ └── oianmol
│ │ └── sampleandroidapp
│ │ └── ExampleUnitTest.kt
├── build.gradle.kts
├── gradle.properties
├── gradle
│ ├── libs.versions.toml
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
├── settings.gradle.kts
├── src
└── main
│ ├── kotlin
│ ├── Main.kt
│ └── dev
│ │ └── oianmol
│ │ └── opentestlab
│ │ ├── Coroutine+Extensions.kt
│ │ └── android
│ │ ├── devicefarm
│ │ ├── Device.kt
│ │ ├── DeviceAvailabilityStore.kt
│ │ ├── DeviceDiscovery.kt
│ │ ├── DeviceFarmService.kt
│ │ └── cli
│ │ │ ├── AABToAPKConverter.kt
│ │ │ ├── Adb.kt
│ │ │ ├── AdbPathFinder.kt
│ │ │ └── CommandLine.kt
│ │ ├── reporting
│ │ └── DeviceFarmReportManagementService.kt
│ │ └── testexec
│ │ ├── DeviceFarmTestExecScope.kt
│ │ ├── TestExecScope.kt
│ │ ├── TestExecutionService.kt
│ │ ├── TestExecutionTaskSpec.kt
│ │ ├── commandrunner
│ │ └── DeviceCommandRunner.kt
│ │ ├── resultreader
│ │ ├── DeviceTestResultReader.kt
│ │ └── utils
│ │ │ ├── XmlTestSuite.kt
│ │ │ └── kotlinXmlMapper.kt
│ │ └── testrunner
│ │ └── ITestRunner.kt
│ └── resources
│ ├── orchestrator-1.4.1.apk
│ └── test-services-1.4.2.apk
└── test-services-1.4.2.apk
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | build/
3 | !gradle/wrapper/gradle-wrapper.jar
4 | !**/src/main/**/build/
5 | !**/src/test/**/build/
6 |
7 | ### IntelliJ IDEA ###
8 | .idea/modules.xml
9 | .idea/jarRepositories.xml
10 | .idea/compiler.xml
11 | .idea/libraries/
12 | *.iws
13 | *.iml
14 | *.ipr
15 | out/
16 | !**/src/main/**/out/
17 | !**/src/test/**/out/
18 |
19 | ### Eclipse ###
20 | .apt_generated
21 | .classpath
22 | .factorypath
23 | .project
24 | .settings
25 | .springBeans
26 | .sts4-cache
27 | bin/
28 | !**/src/main/**/bin/
29 | !**/src/test/**/bin/
30 |
31 | ### NetBeans ###
32 | /nbproject/private/
33 | /nbbuild/
34 | /dist/
35 | /nbdist/
36 | /.nb-gradle/
37 |
38 | ### VS Code ###
39 | .vscode/
40 |
41 | ### Mac OS ###
42 | .DS_Store
43 | local.properties
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
19 |
20 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.idea/other.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/workspace.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | {
94 | "associatedIndex": 1
95 | }
96 |
97 |
98 |
99 |
100 |
101 |
102 | {
103 | "keyToString": {
104 | "Gradle.Build OpenTestLabAndroid.executor": "Run",
105 | "Gradle.OpenTestLabAndroid [build].executor": "Run",
106 | "Gradle.OpenTestLabAndroid [clean].executor": "Run",
107 | "Gradle.OpenTestLabAndroid [publishToMavenLocal].executor": "Run",
108 | "Gradle.OpenTestLabAndroid [run].executor": "Run",
109 | "Gradle.OpenTestLabAndroid:androidTestLibrary [build].executor": "Run",
110 | "Gradle.OpenTestLabAndroid:androidTestLibrary [publishToMavenLocal].executor": "Run",
111 | "Gradle.OpenTestLabAndroid:androidplugin [build].executor": "Run",
112 | "Gradle.OpenTestLabAndroid:androidplugin [publishPluginMavenPublicationToMavenLocalRepository].executor": "Run",
113 | "Gradle.OpenTestLabAndroid:androidplugin [publishToMavenLocal].executor": "Run",
114 | "Kotlin.MainKt.executor": "Run",
115 | "RunOnceActivity.ShowReadmeOnStart": "true",
116 | "RunOnceActivity.cidr.known.project.marker": "true",
117 | "RunOnceActivity.readMode.enableVisualFormatting": "true",
118 | "cf.first.check.clang-format": "false",
119 | "cidr.known.project.marker": "true",
120 | "dart.analysis.tool.window.visible": "false",
121 | "git-widget-placeholder": "master",
122 | "jdk.selected.JAVA_MODULE": "corretto-17",
123 | "kotlin-language-version-configured": "true",
124 | "last_opened_file_path": "C:/Users/anmol/StudioProjects/OpenTestLabAndroid/sampleandroidapp",
125 | "node.js.detected.package.eslint": "true",
126 | "node.js.selected.package.eslint": "(autodetect)",
127 | "node.js.selected.package.tslint": "(autodetect)",
128 | "nodejs_package_manager_path": "npm",
129 | "project.structure.last.edited": "Modules",
130 | "project.structure.proportion": "0.0",
131 | "project.structure.side.proportion": "0.0",
132 | "settings.editor.selected.configurable": "experimental",
133 | "show.migrate.to.gradle.popup": "false",
134 | "vue.rearranger.settings.migration": "true"
135 | },
136 | "keyToStringList": {
137 | "kotlin-gradle-user-dirs": [
138 | "/Users/chameleon/.gradle"
139 | ]
140 | }
141 | }
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 | true
168 | true
169 | false
170 | false
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 | true
190 | true
191 | false
192 | false
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 | true
212 | true
213 | false
214 | false
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 | true
234 | true
235 | false
236 | false
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 | 1713795780975
269 |
270 |
271 | 1713795780975
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 | 1720377072077
281 |
282 |
283 |
284 | 1720377072077
285 |
286 |
287 |
288 | 1720417971045
289 |
290 |
291 |
292 | 1720417971045
293 |
294 |
295 |
296 | 1720441538025
297 |
298 |
299 |
300 | 1720441538025
301 |
302 |
303 |
304 | 1720443472439
305 |
306 |
307 |
308 | 1720443472439
309 |
310 |
311 |
312 | 1720498442958
313 |
314 |
315 |
316 | 1720498442958
317 |
318 |
319 |
320 | 1720611477435
321 |
322 |
323 |
324 | 1720611477435
325 |
326 |
327 |
328 | 1720611612034
329 |
330 |
331 |
332 | 1720611612034
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 | file://$PROJECT_DIR$/androidplugin/src/main/java/dev/oianmol/opentestlab/tasks/AndroidTestDeviceFarmTask.kt
355 | 98
356 |
357 |
358 |
359 | file://$PROJECT_DIR$/src/main/kotlin/dev/oianmol/opentestlab/android/testexec/testrunner/ITestRunner.kt
360 | 106
361 |
362 |
363 |
364 | file://$PROJECT_DIR$/src/main/kotlin/dev/oianmol/opentestlab/android/testexec/TestExecutionService.kt
365 | 23
366 |
367 |
368 |
369 | file://$PROJECT_DIR$/src/main/kotlin/dev/oianmol/opentestlab/android/testexec/TestExecutionService.kt
370 | 52
371 |
372 |
373 |
374 | file://$PROJECT_DIR$/src/main/kotlin/dev/oianmol/opentestlab/android/devicefarm/DeviceFarmService.kt
375 | 13
376 |
377 |
378 |
379 | file://$PROJECT_DIR$/src/main/kotlin/dev/oianmol/opentestlab/android/devicefarm/cli/Adb.kt
380 | 56
381 |
382 |
383 |
384 |
385 |
386 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # OpenTestLabAndroid
2 |
3 |
4 |
5 |
6 | ## Overview
7 |
8 | This project enables Android developers to execute automation test cases on self-hosted servers.
9 |
10 | ## Features
11 |
12 | - Test Sharding - So more devices the better it gets 😈
13 | - Report management includes Junit XML styles report with metrics
14 | - Video recordings of every automation test!
15 |
16 | ## Setup Instructions
17 |
18 | - Configure your test server's IP address in your Android projects build.gradle(.kts)
19 | ```
20 | openTestLabConfigure {
21 | serverAddress = "localhost:8081"
22 | ignoreFailures = true
23 | }
24 | ```
25 |
26 | ## Demo Video!
27 |
28 | https://github.com/oianmol/OpenTestLabAndroid/assets/4393101/7555f848-dc0a-43dd-868d-bd58d1f66d52
29 |
30 |
31 | ## Sample Test Report Junit Report file and the test video recording!
32 |
33 |
34 |
35 | https://github.com/oianmol/OpenTestLabAndroid/assets/4393101/2d8db5da-df89-4bb8-8c6b-98386f041451
36 |
37 | ## Features
38 |
39 | - Run automation tests on a self-hosted server
40 | - Upload builds to this server and groups them by a unique build number
41 | - Collects reports from the test execution along with Junit Formate reports and recorded videos.
42 |
43 | ## Prerequisites
44 |
45 | - Java Development Kit
46 | - Android SDK
47 | - [Any other required tools or libraries] (TBD)
48 |
49 | ## Installation
50 |
51 | 1. **Clone the repository:**
52 |
53 | ```bash
54 | git clone https://github.com/oianmol/OpenTestLabAndroid.git
55 | cd OpenTestLabAndroid
56 | ```
57 |
58 | 2 **Set up the Android SDK:**
59 |
60 | Ensure the `ANDROID_HOME` environment variable is set to your Android SDK path.
61 |
62 | ```bash
63 | export ANDROID_HOME=/path/to/your/android/sdk
64 | ```
65 |
66 | 3 **Make sure JDK is there?**
67 |
68 | 4 **TBD**
69 |
70 | ## Usage
71 |
72 | 1. **Start the server:**
73 |
74 | ```bash
75 | execute Main.kt!
76 | ```
77 |
78 | 2. **Look at the sample android app:**
79 |
80 | Place your test cases in the `androidTest` directory.
81 |
82 | 3. **Execute tests:**
83 |
84 | ```bash
85 | checkout the accompanied gradle plugin it should generate a gradle task for you to run the android tests on your remote server.
86 | ```
87 |
88 | ## Configuration
89 |
90 | - **Server settings:** [Provide details on how to configure the server settings]
91 | - **Device configurations:** [Details on configuring devices for testing]
92 |
93 | ## Logging and Reporting
94 |
95 | - Logs are stored in the `logs` directory.
96 | - Test reports can be found in the `reports` directory.
97 |
98 | ## Contributing
99 |
100 | We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for more details.
101 |
102 | ## License
103 |
104 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
105 |
106 | ## Contact
107 |
108 | For any questions or support, please open an issue on GitHub or contact [anmol.verma4@gmail.com].
109 |
110 | ---
111 |
112 | Feel free to modify this template according to your project's specifics.
113 |
--------------------------------------------------------------------------------
/androidplugin/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `kotlin-dsl`
3 | kotlin("jvm") version "1.9.23"
4 | id("java-gradle-plugin")
5 | id("maven-publish")
6 | id("com.google.protobuf") version "0.9.0"
7 | }
8 |
9 | group = "dev.oianmol"
10 | version = "0.0.1"
11 |
12 | publishing {
13 | publications {
14 | create("mavenJava") {
15 | from(components["java"])
16 | artifactId = "opentestlab-plugin" // Replace with your artifact ID
17 | }
18 | }
19 | repositories {
20 | mavenLocal()
21 | }
22 | }
23 |
24 | object Versions {
25 | const val GRPC = "1.57.2"
26 | const val GRPC_KOTLIN = "1.3.1"
27 | const val PROTOBUF = "3.24.2"
28 | const val COROUTINES = "1.7.3"
29 | }
30 |
31 |
32 | repositories {
33 | google()
34 | gradlePluginPortal()
35 | maven {
36 | setUrl("https://plugins.gradle.org/m2/")
37 | }
38 | mavenCentral()
39 | }
40 |
41 |
42 | dependencies {
43 | compileOnly("com.android.tools.build:gradle:3.6.1")
44 |
45 | compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23")
46 | protobuf(project(":protos"))
47 | compileOnly("org.apache.tomcat:annotations-api:6.0.53")
48 | implementation("io.grpc:grpc-netty:1.57.2")
49 | implementation("io.grpc:grpc-stub:1.57.2")
50 | implementation("io.grpc:grpc-protobuf-lite:1.57.2")
51 | implementation("com.google.protobuf:protobuf-java-util:3.24.3")
52 | implementation("com.google.protobuf:protobuf-kotlin:3.24.3")
53 | implementation("io.grpc:grpc-kotlin-stub:1.3.1")
54 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
55 | implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.12.7")
56 | }
57 |
58 | protobuf {
59 | protoc {
60 | artifact = "com.google.protobuf:protoc:3.24.3"
61 | }
62 | plugins {
63 | create("grpc") {
64 | artifact = "io.grpc:protoc-gen-grpc-java:1.57.2"
65 | }
66 | create("grpckt") {
67 | artifact = "io.grpc:protoc-gen-grpc-kotlin:1.3.1:jdk8@jar"
68 | }
69 | }
70 | generateProtoTasks {
71 | all().forEach {
72 | it.builtins {
73 | named("java") {
74 | option("lite")
75 | }
76 | }
77 | it.plugins {
78 | create("grpc"){
79 | option("lite")
80 | }
81 | create("grpckt")
82 | }
83 | }
84 | }
85 | }
86 |
87 |
88 |
89 | gradlePlugin {
90 | plugins {
91 | register("androidDeviceFarm") {
92 | id = "dev.oianmol.androidDeviceFarm"
93 | implementationClass = "dev.oianmol.opentestlab.tasks.DeviceFarmPlugin"
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/androidplugin/src/main/java/dev/oianmol/opentestlab/tasks/AndroidTestDeviceFarmTask.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.opentestlab.tasks
2 |
3 | import com.google.protobuf.ByteString
4 | import dev.oianmol.opentestlab.*
5 | import io.grpc.ManagedChannel
6 | import io.grpc.ManagedChannelBuilder
7 | import kotlinx.coroutines.*
8 | import kotlinx.coroutines.flow.FlowCollector
9 | import kotlinx.coroutines.flow.flow
10 | import kotlinx.coroutines.flow.flowOn
11 | import org.gradle.api.DefaultTask
12 | import org.gradle.api.GradleException
13 | import org.gradle.api.Project
14 | import org.gradle.api.tasks.Input
15 | import org.gradle.api.tasks.TaskAction
16 | import java.io.File
17 | import java.util.concurrent.TimeUnit.SECONDS
18 |
19 | abstract class AndroidTestDeviceFarmTask : DefaultTask() {
20 |
21 | @Input
22 | var appBinary: String? = null
23 |
24 | @Input
25 | var applicationPackageName: String? = null
26 |
27 | @Input
28 | var applicationTestPackageName: String? = null
29 |
30 | @Input
31 | var testAppBinary: String? = null
32 |
33 | @Input
34 | var serverAddress: String? = null
35 |
36 | @TaskAction
37 | fun connectDevices() {
38 | requireNotNull(appBinary) {
39 | "APK/AAB was not built for executing test cases! please assemble first"
40 | }
41 |
42 | requireNotNull(testAppBinary) {
43 | "AndroidTest APK/AAB was not built for executing test cases! please assemble first"
44 | }
45 |
46 | logger.lifecycle("appBinary ${appBinary} testAppBinary${testAppBinary}")
47 | runBlocking {
48 | val channel = managedChannel(serverAddress)
49 | runCatchingCancellable {
50 | val apk = File(appBinary)
51 | val testApk = File(testAppBinary)
52 | with(DeviceFarmServiceGrpcKt.DeviceFarmServiceCoroutineStub(channel)) {
53 | deviceFarmInfo(this).availableDevices()?.let {
54 | dumpDebugLog("Executing Tests for Test APK ${testApk.absolutePath}")
55 | val build = buildIdentifier(project)
56 |
57 | with(TestExecutionServiceGrpcKt.TestExecutionServiceCoroutineStub(channel)) {
58 | uploadApks(this, testApk, build, apk)
59 |
60 | executeTests(this, build)
61 | }
62 |
63 | val directory = fileForTestResults(project)
64 |
65 | with(ReportManagementServiceGrpcKt.ReportManagementServiceCoroutineStub(channel)) {
66 | pullReportsFromDeviceFarm(this, build, directory, project)
67 | }
68 |
69 | withResult(directory.listFiles())
70 |
71 | close(channel)
72 | } ?: run {
73 | close(channel)
74 | throw GradleException(
75 | "We do not have any available devices to test this 🤕",
76 | )
77 | }
78 | }
79 | }.exceptionOrNull()?.let {
80 | closeAndExit(channel, it)
81 | }
82 | }
83 | }
84 |
85 | private fun closeAndExit(channel: ManagedChannel, it: Throwable) {
86 | close(channel)
87 | if (it is GradleException) {
88 | dumpDebugLog(it.stackTraceToString())
89 | throw it
90 | } else {
91 | throw it
92 | }
93 | }
94 |
95 | private suspend fun executeTests(
96 | client: TestExecutionServiceGrpcKt.TestExecutionServiceCoroutineStub,
97 | build: String,
98 | ) {
99 | client.executeTests(
100 | DeviceFarmTestSpec.newBuilder()
101 | .setCiUniqueBuildNumber(build)
102 | .setListenerClass("dev.oianmol.sampleandroidapp.runner.OpenAndroidTestRunListener")
103 | .setInstrumentPackage(applicationPackageName)
104 | .setPackageName(applicationPackageName)
105 | .setTestPackageName(applicationTestPackageName)
106 | .setCustomRunner("androidx.test.runner.AndroidJUnitRunner")
107 | .build(),
108 | )
109 | }
110 |
111 | private suspend fun uploadApks(
112 | client: TestExecutionServiceGrpcKt.TestExecutionServiceCoroutineStub,
113 | testApk: File,
114 | build: String,
115 | apk: File,
116 | ) {
117 | val uploadJob = runCatching {
118 | client.uploadTestApk(
119 | flow {
120 | uploadAndroidApk(this, FileType.TestApk, testApk, build)
121 | uploadAndroidApk(this, FileType.AndroidApk, apk, build)
122 | currentCoroutineContext().cancel()
123 | },
124 | ).flowOn(Dispatchers.IO).collect {
125 | dumpDebugLog(it.toString())
126 | }
127 | }
128 | dumpDebugLog(uploadJob.toString())
129 | }
130 |
131 | private suspend fun uploadAndroidApk(
132 | flowCollector: FlowCollector,
133 | androidApk: FileType,
134 | testApk: File,
135 | build: String,
136 | ) {
137 | var totalSize = testApk.length()
138 | val stream = testApk.inputStream()
139 | stream.use { fileInputStream ->
140 | while (totalSize != 0L) {
141 | val bytes = ByteArray(Math.min(4086L, totalSize).toInt()) //
142 | // we read either 4086 or less bytes pending to read from stream
143 | val read = withContext(Dispatchers.IO) {
144 | fileInputStream.read(bytes)
145 | }
146 | // we reduce the read amount from total size
147 | totalSize -= read
148 | flowCollector.emit(
149 | TestApkUpload.newBuilder()
150 | .setCiUniqueBuildNumber(build)
151 | .setFileType(androidApk)
152 | .setTestApk(ByteString.copyFrom(bytes))
153 | .build(),
154 | )
155 | }
156 | }
157 | }
158 |
159 | private fun close(channel: ManagedChannel) {
160 | channel.shutdown().awaitTermination(5L, SECONDS)
161 | }
162 |
163 | private fun DeviceFarmInfo.availableDevices(): MutableList? {
164 | return this.machinesList.firstOrNull()?.devicesList?.takeIf { it.isNotEmpty() }
165 | }
166 |
167 | private fun dumpDebugLog(message: String) {
168 | project.logger.lifecycle(message)
169 | }
170 |
171 | private fun withResult(testResultFiles: Array?) {
172 | dumpDebugLog("We will store the test results so that we upload them to CircleCI artifacts.")
173 | testResultFiles?.filter { it.isFile && it.extension == "xml" }?.takeIf { it.isNotEmpty() }
174 | ?.let {
175 | val content = it.map {
176 | it.readText()
177 | }
178 | val hasFailures = content.filter {
179 | it.contains("
199 | dumpDebugLog("Machine ${index.plus(1)}...$machine")
200 | }
201 | return deviceFarm
202 | }
203 | }
204 |
205 | suspend fun pullReportsFromDeviceFarm(
206 | client: ReportManagementServiceGrpcKt.ReportManagementServiceCoroutineStub,
207 | build: String,
208 | directory: File,
209 | project: Project,
210 | ) {
211 | runCatching {
212 | client.pullReportFiles(
213 | ReportsRequest.newBuilder()
214 | .setCiUniqueBuildNumber(build)
215 | .build(),
216 | ).flowOn(Dispatchers.IO).collect { reportFiles ->
217 | File(directory, reportFiles.fileName)
218 | .also {
219 | it.appendBytes(reportFiles.reportFile.toByteArray())
220 | }
221 | }
222 | }
223 | }
224 |
225 | fun managedChannel(serverAddress: String?): ManagedChannel = ManagedChannelBuilder.forTarget(serverAddress)
226 | .usePlaintext()
227 | .maxInboundMessageSize(Int.MAX_VALUE)
228 | .maxInboundMetadataSize(Int.MAX_VALUE)
229 | .build()
230 |
231 | fun fileForTestResults(project: Project): File {
232 | val userHome = File(System.getProperty("user.home"))
233 | val testResultsDir = File(userHome, "test-results")
234 | val directory = File(testResultsDir, "androidTestResults")
235 | project.logger.lifecycle("Saving report to ${directory.absolutePath}")
236 | directory.deleteRecursively()
237 | directory.mkdirs()
238 | return directory
239 | }
240 |
241 | fun buildIdentifier(project: Project) =
242 | "${project.getCurrentGitBranchName()}${System.getenv("CIRCLE_BUILD_NUM") ?: "${System.currentTimeMillis()}"}"
243 |
244 | private fun Project.testApks(): Pair {
245 | val testApkFileDir =
246 | File(projectDir.parentFile, "app/build/outputs/apk/androidTest/debug")
247 | val apkDir = File(projectDir.parentFile, "app/build/outputs/apk/debug")
248 |
249 | val testApk =
250 | testApkFileDir.listFiles()?.firstOrNull { it.isFile && it.extension == "apk" }
251 | val apk = apkDir.listFiles()?.first { it.isFile && it.extension == "apk" }
252 | require(testApk != null)
253 | require(apk != null)
254 | return Pair(testApk, apk)
255 | }
256 |
--------------------------------------------------------------------------------
/androidplugin/src/main/java/dev/oianmol/opentestlab/tasks/DeviceFarmPlugin.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.opentestlab.tasks
2 |
3 | import com.android.build.gradle.AppExtension
4 | import com.android.build.gradle.TestedExtension
5 | import com.android.build.gradle.api.TestVariant
6 | import dev.oianmol.opentestlab.tasks.DeviceFarmPlugin.Companion.GRADLE_METHOD_NAME
7 | import dev.oianmol.opentestlab.tasks.DeviceFarmPlugin.Companion.OPEN_TEST_LAB
8 | import org.gradle.api.GradleException
9 | import org.gradle.api.Plugin
10 | import org.gradle.api.Project
11 | import org.gradle.api.Task
12 | import java.util.*
13 |
14 |
15 | class DeviceFarmPlugin : Plugin {
16 | companion object {
17 | const val OPEN_TEST_LAB = "OpenTestLab"
18 | const val GRADLE_METHOD_NAME = "openTestLabConfigure"
19 | }
20 |
21 | override fun apply(project: Project) {
22 | project.setupTestLabExtension()
23 |
24 | project.afterEvaluate {
25 | logger.lifecycle("*************** Open Test Lab Plugin ***************")
26 | setupOpenTestLab()
27 | logger.lifecycle("********************************************************")
28 | }
29 | }
30 | }
31 |
32 | internal fun Project.setupTestLabExtension() {
33 | extensions.create(
34 | GRADLE_METHOD_NAME,
35 | OpenTestLabExtension::class.java,
36 | project
37 | )
38 | }
39 |
40 | internal fun Project.setupOpenTestLab() {
41 | val androidExtension: Any? = project.extensions.findByName("android")
42 | project.extensions.findByType(OpenTestLabExtension::class.java)?.apply {
43 | val openTestLabExtension = this
44 | configureTestTasks(androidExtension as AppExtension, openTestLabExtension)
45 | }
46 | }
47 |
48 | internal fun Project.configureTestTasks(androidExtension: AppExtension, openTestLabExtension: OpenTestLabExtension) {
49 | (androidExtension as TestedExtension).apply {
50 | testVariants.toList().forEach { testVariant ->
51 | val identifier =
52 | testVariant.name.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
53 | tasks.register(
54 | "openDeviceFarmTest$identifier",
55 | AndroidTestDeviceFarmTask::class.java,
56 | ) {
57 | group = OPEN_TEST_LAB
58 | description =
59 | "Execute DeviceFarm Test for $identifier"
60 | val appVariant = androidExtension.applicationVariants.toList()
61 | .firstOrNull {
62 | it.buildType == testVariant.buildType
63 | && it.flavorName == testVariant.flavorName
64 | }!!
65 | dependsOn(arrayOf(resolveAssemble(testVariant)))
66 | dependsOn(arrayOf(resolveTestAssemble(testVariant)))
67 |
68 | doFirst {
69 | if (openTestLabExtension.serverAddress.isNullOrEmpty()) {
70 | throw GradleException("You need to set openTestLabExtension.serverAddress = 'your-server-public-ip' before run")
71 | }
72 | }
73 |
74 | serverAddress = openTestLabExtension.serverAddress
75 | appBinary = appVariant.outputs.firstOrNull()?.outputFile?.absolutePath
76 | applicationPackageName = appVariant.applicationId
77 | applicationTestPackageName = testVariant.applicationId
78 | testAppBinary = testVariant.outputs.firstOrNull()?.outputFile?.absolutePath
79 | }
80 | tasks.register(
81 | "openDeviceFarmFetchReportFor$identifier",
82 | TestLabPullReportTask::class.java,
83 | ) {
84 | group = OPEN_TEST_LAB
85 | serverAddress = openTestLabExtension.serverAddress
86 | description = "Open DeviceFarm Fetch Report"
87 | }
88 | }
89 | }
90 | }
91 |
92 | private fun resolveTestAssemble(variant: TestVariant): Task = try {
93 | variant.assembleProvider.get()
94 | } catch (e: IllegalStateException) {
95 | variant.assemble
96 | }
97 |
98 | private fun resolveAssemble(variant: TestVariant): Task = try {
99 | variant.testedVariant.assembleProvider.get()
100 | } catch (e: IllegalStateException) {
101 | variant.testedVariant.assemble
102 | }
103 |
--------------------------------------------------------------------------------
/androidplugin/src/main/java/dev/oianmol/opentestlab/tasks/OpenTestLabExtension.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.opentestlab.tasks
2 |
3 | import org.gradle.api.Project
4 |
5 | open class OpenTestLabExtension(private val project: Project) {
6 | var serverAddress: String? = null
7 | var ignoreFailures: Boolean = false
8 | }
--------------------------------------------------------------------------------
/androidplugin/src/main/java/dev/oianmol/opentestlab/tasks/TestLabPullReportTask.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.opentestlab.tasks
2 |
3 | import dev.oianmol.opentestlab.ReportManagementServiceGrpcKt
4 | import kotlinx.coroutines.runBlocking
5 | import org.gradle.api.DefaultTask
6 | import org.gradle.api.tasks.Input
7 | import org.gradle.api.tasks.TaskAction
8 | import java.util.concurrent.TimeUnit.SECONDS
9 |
10 | abstract class TestLabPullReportTask : DefaultTask() {
11 |
12 | @Input
13 | var serverAddress: String? = null
14 |
15 | @TaskAction
16 | fun pullTestReports() {
17 | val channel = managedChannel(serverAddress)
18 | runBlocking {
19 | runCatching {
20 | with(ReportManagementServiceGrpcKt.ReportManagementServiceCoroutineStub(channel)) {
21 | val build = buildIdentifier(project)
22 | project.logger.info("Build $build")
23 | val directory = fileForTestResults(project)
24 | project.logger.info("fileForTestResults ${directory.absolutePath}")
25 | pullReportsFromDeviceFarm(this, build, directory, project)
26 | }
27 | }
28 | }
29 | channel.shutdown().awaitTermination(5L, SECONDS)
30 | }
31 | }
--------------------------------------------------------------------------------
/androidplugin/src/main/java/dev/oianmol/opentestlab/tasks/getCurrentGitBranchName.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.opentestlab.tasks
2 |
3 | import org.gradle.api.Project
4 | import java.io.ByteArrayOutputStream
5 |
6 | fun Project.getCurrentGitBranchName(): String {
7 | val stdout = ByteArrayOutputStream()
8 | exec {
9 | this.commandLine("git", "rev-parse", "--abbrev-ref", "HEAD")
10 | this.standardOutput = stdout
11 | }
12 | return stdout.toString().trim()
13 | }
--------------------------------------------------------------------------------
/androidplugin/src/main/java/dev/oianmol/opentestlab/tasks/runCatchingCancellable.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.opentestlab.tasks
2 |
3 | import kotlin.Result
4 | import kotlin.coroutines.cancellation.CancellationException
5 |
6 | suspend fun runCatchingCancellable(block: suspend () -> R): Result {
7 | return try {
8 | Result.success(block())
9 | } catch (c: CancellationException) {
10 | throw c
11 | } catch (e: Throwable) {
12 | Result.failure(e)
13 | }
14 | }
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.google.protobuf.gradle.id
2 |
3 | plugins {
4 | kotlin("jvm") version "1.9.23"
5 | id("java")
6 | application
7 | id("com.google.protobuf") version "0.9.0"
8 | }
9 |
10 | group = "dev.oianmol"
11 | version = "1.0-SNAPSHOT"
12 |
13 | object Versions {
14 | const val GRPC = "1.57.2"
15 | const val GRPC_KOTLIN = "1.3.1"
16 | const val PROTOBUF = "3.24.2"
17 | const val COROUTINES = "1.7.3"
18 | }
19 |
20 |
21 | repositories {
22 | mavenCentral()
23 | google()
24 | }
25 |
26 | dependencies {
27 | protobuf(project(":protos"))
28 | implementation("io.insert-koin:koin-core:3.4.0")
29 | implementation("io.grpc:grpc-netty-shaded:1.57.2")
30 | implementation("com.google.protobuf:protobuf-java-util:3.24.2")
31 | implementation("com.google.protobuf:protobuf-kotlin:3.24.2")
32 | implementation("io.grpc:grpc-protobuf:1.57.2")
33 | implementation("io.grpc:grpc-stub:1.57.2")
34 | implementation("io.grpc:grpc-kotlin-stub:1.3.1")
35 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
36 |
37 | implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.12.7")
38 | implementation("javax.xml.stream:stax-api:1.0-2")
39 |
40 | testImplementation(kotlin("test"))
41 | testImplementation("io.mockk:mockk:1.13.7")
42 | testImplementation("app.cash.turbine:turbine:1.0.0")
43 | testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
44 | testImplementation("io.grpc:grpc-testing:1.51.0")
45 | }
46 |
47 | protobuf {
48 | protoc {
49 | artifact = "com.google.protobuf:protoc:${Versions.PROTOBUF}"
50 | }
51 |
52 | plugins {
53 | id("grpc") {
54 | artifact = "io.grpc:protoc-gen-grpc-java:${Versions.GRPC}"
55 | }
56 | id("grpckt") {
57 | artifact = "io.grpc:protoc-gen-grpc-kotlin:${Versions.GRPC_KOTLIN}:jdk8@jar"
58 | }
59 | }
60 | generateProtoTasks {
61 | ofSourceSet("main").forEach {
62 | it.plugins {
63 | id("grpc") {
64 | option("lite")
65 | }
66 | id("grpckt") {
67 | option("lite")
68 | }
69 | }
70 |
71 | it.builtins {
72 | id("kotlin") {
73 | option("lite")
74 | }
75 | }
76 | }
77 | }
78 | }
79 |
80 | tasks.test {
81 | useJUnitPlatform()
82 | }
83 |
84 | application {
85 | mainClass.set("MainKt")
86 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
2 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.5.0"
3 | kotlin = "1.9.0"
4 | coreKtx = "1.13.1"
5 | junit = "4.13.2"
6 | junitVersion = "1.2.1"
7 | espressoCore = "3.6.1"
8 | lifecycleRuntimeKtx = "2.8.3"
9 | activityCompose = "1.9.0"
10 | composeBom = "2024.04.01"
11 |
12 | [libraries]
13 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
14 | junit = { group = "junit", name = "junit", version.ref = "junit" }
15 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
16 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
17 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
18 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
19 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
20 | androidx-ui = { group = "androidx.compose.ui", name = "ui" }
21 | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
22 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
23 | androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
24 | androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
25 | androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
26 | androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
27 |
28 | [plugins]
29 | android-application = { id = "com.android.application", version.ref = "agp" }
30 | jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
31 |
32 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oianmol/OpenTestLabAndroid/d9bfb402da88f6f1c88172269dfb1bfe5610fba7/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Jul 07 23:37:43 IST 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Use "xargs" to parse quoted args.
209 | #
210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
211 | #
212 | # In Bash we could simply go:
213 | #
214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
215 | # set -- "${ARGS[@]}" "$@"
216 | #
217 | # but POSIX shell has neither arrays nor command substitution, so instead we
218 | # post-process each arg (as a line of input to sed) to backslash-escape any
219 | # character that might be a shell metacharacter, then use eval to reverse
220 | # that process (while maintaining the separation between arguments), and wrap
221 | # the whole thing up as a single "set" statement.
222 | #
223 | # This will of course break if any of these variables contains a newline or
224 | # an unmatched quote.
225 | #
226 |
227 | eval "set -- $(
228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
229 | xargs -n1 |
230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
231 | tr '\n' ' '
232 | )" '"$@"'
233 |
234 | exec "$JAVACMD" "$@"
235 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/orchestrator-1.4.1.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oianmol/OpenTestLabAndroid/d9bfb402da88f6f1c88172269dfb1bfe5610fba7/orchestrator-1.4.1.apk
--------------------------------------------------------------------------------
/protos/build.gradle.kts:
--------------------------------------------------------------------------------
1 | version = "unspecified"
2 | plugins {
3 | `java-library`
4 | }
5 |
6 | java {
7 | sourceSets.getByName("main").resources.srcDir("src/main/proto")
8 | }
--------------------------------------------------------------------------------
/protos/src/main/proto/device_farm.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package dev.oianmol.opentestlab;
4 |
5 | option java_multiple_files = true;
6 |
7 | // Represents a device in the device farm.
8 | message DeviceFarmDevice {
9 | string ipAddress = 1; // The IP address of the device.
10 | string port = 2; // The port of the device.
11 | string serialNumber = 3; // The serial number of the device.
12 | string model = 4; // The model of the device.
13 | }
14 |
15 | // Contains information about the device farm, including test machines.
16 | message DeviceFarmInfo {
17 | repeated TestMachine machines = 1; // A list of test machines in the device farm.
18 | }
19 |
20 | // Represents a test machine in the system.
21 | message TestMachine {
22 | string ipAddress = 1; // The IP address of the test machine.
23 | string name = 2; // The name of the test machine.
24 | string location = 3; // The location of the test machine.
25 | bool isBusy = 4; // Indicates if the test machine is busy.
26 | repeated DeviceFarmDevice devices = 5; // A list of devices connected to the test machine.
27 | }
28 |
29 | // An empty message typically used for requests that do not need any input.
30 | message Empty {}
31 |
32 | // Enum representing the type of file being uploaded.
33 | enum FileType {
34 | TestApk = 0; // Test APK file.
35 | AndroidApk = 1; // Android APK file.
36 | AppBundle = 2; // The android app bundle.
37 | }
38 |
--------------------------------------------------------------------------------
/protos/src/main/proto/device_farm_service.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package dev.oianmol.opentestlab;
4 |
5 | import "device_farm.proto";
6 | import "test_execution.proto";
7 | import "report_management.proto";
8 |
9 | option java_multiple_files = true;
10 |
11 | // Service for managing device farm information.
12 | service DeviceFarmService {
13 | // Retrieves the device farm information.
14 | rpc GetDeviceFarm(Empty) returns (DeviceFarmInfo);
15 | }
16 |
17 | // Service for test execution on the device farm.
18 | service TestExecutionService {
19 | // Uploads a test APK and returns log messages.
20 | rpc UploadTestApk(stream TestApkUpload) returns (stream StreamLogs);
21 |
22 | // Executes tests based on the provided specification and returns the results.
23 | rpc ExecuteTests(DeviceFarmTestSpec) returns (DeviceFarmTestResults);
24 | }
25 |
26 | // Service for managing test reports.
27 | service ReportManagementService {
28 | // Pulls report files for a specific CI build.
29 | rpc PullReportFiles(ReportsRequest) returns (stream ReportFiles);
30 | }
--------------------------------------------------------------------------------
/protos/src/main/proto/report_management.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package dev.oianmol.opentestlab;
4 |
5 | option java_multiple_files = true;
6 |
7 | // Represents report files.
8 | message ReportFiles {
9 | bytes reportFile = 1; // The binary content of the report file.
10 | string fileName = 2; // The name of the report file.
11 | }
12 |
13 | // Request for pulling report files.
14 | message ReportsRequest {
15 | string ciUniqueBuildNumber = 1; // The unique build number from the CI system.
16 | }
17 |
--------------------------------------------------------------------------------
/protos/src/main/proto/test_execution.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 | import "device_farm.proto";
3 |
4 | package dev.oianmol.opentestlab;
5 |
6 | option java_multiple_files = true;
7 |
8 | // Specifies the test details to be executed on the device farm.
9 | message DeviceFarmTestSpec {
10 | string ciUniqueBuildNumber = 1; // The unique build number from the CI system.
11 | string listenerClass = 2; // The class name of the test listener.
12 | string testPackageFilter = 3; // The filter for test packages.
13 | string instrumentPackage = 4; // The instrumentation package name.
14 | string customRunner = 5; // The custom test runner class name.
15 | string packageName = 6;
16 | string testPackageName = 7;
17 | }
18 |
19 | // Contains the results of the tests executed.
20 | message DeviceFarmTestResults {
21 |
22 | // Represents a name-value pair property.
23 | message Property {
24 | string name = 1; // The name of the property.
25 | string value = 2; // The value of the property.
26 | }
27 |
28 | // Contains a list of properties.
29 | message Properties {
30 | repeated Property property = 1; // A list of properties.
31 | }
32 |
33 | // Represents a single test case result.
34 | message Testcase {
35 | string classname = 1; // The class name of the test case.
36 | string name = 2; // The name of the test case.
37 | string status = 3; // The status of the test case (e.g., passed, failed).
38 | uint32 time = 4; // The time taken by the test case.
39 | string trace = 5; // The stack trace if the test case failed.
40 | string message = 6; // The failure message if the test case failed.
41 | string type = 7; // The type of the test case.
42 | }
43 |
44 | // Represents a suite of tests.
45 | message Testsuite {
46 | uint32 errors = 1; // The number of errors in the test suite.
47 | uint32 failures = 2; // The number of failures in the test suite.
48 | string hostname = 3; // The hostname where the test suite was run.
49 | string name = 4; // The name of the test suite.
50 | Properties properties = 5; // The properties of the test suite.
51 | uint32 skipped = 6; // The number of skipped tests in the test suite.
52 | repeated Testcase testcase = 7; // A list of test cases in the test suite.
53 | uint32 tests = 8; // The total number of tests in the test suite.
54 | uint32 time = 9; // The total time taken by the test suite.
55 | string timestamp = 10; // The timestamp when the test suite was run.
56 | }
57 |
58 | repeated Testsuite testsuite = 1; // A list of test suites.
59 | }
60 |
61 | // Used for uploading test APK files.
62 | message TestApkUpload {
63 | bytes testApk = 1; // The binary content of the test APK.
64 | string ciUniqueBuildNumber = 2; // The unique build number from the CI system.
65 | FileType fileType = 3; // The type of the file being uploaded.
66 | }
67 |
68 | // Represents a log message stream.
69 | message StreamLogs {
70 | string message = 1; // The log message.
71 | }
72 |
--------------------------------------------------------------------------------
/sampleandroidapp/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
--------------------------------------------------------------------------------
/sampleandroidapp/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/sampleandroidapp/.idea/androidTestResultsUserPreferences.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/sampleandroidapp/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/sampleandroidapp/.idea/deploymentTargetSelector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/sampleandroidapp/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/sampleandroidapp/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/sampleandroidapp/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/sampleandroidapp/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/sampleandroidapp/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/sampleandroidapp/.idea/other.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
--------------------------------------------------------------------------------
/sampleandroidapp/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/sampleandroidapp/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/sampleandroidapp/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application)
3 | alias(libs.plugins.jetbrains.kotlin.android)
4 | alias(libs.plugins.device.farm)
5 | }
6 |
7 | openTestLabConfigure {
8 | serverAddress = "localhost:8081"
9 | ignoreFailures = true
10 | }
11 |
12 | android {
13 | namespace = "dev.oianmol.sampleandroidapp"
14 | compileSdk = 34
15 |
16 | defaultConfig {
17 | applicationId = "dev.oianmol.sampleandroidapp"
18 | minSdk = 21
19 | targetSdk = 34
20 | versionCode = 1
21 | versionName = "1.0"
22 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
23 | testInstrumentationRunnerArguments["listener"] =
24 | "dev.oianmol.sampleandroidapp.runner.OpenAndroidTestRunListener"
25 | testInstrumentationRunnerArguments["clearPackageData"] = "true"
26 | vectorDrawables {
27 | useSupportLibrary = true
28 | }
29 |
30 | testOptions {
31 | unitTests {
32 | isIncludeAndroidResources = true
33 | isReturnDefaultValues = true
34 | }
35 | execution = "ANDROIDX_TEST_ORCHESTRATOR"
36 | }
37 | }
38 |
39 | flavorDimensionList += listOf("version")
40 |
41 | productFlavors {
42 | create("demo") {
43 | dimension = "version"
44 | }
45 |
46 | create("full") {
47 | dimension = "version"
48 | }
49 |
50 | }
51 |
52 | buildTypes {
53 | release {
54 | isMinifyEnabled = false
55 | proguardFiles(
56 | getDefaultProguardFile("proguard-android-optimize.txt"),
57 | "proguard-rules.pro"
58 | )
59 | }
60 | }
61 | kotlin {
62 | jvmToolchain(17)
63 | }
64 | buildFeatures {
65 | compose = true
66 | }
67 | composeOptions {
68 | kotlinCompilerExtensionVersion = "1.5.1"
69 | }
70 | packaging {
71 | resources {
72 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
73 | }
74 | }
75 | }
76 |
77 | dependencies {
78 | implementation(libs.androidx.core.ktx)
79 | implementation(libs.androidx.lifecycle.runtime.ktx)
80 | implementation(libs.androidx.activity.compose)
81 | implementation(platform(libs.androidx.compose.bom))
82 | implementation(libs.androidx.ui)
83 | implementation(libs.androidx.ui.graphics)
84 | implementation(libs.androidx.ui.tooling.preview)
85 | implementation(libs.androidx.material3)
86 | testImplementation(libs.junit)
87 | androidTestImplementation(libs.androidx.runner)
88 | androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0")
89 | androidTestImplementation(libs.androidx.junit)
90 | androidTestImplementation(libs.androidx.espresso.core)
91 | androidTestImplementation(platform(libs.androidx.compose.bom))
92 | androidTestImplementation(libs.androidx.ui.test.junit4)
93 | debugImplementation(libs.androidx.ui.tooling)
94 | debugImplementation(libs.androidx.ui.test.manifest)
95 | }
--------------------------------------------------------------------------------
/sampleandroidapp/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/androidTest/java/dev/oianmol/sampleandroidapp/MainActivityTest.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.sampleandroidapp
2 |
3 |
4 | import androidx.compose.ui.test.assertIsDisplayed
5 | import androidx.compose.ui.test.junit4.createComposeRule
6 | import androidx.compose.ui.test.onNodeWithText
7 | import androidx.test.ext.junit.runners.AndroidJUnit4
8 | import dev.oianmol.sampleandroidapp.ui.theme.SampleandroidappTheme
9 | import org.junit.Rule
10 | import org.junit.Test
11 | import org.junit.runner.RunWith
12 |
13 | @RunWith(AndroidJUnit4::class)
14 | class MainActivityTest {
15 |
16 | @get:Rule
17 | val composeTestRule = createComposeRule()
18 |
19 | @Test
20 | fun greetingDisplayedWithNameAndroid() {
21 | composeTestRule.setContent {
22 | SampleandroidappTheme {
23 | Greeting(name = "Android")
24 | }
25 | }
26 |
27 | composeTestRule.onNodeWithText("Hello Android!").assertIsDisplayed()
28 | }
29 |
30 | @Test
31 | fun greetingDisplayedWithNameWorld() {
32 | composeTestRule.setContent {
33 | SampleandroidappTheme {
34 | Greeting(name = "World")
35 | }
36 | }
37 |
38 | composeTestRule.onNodeWithText("Hello World!").assertIsDisplayed()
39 | }
40 |
41 | @Test
42 | fun greetingDisplayedWithNameJohn() {
43 | composeTestRule.setContent {
44 | SampleandroidappTheme {
45 | Greeting(name = "John")
46 | }
47 | }
48 |
49 | composeTestRule.onNodeWithText("Hello John!").assertIsDisplayed()
50 | }
51 |
52 | @Test
53 | fun greetingDisplayedWithNameJane() {
54 | composeTestRule.setContent {
55 | SampleandroidappTheme {
56 | Greeting(name = "Jane")
57 | }
58 | }
59 |
60 | composeTestRule.onNodeWithText("Hello Jane!").assertIsDisplayed()
61 | }
62 |
63 | @Test
64 | fun greetingDisplayedWithNameTest() {
65 | composeTestRule.setContent {
66 | SampleandroidappTheme {
67 | Greeting(name = "Test")
68 | }
69 | }
70 |
71 | composeTestRule.onNodeWithText("Hello Test!").assertIsDisplayed()
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/androidTest/java/dev/oianmol/sampleandroidapp/rules/InstrumentationTestingUtils.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.sampleandroidapp.rules
2 |
3 | import android.os.Build.VERSION_CODES
4 | import android.provider.Settings.Global
5 | import androidx.annotation.RequiresApi
6 | import androidx.test.platform.app.InstrumentationRegistry
7 | import androidx.test.uiautomator.UiDevice
8 | import java.io.IOException
9 |
10 | object InstrumentationTestingUtils {
11 | @RequiresApi(VERSION_CODES.N)
12 | fun disableAnimations() {
13 | executeShellCommand(
14 | "settings put global " + Global.TRANSITION_ANIMATION_SCALE + " 0.0"
15 | )
16 | executeShellCommand(
17 | "settings put global " + Global.WINDOW_ANIMATION_SCALE + " 0.0"
18 | )
19 | executeShellCommand(
20 | "settings put global " + Global.ANIMATOR_DURATION_SCALE + "0.0"
21 | )
22 | }
23 |
24 | @RequiresApi(VERSION_CODES.N)
25 | fun enableAnimations() {
26 | executeShellCommand(
27 | "settings put global " + Global.TRANSITION_ANIMATION_SCALE + " 1.0"
28 | )
29 | executeShellCommand(
30 | "settings put global " + Global.WINDOW_ANIMATION_SCALE + " 1.0"
31 | )
32 | executeShellCommand(
33 | "settings put global " + Global.ANIMATOR_DURATION_SCALE + " 1.0"
34 | )
35 | }
36 |
37 | fun executeShellCommand(command: String) {
38 | try {
39 | UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
40 | .executeShellCommand(command)
41 | } catch (ignore: IOException) {
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/androidTest/java/dev/oianmol/sampleandroidapp/rules/Logcat.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.sampleandroidapp.rules
2 |
3 | import android.util.Log
4 | import java.io.BufferedReader
5 | import java.io.IOException
6 | import java.io.InputStreamReader
7 | import java.util.concurrent.TimeUnit
8 |
9 | object Logcat {
10 | private const val TAG = "OpentestLabLogcatCapture"
11 | fun clearLogcat() {
12 | try {
13 | Runtime.getRuntime().exec(arrayOf("logcat", "-c"))
14 | } catch (e: IOException) {
15 | Log.e(TAG, "Could not clear logcat", e)
16 | }
17 | }
18 |
19 | /**
20 | * Return adb logs since the start of the specified test.
21 | *
22 | * Based on https://www.braze.com/resources/articles/logcat-junit-android-tests
23 | */
24 | fun getTestLogs(testName: String): String {
25 | val logLines = StringBuilder()
26 |
27 | // A snippet of text that uniquely determines where the relevant logs start in the logcat
28 | val testStartMessage = "TestRunner: started: $testName"
29 | val testFinishMessage = "TestRunner: finished: $testName"
30 |
31 | // When true, write every line from the logcat buffer to the string builder
32 | var recording = false
33 |
34 | // Logcat command:
35 | // -d asks the command to completely dump to our buffer, then return
36 | // -v threadtime sets the output log format
37 | val command = arrayOf("logcat", "-d", "-v", "threadtime")
38 | var bufferedReader: BufferedReader? = null
39 | val timeStart = System.currentTimeMillis()
40 | try {
41 | val process: Process = Runtime.getRuntime().exec(command)
42 | bufferedReader = BufferedReader(InputStreamReader(process.getInputStream()))
43 | var line: String?
44 | while (bufferedReader.readLine().also { line = it } != null) {
45 | if (line?.contains(testStartMessage) == true) {
46 | recording = true
47 | }
48 | if (recording) {
49 | logLines.append(line)
50 | logLines.append('\n')
51 | }
52 | val timeDiff = System.currentTimeMillis().minus(timeStart)
53 | if (TimeUnit.MILLISECONDS.toSeconds(timeDiff) > 20 || line?.contains(
54 | testFinishMessage,
55 | ) == true
56 | ) {
57 | break
58 | }
59 | }
60 | } catch (e: IOException) {
61 | Log.e(TAG, "Failed to run logcat command", e)
62 | } finally {
63 | if (bufferedReader != null) {
64 | try {
65 | bufferedReader.close()
66 | } catch (e: IOException) {
67 | Log.e(TAG, "Failed to close buffered reader", e)
68 | }
69 | }
70 | }
71 | return logLines.toString()
72 | }
73 |
74 | }
75 |
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/androidTest/java/dev/oianmol/sampleandroidapp/runner/OpenAndroidTestRunListener.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.sampleandroidapp.runner
2 |
3 | import android.app.Instrumentation
4 | import android.os.Build
5 | import android.os.Build.VERSION
6 | import android.os.Environment
7 | import android.os.ParcelFileDescriptor
8 | import android.os.ParcelFileDescriptor.AutoCloseInputStream
9 | import android.util.Log
10 | import androidx.test.internal.runner.listener.InstrumentationRunListener
11 | import dev.oianmol.opentestlab.tasks.android.runner.TestSuiteXmlGen
12 | import dev.oianmol.sampleandroidapp.rules.InstrumentationTestingUtils
13 | import kotlinx.coroutines.CoroutineScope
14 | import kotlinx.coroutines.Dispatchers
15 | import kotlinx.coroutines.launch
16 | import kotlinx.coroutines.withContext
17 | import org.junit.runner.Description
18 | import org.junit.runner.Result
19 | import org.junit.runner.notification.Failure
20 | import java.io.File
21 | import java.io.FileInputStream
22 | import java.text.SimpleDateFormat
23 | import java.util.Date
24 | import java.util.TimeZone
25 |
26 | class OpenAndroidTestRunListener : InstrumentationRunListener(),
27 | CoroutineScope by CoroutineScope(Dispatchers.IO) {
28 |
29 | private var videoRecordingDescriptor: ParcelFileDescriptor? = null
30 | private val deviceModel: String = Build.MODEL
31 | private val deviceApiLevel = VERSION.SDK_INT.toString()
32 | private var currentTestSuite: Testsuite = Testsuite()
33 | private fun deviceInfo() = deviceModel + "_" + deviceApiLevel
34 |
35 | private fun startTimeIso(): String {
36 | val df = SimpleDateFormat(
37 | "yyyy-MM-dd'T'HH:mm'Z'",
38 | )
39 | df.timeZone = TimeZone.getTimeZone("UTC")
40 | return df.format(Date())
41 | }
42 |
43 | private fun properties() = Properties(
44 | property = mutableListOf(
45 | Property("model", Build.MODEL),
46 | Property("apiLevel", VERSION.SDK_INT.toString()),
47 | Property("manufacturer", Build.MANUFACTURER),
48 | ),
49 | )
50 |
51 | private fun xmlFile(): File {
52 | val fileName = "androidTest_${currentTestSuite.name}.xml"
53 | val file = File(testFileDir(), fileName)
54 | if (file.exists().not()) {
55 | file.createNewFile()
56 | }
57 | return file
58 | }
59 |
60 | private fun videoFile(methodName: String, className: String): File {
61 | val displayName = "$methodName($className)"
62 | val fileName = "${displayName}.mp4"
63 | return File(testFileDir(), fileName)
64 | }
65 |
66 | override fun testRunStarted(description: Description) {
67 | Log.d("OpenAndroidTestRunListener", description.toString())
68 | currentTestSuite = Testsuite()
69 | currentTestSuite = currentTestSuite.copy(
70 | properties = currentTestSuite.properties ?: properties(),
71 | hostname = currentTestSuite.hostname ?: deviceInfo(),
72 | timestamp = currentTestSuite.timestamp ?: startTimeIso(),
73 | name = currentTestSuite.name ?: deviceInfo(),
74 | testcase = currentTestSuite.testcase,
75 | testStartTime = currentTestSuite.testStartTime ?: System.currentTimeMillis(),
76 | )
77 | }
78 |
79 | private fun startScreenRecord(description: Description) {
80 | launch {
81 | withContext(Dispatchers.IO) {
82 | try {
83 | videoRecordingDescriptor = instrumentation.uiAutomation.executeShellCommand(
84 | "screenrecord ${
85 | videoFile(
86 | methodName = description.methodName,
87 | className = description.className,
88 | )
89 | }",
90 | )
91 | // Read the input stream fully.
92 | val fis: FileInputStream = AutoCloseInputStream(videoRecordingDescriptor)
93 | fis.use {
94 | while (fis.read() != -1);
95 | }
96 | } catch (e: Exception) {
97 | e.printStackTrace()
98 | }
99 | }
100 | }
101 | }
102 |
103 | private fun stopScreenRecord() {
104 | launch {
105 | withContext(Dispatchers.IO) {
106 | try {
107 | videoRecordingDescriptor?.close()
108 | InstrumentationTestingUtils.executeShellCommand("pkill -2 screenrecord")
109 | } catch (e: Exception) {
110 | e.printStackTrace()
111 | }
112 | }
113 | }
114 | }
115 |
116 | override fun testRunFinished(result: Result) {
117 | currentTestSuite.testcase = currentTestSuite.testCases.values.toMutableList()
118 | currentTestSuite = currentTestSuite.copy(
119 | failures = currentTestSuite.testcase.filter { it.status == Status.FAILED.toString() }
120 | .size.toString(),
121 | tests = currentTestSuite.testcase.size.toString(),
122 | skipped = currentTestSuite.testcase.filter { it.status == Status.IGNORED.toString() }
123 | .size.toString(),
124 | errors = currentTestSuite.testcase.filter { it.status == Status.ASSUMPTION_FAILED.toString() }
125 | .size.toString(),
126 | time = currentTestSuite.testcase.sumOf { it.time?.toLongOrNull() ?: 0L }.toString(),
127 | testcase = currentTestSuite.testcase.toMutableList(),
128 | )
129 | writeTestSuite()
130 | }
131 |
132 | private fun writeTestSuite() {
133 | xmlFile().delete()
134 | xmlFile().createNewFile()
135 | if (currentTestSuite.testcase.size == 1) {
136 | // we do not want to write the whole suite information.
137 | TestSuiteXmlGen.printTestResults(currentTestSuite, xmlFile())
138 | }
139 | }
140 |
141 | override fun testStarted(description: Description) {
142 | startScreenRecord(description)
143 | currentTestSuite = currentTestSuite.copy(name = description.displayName)
144 | currentTestSuite.testCases[description.methodName]?.let {
145 | currentTestSuite.testCases[description.methodName] = it.copy(
146 | name = description.methodName,
147 | classname = description.className,
148 | status = Status.STARTED.toString(),
149 | )
150 | } ?: run {
151 | currentTestSuite.testCases[description.methodName] = Testcase(
152 | startTime = System.currentTimeMillis(),
153 | name = description.methodName,
154 | classname = description.className,
155 | )
156 | }
157 | }
158 |
159 | override fun testFinished(description: Description) {
160 | currentTestSuite.testCases[description.methodName]?.let {
161 | val newStatus = if (it.status == Status.STARTED.toString()) {
162 | Status.PASSED.toString()
163 | } else {
164 | it.status
165 | }
166 | currentTestSuite.testCases[description.methodName] = it.copy(
167 | status = newStatus,
168 | name = description.methodName,
169 | classname = description.className,
170 | time = (System.currentTimeMillis() - it.startTime!!).toString(),
171 | )
172 | }
173 | stopScreenRecord()
174 | }
175 |
176 | override fun testFailure(failure: Failure) {
177 | currentTestSuite.testCases[failure.description.methodName]?.let {
178 | currentTestSuite.testCases[failure.description.methodName] = it.copy(
179 | status = Status.FAILED.toString(),
180 | message = failure.message,
181 | type = failure.exception.javaClass.name,
182 | trace = failure.trace,
183 | name = failure.description.methodName,
184 | classname = failure.description.className,
185 | time = (System.currentTimeMillis() - it.startTime!!).toString(),
186 | )
187 | }
188 | }
189 |
190 | override fun testAssumptionFailure(failure: Failure) {
191 | currentTestSuite.testCases[failure.description.methodName]?.let {
192 | currentTestSuite.testCases[failure.description.methodName] = it.copy(
193 | status = Status.ASSUMPTION_FAILED.toString(),
194 | trace = failure.trace,
195 | message = failure.message,
196 | type = failure.exception.javaClass.name,
197 | name = failure.description.methodName,
198 | classname = failure.description.className,
199 | time = (System.currentTimeMillis() - it.startTime!!).toString(),
200 | )
201 | }
202 | }
203 |
204 | override fun testIgnored(description: Description) {
205 | currentTestSuite.testCases[description.methodName]?.let {
206 | currentTestSuite.testCases[description.methodName] = it.copy(
207 | status = Status.IGNORED.toString(),
208 | name = description.methodName,
209 | classname = description.className,
210 | time = (System.currentTimeMillis() - it.startTime!!).toString(),
211 | )
212 | }
213 | }
214 |
215 | override fun setInstrumentation(instr: Instrumentation) {
216 | super.setInstrumentation(instr)
217 | }
218 | }
219 |
220 | fun testFileDir(logs: String = "testlogs"): File = when {
221 | VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
222 | File(
223 | Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)
224 | .toString() + "/opentestlab/$logs",
225 | ).also {
226 | it.mkdirs()
227 | }
228 | // Make file
229 | }
230 |
231 | else -> {
232 | File(
233 | Environment.getExternalStorageDirectory().toString(),
234 | "/opentestlab/$logs",
235 | ).also {
236 | it.mkdirs()
237 | // Make file
238 | }
239 | }
240 | }
241 |
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/androidTest/java/dev/oianmol/sampleandroidapp/runner/TestSuite.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.sampleandroidapp.runner
2 |
3 | data class Testsuite(
4 | val properties: Properties? = null,
5 | var testcase: MutableList = mutableListOf(),
6 | val name: String? = null,
7 | val tests: String? = null,
8 | val failures: String? = null,
9 | val errors: String? = null,
10 | val skipped: String? = null,
11 | val time: String? = null,
12 | val timestamp: String? = null,
13 | val hostname: String? = null,
14 | val testCases: HashMap = hashMapOf(),
15 | val testStartTime: Long? = null,
16 | )
17 |
18 | data class Properties(
19 | val property: MutableList? = null,
20 | )
21 |
22 | data class Property(
23 | val name: String? = null,
24 | val value: String? = null,
25 | )
26 |
27 | data class Testcase(
28 | val name: String? = null,
29 | val classname: String? = null,
30 | val time: String? = null,
31 | val status: String? = null,
32 | val message: String? = null,
33 | val type: String? = null,
34 | val trace: String? = null,
35 | val startTime: Long? = null,
36 | )
37 |
38 | enum class Status {
39 | STARTED, PASSED, FAILED, IGNORED, ASSUMPTION_FAILED
40 | }
41 |
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/androidTest/java/dev/oianmol/sampleandroidapp/runner/TestSuiteXmlGen.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.opentestlab.tasks.android.runner
2 |
3 | import dev.oianmol.sampleandroidapp.runner.Status
4 | import dev.oianmol.sampleandroidapp.runner.Testsuite
5 | import org.w3c.dom.Document
6 | import java.io.File
7 | import java.io.OutputStream
8 | import javax.xml.parsers.DocumentBuilder
9 | import javax.xml.parsers.DocumentBuilderFactory
10 | import javax.xml.transform.OutputKeys
11 | import javax.xml.transform.Transformer
12 | import javax.xml.transform.TransformerFactory
13 | import javax.xml.transform.dom.DOMSource
14 | import javax.xml.transform.stream.StreamResult
15 | import kotlin.time.Duration.Companion.seconds
16 |
17 | object TestSuiteXmlGen {
18 |
19 | private const val TAG_SUITE = "testsuite"
20 | private const val TAG_PROPERTIES = "properties"
21 | private const val TAG_PROPERTY = "property"
22 | private const val TAG_CASE = "testcase"
23 |
24 | private const val TAG_FAILURE = "failure"
25 | private const val TAG_SKIPPED = "skipped"
26 |
27 | private const val ATTRIBUTE_CLASS = "classname"
28 | private const val ATTRIBUTE_ERRORS = "errors"
29 | private const val ATTRIBUTE_FAILURES = "failures"
30 | private const val ATTRIBUTE_MESSAGE = "message"
31 | private const val ATTRIBUTE_NAME = "name"
32 | private const val ATTRIBUTE_SKIPPED = "skipped"
33 | private const val ATTRIBUTE_TESTS = "tests"
34 | private const val ATTRIBUTE_TIME = "time"
35 | private const val ATTRIBUTE_TIMESTAMP = "timestamp"
36 | private const val ATTRIBUTE_TYPE = "type"
37 | private const val ATTRIBUTE_VALUE = "value"
38 |
39 | fun printTestResults(suite: Testsuite, file: File) {
40 | val docFactory: DocumentBuilderFactory = DocumentBuilderFactory.newInstance()
41 | val docBuilder: DocumentBuilder = docFactory.newDocumentBuilder()
42 | val doc: Document = docBuilder.newDocument()
43 | doc.createElement(TAG_SUITE).also { testSuite ->
44 | testSuite.setAttribute(ATTRIBUTE_FAILURES, suite.failures?:"")
45 | testSuite.setAttribute(ATTRIBUTE_SKIPPED, suite.skipped?:"")
46 | testSuite.setAttribute(ATTRIBUTE_ERRORS, suite.errors?:"")
47 | testSuite.setAttribute(ATTRIBUTE_TESTS, suite.tests?:"")
48 | testSuite.setAttribute(
49 | ATTRIBUTE_TIME, suite.time?.toLong()?.div(1.seconds.inWholeMilliseconds.toFloat())
50 | .toString()
51 | )
52 | testSuite.setAttribute(ATTRIBUTE_TIMESTAMP, suite.timestamp?:"")
53 | testSuite.setAttribute(ATTRIBUTE_NAME, suite.name?:"")
54 | doc.appendChild(testSuite)
55 |
56 | val properties = doc.createElement(TAG_PROPERTIES)
57 | suite.properties?.property?.forEach { property ->
58 | doc.createElement(TAG_PROPERTY).also { propTag ->
59 | propTag.setAttribute(ATTRIBUTE_NAME, property.name?:"")
60 | propTag.setAttribute(ATTRIBUTE_VALUE, property.value?:"")
61 | properties.appendChild(propTag)
62 | }
63 | }
64 | testSuite.appendChild(properties)
65 | suite.testCases.values.forEach { testCaseResult ->
66 | doc.createElement(TAG_CASE).also { testCaseElement ->
67 | testCaseElement.setAttribute(ATTRIBUTE_NAME, testCaseResult.name?:"")
68 | testCaseElement.setAttribute(ATTRIBUTE_CLASS, testCaseResult.classname?:"")
69 | testCaseResult.time?.let { time->
70 | testCaseElement.setAttribute(
71 | ATTRIBUTE_TIME, time.toLong().div(1.seconds.inWholeMilliseconds.toFloat())
72 | .toString())
73 | }
74 | if (testCaseResult.status == Status.FAILED.toString()) {
75 | doc.createElement(TAG_FAILURE).let { failure->
76 | failure.setAttribute(ATTRIBUTE_MESSAGE, testCaseResult.message?:"")
77 | failure.setAttribute(ATTRIBUTE_TYPE, testCaseResult.type?:"")
78 | testCaseResult.trace?.let {
79 | val comment = doc.createComment(testCaseResult.trace)
80 | failure.appendChild(comment)
81 | }
82 | testCaseElement.appendChild(failure)
83 | }
84 | }
85 | testSuite.appendChild(testCaseElement)
86 | }
87 | }
88 | writeXml(doc, file.outputStream())
89 | }
90 | }
91 |
92 | private fun writeXml(
93 | doc: Document,
94 | output: OutputStream
95 | ) {
96 | val transformerFactory: TransformerFactory = TransformerFactory.newInstance()
97 | val transformer: Transformer = transformerFactory.newTransformer()
98 |
99 | // pretty print
100 | transformer.setOutputProperty(OutputKeys.INDENT, "yes")
101 | val source = DOMSource(doc)
102 | val result = StreamResult(output)
103 | transformer.transform(source, result)
104 | }
105 |
106 | }
107 |
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
15 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/main/java/dev/oianmol/sampleandroidapp/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.sampleandroidapp
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.activity.enableEdgeToEdge
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.material3.Scaffold
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.tooling.preview.Preview
14 | import dev.oianmol.sampleandroidapp.ui.theme.SampleandroidappTheme
15 |
16 | class MainActivity : ComponentActivity() {
17 | override fun onCreate(savedInstanceState: Bundle?) {
18 | super.onCreate(savedInstanceState)
19 | enableEdgeToEdge()
20 | setContent {
21 | SampleandroidappTheme {
22 | Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
23 | Greeting(
24 | name = "Android",
25 | modifier = Modifier.padding(innerPadding)
26 | )
27 | }
28 | }
29 | }
30 | }
31 | }
32 |
33 | @Composable
34 | fun Greeting(name: String, modifier: Modifier = Modifier) {
35 | Text(
36 | text = "Hello $name!",
37 | modifier = modifier
38 | )
39 | }
40 |
41 | @Preview(showBackground = true)
42 | @Composable
43 | fun GreetingPreview() {
44 | SampleandroidappTheme {
45 | Greeting("Android")
46 | }
47 | }
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/main/java/dev/oianmol/sampleandroidapp/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.sampleandroidapp.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/main/java/dev/oianmol/sampleandroidapp/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.sampleandroidapp.ui.theme
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.darkColorScheme
8 | import androidx.compose.material3.dynamicDarkColorScheme
9 | import androidx.compose.material3.dynamicLightColorScheme
10 | import androidx.compose.material3.lightColorScheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.platform.LocalContext
13 |
14 | private val DarkColorScheme = darkColorScheme(
15 | primary = Purple80,
16 | secondary = PurpleGrey80,
17 | tertiary = Pink80
18 | )
19 |
20 | private val LightColorScheme = lightColorScheme(
21 | primary = Purple40,
22 | secondary = PurpleGrey40,
23 | tertiary = Pink40
24 |
25 | /* Other default colors to override
26 | background = Color(0xFFFFFBFE),
27 | surface = Color(0xFFFFFBFE),
28 | onPrimary = Color.White,
29 | onSecondary = Color.White,
30 | onTertiary = Color.White,
31 | onBackground = Color(0xFF1C1B1F),
32 | onSurface = Color(0xFF1C1B1F),
33 | */
34 | )
35 |
36 | @Composable
37 | fun SampleandroidappTheme(
38 | darkTheme: Boolean = isSystemInDarkTheme(),
39 | // Dynamic color is available on Android 12+
40 | dynamicColor: Boolean = true,
41 | content: @Composable () -> Unit
42 | ) {
43 | val colorScheme = when {
44 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
45 | val context = LocalContext.current
46 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
47 | }
48 |
49 | darkTheme -> DarkColorScheme
50 | else -> LightColorScheme
51 | }
52 |
53 | MaterialTheme(
54 | colorScheme = colorScheme,
55 | typography = Typography,
56 | content = content
57 | )
58 | }
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/main/java/dev/oianmol/sampleandroidapp/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.sampleandroidapp.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oianmol/OpenTestLabAndroid/d9bfb402da88f6f1c88172269dfb1bfe5610fba7/sampleandroidapp/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oianmol/OpenTestLabAndroid/d9bfb402da88f6f1c88172269dfb1bfe5610fba7/sampleandroidapp/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oianmol/OpenTestLabAndroid/d9bfb402da88f6f1c88172269dfb1bfe5610fba7/sampleandroidapp/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oianmol/OpenTestLabAndroid/d9bfb402da88f6f1c88172269dfb1bfe5610fba7/sampleandroidapp/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oianmol/OpenTestLabAndroid/d9bfb402da88f6f1c88172269dfb1bfe5610fba7/sampleandroidapp/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oianmol/OpenTestLabAndroid/d9bfb402da88f6f1c88172269dfb1bfe5610fba7/sampleandroidapp/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oianmol/OpenTestLabAndroid/d9bfb402da88f6f1c88172269dfb1bfe5610fba7/sampleandroidapp/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oianmol/OpenTestLabAndroid/d9bfb402da88f6f1c88172269dfb1bfe5610fba7/sampleandroidapp/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oianmol/OpenTestLabAndroid/d9bfb402da88f6f1c88172269dfb1bfe5610fba7/sampleandroidapp/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oianmol/OpenTestLabAndroid/d9bfb402da88f6f1c88172269dfb1bfe5610fba7/sampleandroidapp/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | sampleandroidapp
3 |
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/sampleandroidapp/app/src/test/java/dev/oianmol/sampleandroidapp/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.sampleandroidapp
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/sampleandroidapp/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | alias(libs.plugins.android.application) apply false
4 | alias(libs.plugins.jetbrains.kotlin.android) apply false
5 | alias(libs.plugins.device.farm) apply false
6 | }
--------------------------------------------------------------------------------
/sampleandroidapp/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
24 | DEVICE_FARM_URL=localhost:8081
--------------------------------------------------------------------------------
/sampleandroidapp/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.5.0"
3 | kotlin = "1.9.0"
4 | coreKtx = "1.13.1"
5 | junit = "4.13.2"
6 | junitVersion = "1.2.1"
7 | espressoCore = "3.6.1"
8 | lifecycleRuntimeKtx = "2.8.3"
9 | activityCompose = "1.9.0"
10 | composeBom = "2024.04.01"
11 | deviceFarm = "0.0.1"
12 | runner = "1.6.1"
13 |
14 | [libraries]
15 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
16 | androidx-runner = { module = "androidx.test:runner", version.ref = "runner" }
17 | junit = { group = "junit", name = "junit", version.ref = "junit" }
18 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
19 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
20 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
21 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
22 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
23 | androidx-ui = { group = "androidx.compose.ui", name = "ui" }
24 | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
25 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
26 | androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
27 | androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
28 | androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
29 | androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
30 |
31 | [plugins]
32 | android-application = { id = "com.android.application", version.ref = "agp" }
33 | jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
34 | device-farm = {id="dev.oianmol.androidDeviceFarm",version.ref="deviceFarm"}
35 |
36 |
--------------------------------------------------------------------------------
/sampleandroidapp/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oianmol/OpenTestLabAndroid/d9bfb402da88f6f1c88172269dfb1bfe5610fba7/sampleandroidapp/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/sampleandroidapp/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Jul 07 23:43:09 IST 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/sampleandroidapp/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/sampleandroidapp/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/sampleandroidapp/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | mavenLocal()
13 | }
14 | }
15 | dependencyResolutionManagement {
16 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
17 | repositories {
18 | google()
19 | mavenCentral()
20 | mavenLocal()
21 | }
22 | }
23 |
24 | rootProject.name = "sampleandroidapp"
25 | include(":app")
26 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | mavenCentral()
4 | gradlePluginPortal()
5 |
6 | google()
7 | maven {
8 | setUrl("https://plugins.gradle.org/m2/")
9 | }
10 | }
11 | }
12 |
13 | plugins {
14 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0"
15 | }
16 |
17 | rootProject.name = "OpenTestLabAndroid"
18 |
19 | include("androidplugin")
20 | include("protos")
21 |
--------------------------------------------------------------------------------
/src/main/kotlin/Main.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol
2 |
3 | import dev.oianmol.dev.oianmol.opentestlab.android.adbandfriends.DeviceDiscoveryViaADB
4 | import dev.oianmol.dev.oianmol.opentestlab.android.reporting.DeviceFarmReportManagementService
5 | import dev.oianmol.dev.oianmol.opentestlab.android.adbandfriends.DeviceFarmService
6 | import dev.oianmol.dev.oianmol.opentestlab.android.testexec.TestExecutionService
7 | import dev.oianmol.opentestlab.android.devicefarm.DefaultDeviceAvailabilityStore
8 | import dev.oianmol.opentestlab.android.testexec.DeviceFarmTestExecScope
9 | import dev.oianmol.opentestlab.android.testexec.commandrunner.DefaultDeviceCommandRunner
10 | import dev.oianmol.opentestlab.android.testexec.resultreader.DefaultDeviceTestResultReader
11 | import dev.oianmol.opentestlab.android.testexec.testrunner.DeviceFarmTestRunner
12 | import io.grpc.ServerBuilder
13 |
14 | fun main() {
15 | val server = ServerBuilder.forPort(8081)
16 | .maxInboundMessageSize(Int.MAX_VALUE)
17 | .addService(DeviceFarmService(deviceDiscovery = DeviceDiscoveryViaADB))
18 | .addService(
19 | TestExecutionService(
20 | testExecScope = DeviceFarmTestExecScope,
21 | testRunner = DeviceFarmTestRunner(
22 | iDiscoverDevices = DeviceDiscoveryViaADB,
23 | deviceCommandRunner = DefaultDeviceCommandRunner,
24 | deviceAvailabilityStore = DefaultDeviceAvailabilityStore,
25 | deviceTestResultReader = DefaultDeviceTestResultReader
26 | )
27 | )
28 | )
29 | .addService(DeviceFarmReportManagementService(testExecScope = DeviceFarmTestExecScope))
30 | .build()
31 | .start()
32 | println("server started at port:${server.port}")
33 | server.awaitTermination()
34 | println("server stopped!")
35 |
36 | }
--------------------------------------------------------------------------------
/src/main/kotlin/dev/oianmol/opentestlab/Coroutine+Extensions.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.dev.oianmol.opentestlab
2 |
3 | import kotlin.Result
4 | import kotlin.coroutines.cancellation.CancellationException
5 |
6 | suspend fun runCatchingCancellable(block: suspend () -> R): Result {
7 | return try {
8 | Result.success(block())
9 | } catch (c: CancellationException) {
10 | throw c
11 | } catch (e: Throwable) {
12 | Result.failure(e)
13 | }
14 | }
--------------------------------------------------------------------------------
/src/main/kotlin/dev/oianmol/opentestlab/android/devicefarm/Device.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.opentestlab.android.devicefarm
2 |
3 | data class Device(
4 | val name: String,
5 | val id: String,
6 | val ip: String = "",
7 | val isConnected: Boolean = false
8 | )
--------------------------------------------------------------------------------
/src/main/kotlin/dev/oianmol/opentestlab/android/devicefarm/DeviceAvailabilityStore.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.opentestlab.android.devicefarm
2 |
3 | import dev.oianmol.opentestlab.DeviceFarmDevice
4 |
5 | interface DeviceAvailabilityStore {
6 | suspend fun markDeviceUnavailable(deviceFarmDevice: DeviceFarmDevice)
7 | suspend fun markDeviceAvailable(deviceFarmDevice: DeviceFarmDevice)
8 | suspend fun isDeviceAvailable(serialNumber: String): Boolean
9 | }
10 |
11 |
12 | object DefaultDeviceAvailabilityStore : DeviceAvailabilityStore {
13 | private val availableDevices = hashMapOf()
14 |
15 | override suspend fun markDeviceUnavailable(deviceFarmDevice: DeviceFarmDevice) {
16 | availableDevices[deviceFarmDevice.serialNumber] = false
17 | }
18 |
19 | override suspend fun markDeviceAvailable(deviceFarmDevice: DeviceFarmDevice) {
20 | availableDevices[deviceFarmDevice.serialNumber] = true
21 | }
22 |
23 | override suspend fun isDeviceAvailable(serialNumber: String): Boolean {
24 | if (availableDevices.containsKey(serialNumber).not()) {
25 | return true
26 | }
27 | return availableDevices[serialNumber] == true
28 | }
29 | }
--------------------------------------------------------------------------------
/src/main/kotlin/dev/oianmol/opentestlab/android/devicefarm/DeviceDiscovery.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.dev.oianmol.opentestlab.android.adbandfriends
2 |
3 | import dev.oianmol.opentestlab.DeviceFarmDevice
4 | import dev.oianmol.opentestlab.TestMachine
5 | import dev.oianmol.dev.oianmol.opentestlab.android.devicefarm.cli.CommandLine
6 | import dev.oianmol.dev.oianmol.opentestlab.android.devicefarm.friends.Adb
7 | import dev.oianmol.opentestlab.android.devicefarm.Device
8 | import java.net.InetAddress
9 |
10 | interface DeviceDiscovery {
11 | fun listDevices(): List
12 | fun hostMachine(): TestMachine
13 | }
14 |
15 | object DeviceDiscoveryViaADB : DeviceDiscovery {
16 |
17 | override fun listDevices(): List {
18 | return Adb.getDevicesConnectedByUSB().map { dadb ->
19 | dadb.toDeviceFarmDevice()
20 | }
21 | }
22 |
23 | override fun hostMachine(): TestMachine {
24 | return testMachine(listDevices())
25 | }
26 | }
27 |
28 | fun Device.toDeviceFarmDevice(): DeviceFarmDevice {
29 | val ip = getDeviceProperty("shell ip -f inet addr show wlan0")?.let { Adb.parseGetDeviceIp(it) }
30 | val serialNumber = getDeviceProperty("shell getprop ro.boot.serialno")
31 | val model = getDeviceProperty("shell getprop ro.product.model")
32 |
33 | return DeviceFarmDevice.newBuilder()
34 | .setIpAddress(ip?.trim() ?: "Unknown")
35 | .setModel(model?.trim() ?: "Unknown")
36 | .setSerialNumber(this.id)
37 | .build()
38 | }
39 |
40 | private fun Device.getDeviceProperty(commandSuffix: String): String? {
41 | return try {
42 | CommandLine.executeCommand(Adb.adbCommand(" -s ${this.id} $commandSuffix")).getOrThrow().trim()
43 | } catch (e: Exception) {
44 | // Log the error
45 | println("Failed to execute command for device ${this.id}: ${e.message}")
46 | null
47 | }
48 | }
49 |
50 | fun testMachine(devices: List): TestMachine {
51 | return TestMachine.newBuilder()
52 | .setIpAddress(InetAddress.getLocalHost().hostAddress)
53 | .setName(InetAddress.getLocalHost().hostName)
54 | .setLocation("PRIMARY DEVICE!")
55 | .addAllDevices(devices)
56 | .build()
57 | }
58 |
--------------------------------------------------------------------------------
/src/main/kotlin/dev/oianmol/opentestlab/android/devicefarm/DeviceFarmService.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.dev.oianmol.opentestlab.android.adbandfriends
2 |
3 | import dev.oianmol.opentestlab.DeviceFarmInfo
4 | import dev.oianmol.opentestlab.DeviceFarmServiceGrpcKt
5 | import dev.oianmol.opentestlab.Empty
6 | import kotlinx.coroutines.CoroutineDispatcher
7 | import kotlinx.coroutines.Dispatchers
8 |
9 | class DeviceFarmService(
10 | private val deviceDiscovery: DeviceDiscovery,
11 | dispatcher: CoroutineDispatcher = Dispatchers.IO
12 | ) : DeviceFarmServiceGrpcKt.DeviceFarmServiceCoroutineImplBase(dispatcher) {
13 | override suspend fun getDeviceFarm(request: Empty): DeviceFarmInfo {
14 | return DeviceFarmInfo.newBuilder()
15 | .addAllMachines( // TODO in future we need to send a list of all machine in the test lab which has devices connected to it
16 | listOf(
17 | deviceDiscovery.hostMachine()
18 | )
19 | )
20 | .build()
21 | }
22 | }
--------------------------------------------------------------------------------
/src/main/kotlin/dev/oianmol/opentestlab/android/devicefarm/cli/AABToAPKConverter.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.opentestlab.android.devicefarm.cli
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlinx.coroutines.withContext
5 | import java.io.BufferedInputStream
6 | import java.io.File
7 | import java.io.FileOutputStream
8 | import java.io.IOException
9 | import java.net.URL
10 | import java.nio.file.Files
11 | import java.nio.file.Paths
12 | import kotlin.io.path.absolutePathString
13 |
14 | class AABToAPKConverter(private val bundleToolUrl: String = LATEST_BUNDLE_TOOL) {
15 | companion object {
16 | const val LATEST_BUNDLE_TOOL =
17 | "https://github.com/google/bundletool/releases/download/1.17.0/bundletool-all-1.17.0.jar"
18 | }
19 |
20 | /**
21 | * Converts an AAB file to an APK set and extracts the universal APK.
22 | *
23 | * @param aabPath The path to the .aab file.
24 | * @param outputDir The directory where the APK set and the universal APK will be saved.
25 | * @param bundleToolPath The path to the bundletool.jar file.
26 | * @throws IOException If an I/O error occurs.
27 | * @throws InterruptedException If the process is interrupted.
28 | */
29 | suspend fun convertAABToAPK(
30 | aabPath: String,
31 | bundleToolPath: String? = null
32 | ): File? {
33 | val aabFile = File(aabPath)
34 | require(aabFile.exists()) { "AAB file does not exist at path: $aabPath" }
35 | val bundleToolPath =
36 | bundleToolPath ?: withContext(Dispatchers.IO) {
37 | Files.createTempDirectory("bundletool")
38 | }.resolve("bundletool.jar").toString()
39 |
40 | val bundleToolFile = File(bundleToolPath)
41 |
42 | // Download the bundletool if it does not exist
43 | if (!bundleToolFile.exists()) {
44 | downloadFile(bundleToolUrl, bundleToolPath)
45 | }
46 | val outputDir = Files.createTempDirectory("aabFiles").absolutePathString()
47 | val apksPath = "${outputDir}/output.apks"
48 |
49 | // Ensure the output directory exists
50 | withContext(Dispatchers.IO) {
51 | Files.createDirectories(Paths.get(outputDir))
52 | }
53 |
54 | // Step 1: Build the APK set
55 | val buildApksCommand = arrayOf(
56 | "java", "-jar", bundleToolPath,
57 | "build-apks",
58 | "--bundle=${aabFile.absolutePath}",
59 | "--output=$apksPath",
60 | "--mode=universal"
61 | )
62 | executeCommand(buildApksCommand)
63 |
64 | // Step 2: Extract the universal APK from the APK set
65 | val unzipCommand = arrayOf(
66 | "unzip", apksPath, "-d", outputDir
67 | )
68 | executeCommand(unzipCommand)
69 |
70 | println("Conversion complete. The APK files are in: $outputDir")
71 | // Step 3: List the generated APK files
72 | val apkFiles = File(outputDir).listFiles { _, name -> name.endsWith(".apk") }
73 | apkFiles?.forEach { println(it.absolutePath) }
74 | return apkFiles.first()
75 | }
76 |
77 | private suspend fun executeCommand(command: Array) = withContext(Dispatchers.IO) {
78 | val processBuilder = ProcessBuilder(*command)
79 | val process = processBuilder.start()
80 | val exitCode = process.waitFor()
81 | if (exitCode != 0) {
82 | throw IOException("Command execution failed: ${command.joinToString(" ")}")
83 | }
84 | }
85 |
86 | private suspend fun downloadFile(fileURL: String, savePath: String) = withContext(Dispatchers.IO) {
87 | val url = URL(fileURL)
88 | val connection = url.openConnection()
89 | connection.connect()
90 |
91 | val inputStream = BufferedInputStream(url.openStream(), 8192)
92 | val outputStream = FileOutputStream(savePath)
93 |
94 | val data = ByteArray(1024)
95 | var count: Int
96 | while (inputStream.read(data).also { count = it } != -1) {
97 | outputStream.write(data, 0, count)
98 | }
99 |
100 | outputStream.flush()
101 | outputStream.close()
102 | inputStream.close()
103 |
104 | println("Downloaded file to: $savePath")
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/main/kotlin/dev/oianmol/opentestlab/android/devicefarm/cli/Adb.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.dev.oianmol.opentestlab.android.devicefarm.friends
2 |
3 | import dev.oianmol.dev.oianmol.opentestlab.android.devicefarm.cli.CommandLine
4 | import dev.oianmol.opentestlab.android.devicefarm.Device
5 | import dev.oianmol.opentestlab.android.devicefarm.cli.AdbPathFinder
6 | import java.util.logging.Logger
7 |
8 | object Adb {
9 |
10 | private val logger = Logger.getLogger("ADB")
11 |
12 | private const val MODEL_INDICATOR = "model:"
13 | private const val DEVICE_INDICATOR = "device:"
14 | private const val END_DEVICE_IP_INDICATOR = "/"
15 | private const val START_DEVICE_IP_INDICATOR = "inet"
16 | private const val ERROR_PARSING_DEVICE_IP_KEY = "Object"
17 | private const val DAEMON_INDICATOR = "daemon"
18 |
19 | private fun parseGetDevicesOutput(adbDevicesOutput: String): List {
20 | println(adbDevicesOutput)
21 | if (adbDevicesOutput.contains(DAEMON_INDICATOR)) return emptyList()
22 |
23 | return adbDevicesOutput.lines()
24 | .drop(1)
25 | .filter { it.isNotBlank() }
26 | .mapNotNull { line ->
27 | val deviceLine = line.split("\\t".toRegex()).firstOrNull() ?: return@mapNotNull null
28 | val id = deviceLine.substringBefore(" ")
29 | val name = parseDeviceName(line)
30 | Device(name, id)
31 | }
32 | }
33 |
34 | fun parseGetDeviceIp(ipInfo: String): String {
35 | return when {
36 | ipInfo.isEmpty() || ipInfo.contains(ERROR_PARSING_DEVICE_IP_KEY) -> ""
37 | else -> try {
38 | val start = ipInfo.indexOf(START_DEVICE_IP_INDICATOR) + START_DEVICE_IP_INDICATOR.length + 1
39 | val end = ipInfo.indexOf(END_DEVICE_IP_INDICATOR, start)
40 | ipInfo.substring(start, end)
41 | } catch (e: StringIndexOutOfBoundsException) {
42 | logger.severe("Error parsing IP: ${e.message}")
43 | ""
44 | }
45 | }
46 | }
47 |
48 | private fun parseDeviceName(line: String): String {
49 | val start = line.indexOf(MODEL_INDICATOR) + MODEL_INDICATOR.length
50 | val end = line.indexOf(DEVICE_INDICATOR).takeIf { it > 0 } ?: line.length
51 | return line.substring(start, end).trim()
52 | }
53 |
54 | private fun adbPath(): String {
55 | val adbPathFinder = AdbPathFinder()
56 | val adbPath = adbPathFinder.findAdbPath()?.trim()
57 | return adbPath!!
58 | }
59 |
60 | fun adbCommand(command: String): String =
61 | "${adbPath()} $command"
62 |
63 | fun getDevicesConnectedByUSB(): Collection {
64 | val adbDevicesOutput: String = CommandLine.executeCommand(adbCommand("devices -l")).getOrThrow()
65 | return parseGetDevicesOutput(adbDevicesOutput)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/main/kotlin/dev/oianmol/opentestlab/android/devicefarm/cli/AdbPathFinder.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.opentestlab.android.devicefarm.cli
2 | import dev.oianmol.dev.oianmol.opentestlab.android.devicefarm.cli.CommandLine
3 | import java.io.File
4 |
5 | class AdbPathFinder {
6 |
7 | fun findAdbPath(): String? {
8 | return when {
9 | isWindows() -> findAdbOnWindows()
10 | else -> CommandLine.executeCommand("which adb").getOrThrow()
11 | }
12 | }
13 |
14 | private fun findAdbOnWindows(): String? {
15 | val adbPaths = listOf(
16 | "${System.getenv("LOCALAPPDATA")}\\Android\\Sdk\\platform-tools\\adb.exe",
17 | "${System.getenv("ProgramFiles")}\\Android\\Sdk\\platform-tools\\adb.exe"
18 | )
19 |
20 | return adbPaths.find { File(it).exists() }
21 | }
22 |
23 | private fun findAdbOnMac(): String? {
24 | val adbPath = "/usr/local/bin/adb"
25 | return if (File(adbPath).exists()) adbPath else null
26 | }
27 |
28 | private fun findAdbOnUnix(): String? {
29 | val adbPath = "/usr/bin/adb"
30 | return if (File(adbPath).exists()) adbPath else null
31 | }
32 |
33 | private fun isWindows(): Boolean {
34 | return System.getProperty("os.name").toLowerCase().contains("win")
35 | }
36 | }
37 |
38 | fun main() {
39 | val adbPathFinder = AdbPathFinder()
40 | val adbPath = adbPathFinder.findAdbPath()
41 | if (adbPath != null) {
42 | println("ADB found at: $adbPath")
43 | } else {
44 | println("ADB not found.")
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/kotlin/dev/oianmol/opentestlab/android/devicefarm/cli/CommandLine.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.dev.oianmol.opentestlab.android.devicefarm.cli
2 |
3 | import java.util.logging.Level
4 | import java.util.logging.Logger
5 |
6 | object CommandLine {
7 | private val logger = Logger.getLogger("deviceConnection")
8 |
9 | private val defaultShell: String by lazy {
10 | if (System.getProperty("os.name").lowercase().contains("win")) {
11 | System.getenv("COMSPEC") ?: "cmd.exe"
12 | } else {
13 | System.getenv("SHELL") ?: "/bin/sh"
14 | }
15 | }
16 |
17 | private val shellOptions: List by lazy {
18 | if (System.getProperty("os.name").lowercase().contains("win")) {
19 | listOf("/c")
20 | } else {
21 | listOf("-l", "-c")
22 | }
23 | }
24 |
25 | fun executeCommand(command: String?): Result {
26 | if (command.isNullOrBlank()) {
27 | logger.warning("Command is null or blank")
28 | return Result.failure(IllegalArgumentException("Command cannot be null or blank"))
29 | }
30 |
31 | println("Executing: $command")
32 | return runCatching {
33 | val pb = ProcessBuilder(defaultShell, *shellOptions.toTypedArray(), command)
34 | val process = pb.start()
35 | val output = StringBuilder()
36 | val errorOutput = StringBuilder()
37 |
38 | process.inputStream.bufferedReader().use { it.forEachLine { line -> output.appendLine(line) } }
39 | process.errorStream.bufferedReader().use { it.forEachLine { line -> errorOutput.appendLine(line) } }
40 |
41 | val exitCode = process.waitFor()
42 | if (exitCode != 0) {
43 | val errorMsg = "Command execution failed with exit code $exitCode: ${errorOutput.toString()}"
44 | logger.severe(errorMsg)
45 | throw RuntimeException(errorMsg)
46 | }
47 |
48 | val result = output.toString()
49 | println(result)
50 | result
51 | }.onFailure {
52 | logger.log(Level.SEVERE, "Command execution failed", it)
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/main/kotlin/dev/oianmol/opentestlab/android/reporting/DeviceFarmReportManagementService.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.dev.oianmol.opentestlab.android.reporting
2 |
3 | import com.google.protobuf.ByteString
4 | import dev.oianmol.opentestlab.ReportFiles
5 | import dev.oianmol.opentestlab.ReportManagementServiceGrpcKt
6 | import dev.oianmol.opentestlab.ReportsRequest
7 | import dev.oianmol.opentestlab.android.testexec.TestExecScope
8 | import kotlinx.coroutines.CoroutineDispatcher
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.flow.flow
11 | import kotlinx.coroutines.flow.flowOn
12 |
13 | class DeviceFarmReportManagementService(
14 | private val testExecScope: TestExecScope,
15 | dispatcher: CoroutineDispatcher = Dispatchers.IO
16 | ) : ReportManagementServiceGrpcKt.ReportManagementServiceCoroutineImplBase(dispatcher) {
17 | override fun pullReportFiles(request: ReportsRequest) = flow {
18 | for (it in testExecScope.workingReportsDir(request).listFiles() ?: emptyArray()) {
19 | val stream = it.inputStream()
20 | var size = it.length()
21 | while (size != 0L) {
22 | val read = ByteArray(Math.min(size, 4086L).toInt())
23 | val readBytes = stream.read(read)
24 | size -= readBytes
25 | emit(
26 | ReportFiles.newBuilder()
27 | .setFileName(it.name)
28 | .setReportFile(ByteString.copyFrom(read))
29 | .build()
30 | )
31 |
32 | }
33 | stream.close()
34 | }
35 | }
36 | .flowOn(Dispatchers.IO)
37 | }
--------------------------------------------------------------------------------
/src/main/kotlin/dev/oianmol/opentestlab/android/testexec/DeviceFarmTestExecScope.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.opentestlab.android.testexec
2 |
3 | import dev.oianmol.opentestlab.DeviceFarmTestSpec
4 | import dev.oianmol.opentestlab.FileType
5 | import dev.oianmol.opentestlab.ReportsRequest
6 | import dev.oianmol.opentestlab.TestApkUpload
7 | import dev.oianmol.opentestlab.android.devicefarm.cli.AABToAPKConverter
8 | import io.grpc.Status
9 | import io.grpc.StatusException
10 | import java.io.File
11 | import java.nio.file.Files
12 | import kotlin.io.path.absolutePathString
13 |
14 | object DeviceFarmTestExecScope : TestExecScope {
15 |
16 | override suspend fun writeFiles(it: TestApkUpload, function: suspend () -> Unit) {
17 | testWorkingDir(it.ciUniqueBuildNumber).apply {
18 | when (it.fileType) {
19 | FileType.TestApk -> androidTestApk(this).appendBytes(it.testApk.toByteArray()).also { function() }
20 | FileType.AndroidApk -> androidAppApk(this).appendBytes(it.testApk.toByteArray()).also { function() }
21 | FileType.AppBundle -> androidAppAab(this).appendBytes(it.testApk.toByteArray()).also { function() }
22 | else -> throw StatusException(Status.INVALID_ARGUMENT)
23 | }
24 | }
25 | }
26 |
27 | override fun createDirs(ciUniqueBuildNumber: String) {
28 | testWorkingDir(ciUniqueBuildNumber).apply {
29 | mkdirs()
30 | // we delete these files if existing already!
31 | androidTestApk(this).delete()
32 | androidAppApk(this).delete()
33 | androidAppAab(this).delete()
34 | }
35 | }
36 |
37 | override fun workingReportsDir(request: ReportsRequest): File {
38 | val workingdir = testWorkingDir(request.ciUniqueBuildNumber)
39 | return File(workingdir, "reports")
40 | }
41 |
42 | override suspend fun prepareFor(request: DeviceFarmTestSpec): TestExecutionTaskSpec {
43 | return with(testWorkingDir(request.ciUniqueBuildNumber)) {
44 | val aabFilePath = androidAppAab(this)
45 | val apkPath = if (aabFilePath.exists() && aabFilePath.length() > 0) {
46 | // if an aab was uploaded we convert it to a universal apk
47 | AABToAPKConverter().convertAABToAPK(
48 | aabPath = androidAppAab(
49 | testResultsDir = this
50 | ).absolutePath
51 | ) ?: throw StatusException(Status.NOT_FOUND)
52 | } else {
53 | androidAppApk(this)
54 | }
55 |
56 | TestExecutionTaskSpec(
57 | workingDir = this,
58 | androidTestApk = androidTestApk(this),
59 | androidAppApk = apkPath,
60 | listenerClass = request.listenerClass,
61 | testPackageFilter = request.testPackageFilter,
62 | instrumentPackage = request.testPackageName,
63 | customRunner = request.customRunner,
64 | packageName = request.packageName,
65 | testPackageName = request.testPackageName,
66 | )
67 | }
68 | }
69 |
70 | fun testWorkingDir(ciUniqueBuildNumber: String) = File(
71 | userHome().plus(File.separator) + "deviceFarm",
72 | ciUniqueBuildNumber
73 | )
74 |
75 | private fun userHome(): String =
76 | System.getProperty("user.home") ?: Files.createTempDirectory("test").absolutePathString()
77 |
78 | private fun apksDir(testResultsDir: File) = File(testResultsDir, "apks").apply { mkdirs() }
79 | private fun androidAppApk(testResultsDir: File) = File(apksDir(testResultsDir), "android.apk")
80 | private fun androidTestApk(testResultsDir: File) = File(apksDir(testResultsDir), "androidTest.apk")
81 | private fun androidAppAab(testResultsDir: File): File = File(apksDir(testResultsDir), "android.aab")
82 | }
--------------------------------------------------------------------------------
/src/main/kotlin/dev/oianmol/opentestlab/android/testexec/TestExecScope.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.opentestlab.android.testexec
2 |
3 | import dev.oianmol.opentestlab.DeviceFarmTestSpec
4 | import dev.oianmol.opentestlab.ReportsRequest
5 | import dev.oianmol.opentestlab.TestApkUpload
6 | import java.io.File
7 |
8 | interface TestExecScope {
9 | suspend fun prepareFor(request: DeviceFarmTestSpec): TestExecutionTaskSpec
10 | fun createDirs(ciUniqueBuildNumber: String)
11 | suspend fun writeFiles(it: TestApkUpload, function: suspend () -> Unit)
12 | fun workingReportsDir(request: ReportsRequest): File
13 | }
--------------------------------------------------------------------------------
/src/main/kotlin/dev/oianmol/opentestlab/android/testexec/TestExecutionService.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.dev.oianmol.opentestlab.android.testexec
2 |
3 | import dev.oianmol.opentestlab.*
4 | import dev.oianmol.opentestlab.android.testexec.TestExecScope
5 | import dev.oianmol.opentestlab.android.testexec.testrunner.ITestRunner
6 | import io.grpc.Status
7 | import io.grpc.StatusException
8 | import kotlinx.coroutines.*
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.coroutines.flow.flow
11 | import kotlinx.coroutines.flow.flowOn
12 | import java.util.logging.Logger
13 | import kotlin.time.Duration.Companion.minutes
14 | import kotlin.time.Duration.Companion.seconds
15 |
16 | class TestExecutionService(
17 | private val testExecScope: TestExecScope,
18 | private val testRunner: ITestRunner,
19 | dispatcher: CoroutineDispatcher = Dispatchers.IO
20 | ) : TestExecutionServiceGrpcKt.TestExecutionServiceCoroutineImplBase(dispatcher) {
21 | private val logger = Logger.getLogger("TestExecutionService")
22 |
23 | override suspend fun executeTests(request: DeviceFarmTestSpec): DeviceFarmTestResults {
24 | val taskExecSpec = testExecScope.prepareFor(request)
25 |
26 | if (testRunner.canExecute(taskExecSpec)) {
27 | return testRunner.execute(taskExecSpec)
28 | } else {
29 | println("we will now continue to retry for 10 minutes with 10 sec delay")
30 | runCatching { // we eat up cancellation exception here by not calling runCatchingCancellable
31 | withTimeout(10.minutes) { // keep trying for 10 minutes
32 | while (this.isActive) {
33 | delay(10.seconds)
34 | println("Retrying to check available device")
35 | if (testRunner.canExecute(taskExecSpec)) {
36 | break
37 | }
38 | }
39 | }
40 | }
41 | val canExecute = testRunner.canExecute(taskExecSpec)
42 | println("can execute after 10 minutes $canExecute ?")
43 | if (canExecute.not()) {
44 | println("Caused ${Status.RESOURCE_EXHAUSTED}")
45 | throw StatusException(Status.RESOURCE_EXHAUSTED)
46 | } else {
47 | return testRunner.execute(taskExecSpec)
48 | }
49 | }
50 | }
51 |
52 | override fun uploadTestApk(requests: Flow): Flow = flow {
53 | var dirCreated = false
54 | requests.collect { testApkUpload ->
55 | if (dirCreated.not()) {
56 | testExecScope.createDirs(testApkUpload.ciUniqueBuildNumber)
57 | emit(
58 | StreamLogs.newBuilder()
59 | .setMessage("Test Directory created for ${testApkUpload.ciUniqueBuildNumber}").build()
60 | )
61 | dirCreated = true
62 | }
63 | if (testApkUpload.testApk.isEmpty) {
64 | currentCoroutineContext().cancel()
65 | } else {
66 | testExecScope.writeFiles(testApkUpload) {
67 | emit(StreamLogs.newBuilder().setMessage("Writing file ${testApkUpload.fileType.name}").build())
68 | }
69 | }
70 | }
71 | }.flowOn(Dispatchers.IO)
72 | }
--------------------------------------------------------------------------------
/src/main/kotlin/dev/oianmol/opentestlab/android/testexec/TestExecutionTaskSpec.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.opentestlab.android.testexec
2 |
3 | import java.io.File
4 |
5 | data class TestExecutionTaskSpec(
6 | val workingDir: File,
7 | val androidTestApk: File,
8 | val androidAppApk: File,
9 | val listenerClass: String,
10 | val testPackageFilter: String,
11 | val instrumentPackage: String,
12 | val customRunner: String,
13 | val packageName: String,
14 | val testPackageName: String,
15 | )
--------------------------------------------------------------------------------
/src/main/kotlin/dev/oianmol/opentestlab/android/testexec/commandrunner/DeviceCommandRunner.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.opentestlab.android.testexec.commandrunner
2 |
3 | import dev.oianmol.opentestlab.DeviceFarmDevice
4 | import dev.oianmol.dev.oianmol.opentestlab.android.devicefarm.friends.Adb
5 | import dev.oianmol.dev.oianmol.opentestlab.android.devicefarm.cli.CommandLine
6 | import dev.oianmol.opentestlab.android.testexec.TestExecutionTaskSpec
7 | import java.io.File
8 | import java.util.logging.Logger
9 |
10 | interface DeviceCommandRunner {
11 | fun deviceConnection(device: DeviceFarmDevice): IDeviceConnection
12 | }
13 |
14 | object DefaultDeviceCommandRunner : DeviceCommandRunner {
15 | override fun deviceConnection(device: DeviceFarmDevice): IDeviceConnection {
16 | return DefaultDeviceConnection(device)
17 | }
18 | }
19 |
20 | interface IDeviceConnection {
21 | fun uninstallApks(testPackageName: String, packageName: String)
22 | fun deleteTestResults()
23 | fun pullTestResults(testExecutionTaskSpec: TestExecutionTaskSpec)
24 | fun installApks(testExecutionTaskSpec: TestExecutionTaskSpec)
25 | fun runAndroidTests(
26 | testExecutionTaskSpec: TestExecutionTaskSpec,
27 | totalDevices: Int,
28 | currentDeviceIndex: Int
29 | )
30 |
31 | fun installOrchestratorApk()
32 | }
33 |
34 |
35 | class DefaultDeviceConnection(private val device: DeviceFarmDevice) : IDeviceConnection {
36 | private val logger = Logger.getLogger("deviceConnection")
37 |
38 | override fun uninstallApks(
39 | testPackageName: String,
40 | packageName: String
41 | ) {
42 | println("uninstallApks$device")
43 | CommandLine.executeCommand(Adb.adbCommand(" -s ${device.serialNumber} shell pm uninstall $testPackageName"))
44 | CommandLine.executeCommand(Adb.adbCommand(" -s ${device.serialNumber} shell pm uninstall $packageName"))
45 | }
46 |
47 | override fun installApks(testExecutionTaskSpec: TestExecutionTaskSpec) {
48 | println("installApks$device")
49 | CommandLine.executeCommand(Adb.adbCommand(" -s ${device.serialNumber} install -t ${testExecutionTaskSpec.androidTestApk.absolutePath}"))
50 | .also {
51 | println("install test apk ${it.getOrThrow()}")
52 | }
53 | CommandLine.executeCommand(Adb.adbCommand(" -s ${device.serialNumber} install ${testExecutionTaskSpec.androidAppApk.absolutePath}"))
54 | .also {
55 | println("install android apk ${it.getOrThrow()}")
56 | }
57 | }
58 |
59 | override fun installOrchestratorApk() {
60 | CommandLine.executeCommand(Adb.adbCommand(" -s ${device.serialNumber} uninstall androidx.test.services"))
61 | CommandLine.executeCommand(Adb.adbCommand(" -s ${device.serialNumber} uninstall androidx.test.orchestrator"))
62 |
63 | val api = deviceApiLevel()
64 |
65 | val orchapk = File("orchestrator-1.4.1.apk")
66 | val services = File("test-services-1.4.2.apk")
67 |
68 | var forceQuery = ""
69 | loadApks(orchapk, services)
70 |
71 | if ((api.toIntOrNull() ?: 0) >= 30) {
72 | forceQuery = "--force-queryable"
73 | }
74 |
75 | CommandLine.executeCommand(Adb.adbCommand(" -s ${device.serialNumber} install $forceQuery -r ${orchapk.absolutePath}"))
76 | CommandLine.executeCommand(Adb.adbCommand(" -s ${device.serialNumber} install $forceQuery -r ${services.absolutePath}"))
77 | }
78 |
79 | private fun loadApks(orchapk: File, services: File) {
80 | if (!orchapk.exists()) {
81 | this.javaClass.classLoader
82 | .getResource("orchestrator-1.4.1.apk")
83 | ?.openStream()
84 | ?.transferTo(orchapk.outputStream())
85 | }
86 | if (services.exists().not()) {
87 | this.javaClass.classLoader
88 | .getResource("test-services-1.4.2.apk")
89 | ?.openStream()
90 | ?.transferTo(services.outputStream())
91 | }
92 | }
93 |
94 | private fun deviceApiLevel() =
95 | CommandLine.executeCommand(Adb.adbCommand(" -s ${device.serialNumber} shell getprop ro.build.version.sdk"))
96 | .getOrThrow().trim()
97 |
98 | override fun runAndroidTests(
99 | testExecutionTaskSpec: TestExecutionTaskSpec,
100 | totalDevices: Int,
101 | currentDeviceIndex: Int
102 | ) {
103 | println("runAndroidTests$device")
104 |
105 | val command =
106 | """ -s ${device.serialNumber} shell 'CLASSPATH=${'$'}(pm path androidx.test.services) app_process / \
107 | androidx.test.services.shellexecutor.ShellMain am instrument -w -e \
108 | targetInstrumentation ${testExecutionTaskSpec.instrumentPackage}/${testExecutionTaskSpec.customRunner} \
109 | -e numShards $totalDevices -e shardIndex $currentDeviceIndex \
110 | -e listener ${testExecutionTaskSpec.listenerClass} \
111 | -e clearPackageData true \
112 | androidx.test.orchestrator/.AndroidTestOrchestrator'"""
113 |
114 | logger.info(command)
115 |
116 | /**
117 | * adb shell 'CLASSPATH=$(pm path androidx.test.services) app_process / \androidx.test.services.shellexecutor.ShellMain am instrument -w -e \
118 | * targetInstrumentation com.xyz.package.test/androidx.test.runner.AndroidJUnitRunner \androidx.test.orchestrator/.AndroidTestOrchestrator'
119 | */
120 | CommandLine.executeCommand(
121 | Adb.adbCommand(command).also { println(it) }
122 | ).getOrThrow().also { println(it) }
123 | }
124 |
125 | override fun deleteTestResults() {
126 | println("deleteTestResults$device")
127 | // TODO check this logic
128 | if ((deviceApiLevel().toIntOrNull() ?: 0) >= 30) {
129 | CommandLine.executeCommand(Adb.adbCommand(" -s ${device.serialNumber} shell rm -f -rR -v /storage/self/primary/Documents/opentestlab/testlogs"))
130 | } else {
131 | CommandLine.executeCommand(Adb.adbCommand(" -s ${device.serialNumber} shell rm -f -rR -v /storage/self/primary/opentestlab/testlogs"))
132 | }
133 | }
134 |
135 | override fun pullTestResults(
136 | testExecutionTaskSpec: TestExecutionTaskSpec,
137 | ) {
138 | val reportsDir = File(testExecutionTaskSpec.workingDir, "reports")
139 | reportsDir.mkdirs()
140 | println("pullTestResults $device into ${reportsDir.absolutePath}")
141 |
142 | if ((deviceApiLevel().toIntOrNull() ?: 0) >= 30) {
143 | CommandLine.executeCommand(
144 | Adb.adbCommand(
145 | "-s ${device.serialNumber} pull " +
146 | "/storage/self/primary/Documents/opentestlab/testlogs/. " +
147 | "${reportsDir.absolutePath}/"
148 | )
149 | )
150 | } else {
151 | CommandLine.executeCommand(
152 | Adb.adbCommand(
153 | "-s ${device.serialNumber} pull " +
154 | "/storage/self/primary/opentestlab/testlogs/. " +
155 | "${reportsDir.absolutePath}/"
156 | )
157 | )
158 | }
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/src/main/kotlin/dev/oianmol/opentestlab/android/testexec/resultreader/DeviceTestResultReader.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.opentestlab.android.testexec.resultreader
2 |
3 | import dev.oianmol.opentestlab.DeviceFarmTestResults
4 | import dev.oianmol.opentestlab.android.testexec.TestExecutionTaskSpec
5 | import dev.oianmol.opentestlab.android.testexec.resultreader.utils.XmlTestSuite
6 | import dev.oianmol.opentestlab.android.testexec.resultreader.utils.parseAs
7 | import java.nio.file.Files
8 | import kotlin.io.path.extension
9 | import kotlin.io.path.reader
10 |
11 | interface DeviceTestResultReader {
12 | fun readTestResultFor(testExecutionTaskSpec: TestExecutionTaskSpec): List
13 | }
14 |
15 | object DefaultDeviceTestResultReader : DeviceTestResultReader {
16 | override fun readTestResultFor(testExecutionTaskSpec: TestExecutionTaskSpec): List {
17 | return testExecutionTaskSpec.workingDir.listFiles()?.map { it.toPath() }
18 | ?.filter { Files.isRegularFile(it) && it.extension == "xml" }
19 | ?.toList()
20 | ?.map {
21 | val testResultItem = it.reader().readText().parseAs()
22 | DeviceFarmTestResults.Testsuite.newBuilder()
23 | .setTests(testResultItem.tests?.toIntOrNull() ?: -1)
24 | .setErrors(testResultItem.errors?.toIntOrNull() ?: -1)
25 | .setFailures(testResultItem.failures?.toIntOrNull() ?: -1)
26 | .setFailures(testResultItem.skipped?.toIntOrNull() ?: -1)
27 | .setName(it.toFile().name ?: testResultItem.name ?: "Name")
28 | .setHostname(testResultItem.hostname ?: "")
29 | .setTime(testResultItem.time?.toIntOrNull() ?: -1)
30 | .setTimestamp(testResultItem.timestamp)
31 | .setProperties(
32 | DeviceFarmTestResults.Properties.newBuilder()
33 | .addAllProperty(testResultItem.properties?.property?.map {
34 | DeviceFarmTestResults.Property.newBuilder()
35 | .setName(it.name)
36 | .setValue(it.value)
37 | .build()
38 | })
39 | .build()
40 | )
41 | .addAllTestcase(testResultItem.testcase?.map {
42 | DeviceFarmTestResults.Testcase.newBuilder()
43 | .setName(it.name)
44 | .setClassname(it.classname)
45 | .setStatus(it.status)
46 | .setMessage(it.message)
47 | .setType(it.type)
48 | .setTime(it.time?.toIntOrNull() ?: -1)
49 | .setTrace(it.trace)
50 | .build()
51 | }).build()
52 | } ?: emptyList()
53 | }
54 | }
--------------------------------------------------------------------------------
/src/main/kotlin/dev/oianmol/opentestlab/android/testexec/resultreader/utils/XmlTestSuite.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.opentestlab.android.testexec.resultreader.utils
2 |
3 | import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement
4 | import javax.xml.bind.annotation.XmlAttribute
5 |
6 | @JacksonXmlRootElement(localName = "testsuite")
7 | data class XmlTestSuite(
8 | val properties: Properties? = null,
9 | val testcase: MutableList? = null,
10 | @XmlAttribute
11 | val name: String? = null,
12 | @XmlAttribute
13 | val tests: String? = null,
14 | @XmlAttribute
15 | val failures: String? = null,
16 | @XmlAttribute
17 | val errors: String? = null,
18 | @XmlAttribute
19 | val skipped: String? = null,
20 | @XmlAttribute
21 | val time: String? = null,
22 | @XmlAttribute
23 | val timestamp: String? = null,
24 | @XmlAttribute
25 | val hostname: String? = null,
26 | )
27 |
28 | data class Properties(
29 | val property: MutableList? = null,
30 | )
31 |
32 | data class Property(
33 | @XmlAttribute
34 | val name: String? = null,
35 | @XmlAttribute
36 | val value: String? = null,
37 | )
38 |
39 | data class Testcase(
40 | @XmlAttribute
41 | val name: String? = null,
42 | @XmlAttribute
43 | val classname: String? = null,
44 | @XmlAttribute
45 | val time: String? = null,
46 | @XmlAttribute
47 | val status: String? = null,
48 | @XmlAttribute
49 | val trace: String? = null,
50 | @XmlAttribute
51 | val message: String? = null,
52 | @XmlAttribute
53 | val type: String? = null,
54 | )
55 |
56 | enum class Status {
57 | PASSED, FAILED, IGNORED, ASSUMPTION_FAILED
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/kotlin/dev/oianmol/opentestlab/android/testexec/resultreader/utils/kotlinXmlMapper.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.opentestlab.android.testexec.resultreader.utils
2 |
3 | import com.fasterxml.jackson.databind.DeserializationFeature
4 | import com.fasterxml.jackson.databind.MapperFeature
5 | import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule
6 | import com.fasterxml.jackson.dataformat.xml.XmlMapper
7 |
8 | internal val kotlinXmlMapper = XmlMapper(JacksonXmlModule().apply {
9 | setDefaultUseWrapper(false)
10 | }).configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true)
11 | .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
12 |
13 |
14 | internal inline fun String.parseAs(): T {
15 | return kotlinXmlMapper.readValue(this,T::class.java)
16 | }
--------------------------------------------------------------------------------
/src/main/kotlin/dev/oianmol/opentestlab/android/testexec/testrunner/ITestRunner.kt:
--------------------------------------------------------------------------------
1 | package dev.oianmol.opentestlab.android.testexec.testrunner
2 |
3 | import dev.oianmol.opentestlab.DeviceFarmDevice
4 | import dev.oianmol.opentestlab.DeviceFarmTestResults
5 | import dev.oianmol.dev.oianmol.opentestlab.android.adbandfriends.DeviceDiscovery
6 | import dev.oianmol.dev.oianmol.opentestlab.runCatchingCancellable
7 | import dev.oianmol.opentestlab.android.devicefarm.DeviceAvailabilityStore
8 | import dev.oianmol.opentestlab.android.testexec.TestExecutionTaskSpec
9 | import dev.oianmol.opentestlab.android.testexec.commandrunner.DeviceCommandRunner
10 | import dev.oianmol.opentestlab.android.testexec.resultreader.DeviceTestResultReader
11 | import kotlinx.coroutines.async
12 | import kotlinx.coroutines.awaitAll
13 | import kotlinx.coroutines.coroutineScope
14 | import java.util.logging.Logger
15 |
16 | interface ITestRunner {
17 | suspend fun canExecute(testExecutionTaskSpec: TestExecutionTaskSpec): Boolean
18 | suspend fun execute(testExecutionTaskSpec: TestExecutionTaskSpec): DeviceFarmTestResults
19 | }
20 |
21 | class DeviceFarmTestRunner(
22 | iDiscoverDevices: DeviceDiscovery,
23 | deviceCommandRunner: DeviceCommandRunner,
24 | deviceAvailabilityStore: DeviceAvailabilityStore,
25 | deviceTestResultReader: DeviceTestResultReader
26 | ) : ITestRunner,
27 | DeviceDiscovery by iDiscoverDevices,
28 | DeviceCommandRunner by deviceCommandRunner,
29 | DeviceAvailabilityStore by deviceAvailabilityStore,
30 | DeviceTestResultReader by deviceTestResultReader {
31 | private val logger = Logger.getLogger("OpenTestLabTestRunner")
32 |
33 | override suspend fun canExecute(testExecutionTaskSpec: TestExecutionTaskSpec): Boolean {
34 | val allDevices = listDevices()
35 | println("allDevices$allDevices")
36 | val availableDevices = allDevices.filter { isDeviceAvailable(it.serialNumber) }
37 | println("availableDevices$availableDevices")
38 | return availableDevices.isNotEmpty()
39 | }
40 |
41 | override suspend fun execute(testExecutionTaskSpec: TestExecutionTaskSpec): DeviceFarmTestResults {
42 | val deviceFarmTestResults = DeviceFarmTestResults.newBuilder()
43 |
44 | val allDevices = listDevices()
45 |
46 | val devices = allDevices.filter { isDeviceAvailable(it.serialNumber) }
47 |
48 | coroutineScope {
49 | runCatching {
50 | val deviceInstalledTo = mutableListOf()
51 | devices.map { device ->
52 | async {
53 | runCatchingCancellable {
54 | markDeviceUnavailable(device)
55 |
56 | val deviceConnection = deviceConnection(device = device)
57 |
58 | runCatchingCancellable {
59 | deviceConnection.uninstallApks(
60 | testPackageName = testExecutionTaskSpec.testPackageName,
61 | packageName = testExecutionTaskSpec.packageName
62 | )
63 | }
64 |
65 | deviceConnection.installApks(testExecutionTaskSpec = testExecutionTaskSpec)
66 |
67 | deviceConnection.installOrchestratorApk()
68 |
69 | deviceInstalledTo.add(device)
70 | }.exceptionOrNull()?.printStackTrace()
71 | }
72 | }.awaitAll()
73 |
74 | val totalDevices = deviceInstalledTo.size
75 |
76 | deviceInstalledTo.mapIndexed { currentDeviceIndex, device ->
77 | async {
78 | runCatchingCancellable {
79 | val deviceConnection = deviceConnection(device)
80 |
81 | deviceConnection.deleteTestResults() // previous if any
82 |
83 | runCatchingCancellable {
84 | deviceConnection.runAndroidTests(
85 | testExecutionTaskSpec = testExecutionTaskSpec,
86 | totalDevices = totalDevices,
87 | currentDeviceIndex = currentDeviceIndex
88 | )
89 | }.fold(onSuccess = {
90 | it
91 | }, onFailure = {
92 | it.printStackTrace()
93 | })
94 |
95 | deviceConnection.pullTestResults(testExecutionTaskSpec)
96 |
97 | deviceConnection.deleteTestResults()
98 |
99 | deviceConnection.uninstallApks(
100 | testPackageName = testExecutionTaskSpec.testPackageName,
101 | packageName = testExecutionTaskSpec.packageName
102 | )
103 |
104 | markDeviceAvailable(deviceFarmDevice = device)
105 |
106 | }.exceptionOrNull()?.let { throwable ->
107 | throwable.printStackTrace()
108 | markDeviceAvailable(deviceFarmDevice = device)
109 | throw throwable
110 | }
111 | }
112 | }.awaitAll()
113 | // wait for all jobs to complete
114 | }.exceptionOrNull()?.let {
115 | it.printStackTrace()
116 | devices.forEach { deviceFarmDevice ->
117 | markDeviceAvailable(deviceFarmDevice)
118 | }
119 | }
120 | }
121 |
122 | println(deviceFarmTestResults.toString())
123 | return deviceFarmTestResults.build()
124 | }
125 | }
--------------------------------------------------------------------------------
/src/main/resources/orchestrator-1.4.1.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oianmol/OpenTestLabAndroid/d9bfb402da88f6f1c88172269dfb1bfe5610fba7/src/main/resources/orchestrator-1.4.1.apk
--------------------------------------------------------------------------------
/src/main/resources/test-services-1.4.2.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oianmol/OpenTestLabAndroid/d9bfb402da88f6f1c88172269dfb1bfe5610fba7/src/main/resources/test-services-1.4.2.apk
--------------------------------------------------------------------------------
/test-services-1.4.2.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oianmol/OpenTestLabAndroid/d9bfb402da88f6f1c88172269dfb1bfe5610fba7/test-services-1.4.2.apk
--------------------------------------------------------------------------------