Skip to content

Commit eca597f

Browse files
committed
build: extract code coverage from testing
1 parent 843c749 commit eca597f

10 files changed

Lines changed: 150 additions & 144 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
plugins {
2+
id 'jacoco'
3+
}
4+
5+
extensions.configure(JacocoPluginExtension) {
6+
it.toolVersion = jacocoVersion
7+
}
8+
9+
// Configuration for declaring which projects contribute coverage data.
10+
def coverageDataProjects = configurations.register('coverageDataProjects') {
11+
canBeConsumed = false
12+
canBeResolved = true
13+
}
14+
15+
// Lazily collect source directories and class files from all coverageDataProjects dependencies.
16+
def covProjectList = coverageDataProjects.map {
17+
it.dependencies.withType(ProjectDependency).collect {
18+
project.project(it.path)
19+
}
20+
}
21+
22+
def allSourceDirs = covProjectList.map {
23+
it.findAll { it.plugins.hasPlugin('java') }
24+
.collectMany {
25+
it.extensions.getByType(SourceSetContainer).named('main').get()
26+
.allSource.sourceDirectories.files
27+
}
28+
}
29+
30+
def allClassDirs = covProjectList.map {
31+
it.findAll { it.plugins.hasPlugin('java') }
32+
.collectMany {
33+
it.extensions.getByType(SourceSetContainer).named('main').get()
34+
.output.files
35+
}
36+
}
37+
38+
def allExecFiles = covProjectList.map {
39+
it.collectMany {
40+
it.fileTree(it.layout.buildDirectory.dir('jacoco')) {
41+
include('**/*.exec')
42+
}.files
43+
}
44+
}
45+
46+
// Register the aggregated coverage report task.
47+
// This merges JaCoCo execution data from all coverageDataProjects into a single report.
48+
// Task dependencies on all Test tasks (test, integrationTest, etc.) in the declared
49+
// projects are derived automatically — no hard-coded project paths needed.
50+
tasks.register('jacocoAggregatedReport', JacocoReport) {
51+
description = 'Generates aggregated JaCoCo coverage report across all subprojects.'
52+
group = 'verification'
53+
54+
classDirectories.from(allClassDirs)
55+
executionData.from(allExecFiles)
56+
sourceDirectories.from(allSourceDirs)
57+
58+
reports {
59+
xml.required = true
60+
html.required = true
61+
csv.required = false
62+
}
63+
}
64+
65+
// After evaluation, wire dependsOn for every Test task in every coverage project.
66+
// This ensures all .exec files exist before the aggregated report collects them.
67+
afterEvaluate {
68+
def projects = coverageDataProjects.get().dependencies
69+
.withType(ProjectDependency)
70+
.collect { project.project(it.path) }
71+
72+
tasks.named('jacocoAggregatedReport') {reportTask ->
73+
projects.each {
74+
it.tasks.withType(Test).configureEach { testTask ->
75+
reportTask.dependsOn(testTask)
76+
}
77+
}
78+
}
79+
}
80+
81+
pluginManager.withPlugin('base') {
82+
tasks.named('check') {
83+
dependsOn('jacocoAggregatedReport')
84+
}
85+
}
86+

build-logic/src/main/groovy/config.code-coverage.gradle

Lines changed: 34 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -6,81 +6,52 @@ extensions.configure(JacocoPluginExtension) {
66
it.toolVersion = jacocoVersion
77
}
88

