Skip to content

Commit

Permalink
feat(android): test plugin support (#862)
Browse files Browse the repository at this point in the history
* feat(gradle-plugin): Added com.android.test plugin support

---------

Co-authored-by: yaroslav.kolotilov <yaroslav.kolotilov@quadcode.com>
  • Loading branch information
Malinskiy and yaroslav.kolotilov authored Nov 15, 2023
1 parent 8d3ca41 commit 78dfc5a
Show file tree
Hide file tree
Showing 10 changed files with 173 additions and 19 deletions.
11 changes: 10 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
.idea/modules
.idea/.name
.idea/compiler.xml
.idea/kotlinc.xml
.idea/misc.xml
.idea/modules.xml


# Generated files
.idea/**/contentModel.xml
Expand All @@ -28,6 +35,8 @@
.idea/**/gradle.xml
.idea/**/libraries

**/local.properties

# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
Expand Down Expand Up @@ -275,4 +284,4 @@ DerivedData
*.ipa
*.xcuserstate
*.profraw
sample/ios-app/Marathondevices
sample/ios-app/Marathondevices
27 changes: 27 additions & 0 deletions .idea/runConfigurations/android_test_gradle_plugin.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ internal class BillingReporter(
}

usageTracker.trackEvent(Event.Devices(bills.size))
val result = executionReport.summary.pools.map { it.failed.size == 0 }.reduce { acc, b -> acc && b }
val result = executionReport.summary.pools.map { it.failed.isEmpty() }.reduceOrNull { acc, b -> acc && b } ?: true
val flakiness = executionReport.summary.pools.sumOf { it.rawDurationMillis - it.durationMillis / 1000 }
val durationSeconds = ((Instant.now().toEpochMilli() - defaultStart.toEpochMilli()) / 1000)
usageTracker.trackEvent(Event.Executed(seconds = bills.sumOf { it.duration } / 1000,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.android.build.api.variant.AndroidTest
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.BuiltArtifactsLoader
import com.android.build.api.variant.LibraryAndroidComponentsExtension
import com.android.build.api.variant.TestAndroidComponentsExtension
import com.malinskiy.marathon.config.Configuration
import com.malinskiy.marathon.config.vendor.VendorConfiguration
import com.malinskiy.marathon.gradle.configuration.toStrategy
Expand All @@ -26,6 +27,7 @@ import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.Delete
import org.gradle.api.tasks.TaskProvider
import org.gradle.kotlin.dsl.closureOf
import org.gradle.kotlin.dsl.findByType
import java.io.File

class MarathonPlugin : Plugin<Project> {
Expand All @@ -48,7 +50,9 @@ class MarathonPlugin : Plugin<Project> {

val appExtension = project.extensions.findByType(ApplicationAndroidComponentsExtension::class.java)
val libraryExtension = project.extensions.findByType(LibraryAndroidComponentsExtension::class.java)
val conf = project.extensions.getByName("marathon") as? MarathonExtension ?: throw IllegalStateException("Android extension not found")
val testExtension = project.extensions.findByType(TestAndroidComponentsExtension::class.java)
val conf =
project.extensions.getByName("marathon") as? MarathonExtension ?: throw IllegalStateException("Android extension not found")

when {
appExtension != null -> {
Expand All @@ -74,7 +78,7 @@ class MarathonPlugin : Plugin<Project> {
)

val (generateMarathonfileTaskProvider, testTaskForVariantProvider) = createTasks(
logger, androidTest, bundle, project, conf, sdkDirectory, wrapper, jsonServiceProvider
logger, androidTest.name, bundle, project, conf, sdkDirectory, wrapper, jsonServiceProvider
)
marathonTask.dependsOn(testTaskForVariantProvider)
}
Expand All @@ -96,16 +100,37 @@ class MarathonPlugin : Plugin<Project> {
testArtifactLoader = project.objects.property(BuiltArtifactsLoader::class.java)
.apply { set(testArtifactsLoader) },
)
println(bundle)

val (generateMarathonfileTask, testTaskForVariant) = createTasks(
logger, androidTest, bundle, project, conf, sdkDirectory, wrapper, jsonServiceProvider
logger, androidTest.name, bundle, project, conf, sdkDirectory, wrapper, jsonServiceProvider
)
marathonTask.dependsOn(testTaskForVariant)
}
}
}

testExtension != null -> {
val sdkDirectory: Provider<Directory> = testExtension.sdkComponents.sdkDirectory
testExtension.onVariants { androidTest ->
logger.info("Applying marathon for ${androidTest.name}")

val testApkFolder: Provider<Directory> = androidTest.artifacts.get(SingleArtifact.APK)
val testArtifactsLoader = androidTest.artifacts.getBuiltArtifactsLoader()

val bundle = GradleAndroidTestBundle.TestOnly(
testApkFolder = project.objects.directoryProperty().apply { set(testApkFolder) },
testArtifactLoader = project.objects.property(BuiltArtifactsLoader::class.java)
.apply { set(testArtifactsLoader) },
)

val (_, testTaskForVariant) = createTasks(
logger, androidTest.name, bundle, project, conf, sdkDirectory, wrapper, jsonServiceProvider
)
marathonTask.dependsOn(testTaskForVariant)

}
}

else -> throw IllegalStateException("No AndroidComponentsExtensions found. Did you apply marathon plugin after applying the application/library plugin?")
}
}
Expand Down Expand Up @@ -151,7 +176,7 @@ class MarathonPlugin : Plugin<Project> {
companion object {
private fun createTasks(
logger: Logger,
variant: AndroidTest,
variantName: String,
bundle: GradleAndroidTestBundle,
project: Project,
config: MarathonExtension,
Expand All @@ -160,7 +185,7 @@ class MarathonPlugin : Plugin<Project> {
jsonServiceProvider: Provider<JsonService>,
): Pair<TaskProvider<GenerateMarathonfileTask>, TaskProvider<MarathonRunTask>> {
val baseOutputDir = config.baseOutputDir?.let { File(it) } ?: File(project.buildDir, "reports/marathon")
val output = File(baseOutputDir, variant.name)
val output = File(baseOutputDir, variantName)

val configurationBuilder = Configuration.Builder(config.name, output).apply {
config.analyticsConfiguration?.toAnalyticsConfiguration()?.let { analyticsConfiguration = it }
Expand Down Expand Up @@ -213,24 +238,24 @@ class MarathonPlugin : Plugin<Project> {

val generateMarathonfileTask =
project.tasks.register(
"$TASK_PREFIX${variant.name.capitalize()}GenerateMarathonfile",
"$TASK_PREFIX${variantName.capitalize()}GenerateMarathonfile",
GenerateMarathonfileTask::class.java
) {
group = Const.GROUP
description = "Generates Marathonfile for '${variant.name}' variation"
flavorName.set(variant.name)
description = "Generates Marathonfile for '${variantName}' variation"
flavorName.set(variantName)
applicationBundle.set(listOf(bundle))
this.configurationBuilder.set(configurationJson)
this.vendorConfigurationBuilder.set(vendorConfigurationJson)
this.jsonService.set(jsonServiceProvider)
sdk.set(sdkDirectory)
marathonfile.set(project.layout.buildDirectory.dir("marathon").map { it.dir(variant.name) }
marathonfile.set(project.layout.buildDirectory.dir("marathon").map { it.dir(variantName) }
.map { it.file("Marathonfile") })
}

val marathonTask = project.tasks.register("$TASK_PREFIX${variant.name.capitalize()}", MarathonRunTask::class.java) {
val marathonTask = project.tasks.register("$TASK_PREFIX${variantName.capitalize()}", MarathonRunTask::class.java) {
group = JavaBasePlugin.VERIFICATION_GROUP
description = "Runs instrumentation tests on all the connected devices for '${variant.name}' " +
description = "Runs instrumentation tests on all the connected devices for '${variantName}' " +
"variation and generates a report with screenshots"
outputs.upToDateWhen { false }
dist.set(wrapper.flatMap { it.dist })
Expand Down
2 changes: 1 addition & 1 deletion sample/android-app/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import com.malinskiy.marathon.config.vendor.android.TestParserConfiguration
plugins {
id("com.android.application")
id("kotlin-android")
id("com.malinskiy.marathon") version "0.8.2-SNAPSHOT"
id("com.malinskiy.marathon") version "0.8.5-SNAPSHOT"
}

android {
Expand Down
1 change: 1 addition & 0 deletions sample/android-app/settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ pluginManagement {
rootProject.name = "android-app"
rootProject.buildFileName = "build.gradle.kts"
include("app")
include("ui-tests")
43 changes: 43 additions & 0 deletions sample/android-app/ui-tests/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import com.malinskiy.marathon.config.vendor.android.TestParserConfiguration

plugins {
id("com.android.test")
id("kotlin-android")
id("com.malinskiy.marathon") version "0.8.5-SNAPSHOT"
}

android {
buildToolsVersion = "34.0.0"
compileSdk = 33

namespace = "com.example.ui_tests"

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = "1.8"
}

defaultConfig {
minSdk = 21
targetSdk = 33

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

targetProjectPath(":app")
}

dependencies {
implementation(TestLibraries.testRunner)
implementation(TestLibraries.extJunit)
implementation(TestLibraries.espressoCore)
}

marathon {
testParserConfiguration = TestParserConfiguration.RemoteTestParserConfiguration()
uncompletedTestRetryQuota = 3
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.example.ui_tests

import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.example.MainActivity

import org.junit.Test
import org.junit.runner.RunWith

import org.junit.Assert.*
import org.junit.Rule

/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {

@get:Rule
val androidScenarioRule = ActivityScenarioRule(MainActivity::class.java)

@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.example", appContext.packageName)
}
}
2 changes: 1 addition & 1 deletion sample/android-library/library/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
plugins {
id("com.android.library")
id("kotlin-android")
id("com.malinskiy.marathon") version "0.8.2-SNAPSHOT"
id("com.malinskiy.marathon") version "0.8.5-SNAPSHOT"
}

android {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,17 +156,25 @@ class AdamAndroidDevice(
when {
stat.exists() && stat.size() > 0.toULong() -> {
val channel = client.execute(
CompatPullFileRequest(remoteFilePath, local, supportedFeatures, coroutineScope = this, size = stat.size().toLong()),
CompatPullFileRequest(
remoteFilePath,
local,
supportedFeatures,
coroutineScope = this,
size = stat.size().toLong()
),
serial = adbSerial
)
for (update in channel) {
progress = update
}
}

stat.exists() && stat.size() == 0.toULong() -> {
local.createNewFile()
progress = 1.0
}

else -> throw TransferException("Couldn't pull file $remoteFilePath from device $serialNumber because it doesn't exist")
}
}
Expand Down Expand Up @@ -279,7 +287,11 @@ class AdamAndroidDevice(
}
}

override suspend fun installPackage(absolutePath: String, reinstall: Boolean, optionalParams: List<String>): MarathonShellCommandResult {
override suspend fun installPackage(
absolutePath: String,
reinstall: Boolean,
optionalParams: List<String>
): MarathonShellCommandResult {
val file = File(absolutePath)
//Very simple escaping for the name of the file
val fileName = file.name.escape()
Expand Down Expand Up @@ -355,7 +367,13 @@ class AdamAndroidDevice(
try {
withTimeoutOrNull(androidConfiguration.timeoutConfiguration.screenrecorder) {
val result = client.execute(ShellCommandRequest(screenRecorderCommand), serial = adbSerial)
logger.debug { "screenrecord output:\n ${result.output},\n exit code: ${result.exitCode}" }
logger.debug {
StringBuilder().apply {
append("screenrecord result: ")
if (result.output.isNotBlank()) append("output='${result.output}'")
append("exit code=${result.exitCode}")
}.toString()
}
}
} catch (e: CancellationException) {
//Ignore
Expand Down

0 comments on commit 78dfc5a

Please sign in to comment.