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

borinquenkid pushed a commit to branch 
test/document-datamapping-core-transformers
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 3b9909fe3384ef57b16a56fa9abdd36aca423cec
Author: Walter Duque de Estrada <[email protected]>
AuthorDate: Thu Aug 13 11:41:33 2026 -0500

    Fix @Tenant compile-time crash and add coverage for TenantTransform's 
closure-resolver path
    
    Writing a test for @Tenant's closure-based tenant resolution (the one
    branch of TenantTransform#buildDelegatingMethodCall the existing
    CurrentTenant/WithoutTenant specs never exercised) surfaced a real bug:
    supplying a non-closure @Tenant value crashed the compiler with an
    internal NullPointerException wrapped as a GroovyBugError instead of
    a clean compile error.
    
    Root cause: the error path called the inherited
    AbstractASTTransformation#addError(String, ASTNode), which reads from
    that class's own sourceUnit field - never populated anywhere in this
    transform's call chain, since sourceUnit is threaded through as a
    method parameter instead. Fixed by reporting the error directly
    through that parameter's error collector, matching the pattern already
    used elsewhere in this codebase for AST transforms that don't rely on
    the inherited field.
    
    Also add tests for @Tenant applied at both method and class level, the
    now-fixed non-closure error path, getAnnotationType(), and the two
    hasTenantAnnotation branches (a method with @WithoutTenant, and being
    called directly with a bare ClassNode) that weren't reached by any
    existing spec.
    
    Coverage moves 65% -> 95% instruction, 67% -> 82% branch.
    
    Co-Authored-By: Claude Sonnet 5 <[email protected]>
---
 .../multitenancy/transform/TenantTransform.groovy  |   8 +-
 .../multitenancy/TenantTransformSpec.groovy        | 168 +++++++++++++++++++++
 2 files changed, 175 insertions(+), 1 deletion(-)

diff --git 
a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/multitenancy/transform/TenantTransform.groovy
 
b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/multitenancy/transform/TenantTransform.groovy
index 0e6b15ff60..358fe731b9 100644
--- 
a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/multitenancy/transform/TenantTransform.groovy
+++ 
b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/multitenancy/transform/TenantTransform.groovy
@@ -33,6 +33,8 @@ import org.codehaus.groovy.ast.expr.VariableExpression
 import org.codehaus.groovy.ast.stmt.BlockStatement
 import org.codehaus.groovy.control.CompilePhase
 import org.codehaus.groovy.control.SourceUnit
+import org.codehaus.groovy.control.messages.SyntaxErrorMessage
+import org.codehaus.groovy.syntax.SyntaxException
 import org.codehaus.groovy.transform.GroovyASTTransformation
 
 import grails.gorm.multitenancy.CurrentTenant
@@ -136,7 +138,11 @@ class TenantTransform extends 
AbstractDatastoreMethodDecoratingTransformation {
                 return makeDelegatingClosureCall(tenantServiceVar, 'withId', 
args(tenantIdVar), params(param(serializableClassNode, VAR_TENANT_ID)), 
originalMethodCallExpr, variableScope)
             }
             else {
-                addError('@Tenant value should be a closure', annotationNode)
+                sourceUnit.getErrorCollector().addErrorAndContinue(
+                    new SyntaxErrorMessage(new SyntaxException('@Tenant value 
should be a closure',
+                        annotationNode.getLineNumber(), 
annotationNode.getColumnNumber(),
+                        annotationNode.getLastLineNumber(), 
annotationNode.getLastColumnNumber()), sourceUnit)
+                )
                 return makeDelegatingClosureCall(tenantServiceVar, 
'withCurrent', params(param(serializableClassNode, VAR_TENANT_ID)), 
originalMethodCallExpr, variableScope)
             }
         }
diff --git 
a/grails-datamapping-core/src/test/groovy/grails/gorm/annotation/multitenancy/TenantTransformSpec.groovy
 
