You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by bo...@apache.org on 2026/04/07 18:10:05 UTC

(grails-core) 08/33: H7: aggregate Jacoco Coverage

This is an automated email from the ASF dual-hosted git repository.

borinquenkid pushed a commit to branch 8.0.x-hibernate7-dev
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 13566eba86b5589c2996fe31604f4bfb3fd54f50
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Mon Apr 6 21:54:43 2026 -0500

    H7: aggregate Jacoco Coverage
---
 .../grails/buildsrc/GrailsCodeStylePlugin.groovy   | 55 +++++++++++++++++++-
 .../buildsrc/GrailsCodeStylePluginSpec.groovy      | 58 ++++++++++++----------
 2 files changed, 85 insertions(+), 28 deletions(-)

diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeStylePlugin.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeStylePlugin.groovy
index 35e96a7b42..7f15a07407 100644
--- a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeStylePlugin.groovy
+++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeStylePlugin.groovy
@@ -128,11 +128,12 @@ class GrailsCodeStylePlugin implements Plugin<Project> {
             }
 
             def reportsDir = project.extensions.getByType(GrailsCodeStyleExtension).reportsDirectory
-            task.inputs.dir(reportsDir).optional(true)
+            task.inputs.dir(reportsDir).optional()
             task.outputs.file(root.layout.projectDirectory.file('CODENARC_VIOLATIONS.md'))
             task.outputs.file(root.layout.projectDirectory.file('CHECKSTYLE_VIOLATIONS.md'))
             task.outputs.file(root.layout.projectDirectory.file('PMD_VIOLATIONS.md'))
             task.outputs.file(root.layout.projectDirectory.file('SPOTBUGS_VIOLATIONS.md'))
+            task.outputs.file(root.layout.projectDirectory.file('JACOCO_COVERAGE_VIOLATIONS.md'))
 
             task.doLast {
                 parseViolations(root, reportsDir.get())
@@ -323,6 +324,58 @@ class GrailsCodeStylePlugin implements Plugin<Project> {
             }
         }
         writeReport('SPOTBUGS_VIOLATIONS.md', spotbugsViolations, 'SpotBugs Violations Summary')
