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

borinquenkid pushed a commit to branch chore/clean-grails-datamapping-support
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit fcb7ac8f2d7711b86afddb26918e2bb6777aa066
Author: Walter Duque de Estrada <[email protected]>
AuthorDate: Fri Aug 21 13:55:53 2026 -0500

    Add missing test coverage for grails-datamapping-support and dedupe 
ConfigSupport usage
    
    Move GrailsVersionSpec to grails-datastore-core, where GrailsVersion 
actually
    lives, and make its current-version assertions hermetic instead of relying 
on
    an incidental transitive dependency on grails-bootstrap. Add specs for
    previously-untested classes in grails-datamapping-support: ConfigSupport,
    AggregatePersistenceContextInterceptor, EntityProxyHandlerAdapter,
    ProxyHandlerAdapter, and GormTransformer (including its @Canonical rejection
    and getKnownEntityNames).
    
    Replace the hibernate5/hibernate7 HibernateGrailsPlugin's inlined copy of
    ConfigSupport.prepareConfig's config-conversion logic with a call to the
    shared method, removing a duplicated code fragment. Add an integration spec
    per module driving doWithSpring() through the real plugin-manager invocation
    path to verify both the Hibernate bean wiring and the config conversion.
    
    Co-Authored-By: Claude Sonnet 5 <[email protected]>
---
 .../plugin/hibernate/HibernateGrailsPlugin.groovy  |  15 +-
 .../hibernate/HibernateGrailsPluginSpec.groovy     |  87 +++++++++
 .../plugin/hibernate/HibernateGrailsPlugin.groovy  |  15 +-
 .../hibernate/HibernateGrailsPluginSpec.groovy     |  87 +++++++++
 .../compiler/gorm/GormTransformerSpec.groovy       |  90 ++++++++++
 .../gorm/plugin/support/ConfigSupportSpec.groovy   |  55 ++++++
 .../proxy/EntityProxyHandlerAdapterSpec.groovy     | 129 ++++++++++++++
 .../gorm/proxy/ProxyHandlerAdapterSpec.groovy      | 112 ++++++++++++
 ...gregatePersistenceContextInterceptorSpec.groovy | 196 +++++++++++++++++++++
 .../core/grailsversion/GrailsVersionSpec.groovy    |  14 +-
 10 files changed, 764 insertions(+), 36 deletions(-)

diff --git 
a/grails-data-hibernate5/grails-plugin/src/main/groovy/grails/plugin/hibernate/HibernateGrailsPlugin.groovy
 
b/grails-data-hibernate5/grails-plugin/src/main/groovy/grails/plugin/hibernate/HibernateGrailsPlugin.groovy
index 366da3bc46..fb23f13cfc 100644
--- 
a/grails-data-hibernate5/grails-plugin/src/main/groovy/grails/plugin/hibernate/HibernateGrailsPlugin.groovy
+++ 
b/grails-data-hibernate5/grails-plugin/src/main/groovy/grails/plugin/hibernate/HibernateGrailsPlugin.groovy
@@ -23,8 +23,6 @@ import groovy.transform.CompileStatic
 
 import org.springframework.beans.factory.support.BeanDefinitionRegistry
 import org.springframework.context.ConfigurableApplicationContext
-import org.springframework.core.convert.converter.Converter
-import org.springframework.core.convert.support.ConfigurableConversionService
 import org.springframework.core.env.PropertyResolver
 
 import grails.config.Config
@@ -33,8 +31,8 @@ import grails.core.GrailsClass
 import grails.orm.bootstrap.HibernateDatastoreSpringInitializer
 import grails.plugins.Plugin
 import grails.util.Environment
-import org.grails.config.PropertySourcesConfig
 import org.grails.core.artefact.DomainClassArtefactHandler
