Skip to content

Commit c800469

Browse files
committed
Merge remote-tracking branch 'upstream/8.0.x-hibernate7-dev' into 8.0.x-hibernate7
2 parents 78fccb9 + 939e5ea commit c800469

File tree

220 files changed

+12974
-1616
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

220 files changed

+12974
-1616
lines changed

AGENTS.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ export GRADLE_OPTS="-Xms2G -Xmx5G"
5252
9. **Test via public APIs** - Tests must exercise behavior through the same APIs an end user calls; never invoke internal implementations, package-private methods, or bypass the public surface directly
5353
10. **Always review and extend tests** - Review existing unit and functional tests before making changes; every code change must include new or enhanced tests that cover the affected behavior
5454
11. **Every code touch must update all tests for the changed class** - When a class is modified, find and update every test that covers it — unit, integration, and TCK. Do not leave any existing test out of sync with the new code.
55-
12. **Clean violations before commit** - Before every automated commit, run `./gradlew clean test aggregateTestFailures --continue` from the root and ensure that `TEST_FAILURES.md` reports no issues and is removed.
55+
12. **Clean violations before commit** - Before every automated commit, run `./gradlew clean aggregateStyleViolations test aggregateTestFailures --continue` from the root and ensure that `CHECKSTYLE_VIOLATIONS.md`, `CODENARC_VIOLATIONS.md`, `PMD_VIOLATIONS.md`, and `TEST_FAILURES.md` report no issues and are removed.
56+
13. **Mandatory test coverage** - Any class touched in a commit MUST be covered with tests that verify all behavior. You must run ALL tests in the affected module(s) and ensure they pass before committing.
5657

5758
## Available Skills
5859

@@ -232,7 +233,8 @@ class MyService { }
232233
2. **Run tests** before submitting: `./gradlew build --rerun-tasks`
233234
3. **Run code style checks**: `./gradlew codeStyle`
234235
4. **Clean style violations**: Before committing, run `./gradlew clean aggregateStyleViolations` from the root and ensure that `CHECKSTYLE_VIOLATIONS.md`, `CODENARC_VIOLATIONS.md`, and `PMD_VIOLATIONS.md` have no issues.
235-
5. **Squash commits** into a single meaningful commit message
236+
5. **Verify test coverage**: Ensure any touched class is covered by tests verifying all behavior. You must run ALL tests in the affected module(s) and ensure they pass before submission.
237+
6. **Squash commits** into a single meaningful commit message
236238
6. **Reference issues** in PR description (e.g., "Fixes #1234")
237239

238240
### Review Process

build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeStylePlugin.groovy

Lines changed: 147 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ import com.github.spotbugs.snom.Effort
4949
import com.github.spotbugs.snom.SpotBugsExtension
5050
import com.github.spotbugs.snom.SpotBugsPlugin
5151
import com.github.spotbugs.snom.SpotBugsTask
52+
import org.gradle.api.tasks.testing.Test
53+
import org.gradle.testing.jacoco.plugins.JacocoPlugin
54+
import org.gradle.testing.jacoco.plugins.JacocoPluginExtension
55+
import org.gradle.testing.jacoco.tasks.JacocoReport
5256

