borinquenkid commented on code in PR #16819:
URL: https://github.com/apache/grails-core/pull/16819#discussion_r4436310732


##########
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeAnalysisPlugin.groovy:
##########
@@ -57,6 +59,41 @@ class GrailsCodeAnalysisPlugin implements Plugin<Project> {
     static String SPOTBUGS_ENABLED_PROPERTY = 
'grails.code-analysis.enabled.spotbugs'
     static String SPOTBUGS_ENABLED_PROJECTS_PROPERTY = 
'grails.code-analysis.enabled.spotbugs.projects'
 
+    static String PITEST_ENABLED_PROPERTY = 
'grails.code-analysis.enabled.pitest'
+    static String PITEST_ENABLED_PROJECTS_PROPERTY = 
'grails.code-analysis.enabled.pitest.projects'
+    static String PITEST_TARGET_CLASSES_PROPERTY = 
'grails.code-analysis.pitest.targetClasses'
+    static String PITEST_TARGET_TESTS_PROPERTY = 
'grails.code-analysis.pitest.targetTests'
+    static String PITEST_THREADS_PROPERTY = 
'grails.code-analysis.pitest.threads'
+    static String PITEST_MUTATORS_PROPERTY = 
'grails.code-analysis.pitest.mutators'
+
+    static final Set<String> GROOVY_EXCLUDED_METHODS = [
+            '$getStaticMetaClass',
+            'getMetaClass',
+            'setMetaClass',
+            'invokeMethod',
+            'getProperty',
+            'setProperty',

Review Comment:
   Addressed in 95f25fe0cb: removed `getMetaClass`, `setMetaClass`, 
`invokeMethod`, `getProperty`, and `setProperty` from `GROOVY_EXCLUDED_METHODS` 
(compiler-generated methods are already excluded by PITest's `@Generated` 
bytecode filter while handwritten methods are now mutated) and disabled 
upstream blanket `FGROOVY` filtering so `@CompileStatic` Groovy classes are 
mutated alongside Java classes.



##########
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeAnalysisPlugin.groovy:
##########
@@ -219,6 +271,69 @@ class GrailsCodeAnalysisPlugin implements Plugin<Project> {
         }
     }
 
+    private static void configurePitest(Project project, 
GrailsCodeAnalysisExtension extension) {
+        Runnable applyAndConfigure = {
+            if (project.pluginManager.hasPlugin('info.solidsoft.pitest')) {
+                return
+            }
+            project.pluginManager.apply(PitestPlugin)
+
+            def targetClassesProp = 
project.providers.gradleProperty(PITEST_TARGET_CLASSES_PROPERTY)
+            def targetTestsProp = 
project.providers.gradleProperty(PITEST_TARGET_TESTS_PROPERTY)
+            def threadsProp = 
project.providers.gradleProperty(PITEST_THREADS_PROPERTY)
+            def mutatorsProp = 
project.providers.gradleProperty(PITEST_MUTATORS_PROPERTY)
+
+            project.extensions.configure(PitestPluginExtension) { 
PitestPluginExtension pitest ->
+                String configuredPitestVersion = 
project.findProperty('pitestVersion') as String
+                if (configuredPitestVersion) {
+                    pitest.pitestVersion.set(configuredPitestVersion)
+                }
+                String configuredJunit5Version = 
project.findProperty('pitestJunit5PluginVersion') as String
+                if (configuredJunit5Version) {
+                    pitest.junit5PluginVersion.set(configuredJunit5Version)
+                }
+                pitest.useClasspathFile.set(true)
+                pitest.outputFormats.set(['XML', 'HTML'] as Set<String>)
+                pitest.timestampedReports.set(false)
+                pitest.failWhenNoMutations.set(false)
+                pitest.threads.set(threadsProp.map { 
Integer.parseInt(it.trim()) }.orElse(4))
+                pitest.excludedMethods.set(GROOVY_EXCLUDED_METHODS)
+                pitest.excludedClasses.set(GROOVY_EXCLUDED_CLASSES)
+                pitest.avoidCallsTo.set(GROOVY_AVOID_CALLS_TO)
+                
pitest.reportDir.set(extension.reportsDirectory.dir("pitest/${project.name}"))
+
+                if (targetClassesProp.present) {
+                    pitest.targetClasses.set(targetClassesProp.map { 
it.split(',')*.trim().findAll() as Set<String> })
+                } else if (project.group) {
+                    pitest.targetClasses.set(["${project.group}.*" as String, 
'org.grails.*', 'grails.*'] as Set<String>)
+                }

Review Comment:
   Addressed in 95f25fe0cb: `targetClasses` fallback now always includes 
`org.apache.grails.*`, `org.grails.*`, and `grails.*` and lazily evaluates 
`${project.group}.*` via `project.provider`.



##########
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeAnalysisPlugin.groovy:
##########
@@ -219,6 +271,69 @@ class GrailsCodeAnalysisPlugin implements Plugin<Project> {
         }
     }
 
+    private static void configurePitest(Project project, 
GrailsCodeAnalysisExtension extension) {
+        Runnable applyAndConfigure = {
+            if (project.pluginManager.hasPlugin('info.solidsoft.pitest')) {
+                return
+            }
+            project.pluginManager.apply(PitestPlugin)
+
+            def targetClassesProp = 
project.providers.gradleProperty(PITEST_TARGET_CLASSES_PROPERTY)
+            def targetTestsProp = 
project.providers.gradleProperty(PITEST_TARGET_TESTS_PROPERTY)
+            def threadsProp = 
project.providers.gradleProperty(PITEST_THREADS_PROPERTY)
+            def mutatorsProp = 
project.providers.gradleProperty(PITEST_MUTATORS_PROPERTY)
+
+            project.extensions.configure(PitestPluginExtension) { 
PitestPluginExtension pitest ->
+                String configuredPitestVersion = 
project.findProperty('pitestVersion') as String
+                if (configuredPitestVersion) {
+                    pitest.pitestVersion.set(configuredPitestVersion)
+                }
+                String configuredJunit5Version = 
project.findProperty('pitestJunit5PluginVersion') as String
+                if (configuredJunit5Version) {
+                    pitest.junit5PluginVersion.set(configuredJunit5Version)
+                }
+                pitest.useClasspathFile.set(true)
+                pitest.outputFormats.set(['XML', 'HTML'] as Set<String>)
+                pitest.timestampedReports.set(false)
+                pitest.failWhenNoMutations.set(false)
+                pitest.threads.set(threadsProp.map { 
Integer.parseInt(it.trim()) }.orElse(4))
+                pitest.excludedMethods.set(GROOVY_EXCLUDED_METHODS)
+                pitest.excludedClasses.set(GROOVY_EXCLUDED_CLASSES)
+                pitest.avoidCallsTo.set(GROOVY_AVOID_CALLS_TO)
+                
pitest.reportDir.set(extension.reportsDirectory.dir("pitest/${project.name}"))
+
+                if (targetClassesProp.present) {
+                    pitest.targetClasses.set(targetClassesProp.map { 
it.split(',')*.trim().findAll() as Set<String> })
+                } else if (project.group) {
+                    pitest.targetClasses.set(["${project.group}.*" as String, 
'org.grails.*', 'grails.*'] as Set<String>)
+                }
+                if (targetTestsProp.present) {
+                    pitest.targetTests.set(targetTestsProp.map { 
it.split(',')*.trim().findAll() as Set<String> })
+                }

Review Comment:
   Addressed in 95f25fe0cb: `targetTests` now defaults to `["*"]` independently 
of `targetClasses`.



##########
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeAnalysisPlugin.groovy:
##########
@@ -219,6 +271,69 @@ class GrailsCodeAnalysisPlugin implements Plugin<Project> {
         }
     }
 
+    private static void configurePitest(Project project, 
GrailsCodeAnalysisExtension extension) {
+        Runnable applyAndConfigure = {
+            if (project.pluginManager.hasPlugin('info.solidsoft.pitest')) {
+                return
+            }
+            project.pluginManager.apply(PitestPlugin)
+
+            def targetClassesProp = 
project.providers.gradleProperty(PITEST_TARGET_CLASSES_PROPERTY)
+            def targetTestsProp = 
project.providers.gradleProperty(PITEST_TARGET_TESTS_PROPERTY)
+            def threadsProp = 
project.providers.gradleProperty(PITEST_THREADS_PROPERTY)
+            def mutatorsProp = 
project.providers.gradleProperty(PITEST_MUTATORS_PROPERTY)
+
+            project.extensions.configure(PitestPluginExtension) { 
PitestPluginExtension pitest ->
+                String configuredPitestVersion = 
project.findProperty('pitestVersion') as String
+                if (configuredPitestVersion) {
+                    pitest.pitestVersion.set(configuredPitestVersion)
+                }
+                String configuredJunit5Version = 
project.findProperty('pitestJunit5PluginVersion') as String
+                if (configuredJunit5Version) {
+                    pitest.junit5PluginVersion.set(configuredJunit5Version)
+                }
+                pitest.useClasspathFile.set(true)
+                pitest.outputFormats.set(['XML', 'HTML'] as Set<String>)
+                pitest.timestampedReports.set(false)
+                pitest.failWhenNoMutations.set(false)
+                pitest.threads.set(threadsProp.map { 
Integer.parseInt(it.trim()) }.orElse(4))
+                pitest.excludedMethods.set(GROOVY_EXCLUDED_METHODS)
+                pitest.excludedClasses.set(GROOVY_EXCLUDED_CLASSES)
+                pitest.avoidCallsTo.set(GROOVY_AVOID_CALLS_TO)
+                
pitest.reportDir.set(extension.reportsDirectory.dir("pitest/${project.name}"))
+
+                if (targetClassesProp.present) {
+                    pitest.targetClasses.set(targetClassesProp.map { 
it.split(',')*.trim().findAll() as Set<String> })
+                } else if (project.group) {
+                    pitest.targetClasses.set(["${project.group}.*" as String, 
'org.grails.*', 'grails.*'] as Set<String>)
+                }
+                if (targetTestsProp.present) {
+                    pitest.targetTests.set(targetTestsProp.map { 
it.split(',')*.trim().findAll() as Set<String> })
+                }
+                if (mutatorsProp.present) {
+                    pitest.mutators.set(mutatorsProp.map { 
it.split(',')*.trim().findAll() as Set<String> })
+                }
+            }
+        }
+
+        if (project.plugins.hasPlugin('java')) {
+            applyAndConfigure.run()
+        } else {
+            project.plugins.withId('java') {
+                applyAndConfigure.run()
+            }
+        }
+    }
+
+    private static boolean isPitestTaskRequested(Project project) {
+        project.gradle.startParameter.taskNames.any { String requested ->
+            requested == 'pitest'
+                    || requested == "${project.path}:pitest"

Review Comment:
   Addressed in 95f25fe0cb: added an end-to-end TestKit specification with Java 
(including handwritten `getProperty`) and `@CompileStatic` Groovy production 
code and a Spock spec that invokes `:selected:pitest` without explicit 
`enablePitest()`, verifies killed mutants in `mutations.xml`, and verifies 
configuration cache reuse on repeat invocation.



##########
.agents/skills/mutation-testing/SKILL.md:
##########
@@ -0,0 +1,134 @@
+---
+name: mutation-testing
+description: Guide for running targeted PITest bytecode mutation testing and 
verifying Java/Groovy semantic mutants across grails-core modules
+license: Apache-2.0
+---
+<!--
+SPDX-License-Identifier: Apache-2.0
+
+Licensed to the Apache Software Foundation (ASF) under one or more contributor 
license agreements; and to You under the Apache License, Version 2.0.
+-->
+
+## What I Do
+
+- Guide targeted bytecode mutation testing on Java and `@CompileStatic` Groovy 
classes using PITest (`GrailsCodeAnalysisPlugin`).
+- Provide a structured **Java & Groovy Semantic Mutation Checklist** for 
catching high-risk Groovy/Java behavioral regressions that line coverage 
(JaCoCo) and bytecode mutators miss.
+- Enforce assertion depth in Spock specifications so surviving mutants caused 
by smoke-only assertions (e.g. `result != null`, `predicates.length == 1`) are 
eliminated before committing.
+
+## When to Use Me
+
+Activate this skill when:
+
+- Writing, refactoring, or fixing production classes in any `grails-core` 
module and verifying that the accompanying Spock tests actually kill behavioral 
mutants.
+- Converting Java source files to statically-compiled Groovy 
(`java-to-groovy-conversion`).
+- Auditing existing test suites where JaCoCo line coverage is high but 
assertions only check non-nullness or collection sizes.
+
+---
+
+## Tier 1: Targeted PITest Execution (Bytecode Mutation)
+
+`GrailsCodeAnalysisPlugin` integrates `info.solidsoft.pitest` as an opt-in 
analysis tool with Groovy synthetic bytecode exclusions pre-configured.
+
+> [!IMPORTANT]
+> Always scope PITest runs to the specific class(es) and test spec(s) you 
touched using `-Pgrails.code-analysis.pitest.targetClasses` and 
`-Pgrails.code-analysis.pitest.targetTests`. Running unscoped mutation testing 
across an entire module can take tens of minutes.
+
+### Quick Commands
+
+```bash
+# Run PITest on a single class and spec (requesting :pitest auto-enables the 
plugin for that module)
+./gradlew :grails-data-hibernate7-core:pitest \
+  
-Pgrails.code-analysis.pitest.targetClasses=org.grails.orm.hibernate.query.PropertyCriterionHandler
 \
+  
-Pgrails.code-analysis.pitest.targetTests=grails.gorm.tests.hibernatequery.PredicateGeneratorSpec
+
+# Run PITest on multiple classes/wildcards and multiple specs
+./gradlew :grails-data-hibernate7-core:pitest \
+  
-Pgrails.code-analysis.pitest.targetClasses="org.grails.orm.hibernate.query.*CriterionHandler"
 \
+  
-Pgrails.code-analysis.pitest.targetTests="grails.gorm.tests.hibernatequery.PredicateGeneratorSpec,grails.orm.HibernateCriteriaBuilderDirectSpec"
+
+# Override mutator group (e.g. STRONGER, DEFAULTS, ALL) or thread count
+./gradlew :grails-core:pitest \
+  
-Pgrails.code-analysis.pitest.targetClasses="org.grails.core.DefaultGrailsApplication"
 \
+  
-Pgrails.code-analysis.pitest.targetTests="org.grails.core.DefaultGrailsApplicationSpec"
 \

Review Comment:
   Addressed in 95f25fe0cb: updated quick command examples to use existing 
classes and matching spec packages 
(`org.grails.orm.hibernate.query.PredicateGenerator` and 
`grails.core.DefaultGrailsApplication` / 
`grails.core.DefaultGrailsApplicationSpec`).



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to