+import org.grails.datastore.gorm.plugin.support.ConfigSupport
 
 /**
  * Plugin that integrates Hibernate into a Grails application
@@ -72,16 +70,7 @@ class HibernateGrailsPlugin extends Plugin {
 
             GrailsApplication grailsApplication = grailsApplication
             Config config = grailsApplication.config
-            if (config instanceof PropertySourcesConfig) {
-                ConfigurableConversionService conversionService = 
applicationContext.getEnvironment().getConversionService()
-                conversionService.addConverter(new Converter<String, Class>() {
-                    @Override
-                    Class convert(String source) {
-                        Class.forName(source)
-                    }
-                })
-                ((PropertySourcesConfig) 
config).setConversionService(conversionService)
-            }
+            ConfigSupport.prepareConfig(config, applicationContext)
 
             def domainClasses = 
grailsApplication.getArtefacts(DomainClassArtefactHandler.TYPE)
                                                  .collect() { GrailsClass cls 
-> cls.clazz }
diff --git 
a/grails-data-hibernate5/grails-plugin/src/test/groovy/grails/plugin/hibernate/HibernateGrailsPluginSpec.groovy
 
b/grails-data-hibernate5/grails-plugin/src/test/groovy/grails/plugin/hibernate/HibernateGrailsPluginSpec.groovy
new file mode 100644
index 0000000000..f0b43e8711
--- /dev/null
+++ 
b/grails-data-hibernate5/grails-plugin/src/test/groovy/grails/plugin/hibernate/HibernateGrailsPluginSpec.groovy
@@ -0,0 +1,87 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package grails.plugin.hibernate
+
+import grails.core.DefaultGrailsApplication
+import grails.gorm.annotation.Entity
+import org.grails.config.PropertySourcesConfig
+import org.grails.plugins.DefaultGrailsPlugin
+import org.grails.spring.DefaultRuntimeSpringConfiguration
+import org.grails.spring.RuntimeSpringConfiguration
+import org.hibernate.SessionFactory
+import org.hibernate.dialect.H2Dialect
+import org.springframework.context.support.GenericApplicationContext
+import org.springframework.transaction.PlatformTransactionManager
+import spock.lang.AutoCleanup
+import spock.lang.Specification
+
+/**
+ * Drives {@link HibernateGrailsPlugin#doWithSpring()} the same way the real 
{@code GrailsPluginManager} does: via
+ * {@link org.grails.plugins.DefaultGrailsPlugin#doWithRuntimeConfiguration}, 
followed by merging the accumulated
+ * bean definitions into a fresh {@link GenericApplicationContext} and 
refreshing it - exactly what
+ * {@link RuntimeSpringConfiguration#registerBeansWithContext} exists for, and 
how a real Grails Boot application
+ * merges plugin-registered beans into the application's own context. 
Refreshing the plugin's own internal
+ * {@code GrailsApplicationContext} directly doesn't work here: it renames 
Spring's "environment" bean to
+ * "springEnvironment" (a long-standing workaround for GRAILS-7851) which 
breaks the SpEL datasource expressions
+ * that {@code HibernateDatastoreConnectionSourcesRegistrar} registers.
+ */
+class HibernateGrailsPluginSpec extends Specification {
+
+    @AutoCleanup
+    GenericApplicationContext applicationContext
+
+    void "doWithSpring registers the Hibernate beans and prepares the config 
for class conversion"() {
+        given: "a Grails application with one domain class and an H2 test 
datasource"
+        def app = new DefaultGrailsApplication([PluginSpecBook] as Class[], 
getClass().classLoader)
+        app.initialise()
+        def config = new PropertySourcesConfig((Map<String, Object>) [
+                'dataSource.url'        : 
'jdbc:h2:mem:hibernateGrailsPluginSpec;LOCK_TIMEOUT=10000',
+                'dataSource.dialect'    : H2Dialect.name,
+                'hibernate.hbm2ddl.auto': 'create-drop',
+                'some.class'            : PluginSpecBook.name
+        ])
+        app.setConfig(config)
+
+        and: "the plugin driven the same way the real GrailsPluginManager 
drives it"
+        def grailsPlugin = new DefaultGrailsPlugin(HibernateGrailsPlugin, app)
+        RuntimeSpringConfiguration springConfig = new 
DefaultRuntimeSpringConfiguration()
+        grailsPlugin.applicationContext = 
springConfig.unrefreshedApplicationContext
+
+        expect: "the class value cannot be resolved before doWithSpring runs"
+        config.getProperty('some.class', Class) == null
+
+        when: "the plugin is configured, then its beans merged into the 
application's real context"
+        grailsPlugin.doWithRuntimeConfiguration(springConfig)
+        applicationContext = new GenericApplicationContext()
+        springConfig.registerBeansWithContext(applicationContext)
+        applicationContext.refresh()
+
+        then: "the Hibernate beans registered by the plugin are present"
+        applicationContext.getBean('sessionFactory', 
SessionFactory).metamodel.entities.size() == 1
+        applicationContext.getBean(PlatformTransactionManager)
+
+        and: "the config can now resolve a String value as a Class"
+        config.getProperty('some.class', Class) == PluginSpecBook
+    }
+}
+
+@Entity
+class PluginSpecBook {
+    String title
+}
diff --git 
a/grails-data-hibernate7/grails-plugin/src/main/groovy/grails/plugin/hibernate/HibernateGrailsPlugin.groovy
 
