Skip to content

Commit 219e07e

Browse files
authored
Fix: Error when trying to run without Gradle Managed Device in combination with executeAndroidTests=false (#91)
Fixes #90
1 parent ee6f664 commit 219e07e

File tree

15 files changed

+291
-106
lines changed

15 files changed

+291
-106
lines changed

.gitignore

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

gradle/libs.versions.toml

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,17 @@ androidGradlePlugin = { module = "com.android.tools.build:gradle", vers
1111
androidGradlePluginApi = { module = "com.android.tools.build:gradle-api", version.ref = "androidGradlePlugin" }
1212

1313
# Test dependencies
14-
junit = { module = "junit:junit", version = "4.13.2" }
15-
truth = { module = "com.google.truth:truth", version = "1.1.3" }
16-
supportTestRunner = { module = "androidx.test:runner", version = "1.5.2" }
17-
espressoCore = { module = "androidx.test.espresso:espresso-core", version = "3.5.1" }
18-
androidJUnit = { module = "androidx.test.ext:junit", version = "1.1.5" }
19-
commonsCsv = { module = "org.apache.commons:commons-csv", version = "1.10.0" }
20-
kotlinTest = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
21-
robolectric = { module = "org.robolectric:robolectric", version = "4.10.2" }
22-
mockk = { module = "io.mockk:mockk", version = "1.13.5" }
14+
junit = { module = "junit:junit", version = "4.13.2" }
15+
truth = { module = "com.google.truth:truth", version = "1.1.3" }
16+
supportTestRunner = { module = "androidx.test:runner", version = "1.5.2" }
17+
espressoCore = { module = "androidx.test.espresso:espresso-core", version = "3.5.1" }
18+
androidJUnit = { module = "androidx.test.ext:junit", version = "1.1.5" }
19+
commonsCsv = { module = "org.apache.commons:commons-csv", version = "1.10.0" }
20+
kotlinTest = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
21+
robolectric = { module = "org.robolectric:robolectric", version = "4.10.2" }
22+
mockk = { module = "io.mockk:mockk", version = "1.13.5" }
23+
jacksonYaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version = "2.15.3"}
24+
jacksonKotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version = "2.15.3"}
2325

2426
[bundles]
2527
androidInstrumentedTest = ["supportTestRunner", "espressoCore", "androidJUnit"]

plugin/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ dependencies {
8787
testImplementation libs.bundles.jvmTest
8888
testImplementation gradleTestKit()
8989
testImplementation libs.mockk
90+
testImplementation libs.jacksonYaml
91+
testImplementation libs.jacksonKotlin
9092
}
9193

9294
// Setup Jacoco for Gradle TestKit

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ internal fun RootCoveragePluginExtension.getFileFilterPatterns(): List<String> =
3535
internal fun RootCoveragePluginExtension.getBuildVariantFor(project: Project): String =
3636
buildVariantOverrides[project.path] ?: buildVariant
3737