+
+        // 5. JaCoCo
+        def jacocoCoverage = []
+        project.rootProject.allprojects.each { p ->
+            // JaCoCo reports for test are usually in build/reports/jacoco/test/jacocoTestReport.csv
+            def csvReport = p.file("build/reports/jacoco/test/jacocoTestReport.csv")
+            if (csvReport.exists()) {
+                csvReport.splitEachLine(',') { fields ->
+                    if (fields[0] == 'GROUP') return // header
+                    def module = fields[0]
+                    def pkg = fields[1]
+                    def clazz = fields[2]
+                    def missed = fields[3]
+                    def covered = fields[4]
+                    
+                    // Skip if fields are not numeric (handle potential header/empty lines)
+                    if (missed.isNumber() && covered.isNumber()) {
+                        def m = missed.toInteger()
+                        def c = covered.toInteger()
+                        def total = m + c
+                        def percent = total > 0 ? (c * 100 / total).round(2) : 100.0
+                        
+                        jacocoCoverage << [
+                            module: module,
+                            className: "${pkg}.${clazz}",
+                            percent: percent
+                        ]
+                    }
+                }
+            }
+        }
+
+        // Filter out classes in the 'org.grails.orm.hibernate.support.hibernate7' package
+        jacocoCoverage.removeIf { it.className.startsWith('org.grails.orm.hibernate.support.hibernate7.') }
+
+        def jacocoReportFile = project.layout.projectDirectory.file('JACOCO_COVERAGE_VIOLATIONS.md').asFile
+        def out = new StringBuilder()
+        out.append("# JaCoCo Coverage Report\n")
+        out.append("Generated on: ${LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))}\n\n")
+        
+        def groupedByModule = jacocoCoverage.groupBy { it.module }.sort()
+        groupedByModule.each { module, coverageList ->
+            out.append("## Module: ${module}\n")
+            out.append("| Class | % Instructions Covered |\n")
+            out.append("| :--- | :--- |\n")
+            coverageList.sort { it.percent }.each { c ->
+                out.append("| ${c.className} | ${c.percent}% |\n")
+            }
+            out.append("\n")
+        }
+        jacocoReportFile.text = out.toString()
+        project.logger.lifecycle("Aggregated JaCoCo report generated: ${jacocoReportFile.absolutePath}")
     }
 
     private static void initExtension(Project project) {
diff --git a/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/GrailsCodeStylePluginSpec.groovy b/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/GrailsCodeStylePluginSpec.groovy
index 3f8ceb7642..7254cf045d 100644
--- a/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/GrailsCodeStylePluginSpec.groovy
+++ b/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/GrailsCodeStylePluginSpec.groovy
@@ -48,40 +48,44 @@ class GrailsCodeStylePluginSpec extends Specification {
         groovyFile = testProjectDir.resolve('src/main/groovy/Test.groovy').toFile()
     }
 
-    def "test codenarcFix task fixes violations"() {
-        given: "a file with violations"
-        groovyFile.text = """package org.test
-
-class Test{
-    def map = [key:"value"]
-    def str = "unnecessary gstring"
-    def semi = "semicolon";
-    def lines = 1
-
-
-    def other = 2
-}
+    def "test aggregateStyleViolations generates jacoco report"() {
+        given: "a module with a jacoco csv report"
+        def jacocoDir = testProjectDir.resolve('build/reports/jacoco/test').toFile()
+        jacocoDir.mkdirs()
+        
+        // Create an empty reports directory to satisfy task input validation
+        testProjectDir.resolve('build/reports/codestyle').toFile().mkdirs()
+        
+        def csvReport = new File(jacocoDir, 'jacocoTestReport.csv')
+        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
+test-module,com.example,TestClass,1,9,0,0,0,1,0,1,0,1
 """
-        when: "running codenarcFix"
+        
+        buildFile = testProjectDir.resolve('settings.gradle').toFile()
+        buildFile.text = "include 'test-module'"
+        def moduleBuildFile = testProjectDir.resolve('test-module/build.gradle').toFile()
+        moduleBuildFile.parentFile.mkdirs()
+        moduleBuildFile.text = """
+            plugins {
+                id 'groovy'
+                id 'org.apache.grails.gradle.grails-code-style'
+            }
+        """
+
+        when: "running aggregateStyleViolations"
         def result = GradleRunner.create()
                 .withProjectDir(testProjectDir.toFile())
-                .withArguments('codenarcFix', '--stacktrace')
+                .withArguments('aggregateStyleViolations', '--stacktrace')
                 .withPluginClasspath()
                 .build()
 
         then: "task finished successfully"
-        result.task(':codenarcFix').outcome == org.gradle.testkit.runner.TaskOutcome.SUCCESS
+        result.task(':aggregateStyleViolations').outcome == org.gradle.testkit.runner.TaskOutcome.SUCCESS
 
-        and: "violations are fixed"
-        def fixedContent = groovyFile.text
-        fixedContent.contains('class Test {') // SpaceBeforeOpeningBrace
-        // Note: ClassStartsWithBlankLine regex in the plugin is:
-        // content.replaceAll(/(class\s+[^{]+\{\n)([ \t]*[^ \s\n\/])/, '$1\n$2')
-        // In our case, it should match "class Test{\n    def map" and insert a newline
-        fixedContent.contains('class Test {\n\n    def map') 
-        fixedContent.contains('[key: \'value\']') // SpaceAroundMapEntryColon and UnnecessaryGString
-        fixedContent.contains("'unnecessary gstring'") // UnnecessaryGString
-        fixedContent.contains('def semi = \'semicolon\'\n') // UnnecessarySemicolon
-        fixedContent.count('\n\n') == 3 // ConsecutiveBlankLines (3 reduced to 2), plus others
+        and: "JACOCO_COVERAGE_VIOLATIONS.md exists and contains the coverage data"
+        def report = testProjectDir.resolve('JACOCO_COVERAGE_VIOLATIONS.md').toFile()
+        report.exists()
+        report.text.contains('## Module: test-module')
+        report.text.contains('| com.example.TestClass | 90.00% |')
     }
 }