b/grails-data-hibernate7/grails-plugin/src/main/groovy/grails/plugin/hibernate/HibernateGrailsPlugin.groovy
index b758e72302..427d5da6a5 100644
--- 
a/grails-data-hibernate7/grails-plugin/src/main/groovy/grails/plugin/hibernate/HibernateGrailsPlugin.groovy
+++ 
b/grails-data-hibernate7/grails-plugin/src/main/groovy/grails/plugin/hibernate/HibernateGrailsPlugin.groovy
@@ -23,8 +23,6 @@ import groovy.transform.CompileStatic
 
 import org.springframework.beans.factory.support.BeanDefinitionRegistry
 import org.springframework.context.ConfigurableApplicationContext
-import org.springframework.core.convert.converter.Converter
-import org.springframework.core.convert.support.ConfigurableConversionService
 import org.springframework.core.env.PropertyResolver
 
 import grails.config.Config
@@ -33,8 +31,8 @@ import grails.core.GrailsClass
 import grails.orm.bootstrap.HibernateDatastoreSpringInitializer
 import grails.plugins.Plugin
 import grails.util.Environment
-import org.grails.config.PropertySourcesConfig
 import org.grails.core.artefact.DomainClassArtefactHandler
+import org.grails.datastore.gorm.plugin.support.ConfigSupport
 
 /**
  * Plugin that integrates Hibernate into a Grails application
@@ -72,16 +70,7 @@ class HibernateGrailsPlugin extends Plugin {
 
             GrailsApplication grailsApplication = grailsApplication
             Config config = grailsApplication.config
-            if (config instanceof PropertySourcesConfig) {
-                ConfigurableConversionService conversionService = 
applicationContext.getEnvironment().getConversionService()
-                conversionService.addConverter(new Converter<String, Class>() {
-                    @Override
-                    Class convert(String source) {
-                        Class.forName(source)
-                    }
-                })
-                ((PropertySourcesConfig) 
config).setConversionService(conversionService)
-            }
+            ConfigSupport.prepareConfig(config, applicationContext)
 
             def domainClasses = 
grailsApplication.getArtefacts(DomainClassArtefactHandler.TYPE)
                                                  .collect() { GrailsClass cls 
-> cls.clazz }
diff --git 
a/grails-data-hibernate7/grails-plugin/src/test/groovy/grails/plugin/hibernate/HibernateGrailsPluginSpec.groovy
 
b/grails-data-hibernate7/grails-plugin/src/test/groovy/grails/plugin/hibernate/HibernateGrailsPluginSpec.groovy
new file mode 100644
index 0000000000..f0b43e8711
--- /dev/null
+++ 
b/grails-data-hibernate7/grails-plugin/src/test/groovy/grails/plugin/hibernate/HibernateGrailsPluginSpec.groovy
@@ -0,0 +1,87 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package grails.plugin.hibernate
+
+import grails.core.DefaultGrailsApplication
+import grails.gorm.annotation.Entity
+import org.grails.config.PropertySourcesConfig
+import org.grails.plugins.DefaultGrailsPlugin
+import org.grails.spring.DefaultRuntimeSpringConfiguration
+import org.grails.spring.RuntimeSpringConfiguration
+import org.hibernate.SessionFactory
+import org.hibernate.dialect.H2Dialect
+import org.springframework.context.support.GenericApplicationContext
+import org.springframework.transaction.PlatformTransactionManager
+import spock.lang.AutoCleanup
+import spock.lang.Specification
+
+/**
+ * Drives {@link HibernateGrailsPlugin#doWithSpring()} the same way the real 
{@code GrailsPluginManager} does: via
+ * {@link org.grails.plugins.DefaultGrailsPlugin#doWithRuntimeConfiguration}, 
followed by merging the accumulated
+ * bean definitions into a fresh {@link GenericApplicationContext} and 
refreshing it - exactly what
+ * {@link RuntimeSpringConfiguration#registerBeansWithContext} exists for, and 
how a real Grails Boot application
+ * merges plugin-registered beans into the application's own context. 
Refreshing the plugin's own internal
+ * {@code GrailsApplicationContext} directly doesn't work here: it renames 
Spring's "environment" bean to
+ * "springEnvironment" (a long-standing workaround for GRAILS-7851) which 
breaks the SpEL datasource expressions
+ * that {@code HibernateDatastoreConnectionSourcesRegistrar} registers.
+ */
+class HibernateGrailsPluginSpec extends Specification {
+
+    @AutoCleanup
+    GenericApplicationContext applicationContext
+
+    void "doWithSpring registers the Hibernate beans and prepares the config 
for class conversion"() {
+        given: "a Grails application with one domain class and an H2 test 
datasource"
+        def app = new DefaultGrailsApplication([PluginSpecBook] as Class[], 
getClass().classLoader)
+        app.initialise()
+        def config = new PropertySourcesConfig((Map<String, Object>) [
+                'dataSource.url'        : 
'jdbc:h2:mem:hibernateGrailsPluginSpec;LOCK_TIMEOUT=10000',
+                'dataSource.dialect'    : H2Dialect.name,
+                'hibernate.hbm2ddl.auto': 'create-drop',
+                'some.class'            : PluginSpecBook.name
+        ])
+        app.setConfig(config)
+
+        and: "the plugin driven the same way the real GrailsPluginManager 
drives it"
+        def grailsPlugin = new DefaultGrailsPlugin(HibernateGrailsPlugin, app)
+        RuntimeSpringConfiguration springConfig = new 
DefaultRuntimeSpringConfiguration()
+        grailsPlugin.applicationContext = 
springConfig.unrefreshedApplicationContext
+
+        expect: "the class value cannot be resolved before doWithSpring runs"
+        config.getProperty('some.class', Class) == null
+
+        when: "the plugin is configured, then its beans merged into the 
application's real context"
+        grailsPlugin.doWithRuntimeConfiguration(springConfig)
+        applicationContext = new GenericApplicationContext()
+        springConfig.registerBeansWithContext(applicationContext)
+        applicationContext.refresh()
+
+        then: "the Hibernate beans registered by the plugin are present"
+        applicationContext.getBean('sessionFactory', 
SessionFactory).metamodel.entities.size() == 1
+        applicationContext.getBean(PlatformTransactionManager)
+
+        and: "the config can now resolve a String value as a Class"
+        config.getProperty('some.class', Class) == PluginSpecBook
+    }
+}
+
+@Entity
+class PluginSpecBook {
+    String title
+}
diff --git 
a/grails-datamapping-support/src/test/groovy/org/grails/compiler/gorm/GormTransformerSpec.groovy
 
