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 da9531353cd16b1ad1e17e2d6eba1bb3f33fc551 Author: Walter Duque de Estrada <[email protected]> AuthorDate: Thu Aug 13 11:03:18 2026 -0500 Add direct test coverage for AbstractGormASTTransformation's template method AbstractGormASTTransformation sat at 53% coverage despite being the shared base for every GORM annotation-driven AST transform - it was only ever exercised indirectly through subclasses (TenantTransform, RollbackTransform, etc.), all of which only drive its "normal" path: a matching annotation on a not-yet-visited node. Add a spec with a minimal test-only subclass and call the class's own public visit(ASTNode[], SourceUnit) template method directly, covering the two branches no subclass's tests happened to exercise: an annotation that doesn't match the subclass's declared annotation type, and a node that was already visited once (the applied-marker idempotency guard). Also covers getOrder()'s delegation to priority(). Coverage moves 53% -> 61% instruction, 40% -> 68% branch. The one remaining uncovered line (a malformed-astNodes defensive guard) is, like the equivalent guards found elsewhere in this branch's other transform specs, unreachable through any class Groovy's own local-transform dispatch would actually produce. Co-Authored-By: Claude Sonnet 5 <[email protected]> --- .../AbstractGormASTTransformationSpec.groovy | 123 +++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/transform/AbstractGormASTTransformationSpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/transform/AbstractGormASTTransformationSpec.groovy new file mode 100644 index 0000000000..d060cde8da --- /dev/null +++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/transform/AbstractGormASTTransformationSpec.groovy @@ -0,0 +1,123 @@ +/* + * 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.transform + +import groovy.transform.CompileStatic +import groovy.transform.TypeChecked +import org.codehaus.groovy.ast.ASTNode +import org.codehaus.groovy.ast.AnnotatedNode +import org.codehaus.groovy.ast.AnnotationNode +import org.codehaus.groovy.ast.ClassHelper +import org.codehaus.groovy.ast.ClassNode +import org.codehaus.groovy.control.SourceUnit + +import spock.lang.Specification + +/** + * {@code AbstractGormASTTransformation} is only ever exercised indirectly through its concrete + * subclasses (e.g. {@code TenantTransform}, {@code RollbackTransform}), whose tests all drive the class + * through its "normal" path: a matching annotation on a not-yet-visited node. The {@code final visit(ASTNode[], + * SourceUnit)} template method it defines is itself the real, public entry point every subclass inherits + * unchanged, so its own early-return branches - an annotation that doesn't match the subclass's declared + * annotation type, and a node that was already visited once - can be tested directly against a minimal test + * subclass without needing any concrete production subclass's full behavior. + */ +class AbstractGormASTTransformationSpec extends Specification { + + static class RecordingTransformation extends AbstractGormASTTransformation { + + static final Object APPLIED_MARKER = new Object() + + List<ASTNode> visitedNodes = [] + + @Override + void visit(SourceUnit source, AnnotationNode annotationNode, AnnotatedNode annotatedNode) { + visitedNodes << annotatedNode + } + + @Override + protected ClassNode getAnnotationType() { + ClassHelper.make(CompileStatic) + } + + @Override + protected Object getAppliedMarker() { + APPLIED_MARKER + } + + @Override + int priority() { + 7 + } + } + + private static ClassNode newTargetClassNode() { + new ClassNode('com.example.RecordingTarget', 0, ClassHelper.OBJECT_TYPE) + } + + void "visiting a matching, not-yet-applied node runs the subclass visit and marks the node as applied"() { + given: + RecordingTransformation transformation = new RecordingTransformation() + ClassNode targetNode = newTargetClassNode() + AnnotationNode matchingAnnotation = new AnnotationNode(ClassHelper.make(CompileStatic)) + + when: + transformation.visit([matchingAnnotation, targetNode] as ASTNode[], null) + + then: + transformation.visitedNodes == [targetNode] + targetNode.getNodeMetaData(RecordingTransformation.APPLIED_MARKER) == RecordingTransformation.APPLIED_MARKER + } + + void "visiting a class node with an annotation that does not match the subclass's declared annotation type is a no-op"() { + given: + RecordingTransformation transformation = new RecordingTransformation() + ClassNode targetNode = newTargetClassNode() + AnnotationNode mismatchedAnnotation = new AnnotationNode(ClassHelper.make(TypeChecked)) + + when: + transformation.visit([mismatchedAnnotation, targetNode] as ASTNode[], null) + + then: + transformation.visitedNodes.empty + targetNode.getNodeMetaData(RecordingTransformation.APPLIED_MARKER) == null + } + + void "visiting the same node a second time is a no-op because it is already marked as applied"() { + given: + RecordingTransformation transformation = new RecordingTransformation() + ClassNode targetNode = newTargetClassNode() + AnnotationNode matchingAnnotation = new AnnotationNode(ClassHelper.make(CompileStatic)) + + when: + transformation.visit([matchingAnnotation, targetNode] as ASTNode[], null) + transformation.visit([matchingAnnotation, targetNode] as ASTNode[], null) + + then: + transformation.visitedNodes.size() == 1 + } + + void "getOrder delegates to priority"() { + given: + RecordingTransformation transformation = new RecordingTransformation() + + expect: + transformation.getOrder() == 7 + } +}
