blob: da91f3962837617243daee416eac71e1ffeb3caf [file] [log] [blame]
/**
* 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 org.apache.aurora.build
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.tasks.TaskAction
/**
* A task that analyzes the XML coverage report from JaCoCo and fails the build if code coverage
* metrics are not met.
*/
class CoverageReportCheck extends DefaultTask {
// The XML coverage report file.
def coverageReportFile
// Minimum ratio of instructions covered, [0, 1.0].
def minInstructionCoverage
// Minimum ratio of branches covered, [0, 1.0].
def minBranchCoverage
// Classes that may be allowed to have zero test coverage.
def legacyClassesWithoutCoverage
// The amount of wiggle room when requiring min coverage be raised.
def epsilon = 0.005
private def computeCoverage(counterNodes, type) {
def node = counterNodes.find { it.@type == type }
def missed = node.@missed.toInteger()
def covered = node.@covered.toInteger()
return ((double) covered) / (missed + covered)
}
def checkThresholds(coverage, minCoverage, type) {
if (coverage < minCoverage) {
return "$type coverage is $coverage, but must be greater than $minCoverage"
} else {
def floored = Math.floor(coverage * 100) / 100
if (floored > (minCoverage + epsilon)) {
println("$type coverage of $floored exceeds min instruction coverage of $minCoverage"
+ " by more than $epsilon, please raise the threshold!")
} else {
println("$type coverage of $coverage exceeds minimum coverage of $minCoverage.")
}
}
}
def checkGlobalCoverage(coverageCounts) {
def coverageErrors = [
[computeCoverage(coverageCounts, 'INSTRUCTION'), minInstructionCoverage, 'Instruction'],
[computeCoverage(coverageCounts, 'BRANCH'), minBranchCoverage, 'Branch']
].collect() {
return checkThresholds(*it)
}.findAll()
if (!coverageErrors.isEmpty()) {
// We print here and don't fail the build since this metric has proven to be flaky,
// and different JVMs can produce different results.
println(coverageErrors.join('\n'))
}
}
def checkClassCoverage(coverageReport) {
def coverageErrors = coverageReport.package.class.collect { cls ->
def matchedMethods = cls.method
// Ignore static code, it should not count as test coverage.
.findAll({ m -> m.@name != '<clinit>' })
// Ignore classes that only have a constructor. This will avoid tripping for things like
// constant-only utility classes, and 'value' classes like TypeLiteral and Clazz.
.findAll({ m -> m.@name != '<init>' })
// Ignore enums that contain only default methods.
if (matchedMethods.collect { m -> m.@name } == ['values', 'valueOf']) {
return null
}
if (matchedMethods.isEmpty()) {
if (cls.@name in legacyClassesWithoutCoverage) {
return 'Please remove ' + cls.@name + ' from the legacyClassesWithoutCoverage list' \
+ ', this check does not apply for constructor-only classes' \
+ ' or classes with only static class initialization code.'
} else {
return null
}
}
def covered = matchedMethods.collect { m ->
m.counter.find({ c -> c.@type == 'INSTRUCTION' }).@covered.toInteger()}.sum(0)
if (cls.@name in legacyClassesWithoutCoverage) {
if (covered != 0) {
return 'Thanks for adding the first test coverage to: ' + cls.@name \
+ ' please remove it from the legacyClassesWithoutCoverage list'
}
} else if (covered == 0) {
return 'Test coverage missing for ' + cls.@name
}
return null
}.findAll() // Filter nulls.
if (!coverageErrors.isEmpty()) {
throw new GradleException(coverageErrors.join('\n'))
}
}
@TaskAction
def analyze() {
def parser = new XmlSlurper()
parser.setFeature("http://apache.org/xml/features/disallow-doctype-decl", false);
// Avoid trying to load the DTD for the XML document, which does not exist.
parser.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false)
def coverageReport = parser.parse(coverageReportFile)
checkGlobalCoverage(coverageReport.counter)
checkClassCoverage(coverageReport)
}
}