5357
/**
5458
* Convention plugin for Grails code style enforcement.
@@ -77,6 +81,8 @@ class GrailsCodeStylePlugin implements Plugin<Project> {
7781

7882
static String SPOTBUGS_ENABLED_PROPERTY = 'grails.codestyle.enabled.spotbugs'
7983

84+
static String JACOCO_ENABLED_PROPERTY = 'grails.codestyle.enabled.jacoco'
85+
8086
static String IGNORE_FAILURES_PROPERTY = 'grails.codestyle.ignoreFailures'
8187

8288
static String TEST_STYLING_PROPERTY = 'grails.codestyle.enabled.tests'
@@ -88,6 +94,44 @@ class GrailsCodeStylePlugin implements Plugin<Project> {
8894
initExtension(project)
8995
configureCodeStyle(project)
9096
configureAggregation(project)
97+
98+
boolean jacocoEnabled = GradleUtils.lookupProperty(project, JACOCO_ENABLED_PROPERTY, false)
99+
if (jacocoEnabled) {
100+
configureJacoco(project)
101+
if (project == project.rootProject) {
102+
project.logger.info("JaCoCo enabled globally, applying to subprojects")
103+
project.subprojects.each { subproject ->
104+
subproject.pluginManager.withPlugin('java') {
105+
configureJacoco(subproject)
106+
}
107+
subproject.pluginManager.withPlugin('groovy') {
108+
configureJacoco(subproject)
109+
}
110+
}
111+
}
112+
}
113+
}
114+
115+
static void configureJacoco(Project project) {
116+
project.logger.info("Configuring JaCoCo for project: ${project.name}")
117+
project.pluginManager.apply(JacocoPlugin)
118+
119+
project.extensions.configure(JacocoPluginExtension) {
120+
it.toolVersion = "0.8.14"
121+
}
122+
123+
project.tasks.withType(Test).configureEach {
124+
it.finalizedBy 'jacocoTestReport'
125+
}
126+
127+
project.tasks.withType(JacocoReport).configureEach {
128+
it.dependsOn project.tasks.withType(Test)
129+
it.reports {
130+
it.xml.required = true
131+
it.html.required = true
132+
it.csv.required = true
133+
}
134+
}
91135
}
92136

93137
private static void configureAggregation(Project project) {
@@ -101,6 +145,7 @@ class GrailsCodeStylePlugin implements Plugin<Project> {
101145
task.description = 'Aggregates all code style violations into separate reports'
102146

103147
boolean checkTests = GradleUtils.lookupProperty(project, TEST_STYLING_PROPERTY, false)
148+
boolean jacocoEnabled = GradleUtils.lookupProperty(project, JACOCO_ENABLED_PROPERTY, false)
104149

105150
// Dependencies: all check tasks in all subprojects
106151
root.subprojects.each { subproject ->
@@ -125,13 +170,22 @@ class GrailsCodeStylePlugin implements Plugin<Project> {
125170
checkTests || (!t.name.toLowerCase().contains('test') && !t.name.toLowerCase().contains('integrationtest'))
126171
})
127172
}
173+
174+
if (jacocoEnabled) {
175+
task.dependsOn(subproject.tasks.withType(JacocoReport))
176+
}
128177
}
129178

130179
def reportsDir = project.extensions.getByType(GrailsCodeStyleExtension).reportsDirectory
131-
task.inputs.dir(reportsDir).optional(true)
180+
task.inputs.dir(reportsDir).optional()
132181
task.outputs.file(root.layout.projectDirectory.file('CODENARC_VIOLATIONS.md'))
133182
task.outputs.file(root.layout.projectDirectory.file('CHECKSTYLE_VIOLATIONS.md'))
134183
task.outputs.file(root.layout.projectDirectory.file('PMD_VIOLATIONS.md'))
184+
task.outputs.file(root.layout.projectDirectory.file('SPOTBUGS_VIOLATIONS.md'))
185+
186+
if (jacocoEnabled) {
187+
task.outputs.file(root.layout.projectDirectory.file('JACOCO_COVERAGE_VIOLATIONS.md'))
188+
}
135189

136190
task.doLast {
137191
parseViolations(root, reportsDir.get())
@@ -146,6 +200,7 @@ class GrailsCodeStylePlugin implements Plugin<Project> {
146200
slurper.setFeature("http://xml.org/sax/features/namespaces", false)
147201

148202
boolean checkTests = GradleUtils.lookupProperty(project, TEST_STYLING_PROPERTY, false)
203+
boolean jacocoEnabled = GradleUtils.lookupProperty(project, JACOCO_ENABLED_PROPERTY, false)
149204

150205
def getModule = { String fileName ->
151206
def lastDash = fileName.lastIndexOf('-')
@@ -294,6 +349,94 @@ class GrailsCodeStylePlugin implements Plugin<Project> {
294349
}
295350
}
296351
writeReport('CHECKSTYLE_VIOLATIONS.md', checkstyleViolations, 'Checkstyle Violations Summary')
352+
353+
// 4. SpotBugs
354+
def spotbugsViolations = []
355+
def spotbugsDir = reportsDir.dir('spotbugs').asFile
356+
if (spotbugsDir.exists() && GradleUtils.lookupProperty(project, SPOTBUGS_ENABLED_PROPERTY, false)) {
357+
spotbugsDir.eachFileMatch(~/.*\.xml/) { file ->
358+
if (file.size() == 0 || (!checkTests && isTestFile(file.name))) {
359+
return
360+
}
361+
def module = getModule(file.name)
362+
def xml = slurper.parse(file)
363+
xml.BugInstance.each { b ->
364+
def className = b.Class.@classname.text()
365+
if (shouldSkipClass(className)) {
366+
return
367+
}
368+
spotbugsViolations << [
369+
module: module,
370+
className: className,
371+
tool: 'SpotBugs',
372+
type: b.@type.text(),
373+
line: b.SourceLine.@start.text(),
374+
message: b.LongMessage.text().trim()
375+
]
376+
}
377+
}
378+
}
379+
writeReport('SPOTBUGS_VIOLATIONS.md', spotbugsViolations, 'SpotBugs Violations Summary')
380+
381+
// 5. JaCoCo
382+
if (jacocoEnabled) {
383+
project.logger.info("Aggregating JaCoCo coverage reports")
384+
def jacocoCoverage = []
385+
project.rootProject.allprojects.each { p ->
386+
// JaCoCo reports for test are usually in build/reports/jacoco/test/jacocoTestReport.csv
387+
def csvReport = p.file("build/reports/jacoco/test/jacocoTestReport.csv")
388+
if (csvReport.exists()) {
389+
project.logger.debug("Processing JaCoCo report: ${csvReport.absolutePath}")
390+
csvReport.splitEachLine(',') { fields ->
391+
if (fields.size() < 5 || fields[0] == 'GROUP') return // header or malformed line
392+
def module = fields[0]
393+
def pkg = fields[1]
394+
def clazz = fields[2]
395+
def missedStr = fields[3]
396+
def coveredStr = fields[4]
397+
398+
// Skip if fields are not numeric
399+
if (missedStr.isNumber() && coveredStr.isNumber()) {
400+
def m = missedStr.toInteger()
401+
def c = coveredStr.toInteger()
402+
def total = m + c
403+
def percent = total > 0 ? (c * 100 / total).round(2) : 100.0
404+
405+
jacocoCoverage << [
406+
module : module,
407+
className: "${pkg}.${clazz}",
408+
percent : percent
409+
]
410+
}
411+
}
412+
}
413+
}
414+
415+
if (!jacocoCoverage.isEmpty()) {
416+
// Filter out classes in the 'org.grails.orm.hibernate.support.hibernate7' package
417+
jacocoCoverage.removeIf { it.className.startsWith('org.grails.orm.hibernate.support.hibernate7.') }
418+
419+
def jacocoReportFile = project.layout.projectDirectory.file('JACOCO_COVERAGE_VIOLATIONS.md').asFile
420+
def out = new StringBuilder()
421+
out.append("# JaCoCo Coverage Report\n")
422+
out.append("Generated on: ${LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))}\n\n")
423+
424+
def groupedByModule = jacocoCoverage.groupBy { it.module }.sort()
425+
groupedByModule.each { module, coverageList ->
426+
out.append("## Module: ${module}\n")
427+
out.append("| Class | % Instructions Covered |\n")
428+
out.append("| :--- | :--- |\n")
429+
coverageList.sort { it.percent }.each { c ->
430+
out.append("| ${c.className} | ${c.percent}% |\n")
431+
}
432+
out.append("\n")
433+
}
434+
jacocoReportFile.text = out.toString()
435+
project.logger.lifecycle("Aggregated JaCoCo report generated: ${jacocoReportFile.absolutePath}")
436+
} else {
437+
project.logger.info("No JaCoCo coverage reports found to aggregate")
438+
}
439+
}
297440
}
298441

299442
private static void initExtension(Project project) {
@@ -451,7 +594,7 @@ class GrailsCodeStylePlugin implements Plugin<Project> {
451594
}
452595

453596
if (!GradleUtils.lookupProperty(project, TEST_STYLING_PROPERTY, false)) {
454-
project.tasks.named('checkstyleTest') {
597+
project.tasks.matching { it.name == 'checkstyleTest' }.configureEach {
455598
it.enabled = false // Do not check test sources at this time
456599
}
457600
}
@@ -661,10 +804,8 @@ class GrailsCodeStylePlugin implements Plugin<Project> {
661804
project.afterEvaluate {
662805
// Do not check test sources at this time
663806
['codenarcIntegrationTest', 'codenarcTest'].each { testTaskName ->
664-
if (project.tasks.names.contains(testTaskName)) {
665-
project.tasks.named(testTaskName) {
666-
it.enabled = false
667-
}
807+
project.tasks.matching { it.name == testTaskName }.configureEach {
808+
it.enabled = false
668809
}
669810
}
670811
}

build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/GrailsCodeStylePluginSpec.groovy

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,90 @@ class GrailsCodeStylePluginSpec extends Specification {
4848
groovyFile = testProjectDir.resolve('src/main/groovy/Test.groovy').toFile()
4949
}
5050

51+
def "test codeStyle and aggregation tasks including jacoco"() {
52+
given: "a project structure with violations and jacoco report"
53+
testProjectDir.resolve('build/reports/codestyle/checkstyle').toFile().mkdirs()
54+
testProjectDir.resolve('build/reports/codestyle/codenarc').toFile().mkdirs()
55+
def jacocoDir = testProjectDir.resolve('build/reports/jacoco/test').toFile()
56+
jacocoDir.mkdirs()
57+
58+
buildFile = testProjectDir.resolve('settings.gradle').toFile()
59+
buildFile.text = "include 'app-module'"
60+
def moduleDir = testProjectDir.resolve('app-module')
61+
def moduleBuildFile = moduleDir.resolve('build.gradle').toFile()
62+
moduleBuildFile.parentFile.mkdirs()
63+
moduleBuildFile.text = """
64+
plugins {
65+
id 'groovy'
66+
id 'jacoco'
67+
id 'org.apache.grails.gradle.grails-code-style'
68+
}
69+
repositories {
70+
mavenCentral()
71+
}
72+
dependencies {
73+
implementation 'org.apache.groovy:groovy:4.0.11'
74+
}
75+
"""
76+
def sourceFile = moduleDir.resolve('src/main/groovy/com/example/AppClass.groovy')
77+
sourceFile.toFile().parentFile.mkdirs()
78+
sourceFile.toFile().text = "package com.example\nclass AppClass {}"
79+
80+
def checkstyleReport = testProjectDir.resolve('build/reports/codestyle/checkstyle/app-module-checkstyleMain.xml').toFile()
81+
checkstyleReport.parentFile.mkdirs()
82+
checkstyleReport.text = """<?xml version="1.0" encoding="UTF-8"?>
83+
<checkstyle version="10.0">
84+
<file name="${sourceFile.toFile().absolutePath}">
85+
<error line="1" column="1" severity="error" message="Missing a Javadoc comment." source="com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocPackageCheck"/>
86+
</file>
87+
</checkstyle>
88+
"""
89+
def codenarcReport = testProjectDir.resolve('build/reports/codestyle/codenarc/app-module-codenarcMain.xml').toFile()
90+
codenarcReport.parentFile.mkdirs()
91+
codenarcReport.text = """<?xml version="1.0" encoding="UTF-8"?>
92+
<CodeNarc version="3.1.0">
93+
<Package name="com.example">
94+
<File name="AppClass.groovy">
95+
<Violation ruleName="EmptyClass" priority="2" lineNumber="1">
96+
<Message>The class is empty</Message>
97+
</Violation>
98+
</File>
99+
</Package>
100+
</CodeNarc>
101+
"""
102+
def csvReport = new File(jacocoDir, 'jacocoTestReport.csv')
103+
csvReport.text = """GROUP,PACKAGE,CLASS,INSTRUCTION_MISSED,INSTRUCTION_COVERED,BRANCH_MISSED,BRANCH_COVERED,LINE_MISSED,LINE_COVERED,COMPLEXITY_MISSED,COMPLEXITY_COVERED,METHOD_MISSED,METHOD_COVERED
104+
app-module,com.example,AppClass,1,9,0,0,0,1,0,1,0,1
105+
"""
106+
107+
when: "running aggregateStyleViolations with jacoco enabled"
108+
def result = GradleRunner.create()
109+
.withProjectDir(testProjectDir.toFile())
110+
.withArguments('aggregateStyleViolations', '-Pgrails.codestyle.enabled.jacoco=true', '-x', 'checkstyleMain', '-x', 'codenarcMain', '--stacktrace')
111+
.withPluginClasspath()
112+
.build()
113+
114+
then: "task finished successfully"
115+
result.task(':aggregateStyleViolations').outcome == org.gradle.testkit.runner.TaskOutcome.SUCCESS
116+
117+
and: "violation reports are generated"
118+
testProjectDir.resolve('CHECKSTYLE_VIOLATIONS.md').toFile().exists()
119+
testProjectDir.resolve('CODENARC_VIOLATIONS.md').toFile().exists()
120+
testProjectDir.resolve('JACOCO_COVERAGE_VIOLATIONS.md').toFile().exists()
121+
122+
def checkstyleMd = testProjectDir.resolve('CHECKSTYLE_VIOLATIONS.md').toFile().text
123+
checkstyleMd.contains('## Module: app-module')
124+
checkstyleMd.contains('| com.example.AppClass | Checkstyle | JavadocPackageCheck | 1 | Missing a Javadoc comment. |')
125+
126+
def codenarcMd = testProjectDir.resolve('CODENARC_VIOLATIONS.md').toFile().text
127+
codenarcMd.contains('## Module: app-module')
128+
codenarcMd.contains('| com.example.AppClass | CodeNarc | EmptyClass | 1 | The class is empty |')
129+
130+
def jacocoMd = testProjectDir.resolve('JACOCO_COVERAGE_VIOLATIONS.md').toFile().text
131+
jacocoMd.contains('## Module: app-module')
132+
jacocoMd.contains('| com.example.AppClass | 90.00% |')
133+
}
134+
51135
def "test codenarcFix task fixes violations"() {
52136
given: "a file with violations"
53137
groovyFile.text = """package org.test
@@ -75,13 +159,11 @@ class Test{
75159
and: "violations are fixed"
76160
def fixedContent = groovyFile.text
77161
fixedContent.contains('class Test {') // SpaceBeforeOpeningBrace
78-
// Note: ClassStartsWithBlankLine regex in the plugin is:
79-
// content.replaceAll(/(class\s+[^{]+\{\n)([ \t]*[^ \s\n\/])/, '$1\n$2')
80-
// In our case, it should match "class Test{\n def map" and insert a newline
81162
fixedContent.contains('class Test {\n\n def map')
82-
fixedContent.contains('[key: \'value\']') // SpaceAroundMapEntryColon and UnnecessaryGString
163+
fixedContent.contains("[key: 'value']") // SpaceAroundMapEntryColon and UnnecessaryGString
83164
fixedContent.contains("'unnecessary gstring'") // UnnecessaryGString
84-
fixedContent.contains('def semi = \'semicolon\'\n') // UnnecessarySemicolon
85-
fixedContent.count('\n\n') == 3 // ConsecutiveBlankLines (3 reduced to 2), plus others
165+
fixedContent.contains("def semi = 'semicolon'") // UnnecessarySemicolon
166+
!fixedContent.contains(";")
167+
fixedContent.count('\n\n') == 3 // ConsecutiveBlankLines
86168
}
87169
}

build.gradle

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
plugins {
1919
id 'org.apache.grails.gradle.test-aggregation'
20+
id 'org.apache.grails.gradle.grails-code-style'
2021
}
2122

2223
import java.time.Instant
@@ -87,11 +88,6 @@ subprojects {
8788
cacheChangingModulesFor(cacheHours, 'hours')
8889
}
8990
}
90-
pluginManager.withPlugin('jacoco') {
91-
jacoco {
92-
toolVersion = "0.8.14" // Set the specific tool version here
93-
}
94-
}
9591
}
9692

9793
interface ExecSupport {

dependencies.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,14 @@ ext {
3838
'jquery.version' : '3.7.1',
3939
'objenesis.version' : '3.4',
4040
'spring-boot.version' : '4.0.5',
41+
'testcontainers.version' : '1.20.1',
4142
]
4243

4344
// Note: the name of the dependency must be the prefix of the property name so properties in the pom are resolved correctly
4445
gradleBomPlatformDependencies = [
4546
'spring-boot-bom' : "org.springframework.boot:spring-boot-dependencies:${gradleBomDependencyVersions['spring-boot.version']}",
4647
'gradle-spock-bom': "org.spockframework:spock-bom:${gradleBomDependencyVersions['gradle-spock.version']}",
48+
'testcontainers-bom': "org.testcontainers:testcontainers-bom:${gradleBomDependencyVersions['testcontainers.version']}",
4749
]
4850

4951
// Note: the name of the dependency must be the prefix of the property name so properties in the pom are resolved correctly

0 commit comments

Comments
 (0)