b/grails-datamapping-support/src/test/groovy/org/grails/compiler/gorm/GormTransformerSpec.groovy
new file mode 100644
index 0000000000..ad6afd1f5f
--- /dev/null
+++ 
b/grails-datamapping-support/src/test/groovy/org/grails/compiler/gorm/GormTransformerSpec.groovy
@@ -0,0 +1,90 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.compiler.gorm
+
+import org.codehaus.groovy.ast.ClassNode
+import org.codehaus.groovy.classgen.GeneratorContext
+import org.codehaus.groovy.control.CompilationFailedException
+import org.codehaus.groovy.control.CompilationUnit
+import org.codehaus.groovy.control.MultipleCompilationErrorsException
+import org.codehaus.groovy.control.Phases
+import org.codehaus.groovy.control.SourceUnit
+import org.grails.core.artefact.DomainClassArtefactHandler
+import spock.lang.Specification
+
+/**
+ * Exercises {@link GormTransformer} the same way the production global 
transform drives it: the injector's
+ * {@code performInjection} is invoked directly on the parsed {@link 
ClassNode} during canonicalization, then
+ * compilation continues to class generation.
+ */
+class GormTransformerSpec extends Specification {
+
+    void "getArtefactTypes returns the domain class artefact type"() {
+        expect:
+        new GormTransformer().artefactTypes == 
[DomainClassArtefactHandler.TYPE] as String[]
+    }
+
+    void "a class marked with @Canonical fails compilation"() {
+        when:
+        compile('CanonicalBook', '''
+            @groovy.transform.Canonical
+            class CanonicalBook {
+                String title
+            }
+        ''')
+
+        then:
+        MultipleCompilationErrorsException e = thrown()
+        e.message.contains('@groovy.transform.Canonical')
+    }
+
+    void "getKnownEntityNames includes a class after performInjection has 
visited it"() {
+        given:
+        String className = 'GormTransformerSpecKnownEntity'
+
+        when:
+        compileToPhase(className, """
+            class ${className} {
+                String name
+            }
+        """, Phases.CANONICALIZATION)
+
+        then:
+        GormTransformer.getKnownEntityNames().contains(className)
+    }
+
+    private void compile(String className, String source) {
+        compileToPhase(className, source, Phases.CLASS_GENERATION)
+    }
+
+    private void compileToPhase(String className, String source, int phase) {
+        CompilationUnit cu = new CompilationUnit(new GroovyClassLoader())
+        cu.addSource(className, source)
+        GormTransformer transformer = new GormTransformer()
+        cu.addPhaseOperation(new CompilationUnit.PrimaryClassNodeOperation() {
+            @Override
+            void call(SourceUnit src, GeneratorContext context, ClassNode cn) 
throws CompilationFailedException {
+                if (cn.nameWithoutPackage == className) {
+                    transformer.performInjection(src, cn)
+                }
+            }
+        }, Phases.CANONICALIZATION)
+        cu.compile(phase)
+    }
+}
diff --git 
a/grails-datamapping-support/src/test/groovy/org/grails/datastore/gorm/plugin/support/ConfigSupportSpec.groovy
 
