name.remal.gradle_plugins.toolkit.build_logic.jacoco.gradle Maven / Gradle / Ivy
import static java.nio.file.StandardOpenOption.READ
import static java.nio.file.StandardOpenOption.WRITE
import static org.gradle.api.reporting.Report.OutputType
import java.nio.channels.FileChannel
import java.nio.channels.OverlappingFileLockException
import java.time.Duration
import javax.annotation.Nullable
if (project.isBuildSrcProject) return
allprojects {
pluginManager.withPlugin('java') {
apply plugin: 'jacoco'
}
pluginManager.withPlugin('jacoco') {
jacoco {
toolVersion = project.configurations.projectDependencyConstraints
.allDependencyConstraints
.matching { it.group == 'org.jacoco' && it.name == 'org.jacoco.agent' }
.matching { it.version != null && !it.version.isEmpty() }
.first()
.version
}
tasks.withType(Test).configureEach { Test task ->
task.onlyIf {
JacocoTaskExtension jacocoTaskExtension = task.extensions.getByType(JacocoTaskExtension)
jacocoTaskExtension.excludes.add('org.gradle.*')
return true
}
}
tasks.withType(JacocoReport).configureEach {
reports.html.required = true
reports.xml.required = true
}
tasks.withType(Test).all { Test testTask ->
TaskCollection reportTasks = tasks.withType(JacocoReport).matching { it.name == "jacoco${name.capitalize()}Report" }
testTask.finalizedBy(reportTasks)
reportTasks.all { JacocoReport reportTask ->
reportTask.dependsOn(testTask)
reportTask.dependsOn(sourceSets.main.allJava)
reportTask.dependsOn(tasks.matching { ['generateJava', 'generateGroovy', 'generateKotlin', 'generateResources'].contains(it.name) })
}
}
tasks.withType(Test).matching { it.name == 'functionalTest' || it.name == 'testFunctional' }.configureEach { Test task ->
task.ignoreFailures = true
FailsCounterTestListener failsCounterTestListener = new FailsCounterTestListener()
task.addTestListener(failsCounterTestListener)
task.doLast {
JacocoTaskExtension jacocoTaskExtension = task.extensions.getByType(JacocoTaskExtension)
File execFile = jacocoTaskExtension.destinationFile?.absoluteFile
if (execFile == null) return
long startTime = System.nanoTime()
long maxTimeout = Duration.ofSeconds(10).toNanos()
long timeoutTime = startTime + maxTimeout
for (int attempt = 1; System.nanoTime() < timeoutTime; ++attempt) {
if (attempt > 1) Thread.sleep(1_000)
if (!execFile.exists()) continue
try {
FileChannel.open(execFile.toPath(), READ, WRITE).withCloseable { FileChannel channel ->
channel.lock(0, Long.MAX_VALUE, true).close()
}
} catch (IOException | OverlappingFileLockException ignored) {
continue
}
break
}
}
task.doLast {
Throwable exception = failsCounterTestListener.exception
if (exception != null) {
throw exception
}
String exceptionMessage = failsCounterTestListener.createExceptionMessage()
if (exceptionMessage != null) {
throw new GradleException(exceptionMessage)
}
}
}
}
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
JacocoReport jacocoReportMerged = tasks.create('jacocoReportMerged', JacocoReport) { JacocoReport task ->
apply plugin: 'jacoco'
task.group = 'verification'
task.reports.all { ConfigurableReport report ->
DirectoryProperty reportsDir = project.jacoco.reportsDirectory
if (report.getOutputType() == OutputType.DIRECTORY) {
report.outputLocation.convention(reportsDir.dir("${task.name}/${report.name}"))
} else {
report.outputLocation.convention(reportsDir.file("${task.name}/${task.name}.${report.name}"))
}
}
allprojects {
pluginManager.withPlugin('jacoco') {
TaskCollection testTasks = tasks.withType(Test)
task.dependsOn(testTasks)
TaskCollection reportTasks = tasks.withType(JacocoReport).matching { it.name.matches(/jacoco[A-Z].*Report/) }
task.dependsOn(reportTasks)
task.executionData(testTasks)
pluginManager.withPlugin('java') {
task.sourceSets(sourceSets.main)
}
}
}
}
tasks.register('displayTotalCodeCoverage') { Task task ->
task.dependsOn('jacocoReportMerged')
task.group = 'verification'
doLast {
File xmlReportFile = jacocoReportMerged.reports.xml.outputLocation.asFile.get()
Class XmlParser = Class.forName('groovy.xml.XmlParser')
def xmlParser = XmlParser.getConstructor(Boolean.TYPE, Boolean.TYPE, Boolean.TYPE).newInstance(false, false, true)
Closure setFeatureQuietly = { String feature, Object value ->
try {
xmlParser.setFeature(feature, value)
} catch (Exception ignored) {
// do nothing
}
}
setFeatureQuietly('http://apache.org/xml/features/disallow-doctype-decl', false)
setFeatureQuietly('http://apache.org/xml/features/nonvalidating/load-external-dtd', false)
setFeatureQuietly('http://javax.xml.XMLConstants/feature/secure-processing', true)
Node reportNode = xmlParser.parse(xmlReportFile)
reportNode.counter?.forEach { Node counterNode ->
String type = counterNode['@type']
long missed = (counterNode['@missed'] ?: '0').toString().toLong()
long covered = (counterNode['@covered'] ?: '0').toString().toLong()
long total = missed + covered
double coverage = ((double) covered) / total
if ('LINE'.equalsIgnoreCase(type)) {
project.logger.quiet(String.format('Total code coverage: %.2f%% lines', coverage * 100.0))
}
}
}
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
class FailsCounterTestListener implements TestListener {
private boolean failed = false
private Throwable exception
boolean isFailed() {
return failed
}
private void processTestResult(TestResult testResult) {
if (testResult.exception != null
|| !testResult.exceptions.isEmpty()
|| testResult.failedTestCount > 0
) {
failed = true
}
if (testResult.exceptions.size() == 1) {
exception = testResult.exceptions.get(0)
}
}
@Nullable
String createExceptionMessage() {
if (!failed) {
return null
}
return "Some tests failed"
}
@Nullable
Throwable getException() {
return exception
}
@Override
void beforeSuite(TestDescriptor testDescriptor) {
// do nothing
}
@Override
void afterSuite(TestDescriptor testDescriptor, TestResult testResult) {
processTestResult(testResult)
}
@Override
void beforeTest(TestDescriptor testDescriptor) {
// do nothing
}
@Override
void afterTest(TestDescriptor testDescriptor, TestResult testResult) {
processTestResult(testResult)
}
}