Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,6 @@ ehthumbs.db
ehthumbs_vista.db

# Folder config file
[Dd]esktop.ini
[Dd]esktop.ini

out/
4 changes: 4 additions & 0 deletions gradle/versions.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,8 @@ ext {
mockitoLibraryVersion = "1.2"
dexMakerLibraryVersion = "1.2"
//#### The ABOVE are TSL version variables - END

kotlinVersion = '1.5.30'
spotBugsGradlePluginVersion = '4.7.1'
jupiterApiVersion = '5.6.0'
}
17 changes: 13 additions & 4 deletions plugins/buildsystem/build.gradle
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
buildscript {
apply from: rootProject.file("gradle/versions.gradle")
}

plugins {
id 'java-gradle-plugin'
id 'org.jetbrains.kotlin.jvm' version '1.5.30'
id 'maven-publish'
id 'com.gradle.plugin-publish' version '0.14.0'
}

group 'com.microsoft.identity'
version '0.1.1'
version '0.2.0'


pluginBundle {
Expand All @@ -32,10 +37,14 @@ repositories {
}

dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0'
testImplementation "org.junit.jupiter:junit-jupiter-api:${rootProject.ext.jupiterApiVersion}"
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
implementation 'com.android.tools.build:gradle:4.1.0'
implementation 'gradle.plugin.com.github.spotbugs.snom:spotbugs-gradle-plugin:4.7.1'
implementation "com.android.tools.build:gradle:${rootProject.ext.gradleVersion}"
implementation "gradle.plugin.com.github.spotbugs.snom:spotbugs-gradle-plugin:${rootProject.ext.spotBugsGradlePluginVersion}"

implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${rootProject.ext.kotlinVersion}"
testImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${rootProject.ext.kotlinVersion}"
testImplementation "org.jetbrains.kotlin:kotlin-test:${rootProject.ext.kotlinVersion}"
}

