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

borinquenkid pushed a commit to branch chore/lint-proxy-package
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 245557f72453f12e9a433f829340962341ab6eef
Author: Walter Duque de Estrada <[email protected]>
AuthorDate: Sat Aug 15 18:29:31 2026 -0500

    Add unit test coverage for grails-datamapping-core proxy package
    
    GroovyProxyFactory and ProxyInstanceMetaClass had no tests in this
    module despite implementing the core Groovy-based proxy contract used
    outside Hibernate/Neo4j. Also picks up a CodeNarc auto-fix replacing a
    fully-qualified @CompileDynamic annotation with an import.
    
    Co-Authored-By: Claude Sonnet 5 <[email protected]>
---
 .../datastore/gorm/proxy/GroovyProxyFactory.groovy |   5 +-
 .../gorm/proxy/GroovyProxyFactorySpec.groovy       | 148 +++++++++++
 .../gorm/proxy/ProxyInstanceMetaClassSpec.groovy   | 271 +++++++++++++++++++++
 3 files changed, 422 insertions(+), 2 deletions(-)

diff --git 
a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/proxy/GroovyProxyFactory.groovy
 
b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/proxy/GroovyProxyFactory.groovy
index 3b14dc1b0d..7f0ca2e47f 100644
--- 
a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/proxy/GroovyProxyFactory.groovy
+++ 
b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/proxy/GroovyProxyFactory.groovy
@@ -18,6 +18,7 @@
  */
 package org.grails.datastore.gorm.proxy
 
+import groovy.transform.CompileDynamic
 import groovy.transform.CompileStatic
 import org.codehaus.groovy.runtime.HandleMetaClass
 import org.codehaus.groovy.runtime.InvokerHelper
@@ -70,7 +71,7 @@ class GroovyProxyFactory implements ProxyFactory {
         }
     }
 
