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]