b/grails-datamapping-support/src/test/groovy/org/grails/datastore/gorm/plugin/support/ConfigSupportSpec.groovy
new file mode 100644
index 0000000000..9b0e127fb0
--- /dev/null
+++ 
b/grails-datamapping-support/src/test/groovy/org/grails/datastore/gorm/plugin/support/ConfigSupportSpec.groovy
@@ -0,0 +1,55 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm.plugin.support
+
+import org.grails.config.PropertySourcesConfig
+import org.springframework.context.support.StaticApplicationContext
+import org.springframework.core.env.PropertyResolver
+import spock.lang.Specification
+
+class ConfigSupportSpec extends Specification {
+
+    void "prepareConfig registers a String to Class converter so config values 
can be resolved as classes"() {
+        given:
+        def config = new PropertySourcesConfig(['some.class': String.name])
+        def applicationContext = new StaticApplicationContext()
+        applicationContext.refresh()
+
+        expect: "the class value cannot be resolved before the config is 
prepared"
+        config.getProperty('some.class', Class) == null
+
+        when:
+        ConfigSupport.prepareConfig(config, applicationContext)
+
+        then:
+        config.getProperty('some.class', Class) == String
+    }
+
+    void "prepareConfig does nothing when the config is not a 
PropertySourcesConfig"() {
+        given:
+        def config = Mock(PropertyResolver)
+        def applicationContext = 
Mock(org.springframework.context.ConfigurableApplicationContext)
+
+        when:
+        ConfigSupport.prepareConfig(config, applicationContext)
+
+        then:
+        0 * applicationContext._
+    }
+}
diff --git 
a/grails-datamapping-support/src/test/groovy/org/grails/datastore/gorm/proxy/EntityProxyHandlerAdapterSpec.groovy
 
