Skip to content

Commit 535dfaa

Browse files
authored
Add coverage tasks for reports on module level (#29)
1 parent 2aed1ec commit 535dfaa

File tree

3 files changed

+182
-81
lines changed

3 files changed

+182
-81
lines changed

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

Lines changed: 108 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,18 @@ class RootCoveragePlugin : Plugin<Project> {
1919

2020
override fun apply(project: Project) {
2121
if (project.rootProject !== project) {
22-
throw GradleException("The RootCoveragePlugin cannot be applied to project '${project.name}' because it is not the root project. Build file: ${project.buildFile}")
22+
throw GradleException(
23+
"The RootCoveragePlugin cannot be applied to project '${project.name}' because it" +
24+
" is not the root project. Build file: ${project.buildFile}"
25+
)
2326
}
2427
rootProjectExtension = project.extensions.create("rootCoverage", RootCoveragePluginExtension::class.java)
2528

2629
if (project.plugins.withType(JacocoPlugin::class.java).isEmpty()) {
27-
project.logger.warn("Warning: Jacoco plugin was not found for project: '${project.name}', it has been applied automatically, but you should do this manually. Build file: ${project.buildFile}")
30+
project.logger.warn(
31+
"Warning: Jacoco plugin was not found for project: '${project.name}', it has been" +
32+
" applied automatically, but you should do this manually. Build file: ${project.buildFile}"
33+
)
2834
project.plugins.apply(JacocoPlugin::class.java)
2935
}
3036

@@ -96,14 +102,51 @@ class RootCoveragePlugin : Plugin<Project> {
96102
private fun <T : BaseVariant> assertVariantExists(set: DomainObjectSet<T>, buildVariant: String, project: Project) {
97103
set.find {
98104
it.name.capitalize() == buildVariant.capitalize()
99-
} ?: throw GradleException("Build variant `$buildVariant` required for module `${project.name}` does not exist. Make sure to use a proper build variant configuration using rootCoverage.buildVariant and rootCoverage.buildVariantOverrides.")
105+
}
106+
?: throw GradleException(
107+
"Build variant `$buildVariant` required for module `${project.name}` does not exist. Make sure to use" +
108+
" a proper build variant configuration using rootCoverage.buildVariant and" +
109+
" rootCoverage.buildVariantOverrides."
110+
)
111+
}
112+
113+
private fun createSubProjectCoverageTask(subProject: Project) {
114+
// Aggregates jacoco results from the app sub-project and bankingright sub-project and generates a report.
115+
// The report can be found at the root of the project in /build/reports/jacoco, so don't look in
116+
// /app/build/reports/jacoco you will only find the app sub-project report there.
117+
val task = subProject.tasks.create("coverageReport", JacocoReport::class.java)
118+
task.group = "reporting"
119+
task.description = "Generates a Jacoco for this Gradle module."
120+
121+
task.reports.html.isEnabled = rootProjectExtension.generateHtml
122+
task.reports.xml.isEnabled = rootProjectExtension.generateXml
123+
task.reports.csv.isEnabled = rootProjectExtension.generateCsv
124+
125+
task.reports.html.destination = subProject.file("${subProject.buildDir}/reports/jacoco")
126+
task.reports.xml.destination = subProject.file("${subProject.buildDir}/reports/jacoco.xml")
127+
task.reports.csv.destination = subProject.file("${subProject.buildDir}/reports/jacoco.csv")
128+
129+
// Add some run-time checks.
130+
task.doFirst {
131+
val extension = subProject.extensions.findByName("android")
132+
if (extension != null) {
133+
val buildVariant = getBuildVariantFor(subProject)
134+
when (extension) {
135+
is LibraryExtension -> assertVariantExists(extension.libraryVariants, buildVariant, subProject)
136+
is AppExtension -> assertVariantExists(extension.applicationVariants, buildVariant, subProject)
137+
}
138+
}
139+
}
140+
141+
task.addSubProject(task.project)
100142
}
101143

102144
private fun createCoverageTaskForRoot(project: Project) {
103145
// Aggregates jacoco results from the app sub-project and bankingright sub-project and generates a report.
104146
// The report can be found at the root of the project in /build/reports/jacoco, so don't look in
105147
// /app/build/reports/jacoco you will only find the app sub-project report there.
106-
val task = project.tasks.create("rootCodeCoverageReport", JacocoReport::class.java)
148+
149+
val task = project.tasks.create("rootCoverageReport", JacocoReport::class.java)
107150
task.group = "reporting"
108151
task.description = "Generates a Jacoco report with combined results from all the subprojects."
109152

@@ -130,23 +173,40 @@ class RootCoveragePlugin : Plugin<Project> {
130173
}
131174

132175
// Configure the root task with sub-tasks for the sub-projects.
133-
project.subprojects.forEach {
176+
task.project.subprojects.forEach {
134177
it.afterEvaluate { subProject ->
135-
createCoverageTaskForSubProject(subProject, task)
178+
task.addSubProject(subProject)
179+
createSubProjectCoverageTask(subProject)
136180
}
137181
}
182+
183+
project.tasks.create("rootCodeCoverageReport").apply {
184+
doFirst {
185+
logger.warn(
186+
"The rootCodeCoverageReport task has been renamed in favor of rootCoverageReport, please" +
187+
" rename any references to this task."
188+
)
189+
}
190+
dependsOn("rootCoverageReport")
191+
}
138192
}
139193

140-
private fun createCoverageTaskForSubProject(subProject: Project, task: JacocoReport) {
194+
private fun JacocoReport.addSubProject(subProject: Project) {
141195
// Only Android Application and Android Library modules are supported for now.
142196
val extension = subProject.extensions.findByName("android")
143197
if (extension == null) {
144198
// TODO support java modules?
145-
subProject.logger.warn("Note: Skipping code coverage for module '${subProject.name}', currently the RootCoveragePlugin does not yet support Java Library Modules.")
199+
subProject.logger.warn(
200+
"Note: Skipping code coverage for module '${subProject.name}', currently the" +
201+
" RootCoveragePlugin does not yet support Java Library Modules."
202+
)
146203
return
147204
} else if (extension is com.android.build.gradle.FeatureExtension) {
148205
// TODO support feature modules?
149-
subProject.logger.warn("Note: Skipping code coverage for module '${subProject.name}', currently the RootCoveragePlugin does not yet support Android Feature Modules.")
206+
subProject.logger.warn(
207+
"Note: Skipping code coverage for module '${subProject.name}', currently the" +
208+
" RootCoveragePlugin does not yet support Android Feature Modules."
209+
)
150210
return
151211
}
152212

@@ -157,74 +217,68 @@ class RootCoveragePlugin : Plugin<Project> {
157217
extension.libraryVariants.all { variant ->
158218
if (variant.buildType.isTestCoverageEnabled && variant.name.capitalize() == buildVariant.capitalize()) {
159219
if (subProject.plugins.withType(JacocoPlugin::class.java).isEmpty()) {
160-
subProject.logger.warn("Warning: Jacoco plugin was not found for project: '${subProject.name}', it has been applied automatically but you should do this manually. Build file: ${subProject.buildFile}")
220+
subProject.logger.info(
221+
"Jacoco plugin was not found for project: '${subProject.name}', it" +
222+
" has been applied automatically but you should do this manually. Build file:" +
223+
" ${subProject.buildFile}"
224+
)
161225
subProject.plugins.apply(JacocoPlugin::class.java)
162226
}
163-
val subTask = createTask(subProject, variant)
164-
addSubTaskDependencyToRootTask(task, subTask)
227+
addSubProjectVariant(subProject, variant)
165228
}
166229
}
167230
}
168231
is AppExtension -> {
169232
extension.applicationVariants.all { variant ->
170233
if (variant.buildType.isTestCoverageEnabled && variant.name.capitalize() == buildVariant.capitalize()) {
171234
if (subProject.plugins.withType(JacocoPlugin::class.java).isEmpty()) {
172-
subProject.logger.warn("Jacoco plugin not applied to project: '${subProject.name}'! RootCoveragePlugin automatically applied it but you should do this manually: ${subProject.buildFile}")
235+
subProject.logger.info(
236+
"Jacoco plugin was not found for project: '${subProject.name}', it" +
237+
" has been applied automatically but you should do this manually. Build file:" +
238+
" ${subProject.buildFile}"
239+
)
173240
subProject.plugins.apply(JacocoPlugin::class.java)
174241
}
175-
val subTask = createTask(subProject, variant)
176-
addSubTaskDependencyToRootTask(task, subTask)
242+
addSubProjectVariant(subProject, variant)
177243
}
178244
}
179245
}
180246
}
181247
}
182248

183-
private fun createTask(project: Project, variant: BaseVariant): RootCoverageModuleTask {
249+
private fun JacocoReport.addSubProjectVariant(subProject: Project, variant: BaseVariant) {
184250
val name = variant.name.capitalize()
185251

186-
val codeCoverageReportTask = project.tasks.register("codeCoverageReport$name", RootCoverageModuleTask::class.java)
187-
codeCoverageReportTask.configure { task ->
188-
task.group = null // null makes sure the group does not show in the gradle-view in Android Studio/Intellij
189-
task.description = "Generate unified Jacoco code codecoverage report"
190-
191-
if (rootProjectExtension.shouldExecuteUnitTests()) {
192-
task.dependsOn("test${name}UnitTest")
193-
}
194-
if (rootProjectExtension.shouldExecuteAndroidTests()) {
195-
task.dependsOn("connected${name}AndroidTest")
196-
}
252+
// Gets the relative path from this task to the subProject
253+
val path = project.relativePath(subProject.path).removeSuffix(":")
197254

198-
// Collect the class files based on the Java Compiler output
199-
val javaClassOutput = variant.javaCompileProvider.get().outputs
200-
val javaClassTrees = javaClassOutput.files.map { file ->
201-
project.fileTree(file, excludes = getFileFilterPatterns()).excludeNonClassFiles()
202-
}
203-
204-
// TODO: No idea how to dynamically get the kotlin class files output folder, so for now this is hardcoded.
205-
// TODO: For some reason the tmp/kotlin-classes folder does not use the variant.dirName property, for now we instead use the variant.name.
206-
val kotlinClassFolder = "${project.buildDir}/tmp/kotlin-classes/${variant.name}"
207-
project.logger.info("Kotlin class folder for variant '${variant.name}': $kotlinClassFolder")
208-
209-
val kotlinClassTree = project.fileTree(kotlinClassFolder, excludes = getFileFilterPatterns()).excludeNonClassFiles()
210-
211-
// getSourceFolders returns ConfigurableFileCollections, but we only need the base directory of each ConfigurableFileCollection.
212-
val sourceFiles = variant.getSourceFolders(SourceKind.JAVA).map { file -> file.dir }
255+
// Add dependencies to the test tasks of the subProject
256+
if (rootProjectExtension.shouldExecuteUnitTests()) {
257+
dependsOn("$path:test${name}UnitTest")
258+
}
259+
if (rootProjectExtension.shouldExecuteAndroidTests()) {
260+
dependsOn("$path:connected${name}AndroidTest")
261+
}
213262

214-
task.sourceDirectories = project.files(sourceFiles)
215-
task.classDirectories = project.files(javaClassTrees, kotlinClassTree)
216-
task.executionData = getExecutionDataFileTree(project)
263+
// Collect the class files based on the Java Compiler output
264+
val javaClassOutput = variant.javaCompileProvider.get().outputs
265+
val javaClassTrees = javaClassOutput.files.map { file ->
266+
subProject.fileTree(file, excludes = getFileFilterPatterns()).excludeNonClassFiles()
217267
}
218-
return codeCoverageReportTask.get()
219-
}
220268

221-
private fun addSubTaskDependencyToRootTask(rootTask: JacocoReport, subModuleTask: RootCoverageModuleTask) {
269+
// TODO: No idea how to dynamically get the kotlin class files output folder, so for now this is hardcoded.
270+
// TODO: For some reason the tmp/kotlin-classes folder does not use the variant.dirName property, for now we instead use the variant.name.
271+
val kotlinClassFolder = "${subProject.buildDir}/tmp/kotlin-classes/${variant.name}"
272+
subProject.logger.info("Kotlin class folder for variant '${variant.name}': $kotlinClassFolder")
273+
274+
val kotlinClassTree =
275+
subProject.fileTree(kotlinClassFolder, excludes = getFileFilterPatterns()).excludeNonClassFiles()
222276

223-
// Make the root task depend on the sub-project code coverage task
224-
rootTask.dependsOn(subModuleTask)
277+
// getSourceFolders returns ConfigurableFileCollections, but we only need the base directory of each ConfigurableFileCollection.
278+
val sourceFiles = variant.getSourceFolders(SourceKind.JAVA).map { file -> file.dir }
225279

226-
rootTask.classDirectories.from(subModuleTask.classDirectories)
227-
rootTask.sourceDirectories.from(subModuleTask.sourceDirectories)
228-
rootTask.executionData.from(subModuleTask.executionData)
280+
sourceDirectories.from(subProject.files(sourceFiles))
281+
classDirectories.from(subProject.files(javaClassTrees, kotlinClassTree))
282+
executionData.from(getExecutionDataFileTree(subProject))
229283
}
230-
}
284+
}

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

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

3+
import junit.framework.Assert.assertEquals
34
import org.apache.commons.csv.CSVFormat
45
import org.apache.commons.csv.CSVParser
56
import org.apache.commons.csv.CSVRecord
67
import java.io.File
78
import java.nio.charset.StandardCharsets
8-
import kotlin.test.assertEquals
99

1010
class CoverageReport private constructor(
1111
private val instructionMissedColumn: Int,
1212
private val branchMissedColumn: Int,
1313
private val packageColumn: Int,
1414
private val classColumn: Int,
1515
private val records: List<CSVRecord>) {
16-
17-
private fun CSVRecord?.assertFullCoverageCoverage() {
16+
17+
private fun CSVRecord?.assertCoverage(missedBranches: Int = 0, missedInstructions: Int = 0) {
1818
kotlin.test.assertNotNull(this)
19-
assertEquals(this[instructionMissedColumn]?.toInt(), 0)
20-
assertEquals(this[branchMissedColumn]?.toInt(), 0)
19+
assertEquals(missedInstructions, this[instructionMissedColumn]?.toInt())
20+
assertEquals(missedBranches, this[branchMissedColumn]?.toInt())
2121
}
2222

23-
fun assertFullCoverage(packageName: String, className: String) {
24-
find(packageName, className).assertFullCoverageCoverage()
23+
fun assertCoverage(packageName: String, className: String, missedBranches: Int = 0, missedInstructions: Int = 0) {
24+
find(packageName, className).assertCoverage(missedBranches, missedInstructions)
2525
}
2626

2727
private fun find(packageName: String, className: String): CSVRecord? = records.find {

0 commit comments

Comments
 (0)