You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by sb...@apache.org on 2026/09/04 07:50:26 UTC
(grails-core) 01/01: Merge remote-tracking branch 'origin/7.2.x' into merge/7.2.x-into-8.0.x
This is an automated email from the ASF dual-hosted git repository.
sbglasius pushed a commit to branch 8.0.x
in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit def2c9b9bc4f648726fb69101d7d20b1376f6076
Merge: 55d5076aa5 5075ff30b5
Author: SΓΈren Berg Glasius <so...@glasius.dk>
AuthorDate: Fri Sep 4 09:50:10 2026 +0200
Merge remote-tracking branch 'origin/7.2.x' into merge/7.2.x-into-8.0.x
# Merge conflicts resolved.
# By James Fredley (2) and others
# Via GitHub (5) and SΓΈren Berg Glasius (2)
* origin/7.2.x:
Add test coverage and fix real bugs in grails-datamapping-core's services package (#16156)
ci: pin every action to an ASF-approved SHA
Give TemplateLookupCachingSpec its own command fixture
Fix fields template lookup leak from accessor-keyed memoization
# Conflicts:
# .github/workflows/codestyle.yml
# .github/workflows/gradle.yml
# .github/workflows/groovy-joint-workflow.yml
# .github/workflows/vulnerability-scan.yml
# grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractServiceImplementer.groovy
# grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/services/DefaultTenantServiceSpec.groovy
# grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/services/DefaultTransactionServiceSpec.groovy
.github/workflows/codestyle.yml | 6 +-
.github/workflows/forge-deploy-aws.yml | 4 +-
.github/workflows/gradle.yml | 54 +--
.github/workflows/rat.yml | 2 +-
.github/workflows/release-publish-docs.yml | 2 +-
.github/workflows/release-verify.yml | 2 +-
.github/workflows/release.yml | 6 +-
.github/workflows/vulnerability-scan.yml | 14 +-
...stractDetachedCriteriaServiceImplementor.groovy | 6 +
.../implementers/AbstractSaveImplementer.groovy | 6 +-
.../implementers/AbstractServiceImplementer.groovy | 7 +-
.../implementers/AbstractWhereImplementer.groovy | 3 +
.../services/implementers/DeleteImplementer.groovy | 6 +-
.../implementers/DeleteWhereImplementer.groovy | 2 +-
.../implementers/FindAllByImplementer.groovy | 3 +-
.../implementers/FindOneImplementer.groovy | 5 +
.../FindOneStringQueryImplementer.groovy | 4 +
.../implementers/InterfaceProjectionBuilder.groovy | 39 +-
.../IterableInterfaceProjectionBuilder.groovy | 16 +-
.../services/implementers/SaveImplementer.groovy | 2 +-
.../implementers/UpdateOneImplementer.groovy | 3 +-
.../services/ServiceImplementerEdgeCaseSpec.groovy | 413 +++++++++++++++++++++
.../gorm/services/DefaultTenantServiceSpec.groovy | 2 +-
.../services/DefaultTransactionServiceSpec.groovy | 5 +
.../formfields/FormFieldsTemplateService.groovy | 32 +-
.../formfields/TemplateLookupCachingSpec.groovy | 38 ++
26 files changed, 578 insertions(+), 104 deletions(-)
diff --cc .github/workflows/codestyle.yml
index 28fa643859,1cf5a0dad4..a82a9d12a9
--- a/.github/workflows/codestyle.yml
+++ b/.github/workflows/codestyle.yml
@@@ -38,9 -37,9 +38,9 @@@ jobs
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: liberica
- java-version: 17
+ java-version: 21
- name: "π Setup Gradle"
- uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
+ uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with:
cache-provider: basic # 'basic' uses the MIT-licensed, open-source cache provider; the default 'enhanced' provider (v6+) is proprietary (Gradle commercial Terms of Use)
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
@@@ -72,9 -75,9 +72,9 @@@
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: liberica
- java-version: 17
+ java-version: 21
- name: "π Setup Gradle"
- uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
+ uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with:
cache-provider: basic # 'basic' uses the MIT-licensed, open-source cache provider; the default 'enhanced' provider (v6+) is proprietary (Gradle commercial Terms of Use)
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
@@@ -111,9 -114,9 +111,9 @@@
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: liberica
- java-version: 17
+ java-version: 21
- name: "π Setup Gradle"
- uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
+ uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with:
cache-provider: basic # 'basic' uses the MIT-licensed, open-source cache provider; the default 'enhanced' provider (v6+) is proprietary (Gradle commercial Terms of Use)
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
diff --cc .github/workflows/gradle.yml
index aaab2f3f8e,be7aee29ee..37adf34aea
--- a/.github/workflows/gradle.yml
+++ b/.github/workflows/gradle.yml
@@@ -35,9 -35,9 +35,9 @@@ jobs
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: liberica
- java-version: 17
+ java-version: 21
- name: "ποΈ Restore dependency jar cache"
- uses: actions/cache@v4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
# Cache only downloaded dependency jars and wrapper distributions, never Grails build outputs.
# Keyed by branch version so each release branch maintains its own warm cache.
@@@ -48,10 -48,10 +48,10 @@@
restore-keys: |
gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-
- name: "π Setup Gradle"
- uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
+ uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with:
cache-provider: basic # 'basic' uses the MIT-licensed, open-source cache provider; the default 'enhanced' provider (v6+) is proprietary (Gradle commercial Terms of Use)
- develocity-access-key: ${{ secrets.GRAILS_DEVELOCITY_ACCESS_KEY }}
+ develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
- name: "π Validate grails-core dependency versions"
run: >
./gradlew validateDependencyVersions
@@@ -173,8 -145,7 +173,8 @@@
distribution: liberica
java-version: ${{ matrix.java }}
- name: "ποΈ Restore dependency jar cache"
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
+ id: dependency-cache
- uses: actions/cache/restore@v4
++ uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
# Cache only downloaded dependency jars and wrapper distributions, never Grails build outputs.
# Keyed by branch version so each release branch maintains its own warm cache.
@@@ -255,8 -190,7 +255,8 @@@
distribution: liberica
java-version: ${{ matrix.java }}
- name: "ποΈ Restore dependency jar cache"
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
+ id: dependency-cache
- uses: actions/cache/restore@v4
++ uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
# Cache only downloaded dependency jars and wrapper distributions, never Grails build outputs.
# Keyed by branch version so each release branch maintains its own warm cache.
@@@ -433,8 -313,7 +433,8 @@@
distribution: liberica
java-version: ${{ matrix.java }}
- name: "ποΈ Restore dependency jar cache"
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
+ id: dependency-cache
- uses: actions/cache/restore@v4
++ uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
# Cache only downloaded dependency jars and wrapper distributions, never Grails build outputs.
# Keyed by branch version so each release branch maintains its own warm cache.
@@@ -551,9 -405,9 +551,9 @@@
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: liberica
- java-version: 17
+ java-version: 21
- name: "π Setup Gradle"
- uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
+ uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with:
cache-provider: basic # 'basic' uses the MIT-licensed, open-source cache provider; the default 'enhanced' provider (v6+) is proprietary (Gradle commercial Terms of Use)
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
@@@ -679,19 -492,8 +679,19 @@@
key: gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-${{ hashFiles('**/dependencies.gradle', '**/gradle-wrapper.properties') }}
restore-keys: |
gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-
+ - name: "ποΈ Cache the mongod the embedded server runs"
+ uses: actions/cache@v4
+ with:
+ # Kept apart from the dependency jars: this holds an immutable download from
+ # fastdl.mongodb.org - the archive and the binary extracted out of it - and no database,
+ # so it belongs to no branch and does not change when a dependency version does.
+ # Keyed by the version this entry asks for, because the matrix runs more than one and a
+ # single key would keep whichever job missed first and leave the other restoring 250MB
+ # of a binary it cannot use. No restore-keys, for that same reason.
+ path: ~/.embedmongo
+ key: embedmongo-${{ runner.os }}-${{ matrix.mongodb-version }}
- name: "π Setup Gradle"
- uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
+ uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with:
cache-disabled: true # dependency jars are cached by the explicit branch-keyed step above
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
@@@ -881,9 -571,9 +881,9 @@@
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: liberica
- java-version: 17
+ java-version: 21
- name: "ποΈ Restore dependency jar cache"
- uses: actions/cache@v4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
# Cache only downloaded dependency jars and wrapper distributions, never Grails build outputs.
# Keyed by branch version so each release branch maintains its own warm cache.
@@@ -948,9 -636,9 +948,9 @@@
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: liberica
- java-version: 17
+ java-version: 21
- name: "ποΈ Restore dependency jar cache"
- uses: actions/cache@v4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
# Cache only downloaded dependency jars and wrapper distributions, never Grails build outputs.
# Keyed by branch version so each release branch maintains its own warm cache.
@@@ -1066,9 -702,9 +1066,9 @@@
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: liberica
- java-version: 17
+ java-version: 21
- name: "ποΈ Restore dependency jar cache"
- uses: actions/cache@v4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
# Cache only downloaded dependency jars and wrapper distributions, never Grails build outputs.
# Keyed by branch version so each release branch maintains its own warm cache.
@@@ -1177,9 -813,9 +1177,9 @@@
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: liberica
- java-version: 17
+ java-version: 21
- name: "ποΈ Restore dependency jar cache"
- uses: actions/cache@v4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
# Cache only downloaded dependency jars and wrapper distributions, never Grails build outputs.
# Keyed by branch version so each release branch maintains its own warm cache.
diff --cc .github/workflows/rat.yml
index c4ab66a771,d653be29a9..1aeb13732d
--- a/.github/workflows/rat.yml
+++ b/.github/workflows/rat.yml
@@@ -38,9 -40,9 +38,9 @@@ jobs
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: liberica
- java-version: 17
+ java-version: 21
- name: "π Setup Gradle"
- uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
+ uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with:
cache-provider: basic # 'basic' uses the MIT-licensed, open-source cache provider; the default 'enhanced' provider (v6+) is proprietary (Gradle commercial Terms of Use)
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
diff --cc .github/workflows/release-verify.yml
index 34027e88b4,cf78f5da28..1ab12839d8
--- a/.github/workflows/release-verify.yml
+++ b/.github/workflows/release-verify.yml
@@@ -93,18 -71,8 +93,18 @@@ jobs
with:
distribution: ${{ steps.sdkmanrc.outputs.java-distribution }}
java-version: ${{ steps.sdkmanrc.outputs.java-version }}
+ - name: "βοΈ Export JDK 25 home for the reproducible-build check"
+ # verify-reproducible.sh switches JAVA_HOME to ${JDK_25_HOME} for the
+ # Micronaut island pass; mirror the container's ENV JDK_25_HOME here
+ run: |
+ if [ -z "${JAVA_HOME_25_X64:-}" ]; then
+ echo "β Expected JAVA_HOME_25_X64 to be exported by setup-java" >&2
+ exit 1
+ fi
+ echo "JDK_25_HOME=${JAVA_HOME_25_X64}" >> "${GITHUB_ENV}"
+ "${JAVA_HOME_25_X64}/bin/java" -version
- name: "π Setup Gradle"
- uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
+ uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with:
# verification must run against a pristine environment, never cached state
cache-disabled: true
diff --cc .github/workflows/vulnerability-scan.yml
index 55d2225c12,dad6c6a8f9..4ae478ffe1
--- a/.github/workflows/vulnerability-scan.yml
+++ b/.github/workflows/vulnerability-scan.yml
@@@ -44,29 -44,14 +44,29 @@@ jobs
contents: read
steps:
- name: "π₯ Checkout repository"
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - name: "βοΈ Determine Java version from .sdkmanrc"
+ # Read the JDK the build targets out of the file that already declares it, so a
+ # baseline bump does not leave a stale literal behind here. Only the major is kept:
+ # a scan is outside the reproducible-build surface, so CI takes the runner's current
+ # release of that major rather than pinning the patch version.
+ id: sdkmanrc
+ run: |
+ set -euo pipefail
+ sdkman_java=$(grep -E '^java=' .sdkmanrc | cut -d= -f2 || true)
+ if [ -z "${sdkman_java}" ]; then
+ echo "β Could not determine the java version from .sdkmanrc" >&2
+ exit 1
+ fi
+ echo "Scanning with JDK ${sdkman_java%%.*} (from .sdkmanrc: ${sdkman_java})"
+ echo "java-version=${sdkman_java%%.*}" >> "$GITHUB_OUTPUT"
- name: "βοΈ Setup JDK"
- uses: actions/setup-java@v4
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: liberica
- java-version: 17
+ java-version: ${{ steps.sdkmanrc.outputs.java-version }}
- name: "π Setup Gradle"
- uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
+ uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with:
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
- name: "π Run OSS Index Vulnerability Scan"
@@@ -98,29 -83,14 +98,29 @@@
pull-requests: write
steps:
- name: "π₯ Checkout pull request"
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - name: "βοΈ Determine Java version from .sdkmanrc"
+ # Read the JDK the build targets out of the file that already declares it, so a
+ # baseline bump does not leave a stale literal behind here. Only the major is kept:
+ # a scan is outside the reproducible-build surface, so CI takes the runner's current
+ # release of that major rather than pinning the patch version.
+ id: sdkmanrc
+ run: |
+ set -euo pipefail
+ sdkman_java=$(grep -E '^java=' .sdkmanrc | cut -d= -f2 || true)
+ if [ -z "${sdkman_java}" ]; then
+ echo "β Could not determine the java version from .sdkmanrc" >&2
+ exit 1
+ fi
+ echo "Scanning with JDK ${sdkman_java%%.*} (from .sdkmanrc: ${sdkman_java})"
+ echo "java-version=${sdkman_java%%.*}" >> "$GITHUB_OUTPUT"
- name: "βοΈ Setup JDK"
- uses: actions/setup-java@v4
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: liberica
- java-version: 17
+ java-version: ${{ steps.sdkmanrc.outputs.java-version }}
- name: "π Setup Gradle"
- uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
+ uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with:
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
- name: "π Run OSS Index Vulnerability Scan"
diff --cc grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractServiceImplementer.groovy
index d41cc8b068,c1b7576c5b..6e23e300a7
--- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractServiceImplementer.groovy
+++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractServiceImplementer.groovy
@@@ -45,13 -42,11 +45,12 @@@ import org.grails.datastore.mapping.tra
import static org.codehaus.groovy.ast.ClassHelper.make
import static org.codehaus.groovy.ast.tools.GeneralUtils.args
+import static org.codehaus.groovy.ast.tools.GeneralUtils.callX
import static org.codehaus.groovy.ast.tools.GeneralUtils.castX
import static org.codehaus.groovy.ast.tools.GeneralUtils.classX
- import static org.codehaus.groovy.ast.tools.GeneralUtils.param
import static org.codehaus.groovy.ast.tools.GeneralUtils.propX
-import static org.codehaus.groovy.ast.tools.GeneralUtils.varX
import static org.grails.datastore.gorm.transform.AstMethodDispatchUtils.callD
+import static org.grails.datastore.mapping.reflect.AstUtils.varThis
/**
* Abstract implementation of the {@link ServiceImplementer} interface
@@@ -149,7 -135,7 +148,7 @@@ abstract class AbstractServiceImplement
* @return The datastore expression
*/
protected Expression multiTenantDatastore() {
- return castX(ClassHelper.make(MultiTenantCapableDatastore), datastore())
- return castX(make(MultiTenantCapableDatastore), propX(varX('this'), 'targetDatastore'))
++ return castX(make(MultiTenantCapableDatastore), datastore())
}
/**
diff --cc grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/services/DefaultTenantServiceSpec.groovy
index 865b2004e7,65f39221b2..decfaf3022
--- a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/services/DefaultTenantServiceSpec.groovy
+++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/services/DefaultTenantServiceSpec.groovy
@@@ -24,99 -23,100 +24,99 @@@ import org.grails.datastore.mapping.mod
import org.grails.datastore.mapping.multitenancy.AllTenantsResolver
import org.grails.datastore.mapping.multitenancy.MultiTenancySettings
import org.grails.datastore.mapping.multitenancy.MultiTenantCapableDatastore
-import org.grails.datastore.mapping.multitenancy.TenantResolver
import spock.lang.Specification
+/**
+ * {@code TenantServiceSpec} in the separate grails-datamapping-core-test module already exercises
+ * this class end-to-end through a real transformed service, but that module's test run doesn't
+ * count toward grails-datamapping-core's own JaCoCo report (a different Gradle module/test task
+ * entirely) - so from this module's own coverage perspective, this whole class had 0% coverage.
+ * Every method here delegates to the extensively-tested {@code Tenants}/{@code CurrentTenantHolder}
+ * static API (see {@code TenantsSpec}, item 8 of this plan), so these tests focus on
+ * DefaultTenantService's own logic - the mode guard, the datastore-capability check, and (new in
+ * this PR) the {@code RESOLVING} reentrancy guard - not on re-verifying Tenants' own behaviour.
+ */
class DefaultTenantServiceSpec extends Specification {
- DefaultTenantService tenantService = new DefaultTenantService()
+ DefaultTenantService service = new DefaultTenantService()
- void 'multiTenantDatastore throws when the datastore is not multi-tenant capable'() {
+ void "getDatastore/setDatastore round-trip"() {
given:
- tenantService.datastore = Mock(Datastore)
+ def ds = Mock(Datastore)
when:
- tenantService.eachTenant {}
+ service.setDatastore(ds)
then:
- DatastoreConfigurationException e = thrown(DatastoreConfigurationException)
- e.message.contains('not Multi-Tenant capable')
+ service.getDatastore().is(ds)
}
- void 'currentId throws when multi tenancy mode is NONE'() {
+ void "multiTenantDatastore throws DatastoreConfigurationException for a non-multi-tenant-capable datastore"() {
given:
- MultiTenantCapableDatastore datastore = Mock(MultiTenantCapableDatastore)
- datastore.multiTenancyMode >> MultiTenancySettings.MultiTenancyMode.NONE
- tenantService.datastore = datastore
+ service.setDatastore(Mock(Datastore))
when:
- service.eachTenant { }
- tenantService.currentId()
++ service.eachTenant {}
then:
- DatastoreConfigurationException e = thrown(DatastoreConfigurationException)
- e.message.contains('not configured for Multi-Tenancy')
+ def e = thrown(DatastoreConfigurationException)
+ e.message.contains('is not Multi-Tenant capable')
}
- void 'currentId resolves the tenant id from the tenant resolver'() {
- given:
- TenantResolver tenantResolver = Mock(TenantResolver) {
- resolveTenantIdentifier() >> 'tenant1'
+ private MultiTenantCapableDatastore discriminatorDatastore(List tenantIds = ['tenant1']) {
+ Mock(MultiTenantCapableDatastore) {
+ getMultiTenancyMode() >> MultiTenancySettings.MultiTenancyMode.DISCRIMINATOR
+ getTenantResolver() >> Mock(AllTenantsResolver) {
+ resolveTenantIds() >> tenantIds
+ }
}
- MultiTenantCapableDatastore datastore = Mock(MultiTenantCapableDatastore)
- datastore.multiTenancyMode >> MultiTenancySettings.MultiTenancyMode.DISCRIMINATOR
- datastore.tenantResolver >> tenantResolver
- tenantService.datastore = datastore
-
- expect:
- tenantService.currentId() == 'tenant1'
}
- void 'withoutId throws when multi tenancy mode is NONE'() {
+ void "eachTenant delegates to Tenants.eachTenant for each resolved tenant id"() {
given:
- MultiTenantCapableDatastore datastore = Mock(MultiTenantCapableDatastore)
- datastore.multiTenancyMode >> MultiTenancySettings.MultiTenancyMode.NONE
- tenantService.datastore = datastore
+ service.setDatastore(discriminatorDatastore(['tenant1', 'tenant2']))
+ def seen = []
when:
- tenantService.withoutId { 'result' }
+ service.eachTenant { seen << CurrentTenantHolder.get() }
then:
- thrown(DatastoreConfigurationException)
+ seen == ['tenant1', 'tenant2']
}
- void 'withoutId with a shared connection mode executes the callable without a session'() {
+ void "currentId throws DatastoreConfigurationException when multi tenancy mode is NONE"() {
given:
- MultiTenantCapableDatastore datastore = Mock(MultiTenantCapableDatastore)
- datastore.multiTenancyMode >> MultiTenancySettings.MultiTenancyMode.DISCRIMINATOR
- tenantService.datastore = datastore
+ service.setDatastore(Mock(MultiTenantCapableDatastore) {
+ getMultiTenancyMode() >> MultiTenancySettings.MultiTenancyMode.NONE
+ })
when:
- String result = tenantService.withoutId { -> 'result' }
+ service.currentId()
then:
- result == 'result'
- 0 * datastore.withSession(_)
- 0 * datastore.withNewSession(_, _)
+ def e = thrown(DatastoreConfigurationException)
+ e.message.contains('is not configured for Multi-Tenancy')
}
- void 'withoutId with a non-shared connection mode executes within a new session'() {
+ void "currentId delegates to Tenants.currentId when multi tenancy is configured"() {
given:
- MultiTenantCapableDatastore datastore = Mock(MultiTenantCapableDatastore)
- datastore.multiTenancyMode >> MultiTenancySettings.MultiTenancyMode.DATABASE
- datastore.withNewSession(_, _) >> { args -> args[1].call('session') }
- tenantService.datastore = datastore
+ def ds = discriminatorDatastore()
+ service.setDatastore(ds)
expect:
- tenantService.withoutId { 'result' } == 'result'
+ CurrentTenantHolder.withTenant(ds, 'tenant1') {
+ service.currentId() == 'tenant1'
+ }
}
- void 'withId throws when multi tenancy mode is NONE'() {
+ void "withoutId throws DatastoreConfigurationException when multi tenancy mode is NONE"() {
given:
- MultiTenantCapableDatastore datastore = Mock(MultiTenantCapableDatastore)
- datastore.multiTenancyMode >> MultiTenancySettings.MultiTenancyMode.NONE
- tenantService.datastore = datastore
+ service.setDatastore(Mock(MultiTenantCapableDatastore) {
+ getMultiTenancyMode() >> MultiTenancySettings.MultiTenancyMode.NONE
+ })
when:
- tenantService.withId('tenant1') { 'result' }
+ service.withoutId { -> 'result' }
then:
thrown(DatastoreConfigurationException)
diff --cc grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/services/DefaultTransactionServiceSpec.groovy
index 57188a0afa,99be3d643e..1a95b2e973
--- a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/services/DefaultTransactionServiceSpec.groovy
+++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/services/DefaultTransactionServiceSpec.groovy
@@@ -18,28 -18,169 +18,33 @@@
*/
package org.grails.datastore.gorm.services
+ import org.springframework.transaction.PlatformTransactionManager
+ import org.springframework.transaction.TransactionDefinition
+ import org.springframework.transaction.TransactionStatus
+ import org.springframework.transaction.TransactionSystemException
+
import org.grails.datastore.mapping.core.Datastore
-import org.grails.datastore.mapping.transactions.CustomizableRollbackTransactionAttribute
-import org.grails.datastore.mapping.transactions.TransactionCapableDatastore
import spock.lang.Specification
+/**
+ * The diff only adds the explicit {@code datastore}/{@code getDatastore()}/{@code setDatastore()}
+ * {@code Service} contract (previously implicit/inherited) - the rest of this class (the
+ * {@code withTransaction}/{@code withRollback}/{@code withNewTransaction} overloads) predates this
+ * PR and is already exercised via real {@code TransactionService} usage in the separate
+ * {@code grails-datamapping-core-test} module (which doesn't count toward this module's own
+ * coverage - see item 14's cross-module attribution note), so out of scope here.
+ */
class DefaultTransactionServiceSpec extends Specification {
- DefaultTransactionService transactionService = new DefaultTransactionService()
-
- PlatformTransactionManager transactionManager = Mock(PlatformTransactionManager) {
- getTransaction(_) >> Mock(TransactionStatus)
- }
-
- TransactionCapableDatastore transactionCapableDatastore = Mock(TransactionCapableDatastore) {
- getTransactionManager() >> transactionManager
- }
-
- void 'withTransaction(Closure) executes the callable within a transaction'() {
- given:
- transactionService.datastore = transactionCapableDatastore
-
- when:
- String result = transactionService.withTransaction { status -> 'done' }
-
- then:
- result == 'done'
- 1 * transactionManager.commit(_)
- }
-
- void 'withTransaction(Closure) throws when the datastore does not support transactions'() {
- given:
- transactionService.datastore = Mock(Datastore)
-
- when:
- transactionService.withTransaction { status -> 'done' }
-
- then:
- TransactionSystemException e = thrown(TransactionSystemException)
- e.message.contains('does not support transactions')
- }
-
- void 'withRollback(Closure) executes the callable and rolls back'() {
- given:
- transactionService.datastore = transactionCapableDatastore
-
- expect:
- transactionService.withRollback { status -> 'done' } == 'done'
- }
-
- void 'withRollback(Closure) throws when the datastore does not support transactions'() {
- given:
- transactionService.datastore = Mock(Datastore)
-
- when:
- transactionService.withRollback { status -> 'done' }
-
- then:
- thrown(TransactionSystemException)
- }
-
- void 'withNewTransaction(Closure) executes the callable with PROPAGATION_REQUIRES_NEW'() {
- given:
- transactionService.datastore = transactionCapableDatastore
-
- expect:
- transactionService.withNewTransaction { status -> 'done' } == 'done'
- }
-
- void 'withNewTransaction(Closure) throws when the datastore does not support transactions'() {
- given:
- transactionService.datastore = Mock(Datastore)
-
- when:
- transactionService.withNewTransaction { status -> 'done' }
-
- then:
- thrown(TransactionSystemException)
- }
-
- void 'withTransaction(TransactionDefinition, Closure) executes the callable with the given definition'() {
- given:
- transactionService.datastore = transactionCapableDatastore
- TransactionDefinition definition = new CustomizableRollbackTransactionAttribute()
-
- expect:
- transactionService.withTransaction(definition) { status -> 'done' } == 'done'
- }
-
- void 'withTransaction(TransactionDefinition, Closure) throws when the datastore does not support transactions'() {
- given:
- transactionService.datastore = Mock(Datastore)
- TransactionDefinition definition = new CustomizableRollbackTransactionAttribute()
-
- when:
- transactionService.withTransaction(definition) { status -> 'done' }
-
- then:
- thrown(TransactionSystemException)
- }
-
- void 'withTransaction(Map, Closure) builds a transaction definition from the map'() {
- given:
- transactionService.datastore = transactionCapableDatastore
-
- expect:
- transactionService.withTransaction([readOnly: true]) { status -> 'done' } == 'done'
- }
-
- void 'withTransaction(Map, Closure) throws when the datastore does not support transactions'() {
- given:
- transactionService.datastore = Mock(Datastore)
-
- when:
- transactionService.withTransaction([readOnly: true]) { status -> 'done' }
-
- then:
- thrown(TransactionSystemException)
- }
-
- void 'withRollback(TransactionDefinition, Closure) executes the callable and rolls back'() {
- given:
- transactionService.datastore = transactionCapableDatastore
- TransactionDefinition definition = new CustomizableRollbackTransactionAttribute()
-
- expect:
- transactionService.withRollback(definition) { status -> 'done' } == 'done'
- }
-
- void 'withRollback(TransactionDefinition, Closure) throws when the datastore does not support transactions'() {
- given:
- transactionService.datastore = Mock(Datastore)
- TransactionDefinition definition = new CustomizableRollbackTransactionAttribute()
-
- when:
- transactionService.withRollback(definition) { status -> 'done' }
-
- then:
- thrown(TransactionSystemException)
- }
-
- void 'withNewTransaction(TransactionDefinition, Closure) forces PROPAGATION_REQUIRES_NEW'() {
- given:
- transactionService.datastore = transactionCapableDatastore
- TransactionDefinition definition = new CustomizableRollbackTransactionAttribute()
-
- expect:
- transactionService.withNewTransaction(definition) { status -> 'done' } == 'done'
- }
-
- void 'withNewTransaction(TransactionDefinition, Closure) throws when the datastore does not support transactions'() {
+ void "getDatastore/setDatastore round-trip"() {
given:
- transactionService.datastore = Mock(Datastore)
- TransactionDefinition definition = new CustomizableRollbackTransactionAttribute()
+ def service = new DefaultTransactionService()
+ def datastore = Mock(Datastore)
when:
- transactionService.withNewTransaction(definition) { status -> 'done' }
+ service.setDatastore(datastore)
then:
- thrown(TransactionSystemException)
+ service.getDatastore().is(datastore)
}
}