Skip to content

Commit 58d89d6

Browse files
committed
merge from upstream, refactor
2 parents 52bd407 + fcd2efa commit 58d89d6

File tree

12 files changed

+293
-93
lines changed

12 files changed

+293
-93
lines changed

README.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,30 @@
11
# Firebase Test Lab Plugin for Android
2-
[![Kotlin version badge](https://img.shields.io/badge/kotlin-1.1.60-blue.svg)](http://kotlinlang.org/)
2+
[![Kotlin version badge](https://img.shields.io/badge/kotlin-2.0.0-blue.svg)](http://kotlinlang.org/)
33
[![License](https://img.shields.io/crates/l/rustc-serialize.svg)](https://github.com/piotrmadry/FirebaseTestLab-Android/blob/master/LICENSE)
44

5-
![firebase](https://i.ytimg.com/vi/4_ZEEX1x17k/maxresdefault.jpg)
6-
75
## Introduction
8-
Firebase is actually the most popular developer tool platform, wchich handles almost every aspect of the app. It also gives possibility to run Android Tests on physical or virtual devices hosted in a Google data center through [Firebase Test Lab](https://firebase.google.com/docs/test-lab/). In order to fully exploit the potential of this tool I've created plugin to simplify process of creating tests configurations. It allows to run tests locally as well as on you CI server.
6+
Plugin for which integrates Firebase Test Lab with Android Project. Simplify running Android Tests on Firebase platform locally as well as on using Continuous integration.
97

108
### Contributors
119
- [Jacek Marchwicki](https://github.com/jacek-marchwicki)
1210

1311
#### Available features
12+
1413
- Automatic installation of `gcloud` command line tool
1514
- Creating tasks for testable `buildType`[By default it is `debug`. If you want to change it use `testBuildType "buildTypeName"`]
16-
- Creating tasks for every defined device and configuration separetly [ including Instrumented / Robo tests ]
15+
- Creating tasks for every defined device and configuration separately [ including Instrumented / Robo tests ]
1716
- Creating tasks which runs all configurations at once
1817
- Ability to download tests results to specific location
1918
- Ability to clear directory inside bucket before test run
19+
- Instrumented tests sharding
2020

2121
#### Benefits
22+
2223
- Readability
2324
- Simplicity
2425
- Remote and Local Testing
2526
- Compatible with Gradle 3.0
27+
- Instrumented Tests sharding for parallel test execution
2628

2729
#### Setup
2830

@@ -120,6 +122,9 @@ firebaseTestLab {
120122
121123
// If you are using ABI splits you can remove testing universal APK
122124
// testUniversalApk = false
125+
126+
// For instrumented test you can specify number of shards, which allows to split all the tests for [numShards] times and execute them in parallel
127+
// numShards = 4
123128
124129
// You can set timeout (in seconds) for test
125130
// timeout = 6000

plugin/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ apply plugin: "com.gradle.plugin-publish"
66
apply plugin: "org.gradle.kotlin.kotlin-dsl"
77

88
group = "firebase.test.lab"
9-
version = "1.1.6"
9+
version = "2.0.0"
1010

1111
gradlePlugin {
1212
plugins {

plugin/src/main/java/com/appunite/firebasetestlabplugin/FirebaseTestLabPlugin.kt

Lines changed: 121 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import com.android.build.gradle.api.BaseVariantOutput
66
import com.android.build.gradle.api.TestVariant
77
import com.appunite.firebasetestlabplugin.cloud.CloudTestResultDownloader
88
import com.appunite.firebasetestlabplugin.cloud.FirebaseTestLabProcessCreator
9+
import com.appunite.firebasetestlabplugin.cloud.ProcessData
910
import com.appunite.firebasetestlabplugin.cloud.TestType
1011
import com.appunite.firebasetestlabplugin.model.Device
1112
import com.appunite.firebasetestlabplugin.model.TestResults
13+
import com.appunite.firebasetestlabplugin.tasks.InstrumentationShardingTask
1214
import com.appunite.firebasetestlabplugin.utils.Constants
1315
import org.apache.tools.ant.taskdefs.condition.Os
1416
import org.gradle.api.GradleException
@@ -21,8 +23,9 @@ import org.gradle.kotlin.dsl.register
2123
import groovy.lang.Closure
2224
import java.io.ByteArrayOutputStream
2325
import java.io.File
26+
import java.io.Serializable
2427

25-
internal class FirebaseTestLabPlugin : Plugin<Project> {
28+
class FirebaseTestLabPlugin : Plugin<Project> {
2629

2730
open class HiddenExec : Exec() {
2831
init {
@@ -69,7 +72,7 @@ internal class FirebaseTestLabPlugin : Plugin<Project> {
6972
}
7073
}
7174

72-
data class Sdk(val gcloud: File, val gsutil: File)
75+
data class Sdk(val gcloud: File, val gsutil: File): Serializable
7376

7477
private fun createDownloadSdkTask(project: Project, cloudSdkPath: String?): Sdk =
7578
if (cloudSdkPath != null) {
@@ -190,16 +193,9 @@ internal class FirebaseTestLabPlugin : Plugin<Project> {
190193
throw IllegalStateException("If you want to clear directory before run you need to setup cloudBucketName and cloudDirectoryName")
191194
}
192195

193-
val firebaseTestLabProcessCreator = FirebaseTestLabProcessCreator(
194-
sdk,
195-
cloudBucketName,
196-
cloudDirectoryName,
197-
project.logger
198-
)
199-
200196
(project.extensions.findByName(ANDROID) as AppExtension).apply {
201197
testVariants.toList().forEach { testVariant ->
202-
createGroupedTestLabTask(devices, testVariant, firebaseTestLabProcessCreator, ignoreFailures, downloader)
198+
createGroupedTestLabTask(devices, testVariant, ignoreFailures, downloader, sdk, cloudBucketName, cloudDirectoryName)
203199
}
204200
}
205201

@@ -209,19 +205,23 @@ internal class FirebaseTestLabPlugin : Plugin<Project> {
209205

210206
data class DeviceAppMap(val device: Device, val apk: BaseVariantOutput)
211207

212-
data class Test(val device: Device, val apk: BaseVariantOutput, val testApk: BaseVariantOutput)
213-
208+
data class Test(val device: Device, val apk: BaseVariantOutput, val testApk: BaseVariantOutput): Serializable
209+
214210
private fun createGroupedTestLabTask(
215-
devices: List<Device>,
216-
variant: TestVariant,
217-
firebaseTestLabProcessCreator: FirebaseTestLabProcessCreator,
218-
ignoreFailures: Boolean,
219-
downloader: CloudTestResultDownloader?) {
211+
devices: List<Device>,
212+
variant: TestVariant,
213+
ignoreFailures: Boolean,
214+
downloader: CloudTestResultDownloader?,
215+
sdk: Sdk,
216+
cloudBucketName: String?,
217+
cloudDirectoryName: String?
218+
) {
220219
val variantName = variant.testedVariant?.name?.capitalize() ?: ""
221220

222221
val cleanTask = "firebaseTestLabClean${variantName.capitalize()}"
223222

224-
val runTestsTask = taskPrefixExecute + variantName.capitalize()
223+
val variantSuffix = variantName.capitalize()
224+
val runTestsTask = taskPrefixExecute + variantSuffix
225225
val runTestsTaskInstrumentation = "${runTestsTask}Instrumentation"
226226
val runTestsTaskRobo = "${runTestsTask}Robo"
227227

@@ -266,19 +266,73 @@ internal class FirebaseTestLabPlugin : Plugin<Project> {
266266
dependsOn(taskSetup)
267267
dependsOn(arrayOf(test.apk.assemble))
268268
doLast {
269-
val result = firebaseTestLabProcessCreator.callFirebaseTestLab(test.device, test.apk.outputFile, TestType.Robo)
269+
val result = FirebaseTestLabProcessCreator.callFirebaseTestLab(ProcessData(
270+
sdk = sdk,
271+
gCloudBucketName = cloudBucketName,
272+
gCloudDirectory = cloudDirectoryName,
273+
device = test.device,
274+
apk = test.apk.outputFile,
275+
276+
testType = TestType.Robo
277+
))
270278
processResult(result, ignoreFailures)
271279
}
272280
})
273281
}
274-
275-
val instrumentationTasks = combineAll(appVersions, variant.outputs, {deviceAndMap, testApk -> Test(deviceAndMap.device, deviceAndMap.apk, testApk)})
276-
.map {
277-
test ->
278-
val devicePart = test.device.name.capitalize()
279-
val apkPart = dashToCamelCase(test.apk.name).capitalize()
280-
val testApkPart = test.testApk.let { if (it.filters.isEmpty()) "" else dashToCamelCase(it.name).capitalize() }
281-
val taskName = "$runTestsTaskInstrumentation$devicePart$apkPart$testApkPart"
282+
283+
val testResultFile = File(project.buildDir, "TestResults.txt")
284+
285+
val instrumentationTasks: List<Task> = combineAll(appVersions, variant.outputs)
286+
{ deviceAndMap, testApk -> Test(deviceAndMap.device, deviceAndMap.apk, testApk) }
287+
.map { test ->
288+
val devicePart = test.device.name.capitalize()
289+
val apkPart = dashToCamelCase(test.apk.name).capitalize()
290+
val testApkPart = test.testApk.let { if (it.filters.isEmpty()) "" else dashToCamelCase(it.name).capitalize() }
291+
val taskName = "$runTestsTaskInstrumentation$devicePart$apkPart$testApkPart"
292+
val numShards = test.device.numShards
293+
294+
if (numShards > 0) {
295+
project.tasks.create(taskName, InstrumentationShardingTask::class.java) {
296+
group = Constants.FIREBASE_TEST_LAB
297+
description = "Run Instrumentation test for ${test.device.name} device on $variantName/${test.apk.name} in Firebase Test Lab"
298+
this.processData = ProcessData(
299+
sdk = sdk,
300+
gCloudBucketName = cloudBucketName,
301+
gCloudDirectory = cloudDirectoryName,
302+
device = test.device,
303+
apk = test.apk.outputFile,
304+
testType = TestType.Instrumentation(test.testApk.outputFile)
305+
)
306+
this.stateFile = testResultFile
307+
308+
if (downloader != null) {
309+
mustRunAfter(cleanTask)
310+
}
311+
dependsOn(taskSetup)
312+
dependsOn(arrayOf(test.apk.assemble, test.testApk.assemble))
313+
314+
doFirst {
315+
testResultFile.writeText("")
316+
}
317+
318+
doLast {
319+
val testResults = testResultFile.readText()
320+
val resultCode: Int? = testResults.toIntOrNull()
321+
322+
logger.lifecycle("TESTS RESULTS: Every digit represents single shard.")
323+
logger.lifecycle("\"0\" means -> tests for particular shard passed.")
324+
logger.lifecycle("\"1\" means -> tests for particular shard failed.")
325+
326+
logger.lifecycle("RESULTS_CODE: $resultCode")
327+
logger.lifecycle("When result code is equal to 0 means that all tests for all shards passed, otherwise some of them failed.")
328+
329+
if (resultCode != null) {
330+
processResult(resultCode, ignoreFailures)
331+
}
332+
}
333+
}
334+
335+
} else {
282336
project.task(taskName, closureOf<Task> {
283337
inputs.files(test.testApk.outputFile, test.apk.outputFile)
284338
group = Constants.FIREBASE_TEST_LAB
@@ -289,13 +343,21 @@ internal class FirebaseTestLabPlugin : Plugin<Project> {
289343
dependsOn(taskSetup)
290344
dependsOn(arrayOf(test.apk.assemble, test.testApk.assemble))
291345
doLast {
292-
val result = firebaseTestLabProcessCreator.callFirebaseTestLab(test.device, test.apk.outputFile, TestType.Instrumentation(test.testApk.outputFile))
346+
val result = FirebaseTestLabProcessCreator.callFirebaseTestLab(ProcessData(
347+
sdk = sdk,
348+
gCloudBucketName = cloudBucketName,
349+
gCloudDirectory = cloudDirectoryName,
350+
device = test.device,
351+
apk = test.apk.outputFile,
352+
testType = TestType.Instrumentation(test.testApk.outputFile)
353+
))
293354
processResult(result, ignoreFailures)
294355
}
295356
})
296357
}
358+
}
297359

298-
val allInstrumentation = addExecuteAndDownload(runTestsTaskInstrumentation, downloader, cleanTask, closureOf<Task> {
360+
val allInstrumentation: Task = project.task(runTestsTaskInstrumentation, closureOf<Task> {
299361
group = Constants.FIREBASE_TEST_LAB
300362
description = "Run all Instrumentation tests for $variantName in Firebase Test Lab"
301363
dependsOn(instrumentationTasks)
@@ -316,7 +378,7 @@ internal class FirebaseTestLabPlugin : Plugin<Project> {
316378
}
317379
})
318380

319-
val allRobo = project.task(runTestsTaskRobo, closureOf<Task> {
381+
val allRobo: Task = project.task(runTestsTaskRobo, closureOf<Task> {
320382
group = Constants.FIREBASE_TEST_LAB
321383
description = "Run all Robo tests for $variantName in Firebase Test Lab"
322384
dependsOn(roboTasks)
@@ -337,14 +399,29 @@ internal class FirebaseTestLabPlugin : Plugin<Project> {
337399
}
338400
})
339401

340-
addExecuteAndDownload(runTestsTask, downloader, cleanTask, closureOf<Task> {
402+
project.task(runTestsTask, closureOf<Task> {
341403
group = Constants.FIREBASE_TEST_LAB
342404
description = "Run all tests for $variantName in Firebase Test Lab"
343405
dependsOn(allRobo, allInstrumentation)
344406
})
407+
408+
if (downloader != null) {
409+
listOf(variantSuffix, "${variantSuffix}Instrumentation").map{suffix ->
410+
project.task(taskPrefixDownload + suffix, closureOf<Task> {
411+
group = Constants.FIREBASE_TEST_LAB
412+
description = "Run Android Tests in Firebase Test Lab and download artifacts from google storage"
413+
dependsOn(taskSetup)
414+
dependsOn(taskPrefixExecute + suffix)
415+
mustRunAfter(cleanTask)
345416

417+
doLast {
418+
downloader.getResults()
419+
}
420+
})
421+
}
422+
}
346423
}
347-
424+
348425
private fun processResult(result: TestResults, ignoreFailures: Boolean) {
349426
if (result.isSuccessful) {
350427
project.logger.lifecycle(result.message)
@@ -356,30 +433,22 @@ internal class FirebaseTestLabPlugin : Plugin<Project> {
356433
}
357434
}
358435
}
359-
360-
private fun addExecuteAndDownload(name: String, downloader: CloudTestResultDownloader?, cleanTask: String, taskClosure: Closure<Any?>): Task {
361-
val runTask = project.task(name, taskClosure)
362-
if (downloader != null) {
363-
val configuration = name.substring(taskPrefixExecute.length)
364-
project.task(taskPrefixDownload + configuration, closureOf<Task> {
365-
group = Constants.FIREBASE_TEST_LAB
366-
description = "Run Android Tests for $configuration in Firebase Test Lab and download artifacts from google storage"
367-
dependsOn(taskSetup)
368-
dependsOn(name)
369-
mustRunAfter(cleanTask)
370-
371-
doLast {
372-
downloader.getResults()
373-
}
374-
})
436+
437+
private fun processResult(resultCode: Int, ignoreFailures: Boolean) =
438+
if (resultCode == 0) {
439+
project.logger.lifecycle("SUCCESS: All tests passed.")
440+
} else {
441+
if (ignoreFailures) {
442+
println("FAILURE: Tests failed.")
443+
project.logger.error("FAILURE: Tests failed.")
444+
} else {
445+
throw GradleException("FAILURE: Tests failed.")
446+
}
375447
}
376-
return runTask
377-
}
378448
}
379449

380-
381450
private fun <T1, T2, R> combineAll(l1: Collection<T1>, l2: Collection<T2>, func: (T1, T2) -> R): List<R> =
382451
l1.flatMap { t1 -> l2.map { t2 -> func(t1, t2)} }
383452

384-
private fun dashToCamelCase(dash: String): String =
453+
fun dashToCamelCase(dash: String): String =
385454
dash.split('-', '_').joinToString("") { it.capitalize() }

0 commit comments

Comments
 (0)