b/grails-datamapping-support/src/test/groovy/org/grails/datastore/gorm/proxy/EntityProxyHandlerAdapterSpec.groovy
new file mode 100644
index 0000000000..0c6c6e304d
--- /dev/null
+++ 
b/grails-datamapping-support/src/test/groovy/org/grails/datastore/gorm/proxy/EntityProxyHandlerAdapterSpec.groovy
@@ -0,0 +1,129 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm.proxy
+
+import grails.core.support.proxy.EntityProxyHandler
+import org.grails.datastore.mapping.engine.AssociationQueryExecutor
+import spock.lang.Specification
+
+class EntityProxyHandlerAdapterSpec extends Specification {
+
+    def proxyHandler = Mock(EntityProxyHandler)
+    def adapter = new EntityProxyHandlerAdapter(proxyHandler)
+
+    void "isProxy delegates to the wrapped proxy handler"() {
+        given:
+        def target = new Object()
+
+        when:
+        def result = adapter.isProxy(target)
+
+        then:
+        1 * proxyHandler.isProxy(target) >> true
+        result
+    }
+
+    void "isInitialized delegates to the wrapped proxy handler"() {
+        given:
+        def target = new Object()
+
+        when:
+        def result = adapter.isInitialized(target)
+
+        then:
+        1 * proxyHandler.isInitialized(target) >> true
+        result
+    }
+
+    void "isInitialized with an association name delegates to the wrapped 
proxy handler"() {
+        given:
+        def target = new Object()
+
+        when:
+        def result = adapter.isInitialized(target, "author")
+
+        then:
+        1 * proxyHandler.isInitialized(target, "author") >> false
+        !result
+    }
+
+    void "unwrap delegates to unwrapIfProxy on the wrapped proxy handler"() {
+        given:
+        def proxy = new Object()
+        def unwrapped = new Object()
+
+        when:
+        def result = adapter.unwrap(proxy)
+
+        then:
+        1 * proxyHandler.unwrapIfProxy(proxy) >> unwrapped
+        result.is(unwrapped)
+    }
+
+    void "getIdentifier delegates to getProxyIdentifier on the wrapped proxy 
handler"() {
+        given:
+        def target = new Object()
+
+        when:
+        def result = adapter.getIdentifier(target)
+
+        then:
+        1 * proxyHandler.getProxyIdentifier(target) >> 42L
+        result == 42L
+    }
+
+    void "getProxiedClass delegates to the wrapped proxy handler"() {
+        given:
+        def target = new Object()
+
+        when:
+        def result = adapter.getProxiedClass(target)
+
+        then:
+        1 * proxyHandler.getProxiedClass(target) >> String
+        result == String
+    }
+
+    void "initialize delegates to the wrapped proxy handler"() {
+        given:
+        def target = new Object()
+
+        when:
+        adapter.initialize(target)
+
+        then:
+        1 * proxyHandler.initialize(target)
+    }
+
+    void "createProxy with a session, type and key is not supported"() {
+        when:
+        adapter.createProxy(null, String, "id")
+
+        then:
+        thrown(UnsupportedOperationException)
+    }
+
+    void "createProxy with a session, association query executor and key is 
not supported"() {
+        when:
+        adapter.createProxy(null, (AssociationQueryExecutor) null, "id")
+
+        then:
+        thrown(UnsupportedOperationException)
+    }
+}
diff --git 
a/grails-datamapping-support/src/test/groovy/org/grails/datastore/gorm/proxy/ProxyHandlerAdapterSpec.groovy
 
b/grails-datamapping-support/src/test/groovy/org/grails/datastore/gorm/proxy/ProxyHandlerAdapterSpec.groovy
new file mode 100644
index 0000000000..d05e404405
--- /dev/null
+++ 
b/grails-datamapping-support/src/test/groovy/org/grails/datastore/gorm/proxy/ProxyHandlerAdapterSpec.groovy
@@ -0,0 +1,112 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm.proxy
+
+import org.grails.datastore.mapping.proxy.ProxyHandler
+import spock.lang.Specification
+
+class ProxyHandlerAdapterSpec extends Specification {
+
+    def delegate = Mock(ProxyHandler)
+    def adapter = new ProxyHandlerAdapter(delegate)
+
+    void "getProxyIdentifier delegates to getIdentifier on the wrapped proxy 
handler"() {
+        given:
+        def target = new Object()
+
+        when:
+        def result = adapter.getProxyIdentifier(target)
+
+        then:
+        1 * delegate.getIdentifier(target) >> 42L
+        result == 42L
+    }
+
+    void "getProxiedClass delegates to the wrapped proxy handler"() {
+        given:
+        def target = new Object()
+
+        when:
+        def result = adapter.getProxiedClass(target)
+
+        then:
+        1 * delegate.getProxiedClass(target) >> String
+        result == String
+    }
+
+    void "isProxy delegates to the wrapped proxy handler"() {
+        given:
+        def target = new Object()
+
+        when:
+        def result = adapter.isProxy(target)
+
+        then:
+        1 * delegate.isProxy(target) >> true
+        result
+    }
+
+    void "unwrapIfProxy delegates to unwrap on the wrapped proxy handler"() {
+        given:
+        def proxy = new Object()
+        def unwrapped = new Object()
+
+        when:
+        def result = adapter.unwrapIfProxy(proxy)
+
+        then:
+        1 * delegate.unwrap(proxy) >> unwrapped
+        result.is(unwrapped)
+    }
+
+    void "isInitialized delegates to the wrapped proxy handler"() {
+        given:
+        def target = new Object()
+
+        when:
+        def result = adapter.isInitialized(target)
+
+        then:
+        1 * delegate.isInitialized(target) >> true
+        result
+    }
+
+    void "initialize delegates to the wrapped proxy handler"() {
+        given:
+        def target = new Object()
+
+        when:
+        adapter.initialize(target)
+
+        then:
+        1 * delegate.initialize(target)
+    }
+
+    void "isInitialized with an association name delegates to the wrapped 
proxy handler"() {
+        given:
+        def target = new Object()
+
+        when:
+        def result = adapter.isInitialized(target, "author")
+
+        then:
+        1 * delegate.isInitialized(target, "author") >> false
+        !result
+    }
+}
diff --git 
a/grails-datamapping-support/src/test/groovy/org/grails/datastore/gorm/support/AggregatePersistenceContextInterceptorSpec.groovy
 