test {
Expand Down
63 changes: 63 additions & 0 deletions plugins/buildsystem/docs/CodeCoverage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Code Coverage Plugin


## Intro
- [Code coverage](https://en.wikipedia.org/wiki/Code_coverage) is a software metric used to measure how many lines of our code are executed during automated tests.

- [JaCoCo](https://www.eclemma.org/jacoco/trunk/index.html) is a free Java code coverage tool and the [JaCoCo plugin](https://docs.gradle.org/current/userguide/jacoco_plugin.html) provides code coverage metrics for Java code via integration with JaCoCo.

## Why
In order to generate [JaCoCo](https://www.jacoco.org/jacoco/trunk/doc/index.html) unit test coverage reports for Android projects you need to create `JacocoReport` tasks and configure them by providing paths to source code, execution data and compiled classes. It's not straightforward since Android projects can have different flavors and build types thus requiring additional paths to be set. This plugin configures these `JacocoReport` tasks automatically.

## Usage
```groovy
plugins {
...
id 'com.gradle.plugin-publish' version '0.14.0' // or whatever version is most recent
}

codeCoverageReport{
html.enabled = true // by default it's true
xml.enabled = true // by default it's true
csv.enabled = true // by default it's true

unitTests.enabled = true // whether code coverage tasks for unit tests will be generated
androidTests.enabled = true // whether code coverage tasks for instrumentation tests will be generated

excludeFlavors = [''] // the product flavors to exclude when generating the code coverage tasks

excludeClasses = [''] // additional classes to exclude - most are already catered for

destination = '/some/other/directory' // if you want to configure a custom path to save the code coverage reports, by default your report gets saved in `[project]/build/jacoco/{flavor}{build type}{project}{test type}CoverageReport`

includeNoLocationClasses = true // To include Robolectric tests in the Jacoco report this needs to be true
}

android {
buildTypes {
debug {
testCoverageEnabled true // this instructs the plugin to generate code coverage reports for this build type
...
}
release {
testCoverageEnabled false // this instructs the plugin to NOT generate code coverage reports for this build type
...
}
}
...
productFlavors {
local {}
dist {}
}
}
```

The above configuration creates a `JacocoReport` task for each variant in the form of `{flavor}{build type}{project}{test type}CoverageReport`
```
distDebugAppAndroidTestCoverageReport
distDebugAppUnitTestCoverageReport
localDebugAppAndroidTestCoverageReport
localDebugAppUnitTestCoverageReport
```

By default these are the excluded classes under [Constants](https://github.com/AzureAD/android-complete/blob/paul/code-coverage-plugin/plugins/buildsystem/src/main/java/com/microsoft/identity/buildsystem/codecov/Constants.kt#L25)
7 changes: 7 additions & 0 deletions plugins/buildsystem/gradle/versions.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Variables for plugins project
ext {
kotlinVersion = '1.5.30'
spotBugsGradlePluginVersion = '4.7.1'
jupiterApiVersion = '5.6.0'
gradleVersion = '4.1.0'
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
package com.microsoft.identity.buildsystem;

import com.android.build.gradle.LibraryExtension;
import com.microsoft.identity.buildsystem.codecov.CodeCoverage;
import org.gradle.api.JavaVersion;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
Expand Down Expand Up @@ -52,6 +53,8 @@ public void apply(final Project project) {
});

SpotBugs.applySpotBugsPlugin(project);

CodeCoverage.applyCodeCoveragePlugin(project);
}

private void applyDesugaringToAndroidProject(final Project project){
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// Copyright (c) Microsoft Corporation.
// All rights reserved.
//
// This code is licensed under the MIT License.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files(the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions :
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package com.microsoft.identity.buildsystem.codecov

import com.android.build.gradle.api.BaseVariant
import com.android.build.gradle.api.SourceKind
import org.gradle.api.Project
import org.gradle.api.tasks.testing.Test
import org.gradle.testing.jacoco.plugins.JacocoPlugin
import org.gradle.testing.jacoco.plugins.JacocoPluginExtension
import org.gradle.testing.jacoco.plugins.JacocoTaskExtension
import org.gradle.testing.jacoco.tasks.JacocoReport
import java.io.File

/**
* This class creates code coverage tasks in the given project
*/
object CodeCoverage {

private lateinit var reportExtension: CodeCoverageReportExtension

/**
* Gets the codeCoverageReport configurations and uses them to create the code coverage tasks
*/
@JvmStatic
fun applyCodeCoveragePlugin(project: Project) {
// get the configurations under codeCoverageReport
reportExtension = project.extensions.create("codeCoverageReport", CodeCoverageReportExtension::class.java)

// after build file has been evaluated ... add tasks
project.afterEvaluate { evaluatedProject ->
evaluatedProject.configure()

if (isAndroidProject(evaluatedProject)) {
addJacocoToAndroid(evaluatedProject)
} else if (isJavaProject(evaluatedProject) || isKotlinMultiplatform(evaluatedProject)) {
addJacocoToJava(evaluatedProject)
}
}
}

/**
* Apply some plugin configuration from [CodeCoverageReportExtension] to the project.
*/
private fun Project.configure() {
project.plugins.apply(JacocoPlugin::class.java)

project.extensions.findByType(JacocoPluginExtension::class.java)?.apply {
toolVersion = reportExtension.jacocoVersion
}

tasks.withType(Test::class.java) { testTask ->
testTask.extensions.findByType(JacocoTaskExtension::class.java)?.apply {
// To include Robolectric tests in the Jacoco report, flag -> "includeNolocationClasses" should be set to true
isIncludeNoLocationClasses = reportExtension.includeNoLocationClasses
if (isIncludeNoLocationClasses) {
// This needs to be excluded for JDK 11
// SEE: https://stackoverflow.com/questions/68065743/cannot-run-gradle-test-tasks-because-of-java-lang-noclassdeffounderror-jdk-inte
excludes = listOf("jdk.internal.*")
}
}
}
}

/**
* add jacoco tasks to an android project
*/
private fun addJacocoToAndroid(project: Project) {
project.android().jacoco.version = reportExtension.jacocoVersion

if (reportExtension.unitTests.enabled) {
createTasks(project, TestTypes.UnitTest)
}

if (reportExtension.androidTests.enabled) {
createTasks(project, TestTypes.AndroidTest)
}
}

/**
* Creates the code coverage tasks for the different build variants
*/
private fun createTasks(project: Project, testType: String) {
val excludeFlavors = (reportExtension.excludeFlavors ?: emptyList()).map { it.toLowerCase() }
project.android().variants().all { variant ->
if (shouldCreateTaskForVariant(excludeFlavors, variant, testType)) {
createReportTask(project, variant, testType)
}
}
}

/**
* Creates the code coverage task for the given build variant and adds it to a group (Reporting)
*/
private fun createReportTask(project: Project, variant: BaseVariant, testType: String): JacocoReport {
// get the sources
val sourceDirs = variant.getSourceFolders(SourceKind.JAVA).map { file -> file.dir }
// get the classes
val classesDir = variant.javaCompileProvider.get().destinationDir
// get the test task for this variant
val testTask = getAndroidTestTask(project.tasks, variant, testType)
// get JacocoTaskExtension execution destination
val executionData = getAndroidExecutionDataFile(testTask, variant, testType)

val taskName = "${variant.name}${project.name.capitalize()}${testType}CoverageReport"
return project.tasks.create(taskName, JacocoReport::class.java) { reportTask ->
// set the task attributes
reportTask.dependsOn(testTask)
reportTask.group = "Reporting"
reportTask.description = "Generates Jacoco coverage reports for the ${variant.name} variant."
reportTask.executionData.setFrom(project.filesTree(project.buildDir, includes = setOf(executionData)))
reportTask.sourceDirectories.setFrom(project.files(sourceDirs))

// get the java project tree and exclude the defined excluded classes
val javaTree = project.filesTree(classesDir, excludes = reportExtension.getFileFilterPatterns)

// if kotlin is available, get the kotlin project tree and exclude the defined excluded classes
if (hasKotlin(project.plugins)) {
val kotlinClassesDir = "${project.buildDir}/tmp/kotlin-classes/${variant.name}"
val kotlinTree = project.filesTree(kotlinClassesDir, excludes = reportExtension.getFileFilterPatterns)
reportTask.classDirectories.setFrom(javaTree + kotlinTree)
} else {
reportTask.classDirectories.setFrom(javaTree)
}

configureReport(project, reportTask, taskName)
}
}

private fun addJacocoToJava(project: Project) {
val testTask = project.tasks.getByName("test")
val jacocoTestReportTask = project.tasks.getByName("jacocoTestReport") as JacocoReport

val taskName = "${project.name.decapitalize()}UnitTestCoverageReport"
project.tasks.create(taskName, JacocoReport::class.java) { reportTask ->
// set the task attributes
reportTask.dependsOn(testTask)
reportTask.group = "Reporting"
reportTask.description = "Generates Jacoco coverage reports"
reportTask.executionData.setFrom(jacocoTestReportTask.executionData)
reportTask.sourceDirectories.setFrom(jacocoTestReportTask.sourceDirectories)
reportTask.additionalSourceDirs.setFrom(jacocoTestReportTask.additionalSourceDirs)
reportTask.classDirectories.setFrom(jacocoTestReportTask.classDirectories)

// set destination
configureReport(project, reportTask, taskName)
}
}

private fun configureReport(project: Project, reportTask: JacocoReport, taskName: String) {
reportTask.reports { task ->
// set the outputs enabled according to configs
task.html.isEnabled = reportExtension.html.enabled
task.xml.isEnabled = reportExtension.xml.enabled
task.csv.isEnabled = reportExtension.csv.enabled

// default reports path
val defaultCommonPath = "${project.buildDir}/reports/jacoco/$taskName"
val configuredDestination = reportExtension.destination

// configure destination for html code coverage output
if (reportExtension.html.enabled) {
val path = File(if (configuredDestination.isNullOrBlank()) "$defaultCommonPath/html" else "${configuredDestination.trim()}/html")
task.html.destination = path
}

// configure destination for xml code coverage output
if (reportExtension.xml.enabled) {
val path = File(if (configuredDestination.isNullOrBlank()) "$defaultCommonPath/${taskName}.xml" else "${configuredDestination.trim()}/${taskName}.xml")
task.xml.destination = path
}

// configure destination for csv code coverage output
if (reportExtension.csv.enabled) {
val path = File(if (configuredDestination.isNullOrBlank()) "$defaultCommonPath/${taskName}.csv" else "${configuredDestination.trim()}/${taskName}.csv")
task.csv.destination = path
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) Microsoft Corporation.
// All rights reserved.
//
// This code is licensed under the MIT License.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files(the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions :
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package com.microsoft.identity.buildsystem.codecov

/**
* This class controls some of the configurations used by the code coverage plugin.
*/
open class CodeCoverageReportExtension {

var html = ReportConfig(true) // whether code coverage html output is enabled
var xml = ReportConfig(true) // whether code coverage xml output is enabled
var csv = ReportConfig(true) // whether code coverage csv output is enabled

var unitTests = ReportConfig(true) // whether code coverage targets unit tests
var androidTests = ReportConfig(false) // whether code coverage targets android tests

var destination: String? = null // the destination of the reports - by default it's buildDir/reports/jacoco
var excludeFlavors: Set<String>? = null // add some product flavours to exclude
var excludeClasses: Set<String>? = null // add some classes to exclude

var includeNoLocationClasses: Boolean = true // To include Robolectric tests in the Jacoco report, flag -> "includeNolocationClasses" is set to true

var jacocoVersion: String = "0.8.7" // jacoco version

/**
* get files to exclude
*/
val getFileFilterPatterns: Set<String>
get() {
return DEFAULT_EXCLUDES + (excludeClasses ?: emptySet())
}

}

open class ReportConfig(var enabled: Boolean)
Loading