Skip to content

Commit 386fd3e

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

File tree

6 files changed

+151
-18
lines changed

6 files changed

+151
-18
lines changed

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

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ internal class FirebaseTestLabPlugin : Plugin<Project> {
270270
})
271271
}
272272

273-
val instrumentationTasks = combineAll(appVersions, variant.outputs, {deviceAndMap, testApk -> Test(deviceAndMap.device, deviceAndMap.apk, testApk)})
273+
val instrumentationTasks: List<Task> = combineAll(appVersions, variant.outputs, {deviceAndMap, testApk -> Test(deviceAndMap.device, deviceAndMap.apk, testApk)})
274274
.map {
275275
test ->
276276
val devicePart = test.device.name.capitalize()
@@ -293,7 +293,7 @@ internal class FirebaseTestLabPlugin : Plugin<Project> {
293293
})
294294
}
295295

296-
val allInstrumentation = project.task(runTestsTaskInstrumentation, closureOf<Task> {
296+
val allInstrumentation: Task = project.task(runTestsTaskInstrumentation, closureOf<Task> {
297297
group = Constants.FIREBASE_TEST_LAB
298298
description = "Run all Instrumentation tests for $variantName in Firebase Test Lab"
299299
dependsOn(instrumentationTasks)
@@ -314,7 +314,7 @@ internal class FirebaseTestLabPlugin : Plugin<Project> {
314314
}
315315
})
316316

317-
val allRobo = project.task(runTestsTaskRobo, closureOf<Task> {
317+
val allRobo: Task = project.task(runTestsTaskRobo, closureOf<Task> {
318318
group = Constants.FIREBASE_TEST_LAB
319319
description = "Run all Robo tests for $variantName in Firebase Test Lab"
320320
dependsOn(roboTasks)
@@ -334,6 +334,56 @@ internal class FirebaseTestLabPlugin : Plugin<Project> {
334334
if (roboTasks.isEmpty()) throw IllegalStateException("Nothing match your filter")
335335
}
336336
})
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+
}
337387

338388
project.task(runTestsTask, closureOf<Task> {
339389
group = Constants.FIREBASE_TEST_LAB

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

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,22 @@ internal class FirebaseTestLabProcessCreator(
2929
20 to "A test infrastructure error occurred."
3030
)
3131

32-
fun callFirebaseTestLab(device: Device, apk: File, testType: TestType): TestResults {
33-
val processBuilder = ProcessBuilder(
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()
40+
return TestResults(
41+
isSuccessful = resultCode == 0,
42+
message = resultMessageMap[resultCode] ?: ""
43+
)
44+
}
45+
46+
private fun createProcess(device: Device, apk: File, testType: TestType, shardIndex: Int = -1): ProcessBuilder {
47+
return ProcessBuilder(
3448
sequenceOf(
3549
sdk.gcloud.absolutePath,
3650
"firebase", "test", "android", "run",
@@ -47,22 +61,17 @@ internal class FirebaseTestLabProcessCreator(
4761
.plus(gCloudBucketName?.let { sequenceOf("--results-bucket=$it") } ?: sequenceOf())
4862
.plus(gCloudDirectory?.let { sequenceOf("--results-dir=$it") } ?: sequenceOf())
4963
.plus(if (device.isUseOrchestrator) sequenceOf("--use-orchestrator") else sequenceOf())
50-
.plus(if (device.environmentVariables.isNotEmpty()) sequenceOf("--environment-variables=${device.environmentVariables.joinToString(",")}") else sequenceOf())
64+
.plus(if (device.environmentVariables.isNotEmpty()) sequenceOf("--environment-variables=${device.environmentVariables.joinToString(",")}${addSharding(device, shardIndex)}") else sequenceOf())
5165
.plus(if (device.testTargets.isNotEmpty()) sequenceOf("--test-targets=${device.testTargets.joinToString(",")}") else sequenceOf())
5266
.plus(device.customParamsForGCloudTool)
5367
.plus(device.testRunnerClass?.let { sequenceOf("--test-runner-class=$it") } ?: sequenceOf())
5468
.plus(if (device.timeout > 0) sequenceOf("--timeoutSec=${device.timeout}s") else sequenceOf())
5569
.toList()
5670
)
57-
logger.debug(processBuilder.command().joinToString(separator = " ", transform = { "\"$it\"" }))
58-
val process = processBuilder.start()
59-
process.errorStream.bufferedReader().forEachLine { logger.lifecycle(it) }
60-
process.inputStream.bufferedReader().forEachLine { logger.lifecycle(it) }
61-
62-
val resultCode = process.waitFor()
63-
return TestResults(
64-
isSuccessful = resultCode == 0,
65-
message = resultMessageMap[resultCode] ?: ""
66-
)
71+
}
72+
73+
private fun addSharding(device: Device, shardIndex: Int): String = when {
74+
device.numShards <= 0 && shardIndex >= 0 -> ", numShards=${device.numShards}, shardIndex=$shardIndex"
75+
else -> ""
6776
}
6877
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ class Device(val name: String) {
1212
var filterAbiSplits = false
1313
var abiSplits: Set<String> = setOf()
1414
var isUseOrchestrator = false
15+
var numShards = 0
16+
1517
var environmentVariables: List<String> = listOf()
1618
var customParamsForGCloudTool: List<String> = listOf()
1719
var testTargets: List<String> = listOf()

plugin/src/test/java/com/appunite/firebasetestlabplugin/IntegrationTest.kt

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,4 +299,76 @@ class IntegrationTest {
299299
println("Executing task: ${task.name}")
300300
task.actions.forEach { it.execute(task) }
301301
}
302+
303+
@Test
304+
fun `ensure that tasks are sharded for single device when numShards is filled`() {
305+
val simpleProject = File(javaClass.getResource("simple").file)
306+
val project = ProjectBuilder.builder().withProjectDir(simpleProject).build()
307+
project.plugins.apply("com.android.application")
308+
project.plugins.apply("firebase.test.lab")
309+
project.configure<AppExtension> {
310+
compileSdkVersion(27)
311+
defaultConfig.also {
312+
it.versionCode = 1
313+
it.versionName = "0.1"
314+
it.setMinSdkVersion(27)
315+
it.setTargetSdkVersion(27)
316+
}
317+
}
318+
project.configure<FirebaseTestLabPluginExtension> {
319+
googleProjectId = "test"
320+
keyFile = File(simpleProject, "key.json")
321+
createDevice("myDevice") {
322+
deviceIds = listOf("Nexus6")
323+
numShards = 2
324+
}
325+
}
326+
(project as ProjectInternal).evaluate()
327+
328+
329+
assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMyDeviceDebugNumShards2ShardIndex0", false).isNotEmpty())
330+
assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMyDeviceDebugNumShards2ShardIndex1", false).isNotEmpty())
331+
assertTrue(project.getTasksByName("firebaseTestLabExecuteAllShardForConfigurationMyDeviceDebug", false).isNotEmpty())
332+
}
333+
334+
@Test
335+
fun `ensure that tasks are sharded for two devices when numShards is filled`() {
336+
val simpleProject = File(javaClass.getResource("simple").file)
337+
val project = ProjectBuilder.builder().withProjectDir(simpleProject).build()
338+
project.plugins.apply("com.android.application")
339+
project.plugins.apply("firebase.test.lab")
340+
project.configure<AppExtension> {
341+
compileSdkVersion(27)
342+
defaultConfig.also {
343+
it.versionCode = 1
344+
it.versionName = "0.1"
345+
it.setMinSdkVersion(27)
346+
it.setTargetSdkVersion(27)
347+
}
348+
}
349+
project.configure<FirebaseTestLabPluginExtension> {
350+
googleProjectId = "test"
351+
keyFile = File(simpleProject, "key.json")
352+
createDevice("myDevice") {
353+
deviceIds = listOf("Nexus6")
354+
numShards = 2
355+
}
356+
357+
createDevice("mySecondDevice") {
358+
deviceIds = listOf("Nexus5")
359+
numShards = 3
360+
}
361+
}
362+
(project as ProjectInternal).evaluate()
363+
364+
365+
assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMyDeviceDebugNumShards2ShardIndex0", false).isNotEmpty())
366+
assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMyDeviceDebugNumShards2ShardIndex1", false).isNotEmpty())
367+
assertTrue(project.getTasksByName("firebaseTestLabExecuteAllShardForConfigurationMyDeviceDebug", false).isNotEmpty())
368+
369+
assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMySecondDeviceDebugNumShards3ShardIndex0", false).isNotEmpty())
370+
assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMySecondDeviceDebugNumShards3ShardIndex1", false).isNotEmpty())
371+
assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMySecondDeviceDebugNumShards3ShardIndex2", false).isNotEmpty())
372+
assertTrue(project.getTasksByName("firebaseTestLabExecuteAllShardForConfigurationMySecondDeviceDebug", false).isNotEmpty())
373+
}
302374
}

sample/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ buildscript {
1313
dependencies {
1414
classpath 'com.android.tools.build:gradle:3.4.0-beta03'
1515
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
16-
classpath 'gradle.plugin.firebase.test.lab:plugin:1.1.2'
16+
classpath 'gradle.plugin.firebase.test.lab:plugin:1.1.6'
1717
}
1818
}
1919

sample/gradle/wrapper/gradle-wrapper.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
33
distributionPath=wrapper/dists
44
zipStoreBase=GRADLE_USER_HOME
55
zipStorePath=wrapper/dists
6-
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
6+
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip

0 commit comments

Comments
 (0)