Skip to content

Commit e96c1c0

Browse files
committed
Make scoverage test run cacheable
1 parent f8ea2e0 commit e96c1c0

File tree

1 file changed

+91
-22
lines changed

1 file changed

+91
-22
lines changed

Diff for: src/main/groovy/org/scoverage/ScoveragePlugin.groovy

+91-22
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
package org.scoverage
22

33
import org.apache.commons.io.FileUtils
4+
import org.gradle.api.Action
45
import org.gradle.api.Plugin
56
import org.gradle.api.Project
67
import org.gradle.api.Task
78
import org.gradle.api.invocation.Gradle
89
import org.gradle.api.plugins.PluginAware
910
import org.gradle.api.plugins.scala.ScalaPlugin
11+
import org.gradle.api.provider.Provider
1012
import org.gradle.api.tasks.SourceSet
13+
import org.gradle.api.tasks.Sync
1114
import org.gradle.api.tasks.TaskProvider
1215
import org.gradle.api.tasks.scala.ScalaCompile
1316
import org.gradle.api.tasks.testing.Test
17+
import org.gradle.api.tasks.util.PatternFilterable
1418

1519
import java.nio.file.Files
1620
import java.util.concurrent.ConcurrentHashMap
@@ -20,6 +24,7 @@ import static groovy.io.FileType.FILES
2024
class ScoveragePlugin implements Plugin<PluginAware> {
2125

2226
static final String CONFIGURATION_NAME = 'scoverage'
27+
static final String MERGE_MEASUREMENTS_NAME = 'mergeScoverageMeasurements'
2328
static final String REPORT_NAME = 'reportScoverage'
2429
static final String CHECK_NAME = 'checkScoverage'
2530
static final String COMPILE_NAME = 'compileScoverageScala'
@@ -85,6 +90,26 @@ class ScoveragePlugin implements Plugin<PluginAware> {
8590
}
8691

8792
private void createTasks(Project project, ScoverageExtension extension) {
93+
/**
94+
dataDir is split into subdirectories:
95+
workDir
96+
{testTaskName}MeasurementsDir
97+
{testTaskName}ReportDir.
98+
99+
workDir
100+
Directory where metadata/measurements are "produced"
101+
Two tasks produce files in that directory: compile and test. Because of that, this directory is not
102+
cacheable.
103+
Only one file from this directory is cached: metadata from compile task (because it is a single file).
104+
105+
testMeasurementsDir
106+
Directory where measurements are synced from "workDir". It is registered as an additional output to test task,
107+
which makes the measurements files cacheable.
108+
109+
reportDir
110+
Merges workDir/scoverage.coverage and testMeasurementsDir/scoverage.measurements.* for reporting.
111+
*/
112+
def dataWorkDir = extension.dataDir.map {new File(it, "work") }
88113

89114
ScoverageRunner scoverageRunner = new ScoverageRunner(project.configurations.scoverage)
90115

@@ -106,6 +131,9 @@ class ScoveragePlugin implements Plugin<PluginAware> {
106131
def compileTask = project.tasks[instrumentedSourceSet.getCompileTaskName("scala")]
107132
compileTask.mustRunAfter(originalCompileTask)
108133

134+
// merges measurements from individual reports into one directory for use by globalReportTask
135+
def globalMergeMeasurementsTask = project.tasks.register(MERGE_MEASUREMENTS_NAME, Sync.class)
136+
109137
def globalReportTask = project.tasks.register(REPORT_NAME, ScoverageAggregate)
110138
def globalCheckTask = project.tasks.register(CHECK_NAME)
111139

@@ -122,17 +150,31 @@ class ScoveragePlugin implements Plugin<PluginAware> {
122150
List<ScoverageReport> reportTasks = testTasks.collect { testTask ->
123151
testTask.mustRunAfter(compileTask)
124152

125-
def reportTaskName = "report${testTask.name.capitalize()}Scoverage"
126-
def taskReportDir = project.file("${project.buildDir}/reports/scoverage${testTask.name.capitalize()}")
153+
def cTaskName = testTask.name.capitalize()
154+
155+
def reportTaskName = "report${cTaskName}Scoverage"
156+
def taskReportDir = project.file("${project.buildDir}/reports/scoverage${cTaskName}")
157+
158+
def scoverageSyncMetaWithOutputs =
159+
project.tasks.register("sync${cTaskName}ScoverageData", Sync.class)
160+
161+
scoverageSyncMetaWithOutputs.configure {
162+
dependsOn compileTask, testTask
163+
from(dataWorkDir) {
164+
include("scoverage.coverage")
165+
}
166+
from(dataMeasurementsDir(extension, cTaskName))
167+
into(dataReportDir(extension, cTaskName))
168+
}
127169

128170
project.tasks.create(reportTaskName, ScoverageReport) {
129-
dependsOn originalJarTask, compileTask, testTask
130-
onlyIf { extension.dataDir.get().list() }
171+
dependsOn originalJarTask, compileTask, testTask, scoverageSyncMetaWithOutputs
172+
onlyIf { scoverageSyncMetaWithOutputs.get().getDestinationDir().list() }
131173
group = 'verification'
132174
runner = scoverageRunner
133175
reportDir = taskReportDir
134176
sources = originalSourceSet.scala.getSourceDirectories()
135-
dataDir = extension.dataDir
177+
dataDir = scoverageSyncMetaWithOutputs.map {it.getDestinationDir()}
136178
sourceEncoding.set(detectedSourceEncoding)
137179
coverageOutputCobertura = extension.coverageOutputCobertura
138180
coverageOutputXML = extension.coverageOutputXML
@@ -141,17 +183,29 @@ class ScoveragePlugin implements Plugin<PluginAware> {
141183
}
142184
}
143185

144-
globalReportTask.configure {
186+
globalMergeMeasurementsTask.configure {sync ->
187+
dependsOn(reportTasks)
188+
dependsOn(compileTask)
189+
145190
def dataDirs = reportTasks.findResults { it.dataDir.get() }
146191

147-
dependsOn reportTasks
148-
onlyIf { dataDirs.any { it.list() } }
192+
from(dataWorkDir.map {new File(it, 'scoverage.coverage') })
193+
from(dataDirs) {
194+
exclude("scoverage.coverage")
195+
}
196+
into(project.file("${project.buildDir}/mergedScoverage"))
197+
}
198+
199+
globalReportTask.configure {
200+
dependsOn globalMergeMeasurementsTask
201+
202+
onlyIf { globalMergeMeasurementsTask.get().getDestinationDir().list()}
149203

150204
group = 'verification'
151205
runner = scoverageRunner
152206
reportDir = extension.reportDir
153207
sources = originalSourceSet.scala.getSourceDirectories()
154-
dirsToAggregateFrom = dataDirs
208+
dirsToAggregateFrom = globalMergeMeasurementsTask.map {[it.getDestinationDir()]}
155209
sourceEncoding.set(detectedSourceEncoding)
156210
deleteReportsOnAggregation = false
157211
coverageOutputCobertura = extension.coverageOutputCobertura
@@ -170,8 +224,12 @@ class ScoveragePlugin implements Plugin<PluginAware> {
170224
}
171225

172226
def scalaVersion = resolveScalaVersions(project)
227+
228+
// the compile task creates a store of measured statements
229+
outputs.file(dataWorkDir.map {new File(it, 'scoverage.coverage') })
230+
173231
if (scalaVersion.majorVersion < 3) {
174-
parameters.add("-P:scoverage:dataDir:${extension.dataDir.get().absolutePath}".toString())
232+
parameters.add("-P:scoverage:dataDir:${dataWorkDir.get().absolutePath}".toString())
175233
parameters.add("-P:scoverage:sourceRoot:${extension.project.getRootDir().absolutePath}".toString())
176234
if (extension.excludedPackages.get()) {
177235
def packages = extension.excludedPackages.get().join(';')
@@ -185,10 +243,7 @@ class ScoveragePlugin implements Plugin<PluginAware> {
185243
parameters.add('-Yrangepos')
186244
}
187245
scalaCompileOptions.additionalParameters = parameters
188-
// the compile task creates a store of measured statements
189-
outputs.file(new File(extension.dataDir.get(), 'scoverage.coverage'))
190-
191-
dependsOn project.configurations[CONFIGURATION_NAME]
246+
dependsOn project.configurations.named(CONFIGURATION_NAME)
192247
doFirst {
193248
/*
194249
It is crucial that this would run in `doFirst`, as this resolves the (dependencies of the)
@@ -205,7 +260,7 @@ class ScoveragePlugin implements Plugin<PluginAware> {
205260
}
206261
} else {
207262
parameters.add("-sourceroot:${project.rootDir.absolutePath}".toString())
208-
parameters.add("-coverage-out:${extension.dataDir.get().absolutePath}".toString())
263+
parameters.add("-coverage-out:${dataWorkDir.get().absolutePath}".toString())
209264
scalaCompileOptions.additionalParameters = parameters
210265
}
211266
}
@@ -262,20 +317,26 @@ class ScoveragePlugin implements Plugin<PluginAware> {
262317
def hasAnyReportTask = reportTasks.any { graph.hasTask(it) }
263318

264319
if (hasAnyReportTask) {
320+
265321
project.tasks.withType(Test).each { testTask ->
266322
testTask.configure {
323+
def cTaskName = testTask.name.capitalize()
324+
267325
project.logger.info("Adding instrumented classes to '${path}' classpath")
268326

269327
classpath = project.configurations.scoverage + instrumentedSourceSet.output + classpath
270328

271-
outputs.upToDateWhen {
272-
extension.dataDir.get().listFiles(new FilenameFilter() {
273-
@Override
274-
boolean accept(File dir, String name) {
275-
name.startsWith("scoverage.measurements.")
276-
}
277-
})
329+
doLast {
330+
project.sync {
331+
from(dataWorkDir)
332+
exclude("scoverage.coverage")
333+
into(dataMeasurementsDir(extension, cTaskName))
334+
}
335+
project.delete(project.fileTree(dataWorkDir).exclude("scoverage.coverage"))
278336
}
337+
338+
outputs.dir(dataMeasurementsDir(extension, cTaskName)).withPropertyName("scoverage.measurements")
339+
279340
}
280341
}
281342
}
@@ -323,6 +384,14 @@ class ScoveragePlugin implements Plugin<PluginAware> {
323384
}
324385
}
325386

387+
private Provider<File> dataMeasurementsDir(ScoverageExtension extension, String testName) {
388+
return extension.dataDir.map {new File(it, "${testName}Measurements") }
389+
}
390+
391+
private Provider<File> dataReportDir(ScoverageExtension extension, String testName) {
392+
return extension.dataDir.map { new File(it, "${testName}Report") }
393+
}
394+
326395
private void configureCheckTask(Project project, ScoverageExtension extension,
327396
TaskProvider<Task> globalCheckTask,
328397
TaskProvider<ScoverageAggregate> globalReportTask) {

0 commit comments

Comments
 (0)