All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.bmuschko.gradle.clover.CloverPlugin.groovy Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2011 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.bmuschko.gradle.clover

import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.file.FileCollection
import org.gradle.api.internal.AsmBackedClassGenerator
import org.gradle.api.plugins.GroovyPlugin
import org.gradle.api.plugins.JavaPlugin
import org.gradle.api.tasks.bundling.Jar
import org.gradle.api.tasks.testing.Test

import java.lang.reflect.Constructor
import java.util.concurrent.Callable

/**
 * 

A {@link org.gradle.api.Plugin} that provides a task for creating a code coverage report using Clover.

* * @author Benjamin Muschko */ @Slf4j class CloverPlugin implements Plugin { static final String CONFIGURATION_NAME = 'clover' static final String GENERATE_REPORT_TASK_NAME = 'cloverGenerateReport' static final String AGGREGATE_REPORTS_TASK_NAME = 'cloverAggregateReports' static final String AGGREGATE_DATABASES_TASK_NAME = 'cloverAggregateDatabases' static final String REPORT_GROUP = 'report' static final String CLOVER_GROUP = 'clover' static final String DEFAULT_JAVA_INCLUDES = '**/*.java' static final String DEFAULT_GROOVY_INCLUDES = '**/*.groovy' static final String DEFAULT_JAVA_TEST_INCLUDES = '**/*Test.java' static final String DEFAULT_GROOVY_TEST_INCLUDES = '**/*Test.groovy' static final String DEFAULT_SPOCK_TEST_INCLUDES = '**/*Spec.groovy' static final String DEFAULT_CLOVER_DATABASE = '.clover/clover.db' static final String DEFAULT_CLOVER_SNAPSHOT = '.clover/coverage.db.snapshot' static final String DEFAULT_CLOVER_HISTORY_DIR = '.clover/historypoints' @CompileStatic @Override void apply(Project project) { project.configurations.create(CONFIGURATION_NAME).setVisible(false).setTransitive(true) .setDescription('The Clover library to be used for this project.') CloverPluginConvention cloverPluginConvention = new CloverPluginConvention() project.convention.plugins.clover = cloverPluginConvention AggregateDatabasesTask aggregateDatabasesTask = configureAggregateDatabasesTask(project, cloverPluginConvention) configureActions(project, cloverPluginConvention, aggregateDatabasesTask) configureGenerateCoverageReportTask(project, cloverPluginConvention, aggregateDatabasesTask) configureAggregateReportsTask(project, cloverPluginConvention) } private AggregateDatabasesTask configureAggregateDatabasesTask(Project project, CloverPluginConvention cloverPluginConvention) { project.tasks.withType(AggregateDatabasesTask) { conventionMapping.with { map('initString') { getInitString(cloverPluginConvention) } map('cloverClasspath') { project.configurations.getByName(CONFIGURATION_NAME).asFileTree } } } AggregateDatabasesTask aggregateDatabasesTask = project.tasks.create(AGGREGATE_DATABASES_TASK_NAME, AggregateDatabasesTask) aggregateDatabasesTask.description = 'Aggregates Clover code coverage databases for the project.' aggregateDatabasesTask.group = CLOVER_GROUP aggregateDatabasesTask } @CompileStatic private void configureActions(Project project, CloverPluginConvention cloverPluginConvention, AggregateDatabasesTask aggregateDatabasesTask) { SourceSetsResolver resolver = new SourceSetsResolver(project, cloverPluginConvention) project.tasks.withType(Test) { Test test -> // If it is too late for afterEvaluate configure now if (project.state.executed) { configureActionsForTask(test, project, cloverPluginConvention, resolver, aggregateDatabasesTask) } else { project.afterEvaluate { configureActionsForTask(test, project, cloverPluginConvention, resolver, aggregateDatabasesTask) } } // If we are generating instrumented JAR files make sure the Jar // tasks run after the Test tasks so that the instrumented classes // get packaged in the archives. if (project.hasProperty('cloverInstrumentedJar')) { project.tasks.withType(Jar) { Jar jar -> jar.mustRunAfter test } } } } @CompileStatic private boolean testTaskEnabled(Test test, CloverPluginConvention cloverPluginConvention) { !((cloverPluginConvention.includeTasks && !(test.name in cloverPluginConvention.includeTasks)) || test.name in cloverPluginConvention.excludeTasks) } @CompileStatic private void configureActionsForTask(Test test, Project project, CloverPluginConvention cloverPluginConvention, SourceSetsResolver resolver, AggregateDatabasesTask aggregateDatabasesTask) { if (testTaskEnabled(test, cloverPluginConvention)) { test.classpath += project.configurations.getByName(CONFIGURATION_NAME).asFileTree OptimizeTestSetAction optimizeTestSetAction = createOptimizeTestSetAction(cloverPluginConvention, project, resolver, test) test.doFirst optimizeTestSetAction // add first, gets executed second test.doFirst createInstrumentCodeAction(cloverPluginConvention, project, resolver, test) // add second, gets executed first test.include optimizeTestSetAction // action is also a file inclusion spec test.doLast createCreateSnapshotAction(cloverPluginConvention, project, test) if (project.hasProperty('cloverInstrumentedJar')) { log.info "Skipping RestoreOriginalClassesAction for {} to generate instrumented JAR", test } else { test.doLast createRestoreOriginalClassesAction(resolver, test) } aggregateDatabasesTask.aggregate(test) } } private RestoreOriginalClassesAction createRestoreOriginalClassesAction(SourceSetsResolver resolver, Test testTask) { RestoreOriginalClassesAction restoreOriginalClassesAction = createInstance(RestoreOriginalClassesAction) restoreOriginalClassesAction.conventionMapping.with { map('sourceSets') { resolver.getSourceSets() } map('testSourceSets') { resolver.getTestSourceSets() } } restoreOriginalClassesAction } private CreateSnapshotAction createCreateSnapshotAction(CloverPluginConvention cloverPluginConvention, Project project, Test testTask) { CreateSnapshotAction createSnapshotAction = createInstance(CreateSnapshotAction) createSnapshotAction.conventionMapping.with { map('initString') { getInitString(cloverPluginConvention, testTask) } map('optimizeTests') { cloverPluginConvention.optimizeTests } map('snapshotFile') { getSnapshotFile(project, cloverPluginConvention, true, testTask) } map('cloverClasspath') { project.configurations.getByName(CONFIGURATION_NAME).asFileTree } map('buildDir') { project.buildDir } } createSnapshotAction } private OptimizeTestSetAction createOptimizeTestSetAction(CloverPluginConvention cloverPluginConvention, Project project, SourceSetsResolver resolver, Test testTask) { OptimizeTestSetAction optimizeTestSetAction = createInstance(OptimizeTestSetAction) optimizeTestSetAction.conventionMapping.with { map('initString') { getInitString(cloverPluginConvention, testTask) } map('optimizeTests') { cloverPluginConvention.optimizeTests } map('snapshotFile') { getSnapshotFile(project, cloverPluginConvention, false, testTask) } map('cloverClasspath') { project.configurations.getByName(CONFIGURATION_NAME).asFileTree } map('testSourceSets') { resolver.getTestSourceSets() } map('buildDir') { project.buildDir } } optimizeTestSetAction } private InstrumentCodeAction createInstrumentCodeAction(CloverPluginConvention cloverPluginConvention, Project project, SourceSetsResolver resolver, Test testTask) { InstrumentCodeAction instrumentCodeAction = createInstance(InstrumentCodeAction) instrumentCodeAction.conventionMapping.with { map('initString') { getInitString(cloverPluginConvention, testTask) } map('enabled') { cloverPluginConvention.enabled } map('compileGroovy') { hasGroovyPlugin(project) } map('cloverClasspath') { project.configurations.getByName(CONFIGURATION_NAME).asFileTree } map('testRuntimeClasspath') { getTestRuntimeClasspath(project, testTask).asFileTree } map('groovyClasspath') { getGroovyClasspath(project) } map('buildDir') { project.buildDir } map('sourceSets') { resolver.getSourceSets() } map('testSourceSets') { resolver.getTestSourceSets() } map('sourceCompatibility') { project.sourceCompatibility?.toString() } map('targetCompatibility') { project.targetCompatibility?.toString() } map('includes') { getIncludes(project, cloverPluginConvention) } map('excludes') { cloverPluginConvention.excludes } map('testIncludes') { getTestIncludes(project, cloverPluginConvention) } map('testExcludes') { getTestExcludes(project, cloverPluginConvention) } map('statementContexts') { cloverPluginConvention.contexts.statements } map('methodContexts') { cloverPluginConvention.contexts.methods } map('executable') { cloverPluginConvention.compiler.executable?.absolutePath } map('encoding') { cloverPluginConvention.compiler.encoding } map('instrumentLambda') { cloverPluginConvention.instrumentLambda } map('debug') { cloverPluginConvention.compiler.debug } map('flushinterval') { cloverPluginConvention.flushinterval } map('flushpolicy') { cloverPluginConvention.flushpolicy.name() } map('additionalArgs') { cloverPluginConvention.compiler.additionalArgs } } instrumentCodeAction } private void configureGenerateCoverageReportTask(Project project, CloverPluginConvention cloverPluginConvention, AggregateDatabasesTask aggregateDatabasesTask) { project.tasks.withType(GenerateCoverageReportTask) { GenerateCoverageReportTask generateCoverageReportTask -> dependsOn aggregateDatabasesTask conventionMapping.with { map('initString') { getInitString(cloverPluginConvention) } map('cloverClasspath') { project.configurations.getByName(CONFIGURATION_NAME).asFileTree } map('targetPercentage') { cloverPluginConvention.targetPercentage } map('filter') { cloverPluginConvention.report.filter } map('testResultsDir') { cloverPluginConvention.report.testResultsDir } map('testResultsInclude') { cloverPluginConvention.report.testResultsInclude } } setCloverReportConventionMappings(project, cloverPluginConvention, generateCoverageReportTask) } GenerateCoverageReportTask generateCoverageReportTask = project.tasks.create(GENERATE_REPORT_TASK_NAME, GenerateCoverageReportTask) generateCoverageReportTask.description = 'Generates Clover code coverage report.' generateCoverageReportTask.group = REPORT_GROUP } private void configureAggregateReportsTask(Project project, CloverPluginConvention cloverPluginConvention) { project.tasks.withType(AggregateReportsTask) { AggregateReportsTask aggregateReportsTask -> conventionMapping.with { map('initString') { getInitString(cloverPluginConvention) } map('cloverClasspath') { project.configurations.getByName(CONFIGURATION_NAME).asFileTree } map('subprojectBuildDirs') { project.subprojects.collect { it.buildDir } } map('filter') { cloverPluginConvention.report.filter } map('testResultsDir') { cloverPluginConvention.report.testResultsDir } map('testResultsInclude') { cloverPluginConvention.report.testResultsInclude } } setCloverReportConventionMappings(project, cloverPluginConvention, aggregateReportsTask) } // Only add task to root project if(project == project.rootProject && project.subprojects.size() > 0) { AggregateReportsTask aggregateReportsTask = project.rootProject.tasks.create(AGGREGATE_REPORTS_TASK_NAME, AggregateReportsTask) aggregateReportsTask.description = 'Aggregates Clover code coverage reports.' aggregateReportsTask.group = REPORT_GROUP project.allprojects*.tasks*.withType(GenerateCoverageReportTask) { aggregateReportsTask.dependsOn it } } } /** * Sets Clover report convention mappings. * * @param project Project * @param cloverPluginConvention Clover plugin convention * @param task Task */ private void setCloverReportConventionMappings(Project project, CloverPluginConvention cloverPluginConvention, Task task) { task.conventionMapping.with { map('reportsDir') { new File(project.buildDir, 'reports') } map('xml') { cloverPluginConvention.report.xml } map('json') { cloverPluginConvention.report.json } map('html') { cloverPluginConvention.report.html } map('pdf') { cloverPluginConvention.report.pdf } map('additionalColumns') { cloverPluginConvention.report.columns.getColumns() } cloverPluginConvention.report.historical.with { map('historical') { enabled } map('historyDir') { getHistoryDir(project, cloverPluginConvention) } map('historyIncludes') { historyIncludes } map('packageFilter') { packageFilter } map('from') { from } map('to') { to } map('added') { added } map('movers') { movers } } } } /** * Creates an instance of the specified class, using an ASM-backed class generator. * * @param clazz the type of object to create * @return an instance of the specified type */ @CompileStatic private createInstance(Class clazz) { AsmBackedClassGenerator generator = new AsmBackedClassGenerator() Class instrumentClass = generator.generate(clazz) Constructor constructor = instrumentClass.getConstructor() return constructor.newInstance() } /** * Gets init String that determines location of Clover database. * * @param cloverPluginConvention Clover plugin convention * @return Init String */ @CompileStatic private String getInitString(CloverPluginConvention cloverPluginConvention) { cloverPluginConvention.initString ?: DEFAULT_CLOVER_DATABASE } @CompileStatic private String getInitString(CloverPluginConvention cloverPluginConvention, Test testTask) { "${getInitString(cloverPluginConvention)}-${testTask.name}" } /** * Gets the Clover snapshot file location. * * @param project Project * @param cloverPluginConvention Clover plugin convention * @param force if true, return the snapshot file even if it doesn't exist; if false, don't return the snapshot file if it doesn't exist * @return the Clover snapshot file location */ @CompileStatic private File getSnapshotFile(Project project, CloverPluginConvention cloverPluginConvention, boolean force, Test testTask) { File file = cloverPluginConvention.snapshotFile != null && cloverPluginConvention.snapshotFile != '' ? project.file("${cloverPluginConvention.snapshotFile}-${testTask.name}") : project.file("${DEFAULT_CLOVER_SNAPSHOT}-${testTask.name}") return file.exists() || force ? file : null } /** * Gets the Clover history directory location. * * @param project Project * @param cloverPluginConvention Clover plugin convention * @return the Clover history directory location */ @CompileStatic private File getHistoryDir(Project project, CloverPluginConvention cloverPluginConvention) { File file = cloverPluginConvention.historyDir != null && cloverPluginConvention.historyDir != '' ? project.file(cloverPluginConvention.historyDir) : project.file(DEFAULT_CLOVER_HISTORY_DIR) return file } /** * This construct avoids multiple evaluations of the source sets collections. * Without this each convention call to get the source sets will repeat the * computation giving back the same results. */ @CompileStatic private class SourceSetsResolver { private final Project project private final CloverPluginConvention cloverPluginConvention private final boolean gradleLessThan4 SourceSetsResolver(Project project, CloverPluginConvention cloverPluginConvention) { this.project = project this.cloverPluginConvention = cloverPluginConvention gradleLessThan4 = project.gradle.gradleVersion.split('\\.')[0].toInteger() < 4 } List sourceSets = null @CompileDynamic synchronized List getSourceSets() { if (sourceSets != null) { return sourceSets } sourceSets = new ArrayList() Callable classpathCallable = new Callable() { FileCollection call() { project.sourceSets.main.getCompileClasspath() + project.configurations.getByName(CONFIGURATION_NAME) } } if (hasJavaPlugin(project)) { CloverSourceSet cloverSourceSet = new CloverSourceSet(false) cloverSourceSet.with { srcDirs.addAll(filterNonExistentDirectories(project.sourceSets.main.java.srcDirs)) if (gradleLessThan4) { classesDir = project.sourceSets.main.output.classesDir } else { classesDir = project.sourceSets.main.java.outputDir } classpathProvider = classpathCallable } sourceSets << cloverSourceSet } if (hasGroovyPlugin(project)) { CloverSourceSet cloverSourceSet = new CloverSourceSet(true) cloverSourceSet.with { srcDirs.addAll(filterNonExistentDirectories(project.sourceSets.main.groovy.srcDirs)) if (gradleLessThan4) { classesDir = project.sourceSets.main.output.classesDir } else { classesDir = project.sourceSets.main.groovy.outputDir } classpathProvider = classpathCallable } sourceSets << cloverSourceSet } if (cloverPluginConvention.additionalSourceSets) { cloverPluginConvention.additionalSourceSets.each { additionalSourceSet -> additionalSourceSet.groovy = hasGroovySource(project, additionalSourceSet.srcDirs) additionalSourceSet.classpathProvider = classpathCallable sourceSets << additionalSourceSet } } sourceSets } List testSourceSets = null @CompileDynamic synchronized List getTestSourceSets() { if (testSourceSets != null) { return testSourceSets } testSourceSets = new ArrayList() Callable classpathCallable = new Callable() { FileCollection call() { project.sourceSets.test.getCompileClasspath() + project.configurations.getByName(CONFIGURATION_NAME) } } if (hasJavaPlugin(project)) { CloverSourceSet cloverSourceSet = new CloverSourceSet(false) cloverSourceSet.with { srcDirs.addAll(filterNonExistentDirectories(project.sourceSets.test.java.srcDirs)) if (gradleLessThan4) { classesDir = project.sourceSets.test.output.classesDir } else { classesDir = project.sourceSets.test.java.outputDir } classpathProvider = classpathCallable } testSourceSets << cloverSourceSet } if (hasGroovyPlugin(project)) { CloverSourceSet cloverSourceSet = new CloverSourceSet(true) cloverSourceSet.with { srcDirs.addAll(filterNonExistentDirectories(project.sourceSets.test.groovy.srcDirs)) if (gradleLessThan4) { classesDir = project.sourceSets.test.output.classesDir } else { classesDir = project.sourceSets.test.groovy.outputDir } classpathProvider = classpathCallable } testSourceSets << cloverSourceSet } if (cloverPluginConvention.additionalTestSourceSets) { cloverPluginConvention.additionalTestSourceSets.each { additionalTestSourceSet -> additionalTestSourceSet.groovy = hasGroovySource(project, additionalTestSourceSet.srcDirs) additionalTestSourceSet.classpathProvider = classpathCallable testSourceSets << additionalTestSourceSet } } testSourceSets } @CompileStatic private boolean hasGroovySource(Project project, Collection dirs) { for (File dir : dirs) { if (!project.fileTree(dir: dir, includes: ['**/*.groovy']).getFiles().isEmpty()) { return true } } return false } @CompileStatic private Set filterNonExistentDirectories(Set dirs) { dirs.findAll { it.exists() } } } /** * Gets includes for compilation. Uses includes if set as convention property. Otherwise, use default includes. The * default includes are determined by the fact if Groovy plugin was applied to project or not. * * @param project Project * @param cloverPluginConvention Clover plugin convention * @return Includes */ @CompileStatic private List getIncludes(Project project, CloverPluginConvention cloverPluginConvention) { if(cloverPluginConvention.includes) { return cloverPluginConvention.includes } if(hasGroovyPlugin(project)) { return [DEFAULT_JAVA_INCLUDES, DEFAULT_GROOVY_INCLUDES] } [DEFAULT_JAVA_INCLUDES] } /** * Gets test includes for compilation. Uses includes if set as convention property. Otherwise, use default includes. The * default includes are determined by the fact if Groovy plugin was applied to project or not. * * @param project Project * @param cloverPluginConvention Clover plugin convention * @return Test includes */ @CompileStatic private List getTestIncludes(Project project, CloverPluginConvention cloverPluginConvention) { if(cloverPluginConvention.testIncludes) { return cloverPluginConvention.testIncludes } if(hasGroovyPlugin(project)) { return [DEFAULT_JAVA_TEST_INCLUDES, DEFAULT_GROOVY_TEST_INCLUDES, DEFAULT_SPOCK_TEST_INCLUDES] } [DEFAULT_JAVA_TEST_INCLUDES] } /** * Gets test patterns excluded from instrumentation. The default is empty list - no excludes. * * @param project Project * @param cloverPluginConvention Clover plugin convention * @return Test excludes */ @CompileStatic private List getTestExcludes(Project project, CloverPluginConvention cloverPluginConvention) { if (cloverPluginConvention.testExcludes) { return cloverPluginConvention.testExcludes } [] } /** * Checks to see if Java plugin got applied to project. * * @param project Project * @return Flag */ @CompileStatic private boolean hasJavaPlugin(Project project) { project.plugins.hasPlugin(JavaPlugin) } /** * Checks to see if Groovy or Grails plugins got applied to project. * * @param project Project * @return Flag */ @CompileStatic private boolean hasGroovyPlugin(Project project) { project.plugins.hasPlugin(GroovyPlugin) || project.plugins.hasPlugin('org.grails.grails-core') || project.plugins.hasPlugin('org.grails.grails-plugin') || project.plugins.hasPlugin('org.grails.grails-web') } @CompileStatic private FileCollection getTestRuntimeClasspath(Project project, Test testTask) { testTask.classpath.filter { File file -> !file.directory } + project.configurations.getByName(CONFIGURATION_NAME) } private FileCollection getGroovyClasspath(Project project) { // We use the test sourceSet to derive the GroovyCompile built-in task name // and from there extract the correct GroovyClasspath. This is more closely // matched to the built-in Groovy compiler and still supports a build dependency. def taskName = project.sourceSets.test.getCompileTaskName('groovy') def task = project.tasks.findByName(taskName) if (task == null) { // Fall back to main source set to get this. We should have this // or the test source set using Groovy if this method is called. taskName = project.sourceSets.main.getCompileTaskName('groovy') task = project.tasks.getByName(taskName) } task.getGroovyClasspath() + project.configurations.getByName(CONFIGURATION_NAME) } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy