You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by bo...@apache.org on 2026/08/30 22:31:53 UTC
(grails-core) 03/07: Close coverage gaps on DetachedAssociationFunction and IdentityEnumType
This is an automated email from the ASF dual-hosted git repository.
borinquenkid pushed a commit to branch test/grails-data-hibernate7-coverage
in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 87c7e7e822d9c54e2295875d343d15c4dcfb3a31
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Sun Aug 30 16:40:36 2026 -0500
Close coverage gaps on DetachedAssociationFunction and IdentityEnumType
DetachedAssociationFunction (50%) never had a case for the recursive
Query.Junction-flattening branch. IdentityEnumType (65.6%) was missing
its error/fallback paths in setParameterValues (unloadable class,
Class-attribute fallback, missing enumClass), the BIGINT/VARCHAR jdbc
type selection branches, getBidiEnumMap's reflection-failure and
duplicate-id-warning paths, disassemble/assemble, and nullSafeSet's
null branch — all exercised directly through IdentityEnumType's public
UserType/ParameterizedType contract methods.
DetachedAssociationFunction: 50% -> 100%.
IdentityEnumType: 65.6% -> 91.8% (BidiEnumMap: 88.9% -> 100%).
Co-Authored-By: Claude Sonnet 5 <no...@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QAjfckcY4EJsxranv1RyGV
---
.../grails/gorm/tests/IdentityEnumTypeSpec.groovy | 142 +++++++++++++++++++++
.../query/DetachedAssociationFunctionSpec.groovy | 19 +++
2 files changed, 161 insertions(+)
diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/IdentityEnumTypeSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/IdentityEnumTypeSpec.groovy
index 47b6afdb71..656e5e448f 100644
--- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/IdentityEnumTypeSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/IdentityEnumTypeSpec.groovy
@@ -21,13 +21,18 @@ package grails.gorm.tests
import grails.gorm.annotation.Entity
import grails.gorm.transactions.Rollback
import org.grails.orm.hibernate.HibernateDatastore
+import org.grails.orm.hibernate.cfg.IdentityEnumType
+import org.hibernate.HibernateException
+import org.hibernate.MappingException
import org.springframework.transaction.PlatformTransactionManager
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification
import javax.sql.DataSource
+import java.sql.PreparedStatement
import java.sql.ResultSet
+import java.sql.Types
/**
* Created by graemerocher on 16/11/16.
@@ -62,6 +67,115 @@ class IdentityEnumTypeSpec extends Specification {
resultSet.getInt(1) == 100
FooWithEnum.first().mySuperValue == XEnum.X__TWO
}
+
+ void "test setParameterValues throws a MappingException when the enum class cannot be loaded"() {
+ given:
+ def type = new IdentityEnumType()
+ def props = new Properties()
+ props.setProperty(IdentityEnumType.PARAM_ENUM_CLASS, "does.not.Exist")
+
+ when:
+ type.setParameterValues(props)
+
+ then:
+ def ex = thrown(MappingException)
+ ex.message.contains("does.not.Exist")
+ }
+
+ void "test setParameterValues falls back to a raw Class attribute when no String enumClass property is set"() {
+ given:
+ def type = new IdentityEnumType()
+ def props = new Properties()
+ props.put(IdentityEnumType.PARAM_ENUM_CLASS, XEnum)
+
+ when:
+ type.setParameterValues(props)
+
+ then:
+ noExceptionThrown()
+ type.returnedClass() == XEnum
+ }
+
+ void "test setParameterValues throws a MappingException when no enumClass parameter is provided"() {
+ given:
+ def type = new IdentityEnumType()
+
+ when:
+ type.setParameterValues(new Properties())
+
+ then:
+ def ex = thrown(MappingException)
+ ex.message.contains("enumClass parameter is required")
+ }
+
+ void "test getBidiEnumMap wraps reflection failures in a HibernateException"() {
+ when:
+ IdentityEnumType.getBidiEnumMap(EnumWithoutId)
+
+ then:
+ def ex = thrown(HibernateException)
+ ex.message.contains("BidiEnumMap")
+ }
+
+ void "test getBidiEnumMap logs a warning for duplicate ids but still builds a usable map"() {
+ when:
+ def map = IdentityEnumType.getBidiEnumMap(EnumWithDuplicateIds)
+
+ then:
+ noExceptionThrown()
+ map.getEnumValue(1) in [EnumWithDuplicateIds.A, EnumWithDuplicateIds.B]
+ }
+
+ void "test setParameterValues selects the BIGINT jdbc type for Long-keyed enums"() {
+ given:
+ def type = new IdentityEnumType()
+ def props = new Properties()
+ props.setProperty(IdentityEnumType.PARAM_ENUM_CLASS, EnumWithLongId.name)
+
+ when:
+ type.setParameterValues(props)
+
+ then:
+ type.getSqlType() == Types.BIGINT
+ }
+
+ void "test setParameterValues falls back to VARCHAR jdbc type for a non-standard id type"() {
+ given:
+ def type = new IdentityEnumType()
+ def props = new Properties()
+ props.setProperty(IdentityEnumType.PARAM_ENUM_CLASS, EnumWithCustomId.name)
+
+ when:
+ type.setParameterValues(props)
+
+ then:
+ type.getSqlType() == Types.VARCHAR
+ }
+
+ void "test disassemble and assemble round-trip a cached value"() {
+ given:
+ def type = new IdentityEnumType()
+
+ expect:
+ type.disassemble(XEnum.X__TWO) == XEnum.X__TWO
+ type.assemble(XEnum.X__TWO, null) == XEnum.X__TWO
+ }
+
+ void "test nullSafeSet sets a SQL NULL when the value is null"() {
+ given:
+ def type = new IdentityEnumType()
+ def props = new Properties()
+ props.setProperty(IdentityEnumType.PARAM_ENUM_CLASS, XEnum.name)
+ type.setParameterValues(props)
+ def statement = Mock(PreparedStatement)
+ def options = Mock(org.hibernate.type.descriptor.WrapperOptions)
+
+ when:
+ type.nullSafeSet(statement, null, 1, options)
+
+ then:
+ 1 * statement.setNull(1, Types.INTEGER)
+ }
}
@Entity
@@ -108,3 +222,31 @@ enum XEnum {
name
}
}
+
+enum EnumWithoutId {
+ A, B
+}
+
+enum EnumWithDuplicateIds {
+ A(1), B(1)
+
+ final int id
+
+ EnumWithDuplicateIds(int id) { this.id = id }
+}
+
+enum EnumWithLongId {
+ A(1L), B(2L)
+
+ final Long id
+
+ EnumWithLongId(Long id) { this.id = id }
+}
+
+enum EnumWithCustomId {
+ A('x' as Character), B('y' as Character)
+
+ final Character id
+
+ EnumWithCustomId(Character id) { this.id = id }
+}
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/DetachedAssociationFunctionSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/DetachedAssociationFunctionSpec.groovy
index bdedf6e6c1..c4c3b69b33 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/DetachedAssociationFunctionSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/DetachedAssociationFunctionSpec.groovy
@@ -64,4 +64,23 @@ class DetachedAssociationFunctionSpec extends Specification {
then: "it should NOT extract the internal association criteria (isolation)"
result.isEmpty()
}
+
+ def "apply recursively flattens association criteria nested inside junctions"() {
+ given: "an association criterion, a non-matching criterion, and a nested junction with another association criterion"
+ def association = Mock(org.grails.datastore.mapping.model.types.Association) {
+ getName() >> "test"
+ }
+ def topLevelAssociationCriteria = new DetachedAssociationCriteria(Object, association)
+ def nestedAssociationCriteria = new DetachedAssociationCriteria(Object, association)
+ def nonMatching = new Query.Equals("prop", "value")
+ def nestedJunction = new Query.Conjunction().add(nestedAssociationCriteria)
+ def junction = new Query.Conjunction().add(topLevelAssociationCriteria).add(nonMatching).add(nestedJunction)
+
+ when:
+ def result = function.apply(junction)
+
+ then: "both association criteria are collected, including the one nested in the inner junction"
+ result.size() == 2
+ result.containsAll([topLevelAssociationCriteria, nestedAssociationCriteria])
+ }
}