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 <[email protected]> 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 <[email protected]> --- 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 + } +}
