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
+    }
+}