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]