b/grails-datamapping-core/src/test/groovy/grails/gorm/annotation/multitenancy/TenantTransformSpec.groovy
new file mode 100644
index 0000000000..d2b10d0f51
--- /dev/null
+++ 
b/grails-datamapping-core/src/test/groovy/grails/gorm/annotation/multitenancy/TenantTransformSpec.groovy
@@ -0,0 +1,168 @@
+/*
+ *  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.gorm.annotation.multitenancy
+
+import org.codehaus.groovy.ast.ClassNode
+import org.codehaus.groovy.ast.MethodNode
+import org.codehaus.groovy.ast.ModuleNode
+import org.codehaus.groovy.control.CompilationUnit
+import org.codehaus.groovy.control.MultipleCompilationErrorsException
+import org.codehaus.groovy.control.Phases
+
+import spock.lang.Specification
+
+import org.grails.datastore.gorm.multitenancy.transform.TenantTransform
+
+/**
+ * {@link CurrentTenantTransformSpec} only exercises {@code @CurrentTenant} and
+ * {@code @WithoutTenant}, which both take {@code 
TenantTransform#buildDelegatingMethodCall}'s first
+ * two branches. The third branch - the actual {@code @Tenant} annotation, 
which resolves the tenant
+ * id from a closure supplied as the annotation value - and its own error path 
(a non-closure value)
+ * were unexercised. These specs cover that branch directly, plus the small 
standalone
+ * {@code getAnnotationType()}/{@code hasTenantAnnotation} surface not reached 
by any existing spec.
+ */
+class TenantTransformSpec extends Specification {
+
+    void "test @Tenant with a closure value transforms a method to resolve the 
tenant id from the closure"() {
+        given: 'a service with @Tenant applied at the method level with a 
closure tenant resolver'
+        def bookService = new GroovyShell().evaluate('''
+import grails.gorm.multitenancy.Tenant
+
+class BookService {
+    @Tenant({ "someTenant" })
+    List listBooks() {
+        return ["The Stand"]
+    }
+}
+new BookService()
+
+''')
+        when: 'the list books method is invoked'
+        bookService.listBooks()
+
+        then: 'an exception was thrown because GORM is not setup, proving the 
delegating call was generated and reached'
+        thrown(IllegalStateException)
+    }
+
+    void "test @Tenant applied at the class level transforms every method the 
same way"() {
+        given: 'a service with @Tenant applied at the class level with a 
closure tenant resolver'
+        def bookService = new GroovyShell().evaluate('''
+import grails.gorm.multitenancy.Tenant
+
+@Tenant({ "someTenant" })
+class BookService {
+    List listBooks() {
+        return ["The Stand"]
+    }
+}
+new BookService()
+
+''')
+        when:
+        bookService.listBooks()
+
+        then:
+        thrown(IllegalStateException)
+    }
+
+    void "test @Tenant with a non-closure value fails to compile"() {
+        when:
+        new GroovyClassLoader().parseClass('''
+import grails.gorm.multitenancy.Tenant
+
+class BookService {
+    @Tenant(String)
+    List listBooks() {
+        return ["The Stand"]
+    }
+}
+''')
+
+        then:
+        MultipleCompilationErrorsException e = thrown()
+        e.message.contains('@Tenant value should be a closure')
+    }
+
+    void "getAnnotationType returns the @Tenant annotation type"() {
+        expect:
+        new TenantTransform().getAnnotationType() == 
TenantTransform.TENANT_ANNOTATION_TYPE
+    }
+
+    void "hasTenantAnnotation returns false for a method annotated with 
@WithoutTenant even though the class is annotated with @CurrentTenant"() {
+        given:
+        MethodNode methodNode = compileAndFindMethod('''
+import grails.gorm.multitenancy.CurrentTenant
+import grails.gorm.multitenancy.WithoutTenant
+
+@CurrentTenant
+class BookService {
+    @WithoutTenant
+    List listBooks() {
+        return []
+    }
+}
+''', 'BookService', 'listBooks')
+
+        expect:
+        !TenantTransform.hasTenantAnnotation(methodNode)
+    }
+
+    void "hasTenantAnnotation returns true when called directly with a class 
node carrying @Tenant"() {
+        given:
+        ClassNode classNode = compileAndFindClass('''
+import grails.gorm.multitenancy.Tenant
+
+@Tenant({ "someTenant" })
+class BookService {
+    List listBooks() {
+        return []
+    }
+}
+''', 'BookService')
+
+        expect:
+        TenantTransform.hasTenantAnnotation(classNode)
+    }
+
+    void "hasTenantAnnotation returns false for a plain class node with no 
tenant annotations"() {
+        given:
+        ClassNode classNode = compileAndFindClass('''
+class PlainService {
+    List listBooks() {
+        return []
+    }
+}
+''', 'PlainService')
+
+        expect:
+        !TenantTransform.hasTenantAnnotation(classNode)
+    }
+
+    private static ClassNode compileAndFindClass(String source, String 
className) {
+        CompilationUnit unit = new CompilationUnit(new GroovyClassLoader())
+        def sourceUnit = unit.addSource('Source.groovy', source)
+        unit.compile(Phases.CANONICALIZATION)
+        ModuleNode moduleNode = sourceUnit.getAST()
+        (ClassNode) moduleNode.classes.find { ClassNode cn -> cn.name == 
className }
+    }
+
+    private static MethodNode compileAndFindMethod(String source, String 
className, String methodName) {
+        compileAndFindClass(source, className).methods.find { it.name == 
methodName }
+    }
+}

Reply via email to