Skip to content

Commit a598231

Browse files
Better readme
1 parent ee41c84 commit a598231

39 files changed

+576
-151
lines changed

README.md

+98-33
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
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.
99

1010
#### Available features
11+
- Automatic installation of `gcloud` command line tool
1112
- Creating tasks for testable `buildType`[By default it is `debug`. If you want to change it use `testBuildType "buildTypeName"`]
1213
- Creating tasks for every defined device and configuration separetly [ including Instrumented / Robo tests ]
1314
- Creating tasks which runs all configurations at once
@@ -22,52 +23,116 @@ Firebase is actually the most popular developer tool platform, wchich handles al
2223

2324
#### Setup
2425

25-
``` Groovy
26-
buildscript {
27-
repositories {
28-
maven {
29-
url "https://plugins.gradle.org/m2/"
26+
1. If you don't have a Firebase project for your app, go to the []Firebase console](https://console.firebase.google.com/) and click Create New Project to create one now. You will need ownership or edit permissions in your project.
27+
2. Create a service account related with your firebase project with an Editor role in the [Google Cloud Platform console - IAM/Service Accounts](https://console.cloud.google.com/iam-admin/serviceaccounts/)
28+
3. Copy `Project ID` from [Google Cloud Platform console - HOME](https://console.cloud.google.com/home)
29+
4. Add plugin to your root project `build.gradle`:
30+
```grovy
31+
buildscript {
32+
repositories {
33+
maven { url 'https://jitpack.io' }
34+
}
35+
dependencies {
36+
classpath 'com.github.jacek-marchwicki:FirebaseTestLab-Android:<version_from_github_releases_tab'
37+
}
38+
}
39+
```
40+
5. Add configuration in your project `build.gradle`:
41+
```groovy
42+
apply plugin: 'firebase.test.lab'
43+
44+
firebaseTestLab {
45+
keyFile = file("test-lab-key.json")
46+
googleProjectId = "your-project-app-id"
47+
devices {
48+
nexusEmulator {
49+
deviceIds = ["Nexus6"]
50+
androidApiLevels = [26]
51+
}
52+
}
3053
}
31-
}
32-
dependencies {
33-
classpath "gradle.plugin.firebase.test.lab:plugin:1.0.4"
34-
}
35-
}
54+
```
55+
List of available [devices](https://firebase.google.com/docs/test-lab/images/gcloud-device-list.png)
56+
6. Run your instrumentations tests
57+
58+
```bash
59+
./gradlew firebaseTestLabExecuteDebugInstrumentation
60+
```
61+
62+
Or run robo tests
63+
64+
```bash
65+
./gradlew firebaseTestLabExecuteDebugRobo
66+
```
67+
68+
#### Advanced configuration
69+
70+
You can
71+
``` Goovy
72+
// Setup firebase test lab plugin
73+
firebaseTestLab {
74+
// REQUIRED obtain service key as described inside README
75+
keyFile = file("test-lab-key.json")
76+
// REQUIRED setup google project id ad described inside README
77+
googleProjectId = "your-project-app-id"
3678
37-
apply plugin: "firebase.test.lab"
38-
```
39-
``` Groovy
40-
//For gradle 2.1+
41-
plugins {
42-
id "firebase.test.lab" version "1.0.4"
43-
}
44-
```
79+
// If you want you can ignore test failures
80+
// ignoreFailures = true
4581
46-
#### How to use it
82+
// If you prefer you can use your custom google storage bucket for storing build sources and results
83+
// cloudBucketName = "your-custome-google-storage-bucket-name"
84+
// cloudDirectoryName = "your-custome-directory-name"
4785
48-
Add devices configurations inside `build.gradle`
49-
List of available [devices](https://firebase.google.com/docs/test-lab/images/gcloud-device-list.png)
86+
// If you prefer to install gcloud tool manually you can set path by
87+
// cloudSdkPath = "/user/cloud-sdk/bin"
5088
51-
``` Goovy
52-
firebaseTestLab {
53-
keyFile = file("keys.json")
54-
googleProjectId = "your-project-id"
55-
cloudSdkPath = "/user/cloud-sdk/bin"
56-
cloudBucketName = "bucket-test"
57-
cloudDirectoryName = "androidTests"
58-
clearDirectoryBeforeRun = true
89+
// If you want to change default gcloud installation path (default is in build/gcloud directory)
90+
// you can set environment variable `export CLOUDSDK_INSTALL_DIR=`/cache/your_directory/`
5991
92+
// REQUIRED
6093
devices {
61-
galaxyS7 {
62-
androidApiLevels = [23]
63-
deviceIds = ["herolte"]
94+
// REQUIRED add at least one device
95+
nexusEmulator {
96+
// REQUIRED Choose at least one device id
97+
// you can list all available via `gcloud firebase test android models list` or look on https://firebase.google.com/docs/test-lab/images/gcloud-device-list.png
98+
deviceIds = ["Nexus6"]
99+
100+
// REQUIRED Choose at least one API level
101+
// you can list all available via `gcloud firebase test android models list` for your device model
102+
androidApiLevels = [26]
103+
104+
// You can test app in landscape and portrait
105+
// screenOrientations = [com.appunite.firebasetestlabplugin.model.ScreenOrientation.PORTRAIT, com.appunite.firebasetestlabplugin.model.ScreenOrientation.LANDSCAPE]
106+
107+
// Choose language (default is `en`)
108+
// you can list all available via `gcloud firebase test android locales list`
109+
// locales = ["en"]
110+
111+
// If you are using ABI splits you can filter selected abi
112+
// filterAbiSplits = true
113+
// abiSplits = ["armeabi-v7a", "arm64-v8a", "x86", "x86_64"]
114+
115+
// If you are using ABI splits you can remove testing universal APK
116+
// testUniversalApk = false
117+
118+
// You can set timeout (in seconds) for test
119+
// timeout = 6000
120+
}
121+
// You can define more devices
122+
someOtherDevices {
123+
deviceIds = ["hammerhead", "shamu"]
124+
androidApiLevels = [21]
64125
}
65126
}
66127
}
67128
```
68129

130+
For more precise test selection run
69131

132+
```bash
133+
./gradlew tasks
134+
```
70135

71-
136+
to discover all available test options
72137

73138

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

+73-22
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ 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.TestType
910
import com.appunite.firebasetestlabplugin.model.Device
1011
import com.appunite.firebasetestlabplugin.model.TestResults
11-
import com.appunite.firebasetestlabplugin.model.TestType
1212
import com.appunite.firebasetestlabplugin.utils.Constants
1313
import org.apache.tools.ant.taskdefs.condition.Os
1414
import org.gradle.api.*
@@ -194,29 +194,32 @@ internal class FirebaseTestLabPlugin : Plugin<Project> {
194194

195195
(project.extensions.findByName(ANDROID) as AppExtension).apply {
196196
testVariants.toList().forEach { testVariant ->
197-
createGroupedTestLabTask(TestType.INSTRUMENTATION, devices, testVariant, firebaseTestLabProcessCreator, ignoreFailures, downloader)
198-
createGroupedTestLabTask(TestType.ROBO, devices, testVariant, firebaseTestLabProcessCreator, ignoreFailures, downloader)
197+
createGroupedTestLabTask(devices, testVariant, firebaseTestLabProcessCreator, ignoreFailures, downloader)
199198
}
200199
}
201200

202201

203202
}
204203
}
205204

205+
data class DeviceAppMap(val device: Device, val apk: BaseVariantOutput)
206+
206207
data class Test(val device: Device, val apk: BaseVariantOutput, val testApk: BaseVariantOutput)
207208

208209
private fun createGroupedTestLabTask(
209-
testType: TestType,
210210
devices: List<Device>,
211211
variant: TestVariant,
212212
firebaseTestLabProcessCreator: FirebaseTestLabProcessCreator,
213213
ignoreFailures: Boolean,
214214
downloader: CloudTestResultDownloader?) {
215215
val variantName = variant.testedVariant?.name?.capitalize() ?: ""
216216

217-
val cleanTask = "firebaseTestLabClean${variantName.capitalize()}${testType.toString().toLowerCase().capitalize()}"
218-
val runTestsTask = "firebaseTestLabExecute${variantName.capitalize()}${testType.toString().toLowerCase().capitalize()}"
219-
val downloadTask = "firebaseTestLabDownload${variantName.capitalize()}${testType.toString().toLowerCase().capitalize()}"
217+
val cleanTask = "firebaseTestLabClean${variantName.capitalize()}"
218+
219+
val runTestsTask = "firebaseTestLabExecute${variantName.capitalize()}"
220+
val runTestsTaskInstrumentation = "${runTestsTask}Instrumentation"
221+
val runTestsTaskRobo = "${runTestsTask}Robo"
222+
val downloadTask = "firebaseTestLabDownload${variantName.capitalize()}"
220223

221224
if (downloader != null) {
222225
project.task(cleanTask, closureOf<Task> {
@@ -229,7 +232,7 @@ internal class FirebaseTestLabPlugin : Plugin<Project> {
229232
})
230233
}
231234

232-
val tasks = combineAll(devices, variant.testedVariant.outputs, variant.outputs, ::Test)
235+
val appVersions = combineAll(devices, variant.testedVariant.outputs, ::DeviceAppMap)
233236
.filter {
234237
val hasAbiSplits = it.apk.filterTypes.contains(VariantOutput.ABI)
235238
if (hasAbiSplits) {
@@ -243,35 +246,55 @@ internal class FirebaseTestLabPlugin : Plugin<Project> {
243246
it.device.testUniversalApk
244247
}
245248
}
249+
val roboTasks = appVersions
250+
.map {
251+
test ->
252+
val devicePart = test.device.name.capitalize()
253+
val apkPart = dashToCamelCase(test.apk.name).capitalize()
254+
val taskName = "$runTestsTaskRobo$devicePart$apkPart"
255+
project.task(taskName, closureOf<Task> {
256+
inputs.files(test.apk.outputFile)
257+
group = Constants.FIREBASE_TEST_LAB
258+
description = "Run Robo test for ${test.device.name} device on $variantName/${test.apk.name} in Firebase Test Lab"
259+
if (downloader != null) {
260+
mustRunAfter(cleanTask)
261+
}
262+
dependsOn(taskSetup)
263+
dependsOn(arrayOf(test.apk.assemble))
264+
doLast {
265+
val result = firebaseTestLabProcessCreator.callFirebaseTestLab(test.device, test.apk.outputFile, TestType.Robo)
266+
processResult(result, ignoreFailures)
267+
}
268+
})
269+
}
270+
271+
val instrumentationTasks = combineAll(appVersions, variant.outputs, {deviceAndMap, testApk -> Test(deviceAndMap.device, deviceAndMap.apk, testApk)})
246272
.map {
247273
test ->
248274
val devicePart = test.device.name.capitalize()
249275
val apkPart = dashToCamelCase(test.apk.name).capitalize()
250276
val testApkPart = test.testApk.let { if (it.filters.isEmpty()) "" else dashToCamelCase(it.name).capitalize() }
251-
val taskName = "$runTestsTask$devicePart$apkPart$testApkPart"
277+
val taskName = "$runTestsTaskInstrumentation$devicePart$apkPart$testApkPart"
252278
project.task(taskName, closureOf<Task> {
253279
inputs.files(test.testApk.outputFile, test.apk.outputFile)
254280
group = Constants.FIREBASE_TEST_LAB
255-
description = "Run Android Tests in Firebase Test Lab"
281+
description = "Run Instrumentation test for ${test.device.name} device on $variantName/${test.apk.name} in Firebase Test Lab"
256282
if (downloader != null) {
257283
mustRunAfter(cleanTask)
258284
}
259285
dependsOn(taskSetup)
260-
dependsOn(* when (testType) {
261-
TestType.INSTRUMENTATION -> arrayOf(test.apk.assemble, test.testApk.assemble)
262-
TestType.ROBO -> arrayOf(test.apk.assemble)
263-
})
286+
dependsOn(arrayOf(test.apk.assemble, test.testApk.assemble))
264287
doLast {
265-
val result = firebaseTestLabProcessCreator.callFirebaseTestLab(testType, test.device, test.apk.outputFile, test.testApk.outputFile)
288+
val result = firebaseTestLabProcessCreator.callFirebaseTestLab(test.device, test.apk.outputFile, TestType.Instrumentation(test.testApk.outputFile))
266289
processResult(result, ignoreFailures)
267290
}
268291
})
269292
}
270293

271-
project.task(runTestsTask, closureOf<Task> {
294+
val allInstrumentation = project.task(runTestsTaskInstrumentation, closureOf<Task> {
272295
group = Constants.FIREBASE_TEST_LAB
273-
description = "Run Android Tests in Firebase Test Lab"
274-
dependsOn(tasks)
296+
description = "Run all Instrumentation tests for $variantName in Firebase Test Lab"
297+
dependsOn(instrumentationTasks)
275298

276299
doFirst {
277300
if (devices.isEmpty()) throw IllegalStateException("You need to set et least one device in:\n" +
@@ -285,10 +308,37 @@ internal class FirebaseTestLabPlugin : Plugin<Project> {
285308
" } " +
286309
"}")
287310

288-
if (tasks.isEmpty()) throw IllegalStateException("Nothing match your filter")
311+
if (instrumentationTasks.isEmpty()) throw IllegalStateException("Nothing match your filter")
289312
}
290313
})
291314

315+
val allRobo = project.task(runTestsTaskRobo, closureOf<Task> {
316+
group = Constants.FIREBASE_TEST_LAB
317+
description = "Run all Robo tests for $variantName in Firebase Test Lab"
318+
dependsOn(roboTasks)
319+
320+
doFirst {
321+
if (devices.isEmpty()) throw IllegalStateException("You need to set et least one device in:\n" +
322+
"firebaseTestLab {" +
323+
" devices {\n" +
324+
" nexus6 {\n" +
325+
" androidApiLevels = [21]\n" +
326+
" deviceIds = [\"Nexus6\"]\n" +
327+
" locales = [\"en\"]\n" +
328+
" }\n" +
329+
" } " +
330+
"}")
331+
332+
if (roboTasks.isEmpty()) throw IllegalStateException("Nothing match your filter")
333+
}
334+
})
335+
336+
project.task(runTestsTask, closureOf<Task> {
337+
group = Constants.FIREBASE_TEST_LAB
338+
description = "Run all tests for $variantName in Firebase Test Lab"
339+
dependsOn(allRobo, allInstrumentation)
340+
})
341+
292342
if (downloader != null) {
293343
project.task(downloadTask, closureOf<Task> {
294344
group = Constants.FIREBASE_TEST_LAB
@@ -309,16 +359,17 @@ internal class FirebaseTestLabPlugin : Plugin<Project> {
309359
project.logger.lifecycle(result.message)
310360
} else {
311361
if (ignoreFailures) {
312-
project.logger.error(Constants.ERROR + result.message)
362+
project.logger.error("Error: ${result.message}")
313363
} else {
314364
throw GradleException(result.message)
315365
}
316366
}
317367
}
318368
}
319369

320-
private fun <T1, T2, T3, R> combineAll(l1: Collection<T1>, l2: Collection<T2>, l3: Collection<T3>, func: (T1, T2, T3) -> R): List<R> =
321-
l1.flatMap { t1 -> l2.flatMap { t2 -> l3.map { t3 -> func(t1, t2, t3) } } }
370+
371+
private fun <T1, T2, R> combineAll(l1: Collection<T1>, l2: Collection<T2>, func: (T1, T2) -> R): List<R> =
372+
l1.flatMap { t1 -> l2.map { t2 -> func(t1, t2)} }
322373

323374
private fun dashToCamelCase(dash: String): String =
324375
dash.split('-', '_').joinToString("") { it.capitalize() }

plugin/src/main/java/com/appunite/firebasetestlabplugin/cloud/CloudTestResultDownloader.kt

+1-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package com.appunite.firebasetestlabplugin.cloud
22

33
import com.appunite.firebasetestlabplugin.FirebaseTestLabPlugin
44
import com.appunite.firebasetestlabplugin.model.ResultTypes
5-
import com.appunite.firebasetestlabplugin.utils.Constants
65
import com.appunite.firebasetestlabplugin.utils.asCommand
76
import org.gradle.api.GradleException
87
import org.gradle.api.logging.Logger
@@ -22,7 +21,7 @@ internal class CloudTestResultDownloader(
2221
return
2322
}
2423
val gCloudFullPath = "$gCloudBucketName/$gCloudDirectory"
25-
logger.lifecycle(Constants.DOWNLOAD_PHASE + "Downloading results from $gCloudFullPath")
24+
logger.lifecycle("DOWNLOAD: Downloading results from $gCloudFullPath")
2625

2726
prepareDownloadDirectory()
2827
downloadTestResults()

0 commit comments

Comments
 (0)