diff --git a/README.md b/README.md index 3357f2a..a99f32e 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,9 @@ gordon { // Default is no filter testFilter.set("ExampleTest.runThisMethod,RunThisWholeTestClass,com.example.runthispackage,com.example.RunTestsWithThisAnnotation") + + // Default is false - to ignore devices that failed during artifacts installation, may be useful with large number of devices and SinglePool strategy + ignoreProblematicDevices.set(true) } ``` diff --git a/gordon-plugin/src/main/kotlin/com/banno/gordon/AdbExtensions.kt b/gordon-plugin/src/main/kotlin/com/banno/gordon/AdbExtensions.kt index 4a8ecd6..3d73b84 100644 --- a/gordon-plugin/src/main/kotlin/com/banno/gordon/AdbExtensions.kt +++ b/gordon-plugin/src/main/kotlin/com/banno/gordon/AdbExtensions.kt @@ -3,6 +3,7 @@ package com.banno.gordon import arrow.core.Either import arrow.core.computations.either import arrow.core.flatMap +import arrow.core.right import com.android.tools.build.bundletool.commands.BuildApksCommand import com.android.tools.build.bundletool.commands.InstallApksCommand import com.android.tools.build.bundletool.device.AdbServer @@ -157,7 +158,9 @@ internal fun List.reinstall( signingConfig: SigningConfig, instrumentationApk: File, installTimeoutMillis: Long, - adb: AdbServer + adb: AdbServer, + ignoreProblematicDevices: Boolean, + problematicDevices: MutableList, ): Either = either.eager { val applicationApkSet = applicationAab?.let { buildApkSet(adb, it, signingConfig) }?.bind() @@ -174,8 +177,23 @@ internal fun List.reinstall( logger.lifecycle("${device.serialNumber}: installing $instrumentationPackage") device.safeUninstall(installTimeoutMillis, instrumentationPackage) device.installApk(installTimeoutMillis, instrumentationApk).bind() - } + }.ignoreErrorIfPossible(device, logger, ignoreProblematicDevices, problematicDevices) } }.awaitAll() }.forEach { it.bind() } } + +private fun Either.ignoreErrorIfPossible( + device: Device, + logger: Logger, + ignoreProblematicDevices: Boolean, + problematicDevices: MutableList, +): Either = fold( + ifLeft = { + Either.conditionally(ignoreProblematicDevices, ifFalse = { it }) { + problematicDevices.add(device) + logger.warn("${device.serialNumber}: ignored installation failure", it) + } + }, + ifRight = { it.right() } +) diff --git a/gordon-plugin/src/main/kotlin/com/banno/gordon/GordonExtension.kt b/gordon-plugin/src/main/kotlin/com/banno/gordon/GordonExtension.kt index 203f0a4..df2e36c 100644 --- a/gordon-plugin/src/main/kotlin/com/banno/gordon/GordonExtension.kt +++ b/gordon-plugin/src/main/kotlin/com/banno/gordon/GordonExtension.kt @@ -16,6 +16,7 @@ abstract class GordonExtension @Inject constructor( val testTimeoutMillis: Property = objects.property() val testFilter: Property = objects.property() val testInstrumentationRunner: Property = objects.property() + val ignoreProblematicDevices: Property = objects.property() init { poolingStrategy.convention(PoolingStrategy.PoolPerDevice) @@ -25,5 +26,6 @@ abstract class GordonExtension @Inject constructor( testTimeoutMillis.convention(120_000) testFilter.convention("") testInstrumentationRunner.convention("") + ignoreProblematicDevices.convention(false) } } diff --git a/gordon-plugin/src/main/kotlin/com/banno/gordon/GordonPlugin.kt b/gordon-plugin/src/main/kotlin/com/banno/gordon/GordonPlugin.kt index 4a6a9ba..61cafb2 100644 --- a/gordon-plugin/src/main/kotlin/com/banno/gordon/GordonPlugin.kt +++ b/gordon-plugin/src/main/kotlin/com/banno/gordon/GordonPlugin.kt @@ -93,6 +93,7 @@ class GordonPlugin : Plugin { task.testTimeoutMillis.set(gordonExtension.testTimeoutMillis) task.extensionTestFilter.set(gordonExtension.testFilter) task.extensionTestInstrumentationRunner.set(gordonExtension.testInstrumentationRunner) + task.ignoreProblematicDevices.set(gordonExtension.ignoreProblematicDevices) } val testedExtension = project.extensions.getByType() diff --git a/gordon-plugin/src/main/kotlin/com/banno/gordon/GordonTestTask.kt b/gordon-plugin/src/main/kotlin/com/banno/gordon/GordonTestTask.kt index 1eaa3b6..c98f586 100644 --- a/gordon-plugin/src/main/kotlin/com/banno/gordon/GordonTestTask.kt +++ b/gordon-plugin/src/main/kotlin/com/banno/gordon/GordonTestTask.kt @@ -3,6 +3,8 @@ package com.banno.gordon import arrow.core.Either import arrow.core.computations.either import arrow.core.left +import arrow.core.right +import com.android.tools.build.bundletool.device.Device import kotlinx.coroutines.Dispatchers import org.gradle.api.DefaultTask import org.gradle.api.file.Directory @@ -67,6 +69,9 @@ internal abstract class GordonTestTask @Inject constructor( @get:Input internal val tabletShortestWidthDp: Property = objects.property() + @get:Internal + internal val ignoreProblematicDevices: Property = objects.property() + @get:Internal internal val retryQuota: Property = objects.property() @@ -140,7 +145,8 @@ internal abstract class GordonTestTask @Inject constructor( reportDirectory.get().asFile.clear().bind() val adb = initializeDefaultAdbServer().bind() - val pools = calculatePools( + val problematicDevices = mutableListOf() + val originalPools = calculatePools( adb, poolingStrategy.get(), tabletShortestWidthDp.get().takeIf { it > -1 } @@ -148,14 +154,8 @@ internal abstract class GordonTestTask @Inject constructor( val testCases = loadTestSuite(instrumentationApk).bind() .filter { it.matchesFilter(testFilters.get()) } - when { - testCases.isEmpty() -> IllegalStateException("No test cases found").left().bind() - pools.isEmpty() -> IllegalStateException("No devices found").left().bind() - pools.any { it.devices.isEmpty() } -> { - val emptyPools = pools.filter { it.devices.isEmpty() }.map { it.poolName } - IllegalStateException("No devices found in pools $emptyPools").left().bind() - } - } + testCases.validateTestCases().bind() + originalPools.validateDevicePools().bind() val applicationAab = applicationAab.get().asFile.takeUnless { it == PLACEHOLDER_APPLICATION_AAB } val applicationPackage = applicationPackage.get().takeUnless { it == PLACEHOLDER_APPLICATION_PACKAGE } @@ -169,7 +169,7 @@ internal abstract class GordonTestTask @Inject constructor( keyPassword = signingConfigCredentials.get().keyPassword ) - pools.flatMap { it.devices }.reinstall( + originalPools.flatMap { it.devices }.reinstall( dispatcher = Dispatchers.Default, logger = logger, applicationPackage = applicationPackage, @@ -179,9 +179,14 @@ internal abstract class GordonTestTask @Inject constructor( signingConfig = signingConfig, instrumentationApk = instrumentationApk, installTimeoutMillis = installTimeoutMillis.get(), - adb = adb + adb = adb, + ignoreProblematicDevices = ignoreProblematicDevices.get(), + problematicDevices = problematicDevices, ).bind() + val pools = originalPools.filterProblematicDevices(problematicDevices) + pools.validateDevicePools().bind() + val testResults = runAllTests( dispatcher = Dispatchers.Default, logger = logger, @@ -222,6 +227,27 @@ internal abstract class GordonTestTask @Inject constructor( } } +internal fun List.validateDevicePools() = + when { + isEmpty() -> IllegalStateException("No devices found").left() + any { it.devices.isEmpty() } -> { + val emptyPools = filter { it.devices.isEmpty() }.map { it.poolName } + IllegalStateException("No devices found in pools $emptyPools").left() + } + else -> Unit.right() + } + +internal fun List.validateTestCases() = + when { + isEmpty() -> IllegalStateException("No test cases found").left() + else -> Unit.right() + } + +internal fun List.filterProblematicDevices(problematicDevices: List) = + map { pool -> + pool.copy(devices = pool.devices - problematicDevices) + } + internal fun TestCase.matchesFilter(filters: List): Boolean { val fullyQualifiedTestMethod = "$fullyQualifiedClassName.$methodName"