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/21 22:31:10 UTC
(grails-core) branch chore/clean-grails-datamapping-async created (now 0b123f71c1)
This is an automated email from the ASF dual-hosted git repository.
borinquenkid pushed a change to branch chore/clean-grails-datamapping-async
in repository https://gitbox.apache.org/repos/asf/grails-core.git
at 0b123f71c1 Add test coverage for grails-datamapping-async and type decorator closures
This branch includes the following new commits:
new 0b123f71c1 Add test coverage for grails-datamapping-async and type decorator closures
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(grails-core) 01/01: Add test coverage for grails-datamapping-async and type decorator closures
Posted by bo...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
borinquenkid pushed a commit to branch chore/clean-grails-datamapping-async
in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 0b123f71c150d83f21ba3fc844455d1297acfa0e
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Fri Aug 21 17:30:10 2026 -0500
Add test coverage for grails-datamapping-async and type decorator closures
The module had no test infrastructure at all, so add the Spock dependency
and specs for GormAsyncStaticApi and AsyncQuery covering task()/list()/
count() delegation and the new-session decorator wrapping. AsyncQuery was
previously untested anywhere in the repo.
Also give both classes' promise decorator closures explicit parameter
types (Closure<?> callable, Object[] args) instead of relying on implicit
inference, fixing an IDE "cannot infer argument types" inspection on the
generic PromiseDecorator SAM coercion.
Co-Authored-By: Claude Sonnet 5 <no...@anthropic.com>
---
grails-datamapping-async/build.gradle | 4 +
.../grails/datastore/gorm/async/AsyncQuery.groovy | 4 +-
.../datastore/gorm/async/GormAsyncStaticApi.groovy | 4 +-
.../datastore/gorm/async/AsyncQuerySpec.groovy | 91 ++++++++++++++++
.../gorm/async/GormAsyncStaticApiSpec.groovy | 115 +++++++++++++++++++++
5 files changed, 214 insertions(+), 4 deletions(-)
diff --git a/grails-datamapping-async/build.gradle b/grails-datamapping-async/build.gradle
index 52cd2cb781..a82ff5d811 100644
--- a/grails-datamapping-async/build.gradle
+++ b/grails-datamapping-async/build.gradle
@@ -70,6 +70,10 @@ dependencies {
}
compileOnly 'org.apache.groovy:groovy'
+
+ testImplementation 'org.spockframework:spock-core'
+
+ testRuntimeOnly 'org.slf4j:slf4j-nop' // Prevents warning about missing slf4j implementation during tests
}
apply {
diff --git a/grails-datamapping-async/src/main/groovy/org/grails/datastore/gorm/async/AsyncQuery.groovy b/grails-datamapping-async/src/main/groovy/org/grails/datastore/gorm/async/AsyncQuery.groovy
index 84ddbb2bb2..f33c172a4c 100644
--- a/grails-datamapping-async/src/main/groovy/org/grails/datastore/gorm/async/AsyncQuery.groovy
+++ b/grails-datamapping-async/src/main/groovy/org/grails/datastore/gorm/async/AsyncQuery.groovy
@@ -32,8 +32,8 @@ class AsyncQuery<E> implements PromiseDecoratorProvider {
/**
* Wraps each promise in a new persistence session
*/
- private List<PromiseDecorator> decorators = [ { Closure callable ->
- return { args ->
+ private List<PromiseDecorator> decorators = [ { Closure<?> callable ->
+ return { Object[] args ->
gormOperations.persistentClass.withNewSession {
callable.call(*args)
}
diff --git a/grails-datamapping-async/src/main/groovy/org/grails/datastore/gorm/async/GormAsyncStaticApi.groovy b/grails-datamapping-async/src/main/groovy/org/grails/datastore/gorm/async/GormAsyncStaticApi.groovy
index 7687d42faf..655895a74b 100644
--- a/grails-datamapping-async/src/main/groovy/org/grails/datastore/gorm/async/GormAsyncStaticApi.groovy
+++ b/grails-datamapping-async/src/main/groovy/org/grails/datastore/gorm/async/GormAsyncStaticApi.groovy
@@ -37,8 +37,8 @@ class GormAsyncStaticApi<D> implements PromiseDecoratorProvider {
/**
* Wraps each promise in a new persistence session
*/
- private List<PromiseDecorator> decorators = [ { Closure callable ->
- return { args -> staticApi.withNewSession { callable.call(*args) } }
+ private List<PromiseDecorator> decorators = [ { Closure<?> callable ->
+ return { Object[] args -> staticApi.withNewSession { callable.call(*args) } }
} as PromiseDecorator ]
GormAsyncStaticApi(GormStaticApi<D> staticApi) {
diff --git a/grails-datamapping-async/src/test/groovy/org/grails/datastore/gorm/async/AsyncQuerySpec.groovy b/grails-datamapping-async/src/test/groovy/org/grails/datastore/gorm/async/AsyncQuerySpec.groovy
new file mode 100644
index 0000000000..15184a32e9
--- /dev/null
+++ b/grails-datamapping-async/src/test/groovy/org/grails/datastore/gorm/async/AsyncQuerySpec.groovy
@@ -0,0 +1,91 @@
+/*
+ * 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.async
+
+import grails.async.Promise
+import grails.async.Promises
+import org.grails.async.factory.SynchronousPromiseFactory
+import org.grails.datastore.gorm.query.GormOperations
+import spock.lang.Specification
+
+class AsyncQuerySpec extends Specification {
+
+ private def originalPromiseFactory
+
+ void setup() {
+ originalPromiseFactory = Promises.promiseFactory
+ Promises.promiseFactory = new SynchronousPromiseFactory()
+ AsyncQuerySpecEntity.newSessionCallCount = 0
+ }
+
+ void cleanup() {
+ Promises.promiseFactory = originalPromiseFactory
+ }
+
+ void "list delegates to the wrapped GORM operations asynchronously within a new session"() {
+ given:
+ def gormOperations = Mock(GormOperations) {
+ getPersistentClass() >> AsyncQuerySpecEntity
+ }
+ def query = new AsyncQuery(gormOperations)
+
+ when:
+ Promise<List> promise = query.list()
+
+ then:
+ 1 * gormOperations.list() >> ["a", "b"]
+ promise.get() == ["a", "b"]
+ AsyncQuerySpecEntity.newSessionCallCount == 1
+ }
+
+ void "count delegates to the wrapped GORM operations asynchronously within a new session"() {
+ given:
+ def gormOperations = Mock(GormOperations) {
+ getPersistentClass() >> AsyncQuerySpecEntity
+ }
+ def query = new AsyncQuery(gormOperations)
+
+ when:
+ Promise<Number> promise = query.count()
+
+ then:
+ 1 * gormOperations.count() >> 3
+ promise.get() == 3
+ AsyncQuerySpecEntity.newSessionCallCount == 1
+ }
+
+ void "getDecorators returns a single decorator that wraps calls in a new session"() {
+ given:
+ def gormOperations = Mock(GormOperations)
+ def query = new AsyncQuery(gormOperations)
+
+ expect:
+ query.decorators.size() == 1
+ }
+}
+
+class AsyncQuerySpecEntity {
+
+ static int newSessionCallCount = 0
+
+ static withNewSession(Closure callable) {
+ newSessionCallCount++
+ callable.call()
+ }
+}
diff --git a/grails-datamapping-async/src/test/groovy/org/grails/datastore/gorm/async/GormAsyncStaticApiSpec.groovy b/grails-datamapping-async/src/test/groovy/org/grails/datastore/gorm/async/GormAsyncStaticApiSpec.groovy
new file mode 100644
index 0000000000..bb5ab3632b
--- /dev/null
+++ b/grails-datamapping-async/src/test/groovy/org/grails/datastore/gorm/async/GormAsyncStaticApiSpec.groovy
@@ -0,0 +1,115 @@
+/*
+ * 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.async
+
+import grails.async.Promise
+import grails.async.Promises
+import org.grails.async.factory.SynchronousPromiseFactory
+import org.grails.datastore.gorm.GormStaticApi
+import org.grails.datastore.mapping.model.PersistentEntity
+import spock.lang.Specification
+
+class GormAsyncStaticApiSpec extends Specification {
+
+ private def originalPromiseFactory
+
+ void setup() {
+ originalPromiseFactory = Promises.promiseFactory
+ Promises.promiseFactory = new SynchronousPromiseFactory()
+ }
+
+ void cleanup() {
+ Promises.promiseFactory = originalPromiseFactory
+ }
+
+ void "task runs the closure inside a new session and returns its result via a promise"() {
+ given:
+ def entity = Mock(PersistentEntity) {
+ getJavaClass() >> String
+ }
+ def staticApi = Mock(GormStaticApi) {
+ getGormPersistentEntity() >> entity
+ }
+ def api = new GormAsyncStaticApi(staticApi)
+
+ when:
+ Promise<String> promise = api.task { "done" }
+
+ then:
+ 1 * staticApi.withNewSession(_) >> { Closure c -> c.call() }
+ promise.get() == "done"
+ }
+
+ void "task does not invoke the closure when withNewSession is never called"() {
+ given:
+ def entity = Mock(PersistentEntity) {
+ getJavaClass() >> String
+ }
+ def staticApi = Mock(GormStaticApi) {
+ getGormPersistentEntity() >> entity
+ }
+ def api = new GormAsyncStaticApi(staticApi)
+
+ when:
+ Promise<String> promise = api.task { "unreached" }
+
+ then:
+ 1 * staticApi.withNewSession(_) >> null
+ promise.get() == null
+ }
+
+ void "list delegates to the static API asynchronously within a new session"() {
+ given:
+ def staticApi = Mock(GormStaticApi) {
+ withNewSession(_) >> { Closure c -> c.call() }
+ }
+ def api = new GormAsyncStaticApi(staticApi)
+
+ when:
+ Promise<List> promise = api.list()
+
+ then:
+ 1 * staticApi.list() >> ["a", "b"]
+ promise.get() == ["a", "b"]
+ }
+
+ void "count delegates to the static API asynchronously within a new session"() {
+ given:
+ def staticApi = Mock(GormStaticApi) {
+ withNewSession(_) >> { Closure c -> c.call() }
+ }
+ def api = new GormAsyncStaticApi(staticApi)
+
+ when:
+ Promise<Integer> promise = api.count()
+
+ then:
+ 1 * staticApi.count() >> 5
+ promise.get() == 5
+ }
+
+ void "getDecorators returns a single decorator that wraps calls in a new session"() {
+ given:
+ def staticApi = Mock(GormStaticApi)
+ def api = new GormAsyncStaticApi(staticApi)
+
+ expect:
+ api.decorators.size() == 1
+ }
+}