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


##########
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:
   CLI/property activation configures PITest before module scripts assign 
`group` (for example, `grails-core/build.gradle:38`), so this branch skips the 
`org.grails.*` and `grails.*` defaults. The upstream group-based default then 
misses legacy-package framework classes, and an unscoped run can succeed 
without mutating them. Always install the namespace fallbacks and evaluate the 
group-dependent pattern lazily.



##########
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:
   These exclusions match method names in every target class, including 
handwritten methods such as `DefaultGrailsApplication.invokeMethod` and 
`getProperty` 
(`grails-core/src/main/groovy/grails/core/DefaultGrailsApplication.java:659,717`).
 That production behavior is never mutated, even when explicitly targeted. 
Remove these blanket exclusions or replace them with a generated-bytecode 
filter, and update the exclusion list in the skill accordingly.



##########
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:
   The added tests only list tasks or inspect extension values, so 
auto-enablement and Spock discovery can fail while all new tests pass. Add a 
small TestKit fixture with Java/static Groovy production code and Spock tests, 
invoke scoped `:selected:pitest` without explicit opt-in, and assert a killed 
mutant in the XML report. Repeat the invocation with the configuration cache to 
verify reuse.



##########
.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:
   The quick commands use nonexistent or mismatched targets: 
`PropertyCriterionHandler` is absent, `*CriterionHandler` matches an interface 
rather than its implementation, and `DefaultGrailsApplication` and its spec 
belong to `grails.core`. These examples can select no executable mutation 
targets, with `failWhenNoMutations=false` concealing the mistake. Use the 
existing production classes and matching spec packages in all three examples.



##########
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:
   When this property is absent, gradle-pitest 1.19.0 defaults `targetTests` to 
`targetClasses`, not to all module tests as the skill documents. Thus setting 
only `targetClasses=grails.core.DefaultGrailsApplication` also filters tests to 
that exact production-class name and excludes `DefaultGrailsApplicationSpec`, 
yielding no useful test coverage. Set an explicit all-tests default 
independently of the production-class selector.



-- 
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