b/grails-datamapping-support/src/test/groovy/org/grails/datastore/gorm/support/AggregatePersistenceContextInterceptorSpec.groovy
new file mode 100644
index 0000000000..ef9276d253
--- /dev/null
+++ 
b/grails-datamapping-support/src/test/groovy/org/grails/datastore/gorm/support/AggregatePersistenceContextInterceptorSpec.groovy
@@ -0,0 +1,196 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm.support
+
+import grails.persistence.support.PersistenceContextInterceptor
+import spock.lang.Specification
+
+class AggregatePersistenceContextInterceptorSpec extends Specification {
+
+    void "isOpen returns false when there are no interceptors"() {
+        given:
+        def interceptor = new AggregatePersistenceContextInterceptor([])
+
+        expect:
+        !interceptor.isOpen()
+    }
+
+    void "isOpen returns true if at least one interceptor is open"() {
+        given:
+        def closed = Mock(PersistenceContextInterceptor) {
+            isOpen() >> false
+        }
+        def open = Mock(PersistenceContextInterceptor) {
+            isOpen() >> true
+        }
+        def interceptor = new AggregatePersistenceContextInterceptor([closed, 
open])
+
+        expect:
+        interceptor.isOpen()
+    }
+
+    void "isOpen returns false when every interceptor is closed"() {
+        given:
+        def first = Mock(PersistenceContextInterceptor) {
+            isOpen() >> false
+        }
+        def second = Mock(PersistenceContextInterceptor) {
+            isOpen() >> false
+        }
+        def interceptor = new AggregatePersistenceContextInterceptor([first, 
second])
+
+        expect:
+        !interceptor.isOpen()
+    }
+
+    void "destroy only destroys interceptors that are open"() {
+        given:
+        def open = Mock(PersistenceContextInterceptor) {
+            isOpen() >> true
+        }
+        def closed = Mock(PersistenceContextInterceptor) {
+            isOpen() >> false
+        }
+        def interceptor = new AggregatePersistenceContextInterceptor([open, 
closed])
+
+        when:
+        interceptor.destroy()
+
+        then:
+        1 * open.destroy()
+        0 * closed.destroy()
+    }
+
+    void "destroy swallows an exception from one interceptor and still 
destroys the rest"() {
+        given:
+        def failing = Mock(PersistenceContextInterceptor) {
+            isOpen() >> true
+            destroy() >> { throw new RuntimeException("boom") }
+        }
+        def healthy = Mock(PersistenceContextInterceptor) {
+            isOpen() >> true
+        }
+        def interceptor = new AggregatePersistenceContextInterceptor([failing, 
healthy])
+
+        when:
+        interceptor.destroy()
+
+        then:
+        noExceptionThrown()
+        1 * healthy.destroy()
+    }
+
+    void "reconnect delegates to every interceptor"() {
+        given:
+        def first = Mock(PersistenceContextInterceptor)
+        def second = Mock(PersistenceContextInterceptor)
+        def interceptor = new AggregatePersistenceContextInterceptor([first, 
second])
+
+        when:
+        interceptor.reconnect()
+
+        then:
+        1 * first.reconnect()
+        1 * second.reconnect()
+    }
+
+    void "clear delegates to every interceptor"() {
+        given:
+        def first = Mock(PersistenceContextInterceptor)
+        def second = Mock(PersistenceContextInterceptor)
+        def interceptor = new AggregatePersistenceContextInterceptor([first, 
second])
+
+        when:
+        interceptor.clear()
+
+        then:
+        1 * first.clear()
+        1 * second.clear()
+    }
+
+    void "disconnect delegates to every interceptor"() {
+        given:
+        def first = Mock(PersistenceContextInterceptor)
+        def second = Mock(PersistenceContextInterceptor)
+        def interceptor = new AggregatePersistenceContextInterceptor([first, 
second])
+
+        when:
+        interceptor.disconnect()
+
+        then:
+        1 * first.disconnect()
+        1 * second.disconnect()
+    }
+
+    void "flush delegates to every interceptor"() {
+        given:
+        def first = Mock(PersistenceContextInterceptor)
+        def second = Mock(PersistenceContextInterceptor)
+        def interceptor = new AggregatePersistenceContextInterceptor([first, 
second])
+
+        when:
+        interceptor.flush()
+
+        then:
+        1 * first.flush()
+        1 * second.flush()
+    }
+
+    void "init delegates to every interceptor"() {
+        given:
+        def first = Mock(PersistenceContextInterceptor)
+        def second = Mock(PersistenceContextInterceptor)
+        def interceptor = new AggregatePersistenceContextInterceptor([first, 
second])
+
+        when:
+        interceptor.init()
+
+        then:
+        1 * first.init()
+        1 * second.init()
+    }
+
+    void "setReadOnly delegates to every interceptor"() {
+        given:
+        def first = Mock(PersistenceContextInterceptor)
+        def second = Mock(PersistenceContextInterceptor)
+        def interceptor = new AggregatePersistenceContextInterceptor([first, 
second])
+
+        when:
+        interceptor.setReadOnly()
+
+        then:
+        1 * first.setReadOnly()
+        1 * second.setReadOnly()
+    }
+
+    void "setReadWrite delegates to every interceptor"() {
+        given:
+        def first = Mock(PersistenceContextInterceptor)
+        def second = Mock(PersistenceContextInterceptor)
+        def interceptor = new AggregatePersistenceContextInterceptor([first, 
second])
+
+        when:
+        interceptor.setReadWrite()
+
+        then:
+        1 * first.setReadWrite()
+        1 * second.setReadWrite()
+    }
+}
diff --git 
a/grails-datamapping-support/src/test/groovy/org/grails/datastore/mapping/core/grailsversion/GrailsVersionSpec.groovy
 