-    @groovy.transform.CompileDynamic
+    @CompileDynamic
     protected Serializable getIdDynamic(obj) {
         if (obj.respondsTo('getId')) {
             return (Serializable)obj.invokeMethod('getId', null)
@@ -121,7 +122,7 @@ class GroovyProxyFactory implements ProxyFactory {
         return proxy
     }
 
-    @groovy.transform.CompileDynamic
+    @CompileDynamic
     protected void setMetaClassDynamic(Object proxy, MetaClass proxyMc) {
         proxy.setMetaClass(proxyMc)
     }
diff --git 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/proxy/GroovyProxyFactorySpec.groovy
 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/proxy/GroovyProxyFactorySpec.groovy
new file mode 100644
index 0000000000..ded7a2d5d9
--- /dev/null
+++ 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/proxy/GroovyProxyFactorySpec.groovy
@@ -0,0 +1,148 @@
+/*
+ *  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.core.Session
+import org.grails.datastore.mapping.engine.AssociationQueryExecutor
+import org.grails.datastore.mapping.engine.EntityPersister
+import spock.lang.Specification
+
+class GroovyProxyFactorySpec extends Specification {
+
+    GroovyProxyFactory proxyFactory = new GroovyProxyFactory()
+
+    void "createProxy returns an initialized-looking instance whose identifier 
is available without loading"() {
+        given:
+        Session session = Mock(Session)
+        session.getPersister(ProxyFactoryTestDomain) >> null
+        session.getMappingContext() >> null
+
+        when:
+        ProxyFactoryTestDomain proxy = proxyFactory.createProxy(session, 
ProxyFactoryTestDomain, 42L)
+
+        then:
+        proxyFactory.isProxy(proxy)
+        proxyFactory.getIdentifier(proxy) == 42L
+        !proxyFactory.isInitialized(proxy)
+        0 * session.retrieve(_, _)
+    }
+
+    void "createProxy uses the session's persister to set the object 
identifier when available"() {
+        given:
+        Session session = Mock(Session)
+        EntityPersister persister = Mock(EntityPersister)
+        session.getPersister(ProxyFactoryTestDomain) >> persister
+
+        when:
+        ProxyFactoryTestDomain proxy = proxyFactory.createProxy(session, 
ProxyFactoryTestDomain, 99L)
+
+        then:
+        1 * persister.setObjectIdentifier(_, 99L)
+        proxyFactory.isProxy(proxy)
+    }
+
+    void "unwrap loads and returns the target for a proxy, caching it as 
initialized"() {
+        given:
+        Session session = Mock(Session)
+        session.getPersister(ProxyFactoryTestDomain) >> null
+        ProxyFactoryTestDomain target = new ProxyFactoryTestDomain(id: 7L, 
name: 'loaded')
+        ProxyFactoryTestDomain proxy = proxyFactory.createProxy(session, 
ProxyFactoryTestDomain, 7L)
+
+        when:
+        Object result = proxyFactory.unwrap(proxy)
+
+        then:
+        1 * session.retrieve(ProxyFactoryTestDomain, 7L) >> target
+        result.is(target)
+
+        and: 'the proxy is now considered initialized without a second 
retrieve'
+        proxyFactory.isInitialized(proxy)
+        0 * session.retrieve(_, _)
+    }
+
+    void "unwrap returns the object unchanged when it is not a proxy"() {
+        given:
+        ProxyFactoryTestDomain plain = new ProxyFactoryTestDomain(id: 1L)
+
+        expect:
+        proxyFactory.unwrap(plain).is(plain)
+        !proxyFactory.isProxy(plain)
+        proxyFactory.isInitialized(plain)
+    }
+
+    void "getIdentifier falls back to invoking getId() on a non-proxied 
object"() {
+        given:
+        ProxyFactoryTestDomain plain = new ProxyFactoryTestDomain(id: 5L)
+
+        expect:
+        proxyFactory.getIdentifier(plain) == 5L
+    }
+
+    void "getProxiedClass returns the runtime class regardless of proxy 
state"() {
+        given:
+        ProxyFactoryTestDomain plain = new ProxyFactoryTestDomain(id: 1L)
+        Session session = Mock(Session)
+        session.getPersister(ProxyFactoryTestDomain) >> null
+        ProxyFactoryTestDomain proxy = proxyFactory.createProxy(session, 
ProxyFactoryTestDomain, 2L)
+
+        expect:
+        proxyFactory.getProxiedClass(plain) == ProxyFactoryTestDomain
+        proxyFactory.getProxiedClass(proxy) == ProxyFactoryTestDomain
+    }
+
+    void "initialize eagerly resolves the proxy target"() {
+        given:
+        Session session = Mock(Session)
+        session.getPersister(ProxyFactoryTestDomain) >> null
+        ProxyFactoryTestDomain target = new ProxyFactoryTestDomain(id: 3L)
+        ProxyFactoryTestDomain proxy = proxyFactory.createProxy(session, 
ProxyFactoryTestDomain, 3L)
+
+        when:
+        proxyFactory.initialize(proxy)
+
+        then:
+        1 * session.retrieve(ProxyFactoryTestDomain, 3L) >> target
+        proxyFactory.isInitialized(proxy)
+    }
+
+    void "association proxies are not supported"() {
+        given:
+        Session session = Mock(Session)
+        AssociationQueryExecutor executor = Mock(AssociationQueryExecutor)
+
+        when:
+        proxyFactory.createProxy(session, executor, 1L)
+
+        then:
+        thrown(UnsupportedOperationException)
+    }
+
+    void "isInitialized(object, associationName) treats a null association as 
initialized"() {
+        given:
+        ProxyFactoryTestDomain owner = new ProxyFactoryTestDomain(id: 1L, 
name: null)
+
+        expect:
+        proxyFactory.isInitialized(owner, 'name')
+    }
+}
+
+class ProxyFactoryTestDomain {
+    Long id
+    String name
+}
diff --git 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/proxy/ProxyInstanceMetaClassSpec.groovy
 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/proxy/ProxyInstanceMetaClassSpec.groovy
new file mode 100644
index 0000000000..f3129010eb
--- /dev/null
+++ 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/proxy/ProxyInstanceMetaClassSpec.groovy
@@ -0,0 +1,271 @@
+/*
+ *  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.core.Session
+import org.springframework.dao.DataIntegrityViolationException
+import spock.lang.Specification
+
+class ProxyInstanceMetaClassSpec extends Specification {
+
+    Session session = Mock(Session)
+    MetaClass delegate = Mock(MetaClass)
+    ProxyInstanceTestTarget target = new ProxyInstanceTestTarget()
+    Object proxy = new Object()
+
+    void setup() {
+        delegate.getTheClass() >> ProxyInstanceTestTarget
+    }
+
+    ProxyInstanceMetaClass newMetaClass() {
+        new ProxyInstanceMetaClass(delegate, session, 11L)
+    }
+
+    void "getKey returns the identifier without resolving the target"() {
+        given:
+        ProxyInstanceMetaClass metaClass = newMetaClass()
+
+        when:
+        Serializable key = metaClass.getKey()
+        boolean initiated = metaClass.isProxyInitiated()
+
+        then:
+        key == 11L
+        !initiated
+        0 * session.retrieve(_, _)
+    }
+
+    void "getProxyTarget lazily loads and caches the target from the 
session"() {
+        given:
+        ProxyInstanceMetaClass metaClass = newMetaClass()
+
+        when:
+        Object first = metaClass.getProxyTarget()
+        Object second = metaClass.getProxyTarget()
+
+        then:
+        1 * session.retrieve(ProxyInstanceTestTarget, 11L) >> target
+        first.is(target)
+        second.is(target)
+        metaClass.isProxyInitiated()
+    }
+
+    void "getProxyTarget throws DataIntegrityViolationException when the 
associated instance no longer exists"() {
+        given:
+        ProxyInstanceMetaClass metaClass = newMetaClass()
+        session.retrieve(ProxyInstanceTestTarget, 11L) >> null
+
+        when:
+        metaClass.getProxyTarget()
+
+        then:
+        thrown(DataIntegrityViolationException)
+    }
+
+    void "invokeMethod handles proxy-aware methods without resolving the 
target"() {
+        given:
+        ProxyInstanceMetaClass metaClass = newMetaClass()
+
+        when:
+        Object isProxyResult = metaClass.invokeMethod(proxy, 'isProxy', [] as 
Object[])
+        Object getIdResult = metaClass.invokeMethod(proxy, 'getId', [] as 
Object[])
+        Object isInitializedResult = metaClass.invokeMethod(proxy, 
'isInitialized', [] as Object[])
+        Object getMetaClassResult = metaClass.invokeMethod(proxy, 
'getMetaClass', [] as Object[])
+
+        then:
+        isProxyResult == true
+        getIdResult == 11L
+        isInitializedResult == false
+        getMetaClassResult.is(metaClass)
+        0 * session.retrieve(_, _)
+    }
+
+    void "invokeMethod resolves the target and delegates for 
getTarget/initialize"() {
+        given:
+        ProxyInstanceMetaClass metaClass = newMetaClass()
+        session.retrieve(ProxyInstanceTestTarget, 11L) >> target
+        delegate.invokeMethod(target, methodName, [] as Object[]) >> target
+
+        expect:
+        metaClass.invokeMethod(proxy, methodName, [] as Object[]).is(target)
+
+        where:
+        methodName << ['getTarget', 'initialize']
+    }
+
+    void "invokeMethod delegates other calls against the resolved target"() {
+        given:
+        ProxyInstanceMetaClass metaClass = newMetaClass()
+        session.retrieve(ProxyInstanceTestTarget, 11L) >> target
+        delegate.invokeMethod(target, 'toString', [] as Object[]) >> 'resolved'
+
+        expect:
+        metaClass.invokeMethod(proxy, 'toString', [] as Object[]) == 'resolved'
+    }
+
+    void "invokeMethod for getClass/getDomainClass only resolves once the 
proxy is already initiated"() {
+        given:
+        ProxyInstanceMetaClass metaClass = newMetaClass()
+        delegate.invokeMethod(proxy, methodName, [] as Object[]) >> 
ProxyInstanceTestTarget
+
+        when: 'not yet initiated, so the delegate is called against the 
uninitialized proxy'
+        Object result = metaClass.invokeMethod(proxy, methodName, [] as 
Object[])
+
+        then:
+        result == ProxyInstanceTestTarget
+        0 * session.retrieve(_, _)
+
+        where:
+        methodName << ['getClass', 'getDomainClass']
+    }
+
+    void "invokeMethod for getClass/getDomainClass resolves the target once 
already initiated"() {
+        given:
+        ProxyInstanceMetaClass metaClass = newMetaClass()
+        session.retrieve(ProxyInstanceTestTarget, 11L) >> target
+        metaClass.getProxyTarget()
+        delegate.invokeMethod(target, 'getClass', [] as Object[]) >> 
ProxyInstanceTestTarget
+
+        expect:
+        metaClass.invokeMethod(proxy, 'getClass', [] as Object[]) == 
ProxyInstanceTestTarget
+    }
+
+    void "invokeMethod does not resolve the target for setMetaClass with a 
MetaClass argument"() {
+        given:
+        ProxyInstanceMetaClass metaClass = newMetaClass()
+        MetaClass newMetaClassArg = Mock(MetaClass)
+        delegate.invokeMethod(proxy, 'setMetaClass', [newMetaClassArg] as 
Object[]) >> null
+
+        when:
+        metaClass.invokeMethod(proxy, 'setMetaClass', [newMetaClassArg] as 
Object[])
+
+        then:
+        0 * session.retrieve(_, _)
+    }
+
+    void "getProperty exposes proxy metadata without resolving the target"() {
+        given:
+        ProxyInstanceMetaClass metaClass = newMetaClass()
+
+        when:
+        Object idResult = metaClass.getProperty(proxy, 'id')
+        Object proxyResult = metaClass.getProperty(proxy, 'proxy')
+        Object initializedResult = metaClass.getProperty(proxy, 'initialized')
+        Object metaClassResult = metaClass.getProperty(proxy, 'metaClass')
+
+        then:
+        idResult == 11L
+        proxyResult == true
+        initializedResult == false
+        metaClassResult.is(metaClass)
+        0 * session.retrieve(_, _)
+    }
+
+    void "getProperty for class/domainClass only resolves once the proxy is 
already initiated"() {
+        given:
+        ProxyInstanceMetaClass metaClass = newMetaClass()
+        delegate.getProperty(proxy, propertyName) >> ProxyInstanceTestTarget
+
+        when:
+        Object result = metaClass.getProperty(proxy, propertyName)
+
+        then:
+        result == ProxyInstanceTestTarget
+        0 * session.retrieve(_, _)
+
+        where:
+        propertyName << ['class', 'domainClass']
+    }
+
+    void "getProperty resolves the target for regular properties"() {
+        given:
+        ProxyInstanceMetaClass metaClass = newMetaClass()
+        session.retrieve(ProxyInstanceTestTarget, 11L) >> target
+        delegate.getProperty(target, 'name') >> 'resolved-name'
+
+        expect:
+        metaClass.getProperty(proxy, 'name') == 'resolved-name'
+    }
+
+    void "setProperty does not resolve the target when replacing the 
metaClass"() {
+        given:
+        ProxyInstanceMetaClass metaClass = newMetaClass()
+        MetaClass newMetaClassArg = Mock(MetaClass)
+
+        when:
+        metaClass.setProperty(proxy, 'metaClass', newMetaClassArg)
+
+        then:
+        1 * delegate.setProperty(proxy, 'metaClass', newMetaClassArg)
+        0 * session.retrieve(_, _)
+    }
+
+    void "setProperty resolves the target for regular properties"() {
+        given:
+        ProxyInstanceMetaClass metaClass = newMetaClass()
+        session.retrieve(ProxyInstanceTestTarget, 11L) >> target
+
+        when:
+        metaClass.setProperty(proxy, 'name', 'new-name')
+
+        then:
+        1 * delegate.setProperty(target, 'name', 'new-name')
+    }
+
+    void "getAttribute exposes proxy metadata without resolving the target"() {
+        given:
+        ProxyInstanceMetaClass metaClass = newMetaClass()
+
+        when:
+        Object idResult = metaClass.getAttribute(proxy, 'id')
+        Object initializedResult = metaClass.getAttribute(proxy, 'initialized')
+
+        then:
+        idResult == 11L
+        initializedResult == false
+        0 * session.retrieve(_, _)
+    }
+
+    void "getAttribute resolves the target for other attributes"() {
+        given:
+        ProxyInstanceMetaClass metaClass = newMetaClass()
+        session.retrieve(ProxyInstanceTestTarget, 11L) >> target
+        delegate.getAttribute(target, 'name') >> 'resolved-name'
+
+        expect:
+        metaClass.getAttribute(proxy, 'name') == 'resolved-name'
+    }
+
+    void "setAttribute always resolves and delegates to the target"() {
+        given:
+        ProxyInstanceMetaClass metaClass = newMetaClass()
+        session.retrieve(ProxyInstanceTestTarget, 11L) >> target
+
+        when:
+        metaClass.setAttribute(proxy, 'name', 'new-name')
+
+        then:
+        1 * delegate.setAttribute(target, 'name', 'new-name')
+    }
+}
+
+class ProxyInstanceTestTarget {
+    Long id
+    String name
+}

Reply via email to