Skip to content

Commit 1d8baad

Browse files
authored
Add: Experimental support for running instrumented tests on Gradle Managed Devices (#84)
This adds experimental support for running instrumented tests on Gradle Managed Devices. Some credit has to go to @bddckr for his initial PR on this (see: #73).
1 parent 723fdb3 commit 1d8baad

File tree

14 files changed

+183
-22
lines changed

14 files changed

+183
-22
lines changed

.github/workflows/build.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,17 @@ jobs:
88
runs-on: macOS-latest
99
steps:
1010
- name: checkout
11-
uses: actions/checkout@v2
12-
- uses: actions/setup-java@v1
11+
uses: actions/checkout@v3
12+
- uses: actions/setup-java@v3
1313
with:
14+
distribution: 'zulu'
1415
java-version: '11'
1516
- name: test
1617
uses: reactivecircus/android-emulator-runner@v2
1718
with:
1819
api-level: 28
1920
script: ./gradlew clean test --stacktrace
2021
- name: upload coverage
21-
uses: codecov/codecov-action@v2
22+
uses: codecov/codecov-action@v3
2223
with:
2324
files: ./plugin/build/reports/jacoco/test/jacocoTestReport.xml

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ repo/
1515
build/
1616
*/out/*/classes/*
1717
plugin/src/test/test-fixtures/multi-module/gradle.properties
18+
plugin/src/test/test-fixtures/multi-module/build.gradle

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,15 @@ rootCoverage {
123123
// Since 1.4: Sets jacoco.includeNoLocationClasses, so you don't have to. Helpful when using Robolectric
124124
// which usually requires this attribute to be true
125125
includeNoLocationClasses false
126+
127+
// Upcoming in 1.7: If set to true instrumented tests will be attempt to run on
128+
// Gradle Managed Devices before trying devices connected through other means (ADB).
129+
runOnGradleManagedDevices false
130+
131+
// Upcoming in 1.7: The name of the Gradle Managed device to run instrumented tests on.
132+
// This is only used if `runOnGradleManagedDevices` is set to true. If not given tests will be
133+
// run on all available Gradle Managed Devices
134+
gradleManagedDeviceName "nexusoneapi30"
126135
}
127136
```
128137

plugin/src/main/kotlin/org/neotech/plugin/rootcoverage/JaCoCoConfiguration.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ internal fun Project.getExecutionDataFileTree(includeUnitTestResults: Boolean, i
6767

6868
// Android Build Tools Plugin 7.1+
6969
buildFolderPatterns.add("outputs/code_coverage/*/connected/*/coverage.ec")
70+
// Gradle Managed Devices
71+
buildFolderPatterns.add("outputs/managed_device_code_coverage/*/coverage.ec")
7072
}
7173
return if(buildFolderPatterns.isEmpty()) {
7274
null

plugin/src/main/kotlin/org/neotech/plugin/rootcoverage/RootCoveragePlugin.kt

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.android.build.api.artifact.MultipleArtifact
55
import com.android.build.api.dsl.BuildType
66
import com.android.build.api.variant.AndroidComponentsExtension
77
import com.android.build.api.variant.Variant
8+
import com.android.build.gradle.BaseExtension
89
import org.gradle.api.GradleException
910
import org.gradle.api.NamedDomainObjectContainer
1011
import org.gradle.api.Plugin
@@ -152,7 +153,24 @@ class RootCoveragePlugin : Plugin<Project> {
152153
dependsOn("$path:test${name}UnitTest")
153154
}
154155
if (rootProjectExtension.shouldExecuteAndroidTests() && (buildType.enableAndroidTestCoverage || buildType.isTestCoverageEnabled)) {
155-
dependsOn("$path:connected${name}AndroidTest")
156+
157+
// Attempt to run on instrumented tests, giving priority to the following devices in this order:
158+
// - A user provided Gradle Managed Device.
159+
// - All Gradle Managed Devices if any is available.
160+
// - All through ADB connected devices.
161+
val gradleManagedDevices = subProject.extensions.getByType(BaseExtension::class.java).testOptions.managedDevices.devices
162+
if(rootProjectExtension.runOnGradleManagedDevices && !rootProjectExtension.gradleManagedDeviceName.isNullOrEmpty()) {
163+
dependsOn("$path:${rootProjectExtension.gradleManagedDeviceName}${name}AndroidTest")
164+
} else if (rootProjectExtension.runOnGradleManagedDevices && gradleManagedDevices.isNotEmpty()) {
165+
dependsOn("$path:allDevices${name}AndroidTest")
166+
} else {
167+
dependsOn("$path:connected${name}AndroidTest")
168+
}
169+
} else {
170+
// If this plugin should not run instrumented tests on it's own, at least make sure it runs after those tasks (if they are
171+
// selected to run as well).
172+
mustRunAfter("$path:allDevices${name}AndroidTest")
173+
mustRunAfter("$path:connected${name}AndroidTest")
156174
}
157175

158176
sourceDirectories.from(variant.sources.java?.all)
@@ -162,6 +180,7 @@ class RootCoveragePlugin : Plugin<Project> {
162180
subProject.fileTree(directory.asFile, excludes = rootProjectExtension.getFileFilterPatterns())
163181
}
164182
})
183+
165184
executionData.from(
166185
subProject.getExecutionDataFileTree(
167186
includeUnitTestResults = rootProjectExtension.includeUnitTestResults && (buildType.enableUnitTestCoverage || buildType.isTestCoverageEnabled),

plugin/src/main/kotlin/org/neotech/plugin/rootcoverage/RootCoveragePluginExtension.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,18 @@ open class RootCoveragePluginExtension {
7373
*/
7474
var includeUnitTestResults = true
7575

76+
/**
77+
* The name of the Gradle Managed device to run instrumented tests on, if null tests will be attempted to run on any
78+
* connected emulator.
79+
*/
80+
var gradleManagedDeviceName: String? = null
81+
82+
/**
83+
* If set to true (default is false) this plugin will attempt to run on Gradle Managed Devices before trying devices
84+
* connected through other means (Non Gradle Managed Devices).
85+
*/
86+
var runOnGradleManagedDevices: Boolean = false
87+
7688
internal fun shouldExecuteAndroidTests() = executeTests && executeAndroidTests && includeAndroidTestResults
7789

7890
internal fun shouldExecuteUnitTests() = executeTests && executeUnitTests && includeUnitTestResults

plugin/src/test/kotlin/org/neotech/plugin/rootcoverage/IntegrationTest.kt

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
package org.neotech.plugin.rootcoverage
22

33
import com.google.common.truth.Truth.assertThat
4+
import groovy.text.SimpleTemplateEngine
45
import org.gradle.testkit.runner.BuildResult
56
import org.gradle.testkit.runner.GradleRunner
67
import org.gradle.testkit.runner.TaskOutcome
8+
import org.junit.Assume
9+
import org.junit.Before
710
import org.junit.Test
811
import org.junit.runner.RunWith
912
import org.junit.runners.Parameterized
13+
import org.neotech.plugin.rootcoverage.util.SimpleTemplate
1014
import org.neotech.plugin.rootcoverage.util.SystemOutputWriter
1115
import org.neotech.plugin.rootcoverage.util.createGradlePropertiesFile
1216
import org.neotech.plugin.rootcoverage.util.createLocalPropertiesFile
17+
import org.neotech.plugin.rootcoverage.util.getProperties
1318
import org.neotech.plugin.rootcoverage.util.put
19+
import org.neotech.plugin.rootcoverage.util.toGroovyString
1420
import java.io.File
1521
import java.util.Properties
1622

@@ -19,11 +25,29 @@ class IntegrationTest(
1925
// Used by Junit as the test name, see @Parameters
2026
@Suppress("unused") private val name: String,
2127
private val projectRoot: File,
22-
private val gradleVersion: String
28+
configurationFile: File,
29+
private val gradleVersion: String,
2330
) {
2431

32+
private val configuration = configurationFile.getProperties()
33+
34+
@Before
35+
fun before(){
36+
// Ignore tests that require Gradle Managed Devices on CI (because GitHub Actions does not seem to support these well).
37+
val isGradleManagedDeviceTest = configuration.getProperty("runOnGradleManagedDevices", "false").toBoolean()
38+
Assume.assumeFalse(System.getenv("GITHUB_ACTIONS") != null && isGradleManagedDeviceTest)
39+
}
40+
2541
@Test
2642
fun execute() {
43+
val template = SimpleTemplate().apply {
44+
putValue("configuration", configuration.toGroovyString())
45+
}
46+
47+
File(projectRoot, "build.gradle.tmp").inputStream().use {
48+
File(projectRoot, "build.gradle").writeText(template.process(it, Charsets.UTF_8))
49+
}
50+
2751
createLocalPropertiesFile(projectRoot)
2852
createGradlePropertiesFile(projectRoot, properties = Properties().apply {
2953
put("android.useAndroidX", "true")
@@ -76,6 +100,17 @@ class IntegrationTest(
76100
private fun BuildResult.assertAppCoverageReport() {
77101
assertThat(task(":app:coverageReport")!!.outcome).isEqualTo(TaskOutcome.SUCCESS)
78102

103+
if (configuration.getProperty("runOnGradleManagedDevices", "false").toBoolean()) {
104+
// Assert that the tests have been run on Gradle Managed Devices
105+
val device = configuration.getProperty("gradleManagedDeviceName", "allDevices")
106+
assertThat(task(":app:${device}DebugAndroidTest")!!.outcome).isEqualTo(TaskOutcome.SUCCESS)
107+
assertThat(task(":library_android:${device}DebugAndroidTest")!!.outcome).isEqualTo(TaskOutcome.SUCCESS)
108+
} else {
109+
// Assert that the tests have been run on connected devices
110+
assertThat(task(":app:connectedDebugAndroidTest")!!.outcome).isEqualTo(TaskOutcome.SUCCESS)
111+
assertThat(task(":library_android:connectedDebugAndroidTest")!!.outcome).isEqualTo(TaskOutcome.SUCCESS)
112+
}
113+
79114
val report = CoverageReport.from(File(projectRoot, "app/build/reports/jacoco.csv"))
80115

81116
report.assertNotInReport("org.neotech.app", "MustBeExcluded")
@@ -124,12 +159,20 @@ class IntegrationTest(
124159
@JvmStatic
125160
fun parameters(): List<Array<Any>> {
126161

127-
val testFixtures = File("src/test/test-fixtures").listFiles()?.filter { it.isDirectory }
128-
?: error("Could not list test fixture directories")
162+
val testFixtures =
163+
File("src/test/test-fixtures").listFiles()?.filter { it.isDirectory } ?: error("Could not list test fixture directories")
129164
val gradleVersions = arrayOf("7.4", "7.4.2", "7.5.1")
130-
return testFixtures.flatMap { file ->
131-
gradleVersions.map { gradleVersion ->
132-
arrayOf("${file.name}-$gradleVersion", file, gradleVersion)
165+
return testFixtures.flatMap { fixture ->
166+
val configurations = File(fixture, "configurations").listFiles() ?: error("Configurations folder not found in $fixture")
167+
configurations.flatMap { configuration ->
168+
gradleVersions.map { gradleVersion ->
169+
arrayOf(
170+
"${fixture.name}-${configuration.nameWithoutExtension}-$gradleVersion",
171+
fixture,
172+
configuration,
173+
gradleVersion
174+
)
175+
}
133176
}
134177
}
135178
}

plugin/src/test/kotlin/org/neotech/plugin/rootcoverage/util/ProjectGeneration.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,19 @@ internal fun Properties.put(properties: Properties) {
2727
}
2828

2929
internal fun File.getProperties(): Properties = inputStream().use {
30-
Properties().apply {
31-
load(it)
32-
}
30+
Properties().apply {
31+
load(it)
32+
}
33+
}
34+
35+
internal fun Properties.toGroovyString(): String = map {
36+
val stringValue = it.value as String
37+
if (stringValue.toBooleanStrictOrNull() != null) {
38+
"${it.key} ${it.value}"
39+
} else {
40+
"${it.key} \"${it.value}\""
3341
}
42+
}.joinToString(separator = System.lineSeparator())
3443

3544
/**
3645
* Tries to resolve the Android SDK directory. This function first tries the ANDROID_HOME system
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package org.neotech.plugin.rootcoverage.util
2+
3+
import java.io.Closeable
4+
import java.io.InputStream
5+
import java.nio.charset.Charset
6+
7+
8+
/**
9+
* Super quick and dirty "template engine" loosely based on mustache style templating.
10+
*/
11+
internal class SimpleTemplate {
12+
13+
private val map = mutableMapOf<String, String>()
14+
15+
fun putValue(key: String, value: String) {
16+
map[key] = value
17+
}
18+
19+
fun process(inputStream: InputStream, charset: Charset): String {
20+
val content = inputStream.bufferedReader(charset).readLines()
21+
val result = mutableListOf<String>()
22+
content.forEach { line ->
23+
var adjustedLine = line
24+
map.forEach {
25+
adjustedLine = line.replace("{{${it.key}}}", it.value)
26+
}
27+
result.add(adjustedLine)
28+
}
29+
return result.joinToString(System.lineSeparator())
30+
}
31+
}

plugin/src/test/test-fixtures/multi-module/app/build.gradle

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,17 @@ android {
3737
unitTests {
3838
includeAndroidResources = true
3939
}
40+
managedDevices {
41+
devices {
42+
nexusoneapi30 (com.android.build.api.dsl.ManagedVirtualDevice) {
43+
device = "Nexus One"
44+
apiLevel = 30
45+
systemImageSource = "aosp-atd"
46+
}
47+
}
48+
}
4049
}
41-
50+
4251
kotlinOptions {
4352
jvmTarget = "1.8"
4453
}

0 commit comments

Comments
 (0)