b/grails-datastore-core/src/test/groovy/org/grails/datastore/mapping/core/grailsversion/GrailsVersionSpec.groovy
similarity index 92%
rename from 
grails-datamapping-support/src/test/groovy/org/grails/datastore/mapping/core/grailsversion/GrailsVersionSpec.groovy
rename to 
grails-datastore-core/src/test/groovy/org/grails/datastore/mapping/core/grailsversion/GrailsVersionSpec.groovy
index 05deff071f..c383b9ceb4 100644
--- 
a/grails-datamapping-support/src/test/groovy/org/grails/datastore/mapping/core/grailsversion/GrailsVersionSpec.groovy
+++ 
b/grails-datastore-core/src/test/groovy/org/grails/datastore/mapping/core/grailsversion/GrailsVersionSpec.groovy
@@ -26,17 +26,11 @@ import spock.lang.Unroll
  */
 class GrailsVersionSpec extends Specification {
 
-    @Unroll
-    void "isAtLeast(#requiredVersion) => #expected"(String requiredVersion, 
boolean expected) {
+    void "isAtLeast(requiredVersion) and isAtLeastMajorMinor(major, minor) 
return false when the current Grails version cannot be resolved"() {
         expect:
-        expected == GrailsVersion.isAtLeast(requiredVersion)
-
-        where:
-        requiredVersion | expected
-        "3.2.0"         | true
-        "3.1.0"         | true
-        "3.3.0"         | true
-        "99.9.9"        | false
+        GrailsVersion.current == null
+        !GrailsVersion.isAtLeast("0.0.1")
+        !GrailsVersion.isAtLeastMajorMinor(0, 1)
     }
 
     @Unroll

Reply via email to