38-
internal fun Project.getExecutionDataFileTree(includeUnitTestResults: Boolean, includeAndroidTestResults: Boolean): FileTree? {
38+
internal fun Project.getExecutionDataFileTree(includeUnitTestResults: Boolean, includeConnectedDevicesResults: Boolean, includeGradleManagedDevicesResults: Boolean): FileTree? {
3939
val buildFolderPatterns = mutableListOf<String>()
4040
if (includeUnitTestResults) {
4141
// TODO instead of hardcoding this, obtain the location from the test tasks, something like this:
@@ -54,7 +54,7 @@ internal fun Project.getExecutionDataFileTree(includeUnitTestResults: Boolean, i
5454
// Android Build Tools Plugin 7.0+
5555
buildFolderPatterns.add("outputs/unit_test_code_coverage/*/*.exec")
5656
}
57-
if (includeAndroidTestResults) {
57+
if (includeConnectedDevicesResults) {
5858

5959
// These are legacy paths for older now unsupported AGP version, they are just here for
6060
// reference and are not added to prevent existing files from polluting results
@@ -67,7 +67,9 @@ 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
70+
}
71+
if(includeGradleManagedDevicesResults) {
72+
// Gradle Managed Devices 7.4
7173
buildFolderPatterns.add("outputs/managed_device_code_coverage/*/coverage.ec")
7274
}
7375
return if(buildFolderPatterns.isEmpty()) {

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,24 +144,37 @@ class RootCoveragePlugin : Plugin<Project> {
144144
if (rootProjectExtension.shouldExecuteUnitTests() && (buildType.enableUnitTestCoverage || buildType.isTestCoverageEnabled)) {
145145
dependsOn("$path:test${name}UnitTest")
146146
}
147+
148+
var runsOnGradleManagedDevices = false
149+
147150
if (rootProjectExtension.shouldExecuteAndroidTests() && (buildType.enableAndroidTestCoverage || buildType.isTestCoverageEnabled)) {
148151

149152
// Attempt to run on instrumented tests, giving priority to the following devices in this order:
150153
// - A user provided Gradle Managed Device.
151154
// - All Gradle Managed Devices if any is available.
152155
// - All through ADB connected devices.
153156
val gradleManagedDevices = subProject.extensions.getByType(BaseExtension::class.java).testOptions.managedDevices.devices
154-
if(rootProjectExtension.runOnGradleManagedDevices && !rootProjectExtension.gradleManagedDeviceName.isNullOrEmpty()) {
157+
if (rootProjectExtension.runOnGradleManagedDevices && !rootProjectExtension.gradleManagedDeviceName.isNullOrEmpty()) {
158+
runsOnGradleManagedDevices = true
155159
dependsOn("$path:${rootProjectExtension.gradleManagedDeviceName}${name}AndroidTest")
156160
} else if (rootProjectExtension.runOnGradleManagedDevices && gradleManagedDevices.isNotEmpty()) {
161+
runsOnGradleManagedDevices = true
157162
dependsOn("$path:allDevices${name}AndroidTest")
158163
} else {
159164
dependsOn("$path:connected${name}AndroidTest")
160165
}
161166
} else {
162167
// If this plugin should not run instrumented tests on it's own, at least make sure it runs after those tasks (if they are
163-
// selected to run as well).
164-
mustRunAfter("$path:allDevices${name}AndroidTest")
168+
// selected to run as well and exists).
169+
//
170+
// In theory we don't need to do this if `rootProjectExtension.includeAndroidTestResults` is false, so we could check that, but
171+
// it also does not hurt.
172+
173+
val executeAndroidTestsOnGradleManagedDevicesTask = project.tasks.findByPath("$path:allDevices${name}AndroidTest")
174+
if(executeAndroidTestsOnGradleManagedDevicesTask != null) {
175+
// This task only exists if a Gradle Managed Device is configured, which may not be the case.
176+
mustRunAfter("$path:allDevices${name}AndroidTest")
177+
}
165178
mustRunAfter("$path:connected${name}AndroidTest")
166179
}
167180

@@ -176,7 +189,8 @@ class RootCoveragePlugin : Plugin<Project> {
176189
executionData.from(
177190
subProject.getExecutionDataFileTree(
178191
includeUnitTestResults = rootProjectExtension.includeUnitTestResults && (buildType.enableUnitTestCoverage || buildType.isTestCoverageEnabled),
179-
includeAndroidTestResults = rootProjectExtension.includeAndroidTestResults && (buildType.enableAndroidTestCoverage || buildType.isTestCoverageEnabled)
192+
includeConnectedDevicesResults = rootProjectExtension.includeAndroidTestResults && (buildType.enableAndroidTestCoverage || buildType.isTestCoverageEnabled) && !runsOnGradleManagedDevices,
193+
includeGradleManagedDevicesResults = rootProjectExtension.includeAndroidTestResults && (buildType.enableAndroidTestCoverage || buildType.isTestCoverageEnabled) && runsOnGradleManagedDevices
180194
)
181195
)
182196
}
Lines changed: 98 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
package org.neotech.plugin.rootcoverage
22

33
import com.google.common.truth.Truth.assertThat
4-
import groovy.text.SimpleTemplateEngine
54
import org.gradle.testkit.runner.BuildResult
65
import org.gradle.testkit.runner.GradleRunner
7-
import org.gradle.testkit.runner.TaskOutcome
86
import org.junit.Assume
97
import org.junit.Before
108
import org.junit.Test
119
import org.junit.runner.RunWith
1210
import org.junit.runners.Parameterized
1311
import org.neotech.plugin.rootcoverage.util.SimpleTemplate
1412
import org.neotech.plugin.rootcoverage.util.SystemOutputWriter
13+
import org.neotech.plugin.rootcoverage.util.assertSuccessful
14+
import org.neotech.plugin.rootcoverage.util.assertTaskSuccess
1515
import org.neotech.plugin.rootcoverage.util.createGradlePropertiesFile
1616
import org.neotech.plugin.rootcoverage.util.createLocalPropertiesFile
17-
import org.neotech.plugin.rootcoverage.util.getProperties
1817
import org.neotech.plugin.rootcoverage.util.put
18+
import org.neotech.plugin.rootcoverage.util.readYaml
1919
import org.neotech.plugin.rootcoverage.util.toGroovyString
2020
import java.io.File
2121
import java.util.Properties
@@ -29,23 +29,47 @@ class IntegrationTest(
2929
private val gradleVersion: String,
3030
) {
3131

32-
private val configuration = configurationFile.getProperties()
32+
private val configuration: TestConfiguration = configurationFile.readYaml()
3333

3434
@Before
35-
fun before(){
35+
fun before() {
3636
// 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()
37+
val isGradleManagedDeviceTest =
38+
configuration.pluginConfiguration.getPropertyValue("runOnGradleManagedDevices")?.toBoolean() ?: false
3839
Assume.assumeFalse(System.getenv("GITHUB_ACTIONS") != null && isGradleManagedDeviceTest)
3940
}
4041

4142
@Test
4243
fun execute() {
43-
val template = SimpleTemplate().apply {
44-
putValue("configuration", configuration.toGroovyString())
45-
}
4644

45+
val templateRootBuildGradleFile = SimpleTemplate().apply {
46+
putValue("configuration", configuration.pluginConfiguration.properties.toGroovyString())
47+
}
4748
File(projectRoot, "build.gradle.tmp").inputStream().use {
48-
File(projectRoot, "build.gradle").writeText(template.process(it, Charsets.UTF_8))
49+
File(projectRoot, "build.gradle").writeText(templateRootBuildGradleFile.process(it, Charsets.UTF_8))
50+
}
51+
52+
val templateAppBuildGradleFile = SimpleTemplate().apply {
53+
putValue(
54+
"managedDevices", if (configuration.projectConfiguration.addGradleManagedDevice) {
55+
"""
56+
managedDevices {
57+
devices {
58+
nexusoneapi30 (com.android.build.api.dsl.ManagedVirtualDevice) {
59+
device = "Nexus One"
60+
apiLevel = 30
61+
systemImageSource = "aosp-atd"
62+
}
63+
}
64+
}
65+
""".trimIndent()
66+
} else {
67+
""
68+
}
69+
)
70+
}
71+
File(projectRoot, "app/build.gradle.tmp").inputStream().use {
72+
File(projectRoot, "app/build.gradle").writeText(templateAppBuildGradleFile.process(it, Charsets.UTF_8))
4973
}
5074

5175
createLocalPropertiesFile(projectRoot)
@@ -57,23 +81,31 @@ class IntegrationTest(
5781
})
5882
})
5983

84+
// Note: rootCodeCoverageReport is the old and deprecated name of the rootCoverageReport task, it is
85+
// used to check whether the old name properly aliases to the new task name.
86+
val gradleCommands = if (configuration.pluginConfiguration.getPropertyValue("executeAndroidTests") == "false") {
87+
listOf("clean", "connectedDebugAndroidTest", "coverageReport", "rootCodeCoverageReport", "--stacktrace")
88+
} else {
89+
listOf("clean", "coverageReport", "rootCodeCoverageReport", "--stacktrace")
90+
}
91+
6092
val runner = GradleRunner.create()
6193
.withProjectDir(projectRoot)
6294
.withGradleVersion(gradleVersion)
6395
.withPluginClasspath()
6496
.forwardStdOutput(SystemOutputWriter.out())
6597
.forwardStdError(SystemOutputWriter.err())
66-
67-
// Note: rootCodeCoverageReport is the old and deprecated name of the rootCoverageReport task, it is
68-
// used to check whether the old name properly aliases to the new task name.
69-
.withArguments("clean", "coverageReport", "rootCodeCoverageReport", "--stacktrace")
98+
.withArguments(gradleCommands)
7099

71100
val result = runner.build()
72101

73-
assertThat(result.output).contains("BUILD SUCCESSFUL")
102+
result.assertSuccessful()
103+
104+
// Assert whether the correct Android Test tasks are executed
105+
result.assertCorrectAndroidTestTasksAreExecuted()
74106

75107
// Assert whether the combined coverage report is what we expected
76-
result.assertRootCoverageReport(File(projectRoot, "build/reports/jacoco.csv"))
108+
result.assertRootCoverageReport()
77109

78110
// Assert whether the per module coverage reports are what we expect
79111
result.assertAppCoverageReport()
@@ -82,13 +114,27 @@ class IntegrationTest(
82114
assertSourceFilesHaveBeenAddedToReport(File(projectRoot, "build/reports/jacoco"))
83115
}
84116

85-
private fun BuildResult.assertRootCoverageReport(file: File) {
86-
assertThat(task(":rootCoverageReport")!!.outcome).isEqualTo(TaskOutcome.SUCCESS)
117+
private fun BuildResult.assertCorrectAndroidTestTasksAreExecuted() {
118+
if (configuration.pluginConfiguration.getPropertyValue("runOnGradleManagedDevices", "false").toBoolean()) {
119+
// Assert that the tests have been run on Gradle Managed Devices
120+
val device = configuration.pluginConfiguration.getPropertyValue("gradleManagedDeviceName", "allDevices")
121+
assertTaskSuccess(":app:${device}DebugAndroidTest")
122+
assertTaskSuccess(":library_android:${device}DebugAndroidTest")
123+
124+
} else {
125+
// Assert that the tests have been run on connected devices
126+
assertTaskSuccess(":app:connectedDebugAndroidTest")
127+
assertTaskSuccess(":library_android:connectedDebugAndroidTest")
128+
}
129+
}
130+
131+
private fun BuildResult.assertRootCoverageReport() {
132+
assertTaskSuccess(":rootCoverageReport")
87133

88-
// Also check if the old task name is still exe
89-
assertThat(task(":rootCodeCoverageReport")!!.outcome).isEqualTo(TaskOutcome.SUCCESS)
134+
// Also check if the old task name is still executed
135+
assertTaskSuccess(":rootCodeCoverageReport")
90136

91-
val report = CoverageReport.from(file)
137+
val report = CoverageReport.from(File(projectRoot, "build/reports/jacoco.csv"))
92138

93139
report.assertCoverage("org.neotech.library.android", "LibraryAndroidJava")
94140
report.assertCoverage("org.neotech.library.android", "LibraryAndroidKotlin")
@@ -98,19 +144,7 @@ class IntegrationTest(
98144
}
99145

100146
private fun BuildResult.assertAppCoverageReport() {
101-
assertThat(task(":app:coverageReport")!!.outcome).isEqualTo(TaskOutcome.SUCCESS)
102-
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-
147+
assertTaskSuccess(":app:coverageReport")
114148
val report = CoverageReport.from(File(projectRoot, "app/build/reports/jacoco.csv"))
115149

116150
report.assertNotInReport("org.neotech.app", "MustBeExcluded")
@@ -120,7 +154,7 @@ class IntegrationTest(
120154
}
121155

122156
private fun BuildResult.assertAndroidLibraryCoverageReport() {
123-
assertThat(task(":library_android:coverageReport")!!.outcome).isEqualTo(TaskOutcome.SUCCESS)
157+
assertTaskSuccess(":library_android:coverageReport")
124158

125159
val report = CoverageReport.from(File(projectRoot, "library_android/build/reports/jacoco.csv"))
126160

@@ -159,22 +193,38 @@ class IntegrationTest(
159193
@JvmStatic
160194
fun parameters(): List<Array<Any>> {
161195

162-
val testFixtures =
163-
File("src/test/test-fixtures").listFiles()?.filter { it.isDirectory } ?: error("Could not list test fixture directories")
196+
val fixture = File("src/test/test-fixtures/multi-module")
197+
164198
val gradleVersions = arrayOf("7.5", "7.5.1", "7.6", "7.6.1")
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-
}
199+
200+
val configurations = File(fixture, "configurations").listFiles() ?: error("Configurations folder not found in $fixture")
201+
return configurations.flatMap { configuration ->
202+
gradleVersions.map { gradleVersion ->
203+
arrayOf(
204+
"${fixture.name}-${configuration.nameWithoutExtension}-$gradleVersion",
205+
fixture,
206+
configuration,
207+
gradleVersion
208+
)
176209
}
177210
}
178211
}
179212
}
213+
214+
data class TestConfiguration(
215+
val projectConfiguration: ProjectConfiguration,
216+
val pluginConfiguration: PluginConfiguration
217+
) {
218+
data class PluginConfiguration(val properties: List<Property> = emptyList()) {
219+
220+
fun getPropertyValue(name: String, defaultValue: String): String = getPropertyValue(name) ?: defaultValue
221+
222+
fun getPropertyValue(name: String): String? = properties.find { it.name == name }?.value
223+
224+
225+
data class Property(val name: String, val value: String)
226+
}
227+
228+
data class ProjectConfiguration(val addGradleManagedDevice: Boolean = true)
229+
}
180230
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package org.neotech.plugin.rootcoverage.util
2+
3+
import com.google.common.truth.Truth.assertThat
4+
import org.gradle.testkit.runner.BuildResult
5+
import org.gradle.testkit.runner.TaskOutcome
6+
7+
fun BuildResult.assertSuccessful() {
8+
assertThat(output).contains("BUILD SUCCESSFUL")
9+
}
10+
11+
fun BuildResult.assertTaskSuccess(taskPath: String) {
12+
assertThat(task(taskPath)!!.outcome).isEqualTo(TaskOutcome.SUCCESS)
13+
}

0 commit comments

Comments
 (0)