Skip to content

Commit aed63f6

Browse files
author
Piotr Mądry
committed
add sharding
1 parent 386fd3e commit aed63f6

File tree

8 files changed

+186
-137
lines changed

8 files changed

+186
-137
lines changed

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 = "1.1.6.23"
1010

1111
gradlePlugin {
1212
plugins {

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

Lines changed: 87 additions & 81 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
@@ -20,8 +22,9 @@ import org.gradle.kotlin.dsl.closureOf
2022
import org.gradle.kotlin.dsl.register
2123
import java.io.ByteArrayOutputStream
2224
import java.io.File
25+
import java.io.Serializable
2326

24-
internal class FirebaseTestLabPlugin : Plugin<Project> {
27+
class FirebaseTestLabPlugin : Plugin<Project> {
2528

2629
open class HiddenExec : Exec() {
2730
init {
@@ -66,7 +69,7 @@ internal class FirebaseTestLabPlugin : Plugin<Project> {
6669
}
6770
}
6871

69-
data class Sdk(val gcloud: File, val gsutil: File)
72+
data class Sdk(val gcloud: File, val gsutil: File): Serializable
7073

7174
private fun createDownloadSdkTask(project: Project, cloudSdkPath: String?): Sdk =
7275
if (cloudSdkPath != null) {
@@ -187,16 +190,9 @@ internal class FirebaseTestLabPlugin : Plugin<Project> {
187190
throw IllegalStateException("If you want to clear directory before run you need to setup cloudBucketName and cloudDirectoryName")
188191
}
189192

190-
val firebaseTestLabProcessCreator = FirebaseTestLabProcessCreator(
191-
sdk,
192-
cloudBucketName,
193-
cloudDirectoryName,
194-
project.logger
195-
)
196-
197193
(project.extensions.findByName(ANDROID) as AppExtension).apply {
198194
testVariants.toList().forEach { testVariant ->
199-
createGroupedTestLabTask(devices, testVariant, firebaseTestLabProcessCreator, ignoreFailures, downloader)
195+
createGroupedTestLabTask(devices, testVariant, ignoreFailures, downloader, sdk, cloudBucketName, cloudDirectoryName)
200196
}
201197
}
202198

@@ -206,14 +202,17 @@ internal class FirebaseTestLabPlugin : Plugin<Project> {
206202

207203
data class DeviceAppMap(val device: Device, val apk: BaseVariantOutput)
208204

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

219218
val cleanTask = "firebaseTestLabClean${variantName.capitalize()}"
@@ -264,19 +263,58 @@ internal class FirebaseTestLabPlugin : Plugin<Project> {
264263
dependsOn(taskSetup)
265264
dependsOn(arrayOf(test.apk.assemble))
266265
doLast {
267-
val result = firebaseTestLabProcessCreator.callFirebaseTestLab(test.device, test.apk.outputFile, TestType.Robo)
266+
val result = FirebaseTestLabProcessCreator.callFirebaseTestLab(ProcessData(
267+
sdk = sdk,
268+
gCloudBucketName = cloudBucketName,
269+
gCloudDirectory = cloudDirectoryName,
270+
device = test.device,
271+
apk = test.apk.outputFile,
272+
273+
testType = TestType.Robo
274+
))
268275
processResult(result, ignoreFailures)
269276
}
270277
})
271278
}
272-
273-
val instrumentationTasks: List<Task> = combineAll(appVersions, variant.outputs, {deviceAndMap, testApk -> Test(deviceAndMap.device, deviceAndMap.apk, testApk)})
274-
.map {
275-
test ->
276-
val devicePart = test.device.name.capitalize()
277-
val apkPart = dashToCamelCase(test.apk.name).capitalize()
278-
val testApkPart = test.testApk.let { if (it.filters.isEmpty()) "" else dashToCamelCase(it.name).capitalize() }
279-
val taskName = "$runTestsTaskInstrumentation$devicePart$apkPart$testApkPart"
279+
280+
val instrumentationTasks: List<Task> = combineAll(appVersions, variant.outputs)
281+
{ deviceAndMap, testApk -> Test(deviceAndMap.device, deviceAndMap.apk, testApk) }
282+
.map { test ->
283+
val devicePart = test.device.name.capitalize()
284+
val apkPart = dashToCamelCase(test.apk.name).capitalize()
285+
val testApkPart = test.testApk.let { if (it.filters.isEmpty()) "" else dashToCamelCase(it.name).capitalize() }
286+
val taskName = "$runTestsTaskInstrumentation$devicePart$apkPart$testApkPart"
287+
val numShards = test.device.numShards
288+
289+
val file = File(project.buildDir, "TestResults.txt")
290+
291+
if (numShards > 0) {
292+
project.tasks.create(taskName, InstrumentationShardingTask::class.java) {
293+
group = Constants.FIREBASE_TEST_LAB
294+
description = "Run Instrumentation test for ${test.device.name} device on $variantName/${test.apk.name} in Firebase Test Lab"
295+
this.processData = ProcessData(
296+
sdk = sdk,
297+
gCloudBucketName = cloudBucketName,
298+
gCloudDirectory = cloudDirectoryName,
299+
device = test.device,
300+
apk = test.apk.outputFile,
301+
testType = TestType.Instrumentation(test.testApk.outputFile)
302+
)
303+
this.stateFile = file
304+
305+
if (downloader != null) {
306+
mustRunAfter(cleanTask)
307+
}
308+
dependsOn(taskSetup)
309+
dependsOn(arrayOf(test.apk.assemble, test.testApk.assemble))
310+
311+
doLast {
312+
val resultCode = file.readText().toInt()
313+
processResult(resultCode, ignoreFailures)
314+
}
315+
}
316+
317+
} else {
280318
project.task(taskName, closureOf<Task> {
281319
inputs.files(test.testApk.outputFile, test.apk.outputFile)
282320
group = Constants.FIREBASE_TEST_LAB
@@ -287,11 +325,19 @@ internal class FirebaseTestLabPlugin : Plugin<Project> {
287325
dependsOn(taskSetup)
288326
dependsOn(arrayOf(test.apk.assemble, test.testApk.assemble))
289327
doLast {
290-
val result = firebaseTestLabProcessCreator.callFirebaseTestLab(test.device, test.apk.outputFile, TestType.Instrumentation(test.testApk.outputFile))
328+
val result = FirebaseTestLabProcessCreator.callFirebaseTestLab(ProcessData(
329+
sdk = sdk,
330+
gCloudBucketName = cloudBucketName,
331+
gCloudDirectory = cloudDirectoryName,
332+
device = test.device,
333+
apk = test.apk.outputFile,
334+
testType = TestType.Instrumentation(test.testApk.outputFile)
335+
))
291336
processResult(result, ignoreFailures)
292337
}
293338
})
294339
}
340+
}
295341

296342
val allInstrumentation: Task = project.task(runTestsTaskInstrumentation, closureOf<Task> {
297343
group = Constants.FIREBASE_TEST_LAB
@@ -334,63 +380,13 @@ internal class FirebaseTestLabPlugin : Plugin<Project> {
334380
if (roboTasks.isEmpty()) throw IllegalStateException("Nothing match your filter")
335381
}
336382
})
337-
338-
combineAll(appVersions, variant.outputs) { deviceAndMap, testApk -> Test(deviceAndMap.device, deviceAndMap.apk, testApk) }
339-
.map { test ->
340-
val devicePart = test.device.name.capitalize()
341-
val apkPart = dashToCamelCase(test.apk.name).capitalize()
342-
val testApkPart = test.testApk.let { if (it.filters.isEmpty()) "" else dashToCamelCase(it.name).capitalize() }
343-
val numShards = test.device.numShards
344-
if (numShards > 1) {
345-
346-
val shardedTasks: List<Task> = (0 until test.device.numShards).map { shardIndex ->
347-
val taskName = "$runTestsTaskInstrumentation$devicePart$apkPart$testApkPart" + "NumShards$numShards" + "ShardIndex$shardIndex"
348-
project.task(taskName, closureOf<Task> {
349-
inputs.files(test.testApk.outputFile, test.apk.outputFile)
350-
group = Constants.FIREBASE_TEST_LAB
351-
description = "Run Instrumentation test for ${test.device.name} device on $variantName/${test.apk.name} with ShardNumber $numShards and ShardIndex $shardIndex in Firebase Test Lab"
352-
if (downloader != null) {
353-
mustRunAfter(cleanTask)
354-
}
355-
dependsOn(taskSetup)
356-
dependsOn(arrayOf(test.apk.assemble, test.testApk.assemble))
357-
doLast {
358-
val result = firebaseTestLabProcessCreator.callFirebaseTestLab(test.device, test.apk.outputFile, TestType.Instrumentation(test.testApk.outputFile), shardIndex)
359-
processResult(result, ignoreFailures)
360-
}
361-
})
362-
}
363-
364-
val runTestsTaskSharded = "firebaseTestLabExecuteAllShardForConfiguration$devicePart$apkPart$testApkPart"
365-
project.task(runTestsTaskSharded, closureOf<Task> {
366-
group = Constants.FIREBASE_TEST_LAB
367-
description = "Run all tests for $variantName sharded in Firebase Test Lab"
368-
dependsOn(shardedTasks)
369-
370-
doFirst {
371-
if (devices.isEmpty()) throw IllegalStateException("You need to set et least one device in:\n" +
372-
"firebaseTestLab {" +
373-
" devices {\n" +
374-
" nexus6 {\n" +
375-
" androidApiLevels = [21]\n" +
376-
" deviceIds = [\"Nexus6\"]\n" +
377-
" locales = [\"en\"]\n" +
378-
" }\n" +
379-
" } " +
380-
"}")
381-
382-
if (roboTasks.isEmpty()) throw IllegalStateException("Nothing match your filter")
383-
}
384-
})
385-
}
386-
}
387383

388384
project.task(runTestsTask, closureOf<Task> {
389385
group = Constants.FIREBASE_TEST_LAB
390386
description = "Run all tests for $variantName in Firebase Test Lab"
391387
dependsOn(allRobo, allInstrumentation)
392388
})
393-
389+
394390
if (downloader != null) {
395391
project.task(downloadTask, closureOf<Task> {
396392
group = Constants.FIREBASE_TEST_LAB
@@ -405,7 +401,7 @@ internal class FirebaseTestLabPlugin : Plugin<Project> {
405401
})
406402
}
407403
}
408-
404+
409405
private fun processResult(result: TestResults, ignoreFailures: Boolean) {
410406
if (result.isSuccessful) {
411407
project.logger.lifecycle(result.message)
@@ -417,11 +413,21 @@ internal class FirebaseTestLabPlugin : Plugin<Project> {
417413
}
418414
}
419415
}
416+
417+
private fun processResult(resultCode: Int, ignoreFailures: Boolean) =
418+
if (resultCode == 0) {
419+
project.logger.lifecycle("SUCCESS: All tests passed.")
420+
} else {
421+
if (ignoreFailures) {
422+
project.logger.error("FAILURE: Tests failed.")
423+
} else {
424+
throw GradleException("FAILURE: Tests failed.")
425+
}
426+
}
420427
}
421428

422-
423429
private fun <T1, T2, R> combineAll(l1: Collection<T1>, l2: Collection<T2>, func: (T1, T2) -> R): List<R> =
424430
l1.flatMap { t1 -> l2.map { t2 -> func(t1, t2)} }
425431

426-
private fun dashToCamelCase(dash: String): String =
432+
fun dashToCamelCase(dash: String): String =
427433
dash.split('-', '_').joinToString("") { it.capitalize() }

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

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,25 @@ import com.appunite.firebasetestlabplugin.FirebaseTestLabPlugin
44
import com.appunite.firebasetestlabplugin.model.Device
55
import com.appunite.firebasetestlabplugin.model.TestResults
66
import com.appunite.firebasetestlabplugin.utils.joinArgs
7-
import org.gradle.api.logging.Logger
87
import java.io.File
8+
import java.io.Serializable
99

10-
sealed class TestType {
10+
sealed class TestType : Serializable {
1111
object Robo : TestType()
1212
data class Instrumentation(val testApk: File) : TestType()
1313
}
1414

15-
internal class FirebaseTestLabProcessCreator(
16-
private val sdk: FirebaseTestLabPlugin.Sdk,
17-
private val gCloudBucketName: String?,
18-
private val gCloudDirectory: String?,
19-
private val logger: Logger) {
15+
data class ProcessData(
16+
val sdk: FirebaseTestLabPlugin.Sdk,
17+
val gCloudBucketName: String?,
18+
val gCloudDirectory: String?,
19+
val device: Device,
20+
val apk: File,
21+
val testType: TestType,
22+
val shardIndex: Int = -1
23+
) : Serializable
24+
25+
object FirebaseTestLabProcessCreator {
2026

2127
private val resultMessageMap = mapOf(
2228
0 to "All test executions passed.",
@@ -29,39 +35,39 @@ internal class FirebaseTestLabProcessCreator(
2935
20 to "A test infrastructure error occurred."
3036
)
3137

32-
fun callFirebaseTestLab(device: Device, apk: File, testType: TestType, shardIndex: Int = -1): TestResults {
33-
val processBuilder = createProcess(device, apk, testType)
34-
logger.debug(processBuilder.command().joinToString(separator = " ", transform = { "\"$it\"" }))
35-
val process = processBuilder.start()
36-
process.errorStream.bufferedReader().forEachLine { logger.lifecycle(it) }
37-
process.inputStream.bufferedReader().forEachLine { logger.lifecycle(it) }
38-
39-
val resultCode = process.waitFor()
38+
fun callFirebaseTestLab(processData: ProcessData): TestResults {
39+
val process: Process = createProcess(processData).start()
40+
val resultCode: Int = process.let {
41+
it.errorStream.bufferedReader().forEachLine { errorInfo -> println(errorInfo) }
42+
it.inputStream.bufferedReader().forEachLine { info -> println(info) }
43+
process.waitFor()
44+
}
4045
return TestResults(
41-
isSuccessful = resultCode == 0,
42-
message = resultMessageMap[resultCode] ?: ""
46+
isSuccessful = resultCode == 0,
47+
message = resultMessageMap.getOrElse(resultCode) { "Unknown error with code: $resultCode" }
4348
)
4449
}
4550

46-
private fun createProcess(device: Device, apk: File, testType: TestType, shardIndex: Int = -1): ProcessBuilder {
51+
private fun createProcess(processData: ProcessData): ProcessBuilder {
52+
val device: Device = processData.device
4753
return ProcessBuilder(
4854
sequenceOf(
49-
sdk.gcloud.absolutePath,
55+
processData.sdk.gcloud.absolutePath,
5056
"firebase", "test", "android", "run",
5157
"--format=json",
5258
"--device-ids=${device.deviceIds.joinArgs()}",
53-
"--app=$apk",
59+
"--app=$processData.apk",
5460
"--locales=${device.locales.joinArgs()}",
5561
"--os-version-ids=${device.androidApiLevels.joinArgs()}",
5662
"--orientations=${device.screenOrientations.map { orientation -> orientation.gcloudName }.joinArgs()}")
57-
.plus(when (testType) {
63+
.plus(when (processData.testType) {
5864
TestType.Robo -> sequenceOf("--type=robo")
59-
is TestType.Instrumentation -> sequenceOf("--type=instrumentation", "--test=${testType.testApk}")
65+
is TestType.Instrumentation -> sequenceOf("--type=instrumentation", "--test=${processData.testType.testApk}")
6066
})
61-
.plus(gCloudBucketName?.let { sequenceOf("--results-bucket=$it") } ?: sequenceOf())
62-
.plus(gCloudDirectory?.let { sequenceOf("--results-dir=$it") } ?: sequenceOf())
67+
.plus(processData.gCloudBucketName?.let { sequenceOf("--results-bucket=$it") } ?: sequenceOf())
68+
.plus(processData.gCloudDirectory?.let { sequenceOf("--results-dir=$it") } ?: sequenceOf())
6369
.plus(if (device.isUseOrchestrator) sequenceOf("--use-orchestrator") else sequenceOf())
64-
.plus(if (device.environmentVariables.isNotEmpty()) sequenceOf("--environment-variables=${device.environmentVariables.joinToString(",")}${addSharding(device, shardIndex)}") else sequenceOf())
70+
.plus(setupEnvironmentVariables(device, processData.shardIndex))
6571
.plus(if (device.testTargets.isNotEmpty()) sequenceOf("--test-targets=${device.testTargets.joinToString(",")}") else sequenceOf())
6672
.plus(device.customParamsForGCloudTool)
6773
.plus(device.testRunnerClass?.let { sequenceOf("--test-runner-class=$it") } ?: sequenceOf())
@@ -70,8 +76,13 @@ internal class FirebaseTestLabProcessCreator(
7076
)
7177
}
7278

73-
private fun addSharding(device: Device, shardIndex: Int): String = when {
74-
device.numShards <= 0 && shardIndex >= 0 -> ", numShards=${device.numShards}, shardIndex=$shardIndex"
75-
else -> ""
76-
}
79+
private fun setupEnvironmentVariables(device: Device, shardIndex: Int): Sequence<String> =
80+
if (device.environmentVariables.isNotEmpty() || device.numShards > 0)
81+
sequenceOf(StringBuilder()
82+
.append("--environment-variables=")
83+
.append(if (device.environmentVariables.isNotEmpty()) device.environmentVariables.joinToString(",") else "")
84+
.append(if (device.environmentVariables.isNotEmpty() && device.numShards > 0) "," else "")
85+
.append(if (device.numShards > 0) "numShards=${device.numShards},shardIndex=$shardIndex" else "")
86+
.toString())
87+
else sequenceOf()
7788
}

plugin/src/main/java/com/appunite/firebasetestlabplugin/model/Device.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.appunite.firebasetestlabplugin.model
22

3-
class Device(val name: String) {
3+
import java.io.Serializable
4+
5+
class Device(val name: String) : Serializable {
46
var locales: List<String> = listOf("en")
57
var screenOrientations: List<ScreenOrientation> = listOf(ScreenOrientation.PORTRAIT)
68
var androidApiLevels: List<Int> = listOf()

0 commit comments

Comments
 (0)