Skip to content

Add coverage tasks for reports on module level #29

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,18 @@ class RootCoveragePlugin : Plugin<Project> {

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

if (project.plugins.withType(JacocoPlugin::class.java).isEmpty()) {
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}")
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}"
)
project.plugins.apply(JacocoPlugin::class.java)
}

Expand Down Expand Up @@ -96,14 +102,51 @@ class RootCoveragePlugin : Plugin<Project> {
private fun <T : BaseVariant> assertVariantExists(set: DomainObjectSet<T>, buildVariant: String, project: Project) {
set.find {
it.name.capitalize() == buildVariant.capitalize()
} ?: 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.")
}
?: 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."
)
}

private fun createSubProjectCoverageTask(subProject: Project) {
// Aggregates jacoco results from the app sub-project and bankingright sub-project and generates a report.
// The report can be found at the root of the project in /build/reports/jacoco, so don't look in
// /app/build/reports/jacoco you will only find the app sub-project report there.
val task = subProject.tasks.create("coverageReport", JacocoReport::class.java)
task.group = "reporting"
task.description = "Generates a Jacoco for this Gradle module."

task.reports.html.isEnabled = rootProjectExtension.generateHtml
task.reports.xml.isEnabled = rootProjectExtension.generateXml
task.reports.csv.isEnabled = rootProjectExtension.generateCsv

task.reports.html.destination = subProject.file("${subProject.buildDir}/reports/jacoco")
task.reports.xml.destination = subProject.file("${subProject.buildDir}/reports/jacoco.xml")
task.reports.csv.destination = subProject.file("${subProject.buildDir}/reports/jacoco.csv")

// Add some run-time checks.
task.doFirst {
val extension = subProject.extensions.findByName("android")
if (extension != null) {
val buildVariant = getBuildVariantFor(subProject)
when (extension) {
is LibraryExtension -> assertVariantExists(extension.libraryVariants, buildVariant, subProject)
is AppExtension -> assertVariantExists(extension.applicationVariants, buildVariant, subProject)
}
}
}

task.addSubProject(task.project)
}

private fun createCoverageTaskForRoot(project: Project) {
// Aggregates jacoco results from the app sub-project and bankingright sub-project and generates a report.
// The report can be found at the root of the project in /build/reports/jacoco, so don't look in
// /app/build/reports/jacoco you will only find the app sub-project report there.
val task = project.tasks.create("rootCodeCoverageReport", JacocoReport::class.java)

val task = project.tasks.create("rootCoverageReport", JacocoReport::class.java)
task.group = "reporting"
task.description = "Generates a Jacoco report with combined results from all the subprojects."

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

// Configure the root task with sub-tasks for the sub-projects.
project.subprojects.forEach {
task.project.subprojects.forEach {
it.afterEvaluate { subProject ->
createCoverageTaskForSubProject(subProject, task)
task.addSubProject(subProject)
createSubProjectCoverageTask(subProject)
}
}

project.tasks.create("rootCodeCoverageReport").apply {
doFirst {
logger.warn(
"The rootCodeCoverageReport task has been renamed in favor of rootCoverageReport, please" +
" rename any references to this task."
)
}
dependsOn("rootCoverageReport")
}
}

private fun createCoverageTaskForSubProject(subProject: Project, task: JacocoReport) {
private fun JacocoReport.addSubProject(subProject: Project) {
// Only Android Application and Android Library modules are supported for now.
val extension = subProject.extensions.findByName("android")
if (extension == null) {
// TODO support java modules?
subProject.logger.warn("Note: Skipping code coverage for module '${subProject.name}', currently the RootCoveragePlugin does not yet support Java Library Modules.")
subProject.logger.warn(
"Note: Skipping code coverage for module '${subProject.name}', currently the" +
" RootCoveragePlugin does not yet support Java Library Modules."
)
return
} else if (extension is com.android.build.gradle.FeatureExtension) {
// TODO support feature modules?
subProject.logger.warn("Note: Skipping code coverage for module '${subProject.name}', currently the RootCoveragePlugin does not yet support Android Feature Modules.")
subProject.logger.warn(
"Note: Skipping code coverage for module '${subProject.name}', currently the" +
" RootCoveragePlugin does not yet support Android Feature Modules."
)
return
}

Expand All @@ -157,74 +217,68 @@ class RootCoveragePlugin : Plugin<Project> {
extension.libraryVariants.all { variant ->
if (variant.buildType.isTestCoverageEnabled && variant.name.capitalize() == buildVariant.capitalize()) {
if (subProject.plugins.withType(JacocoPlugin::class.java).isEmpty()) {
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}")
subProject.logger.info(
"Jacoco plugin was not found for project: '${subProject.name}', it" +
" has been applied automatically but you should do this manually. Build file:" +
" ${subProject.buildFile}"
)
subProject.plugins.apply(JacocoPlugin::class.java)
}
val subTask = createTask(subProject, variant)
addSubTaskDependencyToRootTask(task, subTask)
addSubProjectVariant(subProject, variant)
}
}
}
is AppExtension -> {
extension.applicationVariants.all { variant ->
if (variant.buildType.isTestCoverageEnabled && variant.name.capitalize() == buildVariant.capitalize()) {
if (subProject.plugins.withType(JacocoPlugin::class.java).isEmpty()) {
subProject.logger.warn("Jacoco plugin not applied to project: '${subProject.name}'! RootCoveragePlugin automatically applied it but you should do this manually: ${subProject.buildFile}")
subProject.logger.info(
"Jacoco plugin was not found for project: '${subProject.name}', it" +
" has been applied automatically but you should do this manually. Build file:" +
" ${subProject.buildFile}"
)
subProject.plugins.apply(JacocoPlugin::class.java)
}
val subTask = createTask(subProject, variant)
addSubTaskDependencyToRootTask(task, subTask)
addSubProjectVariant(subProject, variant)
}
}
}
}
}