9-
// Configuration for declaring which projects contribute coverage data.
10-
def coverageDataProjects = configurations.register('coverageDataProjects') {
11-
canBeConsumed = false
12-
canBeResolved = true
13-
}
9+
pluginManager.withPlugin('groovy') {
10+
// The jacoco plugin automatically creates 'jacocoTestReport' for the 'test' task.
11+
// Configure it to produce XML (for CI tools) and HTML reports.
12+
tasks.named('jacocoTestReport', JacocoReport) {
13+
reports {
14+
xml.required = true
15+
html.required = true
16+
csv.required = false
17+
}
1418

15-
// Lazily collect source directories and class files from all coverageDataProjects dependencies.
16-
def covProjectList = coverageDataProjects.map {
17-
it.dependencies.withType(ProjectDependency).collect {
18-
project.project(it.path)
19+
dependsOn(tasks.named('test'))
1920
}
20-
}
21-
22-
def allSourceDirs = covProjectList.map {
23-
it.findAll { it.plugins.hasPlugin('java') }
24-
.collectMany {
25-
it.extensions.getByType(SourceSetContainer).named('main').get()
26-
.allSource.sourceDirectories.files
27-
}
28-
}
29-
30-
def allClassDirs = covProjectList.map {
31-
it.findAll { it.plugins.hasPlugin('java') }
32-
.collectMany {
33-
it.extensions.getByType(SourceSetContainer).named('main').get()
34-
.output.files
35-
}
36-
}
3721

38-
def allExecFiles = covProjectList.map {
39-
it.collectMany {
40-
it.fileTree(it.layout.buildDirectory.dir('jacoco')) {
41-
include('**/*.exec')
42-
}.files
22+
// Ensure coverage report runs after tests
23+
tasks.named('test') {
24+
finalizedBy(tasks.named('jacocoTestReport'))
4325
}
4426
}
4527

46-
// Register the aggregated coverage report task.
47-
// This merges JaCoCo execution data from all coverageDataProjects into a single report.
48-
// Task dependencies on all Test tasks (test, integrationTest, etc.) in the declared
49-
// projects are derived automatically — no hard-coded project paths needed.
50-
tasks.register('jacocoAggregatedReport', JacocoReport) {
51-
description = 'Generates aggregated JaCoCo coverage report across all subprojects.'
52-
group = 'verification'
28+
// When an integrationTest task is registered (e.g., via the Grails web plugin in example apps),
29+
// register a JaCoCo report task for it. Unlike the 'test' task, the JaCoCo plugin does not
30+
// auto-create report tasks for custom Test tasks.
31+
afterEvaluate { proj ->
5332

54-
classDirectories.from(allClassDirs)
55-
executionData.from(allExecFiles)
56-
sourceDirectories.from(allSourceDirs)
33+
def integrationTestTasks = tasks.withType(Test).matching { it.name == 'integrationTest' }
34+
if (!integrationTestTasks.isEmpty()) {
5735

58-
reports {
59-
xml.required = true
60-
html.required = true
61-
csv.required = false
62-
}
63-
}
36+
def integrationTest = integrationTestTasks.first()
37+
def execFile = integrationTest.extensions.getByType(JacocoTaskExtension).destinationFile
38+
39+
def reportTask = tasks.register('jacocoIntegrationTestReport', JacocoReport) {
40+
description = 'Generates code coverage report for the integrationTest task.'
41+
group = 'verification'
6442

65-
// After evaluation, wire dependsOn for every Test task in every coverage project.
66-
// This ensures all .exec files exist before the aggregated report collects them.
67-
afterEvaluate {
68-
def projects = coverageDataProjects.get().dependencies
69-
.withType(ProjectDependency)
70-
.collect { project.project(it.path) }
43+
executionData.from(execFile)
44+
sourceSets(proj.extensions.getByType(SourceSetContainer).named('main').get())
7145

72-
tasks.named('jacocoAggregatedReport') {reportTask ->
73-
projects.each {
74-
it.tasks.withType(Test).configureEach { testTask ->
75-
reportTask.dependsOn(testTask)
46+
reports {
47+
xml.required = true
48+
html.required = true
49+
csv.required = false
7650
}
51+
52+
dependsOn(integrationTest)
7753
}
78-
}
79-
}
8054

81-
pluginManager.withPlugin('base') {
82-
tasks.named('check') {
83-
dependsOn('jacocoAggregatedReport')
55+
integrationTest.finalizedBy(reportTask)
8456
}
8557
}
86-
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
plugins {
22
id 'config.app-run'
3+
id 'config.code-coverage'
4+
id 'config.code-style'
5+
id 'config.compile'
36
id 'config.grails-assets'
7+
id 'config.testing'
48
id 'org.apache.grails.gradle.grails-web'
59
id 'org.apache.grails.gradle.grails-gsp'
610
}

build-logic/src/main/groovy/config.testing.gradle

Lines changed: 0 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import com.adarshr.gradle.testlogger.TestLoggerExtension
22

33
plugins {
44
id 'com.adarshr.test-logger'
5-
id 'jacoco'
65
}
76

87
def isCi = System.getenv('CI') != null
@@ -21,10 +20,6 @@ extensions.configure(TestLoggerExtension) {
2120
it.showFailed = true
2221
}
2322

24-
extensions.configure(JacocoPluginExtension) {
25-
it.toolVersion = '0.8.12'
26-
}
27-
2823
tasks.withType(Test).configureEach {
2924
onlyIf {
3025
!project.hasProperty('skipTests')
@@ -48,51 +43,3 @@ tasks.withType(Test).configureEach {
4843
showCauses = true
4944
}
5045
}
51-
52-
// The jacoco plugin automatically creates 'jacocoTestReport' for the 'test' task.
53-
// Configure it to produce XML (for CI tools) and HTML reports.
54-
tasks.named('jacocoTestReport', JacocoReport) {
55-
reports {
56-
xml.required = true
57-
html.required = true
58-
csv.required = false
59-
}
60-
61-
dependsOn(tasks.named('test'))
62-
}
63-
64-
// Ensure coverage report runs after tests
65-
tasks.named('test') {
66-
finalizedBy(tasks.named('jacocoTestReport'))
67-
}
68-
69-
// When an integrationTest task is registered (e.g., via the Grails web plugin in example apps),
70-
// register a JaCoCo report task for it. Unlike the 'test' task, the JaCoCo plugin does not
71-
// auto-create report tasks for custom Test tasks.
72-
afterEvaluate { proj ->
73-
74-
def integrationTestTasks = tasks.withType(Test).matching { it.name == 'integrationTest' }
75-
if (!integrationTestTasks.isEmpty()) {
76-
77-
def integrationTest = integrationTestTasks.first()
78-
def execFile = integrationTest.extensions.getByType(JacocoTaskExtension).destinationFile
79-
80-
def reportTask = tasks.register('jacocoIntegrationTestReport', JacocoReport) {
81-
description = 'Generates code coverage report for the integrationTest task.'
82-
group = 'verification'
83-
84-
executionData.from(execFile)
85-
sourceSets(proj.extensions.getByType(SourceSetContainer).named('main').get())
86-
87-
reports {
88-
xml.required = true
89-
html.required = true
90-
csv.required = false
91-
}
92-
93-
dependsOn(integrationTest)
94-
}
95-
96-
integrationTest.finalizedBy(reportTask)
97-
}
98-
}

code-coverage/build.gradle

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
plugins {
2+
id 'config.code-coverage-aggregate'
3+
}
4+
5+
dependencies {
6+
// The plugin project (always included)
7+
coverageDataProjects project(':grails-server-timing')
8+
9+
// Auto-discover all example apps under examples/
10+
rootDir.toPath().resolve('examples').toFile()
11+
.listFiles({ it.directory } as FileFilter)
12+
.each { coverageDataProjects project(":$it.name")
13+
}
14+
}

coverage/build.gradle

Lines changed: 0 additions & 13 deletions
This file was deleted.

examples/app1/build.gradle

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
plugins {
2-
id 'config.code-style'
3-
id 'config.compile'
42
id 'config.example-app'
5-
id 'config.testing'
63
}
74

85
version = projectVersion

examples/app2/build.gradle

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
plugins {
2-
id 'config.code-style'
3-
id 'config.compile'
42
id 'config.example-app'
5-
id 'config.testing'
63
}
74

85
version = projectVersion

plugin/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import org.apache.grails.gradle.publish.GrailsPublishExtension
22

33
plugins {
4+
id 'config.code-coverage'
45
id 'config.code-style'
56
id 'config.compile'
67
id 'config.grails-plugin'

settings.gradle

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@ def isLocal = !isCI
2222
def isReproducibleBuild = System.getenv('SOURCE_DATE_EPOCH') != null
2323
if (isReproducibleBuild) {
2424
gradle.settingsEvaluated {
25-
logger.warn('*************** Remote Build Cache Disabled due to Reproducible Build ********************')
26-
logger.warn("Build date will be set to (SOURCE_DATE_EPOCH=${System.getenv("SOURCE_DATE_EPOCH")})")
25+
logger.warn(
26+
'***** Remote Build Cache Disabled due to Reproducible Build *****\n' +
27+
'Build date will be set to (SOURCE_DATE_EPOCH={})',
28+
System.getenv('SOURCE_DATE_EPOCH')
29+
)
2730
}
2831
}
2932

@@ -33,16 +36,15 @@ buildCache {
3336

3437
rootProject.name = 'grails-server-timing-root'
3538

36-
include 'plugin'
39+
include('plugin')
3740
project(':plugin').name = 'grails-server-timing'
38-
include 'docs'
41+
include('docs')
3942
project(':docs').name = 'grails-server-timing-docs'
40-
include 'coverage'
43+
include('code-coverage')
4144

42-
def examples = file('examples').list()
43-
examples.each { example ->
44-
include example
45-
project(":$example").projectDir = file("examples/$example")
45+
file('examples').listFiles({ it.directory } as FileFilter).each {
46+
include(it.name)
47+
project(":$it.name").projectDir = file("examples/$it.name")
4648
}
4749

4850
dependencyResolutionManagement {

0 commit comments

Comments
 (0)