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/04/09 14:52:51 UTC
(grails-core) 10/40: test(h7): expand HibernateGormInstanceApi coverage to 23 tests
This is an automated email from the ASF dual-hosted git repository.
borinquenkid pushed a commit to branch 8.0.x-hibernate7-dev
in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit e3bb5a4a06c1b6e59142a2d62d10819570af5d2d
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Tue Apr 7 15:08:03 2026 -0500
test(h7): expand HibernateGormInstanceApi coverage to 23 tests
- Add ConstrainedPerson entity with blank:false constraint for
validation path coverage
- New tests covering:
* lock(instance) pessimistic write lock
* save(validate:false) skips validation path
* save(deepValidate:false) hits deepValidate argument branch
* validation error returns null and sets errors (failOnError=false)
* validation error with failOnError:true throws exception
* save without flush: arg — shouldFlush falls back to autoFlush
* isDirty/getDirtyPropertyNames return false/empty for transient instance
* getPersistentValue returns null for unknown field and non-attached instance
* autoRetrieveAssociations: null propValue branch, already-contained
branch, and detached author full-fetch path
Co-authored-by: Copilot <22...@users.noreply.github.com>
---
JACOCO_COVERAGE_VIOLATIONS.md | 2 +-
.../hibernate/HibernateGormInstanceApiSpec.groovy | 211 ++++++++++++++++++++-
2 files changed, 211 insertions(+), 2 deletions(-)
diff --git a/JACOCO_COVERAGE_VIOLATIONS.md b/JACOCO_COVERAGE_VIOLATIONS.md
index 7a4ea5db3e..c40c81fb2d 100644
--- a/JACOCO_COVERAGE_VIOLATIONS.md
+++ b/JACOCO_COVERAGE_VIOLATIONS.md
@@ -29,7 +29,7 @@ Generated on: 2026-04-06 16:49:20
| X org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentProperty | 85.28% |
| X org.grails.orm.hibernate.access.TraitPropertyAccessStrategy | 85.35% |
| X org.grails.orm.hibernate.proxy.GrailsBytecodeProvider | 85.71% |
-| org.grails.orm.hibernate.HibernateGormInstanceApi | 85.82% |
+| org.grails.orm.hibernate.HibernateGormInstanceApi | 85.82% | X |
| org.grails.orm.hibernate.HibernateDatastore.ChildHibernateDatastore | 86.36% |
| X org.grails.orm.hibernate.proxy.GrailsProxyFactoryFactory | 86.67% |
| X org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateToManyProperty | 86.77% |
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormInstanceApiSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormInstanceApiSpec.groovy
index 6216a0366e..3ba3c94cb8 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormInstanceApiSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormInstanceApiSpec.groovy
@@ -25,7 +25,7 @@ import grails.gorm.hibernate.HibernateEntity
class HibernateGormInstanceApiSpec extends HibernateGormDatastoreSpec {
def setupSpec() {
- manager.addAllDomainClasses([PersonInstanceApi, BookInstanceApi])
+ manager.addAllDomainClasses([PersonInstanceApi, BookInstanceApi, ConstrainedPerson])
}
def "test save and get"() {
@@ -189,6 +189,207 @@ class HibernateGormInstanceApiSpec extends HibernateGormDatastoreSpec {
then:
person.name == 'Bob'
}
+
+ // -------------------------------------------------------------------------
+ // lock
+ // -------------------------------------------------------------------------
+
+ def "lock acquires a pessimistic write lock on the entity"() {
+ given:
+ Long savedId = PersonInstanceApi.withTransaction {
+ new PersonInstanceApi(name: 'LockUser', age: 22).save(flush: true, failOnError: true)
+ }.id
+
+ when:
+ PersonInstanceApi.withTransaction {
+ def person = PersonInstanceApi.get(savedId)
+ person.lock()
+ }
+
+ then:
+ noExceptionThrown()
+ }
+
+ // -------------------------------------------------------------------------
+ // save — shouldValidate/deepValidate/shouldFail branches
+ // -------------------------------------------------------------------------
+
+ def "save with validate:false skips validation"() {
+ given:
+ def person = new ConstrainedPerson(name: '') // blank name violates constraint
+
+ when:
+ def result = ConstrainedPerson.withTransaction {
+ person.save(validate: false, flush: true)
+ }
+
+ then: "saved without validation — blank name accepted"
+ result != null
+ result.id != null
+ }
+
+ def "save with deepValidate:false still runs validator without deep cascade"() {
+ given:
+ def person = new ConstrainedPerson(name: 'Alice')
+
+ when:
+ def result = ConstrainedPerson.withTransaction {
+ person.save(deepValidate: false, flush: true)
+ }
+
+ then:
+ result != null
+ result.id != null
+ }
+
+ def "save with invalid entity returns null and sets errors when failOnError is false"() {
+ given:
+ def person = new ConstrainedPerson(name: '') // blank violates constraint
+
+ when:
+ def result = ConstrainedPerson.withTransaction {
+ person.save(flush: true)
+ }
+
+ then:
+ result == null
+ person.hasErrors()
+ person.errors.fieldErrors.any { it.field == 'name' }
+ }
+
+ def "save with invalid entity and failOnError:true throws an exception"() {
+ given:
+ def person = new ConstrainedPerson(name: '')
+
+ when:
+ ConstrainedPerson.withTransaction {
+ person.save(failOnError: true, flush: true)
+ }
+
+ then:
+ thrown(Exception)
+ }
+
+ // -------------------------------------------------------------------------
+ // shouldFlush — autoFlush=false (no flush arg)
+ // -------------------------------------------------------------------------
+
+ def "save without flush argument uses autoFlush setting"() {
+ given: "autoFlush is false by default in the test datastore"
+ def person = new PersonInstanceApi(name: 'AutoFlushTest', age: 55)
+
+ when:
+ PersonInstanceApi.withTransaction {
+ person.save() // no flush: argument — relies on autoFlush
+ session.flush() // flush manually so we can verify
+ }
+
+ then:
+ person.id != null
+ PersonInstanceApi.get(person.id)?.name == 'AutoFlushTest'
+ }
+
+ // -------------------------------------------------------------------------
+ // isDirty edge cases
+ // -------------------------------------------------------------------------
+
+ def "isDirty returns false for a non-attached (transient) instance"() {
+ given:
+ def person = new PersonInstanceApi(name: 'Transient', age: 10)
+ // not saved — no EntityEntry in the session
+
+ expect:
+ !person.isDirty()
+ !person.isDirty('name')
+ }
+
+ def "getDirtyPropertyNames returns empty list for a non-attached instance"() {
+ given:
+ def person = new PersonInstanceApi(name: 'Ghost', age: 99)
+
+ expect:
+ person.getDirtyPropertyNames() == []
+ }
+
+ // -------------------------------------------------------------------------
+ // getPersistentValue edge cases
+ // -------------------------------------------------------------------------
+
+ def "getPersistentValue returns null for an unknown field name"() {
+ given:
+ def person = new PersonInstanceApi(name: 'Bob', age: 40)
+ person.save(flush: true)
+
+ expect:
+ person.getPersistentValue('nonExistentField') == null
+ }
+
+ def "getPersistentValue returns null for a non-attached instance"() {
+ given:
+ def person = new PersonInstanceApi(name: 'Detached', age: 5)
+ // not saved
+
+ expect:
+ person.getPersistentValue('name') == null
+ }
+
+ // -------------------------------------------------------------------------
+ // autoRetrieveAssociations — sub-branches via BookInstanceApi
+ // -------------------------------------------------------------------------
+
+ def "save book with null author skips association retrieval (null propValue branch)"() {
+ given:
+ def book = new BookInstanceApi(title: 'Orphan Book') // no author
+
+ when:
+ def result = BookInstanceApi.withTransaction {
+ book.save(flush: true, validate: false)
+ }
+
+ then:
+ result != null
+ result.id != null
+ }
+
+ def "save book with already-managed author skips re-retrieval (contains branch)"() {
+ given:
+ def author = PersonInstanceApi.withTransaction {
+ new PersonInstanceApi(name: 'Managed Author', age: 35).save(flush: true, failOnError: true)
+ }
+ // author is still in session after save — template.contains returns true
+ def book = new BookInstanceApi(title: 'Managed Book', author: author)
+
+ when:
+ def result = BookInstanceApi.withTransaction {
+ book.save(flush: true, validate: false)
+ }
+
+ then:
+ result != null
+ result.id != null
+ }
+
+ def "save book with detached author triggers association re-retrieval (full fetch path)"() {
+ given:
+ def author = PersonInstanceApi.withTransaction {
+ new PersonInstanceApi(name: 'Detached Author', age: 42).save(flush: true, failOnError: true)
+ }
+ // Evict the author via the hibernate template so it is no longer in the session
+ manager.hibernateDatastore.hibernateTemplate.evict(author)
+
+ def book = new BookInstanceApi(title: 'Fetched Book', author: author)
+
+ when:
+ def result = BookInstanceApi.withTransaction {
+ book.save(flush: true, validate: false)
+ }
+
+ then:
+ result != null
+ result.id != null
+ result.author != null
+ result.author.name == 'Detached Author'
+ }
}
@Entity
@@ -202,3 +403,11 @@ class BookInstanceApi implements HibernateEntity<BookInstanceApi> {
String title
static belongsTo = [author: PersonInstanceApi]
}
+
+@Entity
+class ConstrainedPerson implements HibernateEntity<ConstrainedPerson> {
+ String name
+ static constraints = {
+ name blank: false, maxSize: 100
+ }
+}