private fun createTask(project: Project, variant: BaseVariant): RootCoverageModuleTask {
private fun JacocoReport.addSubProjectVariant(subProject: Project, variant: BaseVariant) {
val name = variant.name.capitalize()

val codeCoverageReportTask = project.tasks.register("codeCoverageReport$name", RootCoverageModuleTask::class.java)
codeCoverageReportTask.configure { task ->
task.group = null // null makes sure the group does not show in the gradle-view in Android Studio/Intellij
task.description = "Generate unified Jacoco code codecoverage report"

if (rootProjectExtension.shouldExecuteUnitTests()) {
task.dependsOn("test${name}UnitTest")
}
if (rootProjectExtension.shouldExecuteAndroidTests()) {
task.dependsOn("connected${name}AndroidTest")
}
// Gets the relative path from this task to the subProject
val path = project.relativePath(subProject.path).removeSuffix(":")

// Collect the class files based on the Java Compiler output
val javaClassOutput = variant.javaCompileProvider.get().outputs
val javaClassTrees = javaClassOutput.files.map { file ->
project.fileTree(file, excludes = getFileFilterPatterns()).excludeNonClassFiles()
}

// TODO: No idea how to dynamically get the kotlin class files output folder, so for now this is hardcoded.
// TODO: For some reason the tmp/kotlin-classes folder does not use the variant.dirName property, for now we instead use the variant.name.
val kotlinClassFolder = "${project.buildDir}/tmp/kotlin-classes/${variant.name}"
project.logger.info("Kotlin class folder for variant '${variant.name}': $kotlinClassFolder")

val kotlinClassTree = project.fileTree(kotlinClassFolder, excludes = getFileFilterPatterns()).excludeNonClassFiles()

// getSourceFolders returns ConfigurableFileCollections, but we only need the base directory of each ConfigurableFileCollection.
val sourceFiles = variant.getSourceFolders(SourceKind.JAVA).map { file -> file.dir }
// Add dependencies to the test tasks of the subProject
if (rootProjectExtension.shouldExecuteUnitTests()) {
dependsOn("$path:test${name}UnitTest")
}
if (rootProjectExtension.shouldExecuteAndroidTests()) {
dependsOn("$path:connected${name}AndroidTest")
}

task.sourceDirectories = project.files(sourceFiles)
task.classDirectories = project.files(javaClassTrees, kotlinClassTree)
task.executionData = getExecutionDataFileTree(project)
// Collect the class files based on the Java Compiler output
val javaClassOutput = variant.javaCompileProvider.get().outputs
val javaClassTrees = javaClassOutput.files.map { file ->
subProject.fileTree(file, excludes = getFileFilterPatterns()).excludeNonClassFiles()
}
return codeCoverageReportTask.get()
}

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

val kotlinClassTree =
subProject.fileTree(kotlinClassFolder, excludes = getFileFilterPatterns()).excludeNonClassFiles()

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

rootTask.classDirectories.from(subModuleTask.classDirectories)
rootTask.sourceDirectories.from(subModuleTask.sourceDirectories)
rootTask.executionData.from(subModuleTask.executionData)
sourceDirectories.from(subProject.files(sourceFiles))
classDirectories.from(subProject.files(javaClassTrees, kotlinClassTree))
executionData.from(getExecutionDataFileTree(subProject))
}
}
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
package org.neotech.plugin.rootcoverage

import junit.framework.Assert.assertEquals
import org.apache.commons.csv.CSVFormat
import org.apache.commons.csv.CSVParser
import org.apache.commons.csv.CSVRecord
import java.io.File
import java.nio.charset.StandardCharsets
import kotlin.test.assertEquals

class CoverageReport private constructor(
private val instructionMissedColumn: Int,
private val branchMissedColumn: Int,
private val packageColumn: Int,
private val classColumn: Int,
private val records: List<CSVRecord>) {

private fun CSVRecord?.assertFullCoverageCoverage() {
private fun CSVRecord?.assertCoverage(missedBranches: Int = 0, missedInstructions: Int = 0) {
kotlin.test.assertNotNull(this)
assertEquals(this[instructionMissedColumn]?.toInt(), 0)
assertEquals(this[branchMissedColumn]?.toInt(), 0)
assertEquals(missedInstructions, this[instructionMissedColumn]?.toInt())
assertEquals(missedBranches, this[branchMissedColumn]?.toInt())
}

fun assertFullCoverage(packageName: String, className: String) {
find(packageName, className).assertFullCoverageCoverage()
fun assertCoverage(packageName: String, className: String, missedBranches: Int = 0, missedInstructions: Int = 0) {
find(packageName, className).assertCoverage(missedBranches, missedInstructions)
}

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