![JAR search and dependency download from the Maven repository](/logo.png)
net.saliman.gradle.plugin.cobertura.CoberturaPlugin.groovy Maven / Gradle / Ivy
Show all versions of gradle-cobertura-plugin Show documentation
package net.saliman.gradle.plugin.cobertura
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.UnknownConfigurationException
import org.gradle.api.execution.TaskExecutionGraph
import org.gradle.api.initialization.Settings
import org.gradle.api.invocation.Gradle
import org.gradle.api.plugins.PluginAware
import org.gradle.api.plugins.ReportingBasePlugin
import org.gradle.api.tasks.testing.Test
/**
* Provides Cobertura coverage for Test tasks.
*
* This plugin will create 4 tasks.
*
* The first is the "cobertura" task that users may call to generate coverage
* reports. This task will run the all tasks named "test" from the directory
* where gradle was invoked as well as "test" tasks in any sub projects. In
* addition, it will run all tasks of type {@code Test} in the project applying
* the plugin. It is designed to do the same thing as "gradle test", but with
* all testing tasks running in the applying project and coverage reports
* generated at the end of the build. This task doesn't actually do any work,
* it is used by the plugin to determine user intent. This task will do the
* right thing for most users.
*
* The second task is called "coberturaReport". Users may call this task to
* tell Gradle that coverage reports should be generated after tests are run,
* but without causing any tests to actually run. This allows users to have
* more fine-grained control over the tasks that get run by requiring them to
* specify the desired tasks on the command line.
*
* The third task is the "instrument" task, that will instrument the source
* code. Users won't call it directly, but the plugin will make all test
* tasks depend on it so that instrumentation only happens once, and only if
* the task graph has the "cobertura" or "coberturaReport" task in the execution
* graph.
*
* The fourth task is the "copyCoberturaDatafile" task, which copies the input
* datafile, generated by the instrument task, to the output datafile, which is
* is modified by tests and used to generate the coverage report. This task
* must run whenever we want a coverage report to make sure the data in it is
* for the current run only.
*
* The fifth task is the "generateCoberturaReport" task, which does the actual
* work of generating the coverage reports if the "cobertura" or
* "coberturaReport" tasks are in the execution graph.
*
* This plugin also defines a "cobertura" extension with properties that are
* used to configure the operation of the plugin and its tasks.
*
* The plugin runs cobertura coverage reports for sourceSets.main. A project
* might have have multiple artifacts that use different parts of the code, and
* there may be different test tasks that test different parts of the source,
* but there is almost always only one main source set.
*
* Most of the magic of this plugin happens not at apply time, but when tasks
* are added and when the task graph is ready. If the graph contains the
* "cobertura" or "coberturaReport" task, it will make sure the instrument and
* generateCoberturaReport" tasks are configured to do actual work, and that
* task dependencies are what they need to be for the users to get what they
* want.
*/
class CoberturaPlugin implements Plugin {
// Constants for the tasks created by this plugin that don't have their own
// classes.
static final String COBERTURA_TASK_NAME = 'cobertura'
static final String COBERTURA_REPORT_TASK_NAME = 'coberturaReport'
static final String COBERTURA_CHECK_TASK_NAME = 'coberturaCheck'
def void apply(PluginAware pluginAware) {
if ( pluginAware instanceof Project ) {
doApply(pluginAware)
} else if ( pluginAware instanceof Settings ) {
pluginAware.gradle.allprojects { p ->
p.plugins.apply(CoberturaPlugin)
}
} else if ( pluginAware instanceof Gradle ) {
pluginAware.allprojects { p ->
p.plugins.apply(CoberturaPlugin)
}
} else {
throw new IllegalArgumentException("${pluginAware.getClass()} is currently not supported as an apply target, please report if you need it")
}
}
def void doApply(Project project) {
if ( project.plugins.hasPlugin(CoberturaPlugin) ) {
project.logger.info("Project ${project.name} already has the cobertura plugin")
return
}
project.logger.info("Applying cobertura plugin to $project.name")
project.plugins.apply(ReportingBasePlugin)
def extension = project.extensions.create('cobertura', CoberturaExtension, project)
if (!project.configurations.asMap['cobertura']) {
project.configurations.create('cobertura')
project.afterEvaluate {
// When we're done evaluating, bring in the appropriate version of
// Cobertura. Exclude logging dependencies because it causes conflicts
// with classes Gradle has already loaded.
project.dependencies {
cobertura("net.sourceforge.cobertura:cobertura:${project.extensions.cobertura.coberturaVersion}") {
exclude group: 'log4j', module: 'log4j'
exclude group: 'org.slf4j', module: 'slf4j-api'
}
}
}
}
createTasks(project, extension)
// We need to wait until after the extension is fully configured to fix
// task dependencies.
project.afterEvaluate {
fixTaskDependencies(project, extension)
}
// This marks the slf4j dependency as 'provided' for android projects.
// The actual dependency will be satisfied by the gradle distribution.
// This will not conflict with any dependencies in compile/test configurations.
if (isAndroidProject(project)) {
try {
project.dependencies {
provided 'org.slf4j:slf4j-api:1.7.5'
}
} catch (Exception ignored) { }
}
registerTaskFixupListener(project)
}
/**
* Create the tasks that the Cobertura plugin will use. This method will
* create the following tasks:
*
* - instrument - This is an internal task that does the actual work of
* instrumenting the source code.
* - generateCoberturaReport - This is an internal task that does the
* actual work of generating the Cobertura reports.
* - copyCoberturaDatafile - This is an internal task that makes a copy of
* the instrumentation data file (the .ser file) for tests to use so that
* the original remains untouched. This is needed to make sure
* instrumentation only happens when source code changes.
* - coberturaReport - Users will use this task to tell the plugin that
* they want coverage reports generated after all the tests run. The
* {@code coberturaReport} task doesn't actually cause any tests to run;
* users will need to specify one or more test tasks on the command line
* as well.
* - cobertura
- Users will use this task to run tests and generate
* a coverage report. This task is meant to be a convenience task that is
* simpler than (but not quite the same as) {@code test coberturaReport}
*
*
* @param project the project being configured
*/
private void createTasks(Project project, CoberturaExtension extension) {
CoberturaRunner runner = new CoberturaRunner()
// Create the coberturaReport task. This task, when invoked, indicates
// that users want a coverage report.
project.tasks.create(name: COBERTURA_REPORT_TASK_NAME,
group: 'Cobertura', type: DefaultTask)
Task reportTask = project.tasks.getByName(COBERTURA_REPORT_TASK_NAME)
reportTask.setDescription("Generate Cobertura reports after tests finish.")
// Create the cobertura task. This task, when invoked, indicates that
// users want a coverage report, and they want all tests to run.
project.tasks.create(name: COBERTURA_TASK_NAME,
group: 'Cobertura', type: DefaultTask)
Task coberturaTask = project.tasks.getByName(COBERTURA_TASK_NAME)
coberturaTask.setDescription("Run tests and generate Cobertura coverage reports.")
// If we make cobertura depend on reportTask, it is easier later on to
// determine of we need reports or not.
coberturaTask.dependsOn reportTask
// Create the checkCoverage task. This task, when invoked, indicates that
// users want to check coverage levels, but it does not run any tests..
project.tasks.create(name: COBERTURA_CHECK_TASK_NAME,
group: 'Cobertura', type: DefaultTask)
Task checkCoverageTask = project.tasks.getByName(COBERTURA_CHECK_TASK_NAME)
checkCoverageTask.setDescription("Check test coverage.")
// It doesn't make much sense to check coverage without generating a
// report
checkCoverageTask.dependsOn reportTask
// Create the instrument task that will instrument code. At the moment, it
// is disabled.
project.tasks.create(name: InstrumentTask.NAME,
type: InstrumentTask,
{
configuration = project.extensions.cobertura
classpath = project.configurations.cobertura
})
Task instrumentTask = project.tasks.getByName(InstrumentTask.NAME)
instrumentTask.setDescription("Instrument code for Cobertura coverage reports")
instrumentTask.enabled = false
instrumentTask.runner = runner
// Create the copyCoberturaDatafile task that will copy the .ser file. It
// is also disabled to start.
project.tasks.create(name: CopyDatafileTask.NAME,
type: CopyDatafileTask,
{ configuration = project.extensions.cobertura })
Task copyDatafileTask = project.tasks.getByName(CopyDatafileTask.NAME)
copyDatafileTask.setDescription("Helper task that makes a fresh copy of the Cobertura data file for tests")
copyDatafileTask.enabled = false
copyDatafileTask.dependsOn instrumentTask
// Create the generateCoberturaReport task that will generate the reports.
// Like the others, it starts out disabled.
project.tasks.create(name: GenerateReportTask.NAME,
type: GenerateReportTask,
{
configuration = project.extensions.cobertura
classpath = project.configurations.cobertura
})
Task generateReportTask = project.tasks.getByName(GenerateReportTask.NAME)
generateReportTask.setDescription("Generate a Cobertura report after tests finish.")
generateReportTask.enabled = false
generateReportTask.runner = runner
generateReportTask.reports.all { report ->
report.conventionMapping.with {
enabled = true
destination = {
new File(extension.coverageReportDir, "index.html")
}
}
}
// Create the performCoverageCheck task that will do the work of checking
// the coverage levels, and you guessed it, it is disabled.
project.tasks.create(name: PerformCoverageCheckTask.NAME,
type: PerformCoverageCheckTask,
{
configuration = project.extensions.cobertura
classpath = project.configurations.cobertura
})
Task performCoverageCheckTask = project.tasks.getByName(PerformCoverageCheckTask.NAME)
performCoverageCheckTask.setDescription("Perform a Cobertura test coverage check.")
performCoverageCheckTask.enabled = false
performCoverageCheckTask.runner = runner
performCoverageCheckTask.dependsOn generateReportTask
}
/**
* We need to make several changes to the tasks in the task graph for the
* cobertura plugin to work correctly. The changes need to be made to all
* tasks currently in the project, as well as any new tasks that get added
* later.
* @param project the project applying the plugin. Used for logging.
* @param extension the CoberturaExtension which has the various options for
* the plugin, but also the closures that return classes and test
* tasks so we can set up dependencies properly.
*/
private void fixTaskDependencies(Project project, CoberturaExtension extension) {
Task instrumentTask = project.tasks.getByName(InstrumentTask.NAME)
Task coberturaTask = project.tasks.getByName(COBERTURA_TASK_NAME)
Task performCoverageCheckTask = project.tasks.getByName(PerformCoverageCheckTask.NAME)
Task copyDatafileTask = project.tasks.getByName(CopyDatafileTask.NAME)
// Add a whenTaskAdded listener for all projects from the base down.
extension.coverageClassesTasks.all { task ->
project.logger.info("Making :${project.name}:instrument depend on :${task.project.name}:${task.name}")
instrumentTask.dependsOn task
}
// Tests need to depend on copying the data file and be finalized by the
// perform coverage check task (which in turn depends on generate reports).
// We'll figure out later what is enabled or not.
extension.coverageTestTasks.all { task ->
project.logger.info("Making :${project.name}:cobertura depend on :${task.project.name}:${task.name}")
task.dependsOn copyDatafileTask
task.finalizedBy performCoverageCheckTask
coberturaTask.dependsOn task
}
}
/**
* Register a listener with Gradle. When gradle is ready to run tasks, it
* will call our listener. If the coberturaReport task is in our graph, the
* listener will fix the classpaths of all the test tasks that we are actually
* running.
*
* @param project the project applying the plugin.
*/
private void registerTaskFixupListener(Project project) {
// If the user wants cobertura reports, fix test classpaths, and set them
// to generate reports on failure. If not, disable the 3 tasks that
// instrument code and run reports.
// "whenReady()" is a global event, so closure should be registered exactly
// once for single and multi-project builds.
Gradle gradle = project.gradle
CoberturaExtension extension = project.extensions.findByType(CoberturaExtension)
gradle.taskGraph.whenReady { TaskExecutionGraph graph ->
// See if the user wants a coberturaReport. If so, fix the classpath of
// any test task we are actually running. (we don't need to look for the
// cobertura task because it depends on coberturaReport).
if (graph.hasTask(project.tasks.findByName(COBERTURA_REPORT_TASK_NAME))) {
project.tasks.withType(Test).all { Test test ->
// Only apply classpath changes on Android to the variant we are covering.
if (isAndroidProject(project) && test.name != "test${extension.androidVariant.capitalize()}UnitTest") {
return
}
try {
Configuration config = test.project.configurations['cobertura']
test.systemProperties.put('net.sourceforge.cobertura.datafile', test.project.extensions.cobertura.coverageOutputDatafile)
test.classpath += config
test.outputs.upToDateWhen { false }
test.classpath = project.files("${project.buildDir}/instrumented_classes") + test.classpath
} catch (UnknownConfigurationException e) {
// Eat this. It just means we have a multi-project build, and
// there is test in a project that doesn't have cobertura applied.
}
}
// If we are not doing a dry-run We also need to enable
// instrumentation, file copying and report generation, but not
// coverage checking
if ( !project.gradle.startParameter.dryRun ) {
project.tasks.withType(InstrumentTask).all {
it.enabled = true
}
project.tasks.withType(CopyDatafileTask).each {
it.enabled = true
}
project.tasks.withType(GenerateReportTask).each {
it.enabled = true
}
}
}
// If the user wants to check coverage levels, and we're not doing a
// dry-run we need to enable all 4 of the tasks that do actual work.
if (graph.hasTask(project.tasks.findByName(COBERTURA_CHECK_TASK_NAME))) {
if ( !project.gradle.startParameter.dryRun ) {
project.tasks.withType(InstrumentTask).all {
it.enabled = true
}
project.tasks.withType(CopyDatafileTask).each {
it.enabled = true
}
project.tasks.withType(GenerateReportTask).each {
it.enabled = true
}
project.tasks.withType(PerformCoverageCheckTask).each {
it.enabled = true
}
}
}
}
}
static boolean isAndroidProject(Project project) {
return project.plugins.hasPlugin("com.android.application") || project.plugins.hasPlugin("com.android.library")
}
}