This is an automated email from the ASF dual-hosted git repository. borinquenkid pushed a commit to branch test/abstract-datastore-initializer in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit f5d0465dac04bbe8c91b58fce8de5fb7d571b26a Author: Walter Duque de Estrada <[email protected]> AuthorDate: Thu Aug 13 13:34:44 2026 -0500 Add unit coverage for AbstractDatastoreInitializer and clean up IDE warnings Introduces a shared TestDatastoreInitializer test double and a new AbstractDatastoreInitializerSpec covering constructors, event/message publisher resolution, mapped-class filtering, bean registration helpers, data-service discovery and a full configure() round trip. The existing web-application spec is refactored to reuse the shared double instead of its own private copy. Also resolves several IDE-flagged issues in AbstractDatastoreInitializer: replaces the deprecated Class#newInstance() calls with getDeclaredConstructor().newInstance(), names previously-unused catch parameters, swaps an equals() call for ==, types the loadDataServices closure parameters, and makes containsRegisteredBean/getGrailsValidatorClass static since neither depends on instance state. getCommonConfiguration, getGrailsApplicationClass and isGrailsPresent are left as instance methods (with explanatory @SuppressWarnings) since they are genuine override hooks for downstream datastore initializers. Co-Authored-By: Claude Sonnet 5 <[email protected]> --- .../bootstrap/AbstractDatastoreInitializer.groovy | 32 +- .../AbstractDatastoreInitializerSpec.groovy | 360 +++++++++++++++++++++ ...ctDatastoreInitializerWebApplicationSpec.groovy | 5 - .../gorm/bootstrap/TestDatastoreInitializer.groovy | 95 ++++++ 4 files changed, 479 insertions(+), 13 deletions(-) diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializer.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializer.groovy index 510c47dcdf..5eb8112edc 100644 --- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializer.groovy +++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializer.groovy @@ -261,7 +261,14 @@ abstract class AbstractDatastoreInitializer implements ResourceLoaderAware { } } + /** + * Hook subclasses can override to contribute bean definitions common to every datastore type + * they configure. The {@code registry} and {@code type} parameters are unused by this default + * no-op implementation but are part of the override contract used by overriders such as the + * Hibernate and MongoDB datastore initializers. + */ @CompileDynamic + @SuppressWarnings(['GrMethodMayBeStatic', 'GroovyUnusedDeclaration']) Closure getCommonConfiguration(BeanDefinitionRegistry registry, String type) { return {} } @@ -274,7 +281,7 @@ abstract class AbstractDatastoreInitializer implements ResourceLoaderAware { } protected boolean isMappedClass(String datastoreType, Class cls) { - datastoreType.equals(ClassPropertyFetcher.getStaticPropertyValue(cls, GormProperties.MAPPING_STRATEGY, String)) + datastoreType == ClassPropertyFetcher.getStaticPropertyValue(cls, GormProperties.MAPPING_STRATEGY, String) } abstract Closure getBeanDefinitions(BeanDefinitionRegistry beanDefinitionRegistry) @@ -311,7 +318,7 @@ abstract class AbstractDatastoreInitializer implements ResourceLoaderAware { } } loadDataServices(null) - .each { serviceName, serviceClass -> + .each { String serviceName, Class<?> serviceClass -> "$serviceName"(DatastoreServiceMethodInvokingFactoryBean, serviceClass) { targetObject = ref("${type}Datastore") targetMethod = 'getService' @@ -356,7 +363,7 @@ abstract class AbstractDatastoreInitializer implements ResourceLoaderAware { } @CompileDynamic - protected boolean containsRegisteredBean(Object builder, BeanDefinitionRegistry registry, String beanName) { + protected static boolean containsRegisteredBean(Object builder, BeanDefinitionRegistry registry, String beanName) { registry.containsBeanDefinition(beanName) || (builder.hasProperty('springConfig') && builder.springConfig.containsBean(beanName)) } @@ -386,7 +393,13 @@ abstract class AbstractDatastoreInitializer implements ResourceLoaderAware { */ protected abstract Class<AbstractDatastorePersistenceContextInterceptor> getPersistenceInterceptorClass() + /** + * Not made static: {@code getClass()} intentionally resolves the classloader of the concrete + * subclass instance rather than this base class, which matters when a subclass is loaded by a + * different (e.g. plugin/OSGi) classloader than {@link AbstractDatastoreInitializer} itself. + */ @CompileStatic + @SuppressWarnings('GrMethodMayBeStatic') protected Class getGrailsApplicationClass() { ClassLoader cl = getClass().getClassLoader() if (ClassUtils.isPresent('grails.core.DefaultGrailsApplication', cl)) { @@ -396,6 +409,7 @@ abstract class AbstractDatastoreInitializer implements ResourceLoaderAware { } + @SuppressWarnings('GrMethodMayBeStatic') protected boolean isGrailsPresent() { ClassLoader cl = getClass().getClassLoader() if (ClassUtils.isPresent('grails.core.DefaultGrailsApplication', cl)) { @@ -405,7 +419,7 @@ abstract class AbstractDatastoreInitializer implements ResourceLoaderAware { } @CompileStatic - protected Class getGrailsValidatorClass() { + protected static Class getGrailsValidatorClass() { throw new UnsupportedOperationException('Method getGrailsValidatorClass no longer supported') } @@ -415,13 +429,14 @@ abstract class AbstractDatastoreInitializer implements ResourceLoaderAware { try { Thread.currentThread().contextClassLoader.loadClass('org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader') return true - } catch (e) { + } catch (ignored) { return false } } static void registerBeans(BeanDefinitionRegistry registry, Closure beanDefinitions) { def classLoader = Thread.currentThread().contextClassLoader - def beanReader = classLoader.loadClass('org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader').newInstance(registry) + def readerClass = classLoader.loadClass('org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader') + def beanReader = readerClass.getDeclaredConstructor(BeanDefinitionRegistry).newInstance(registry) beanReader.beans(beanDefinitions) } } @@ -432,14 +447,15 @@ abstract class AbstractDatastoreInitializer implements ResourceLoaderAware { try { Thread.currentThread().contextClassLoader.loadClass('grails.spring.BeanBuilder') return true - } catch (e) { + } catch (ignored) { return false } } static void registerBeans(BeanDefinitionRegistry registry, Closure beanDefinitions) { def classLoader = Thread.currentThread().contextClassLoader - def beanBuilder = classLoader.loadClass('grails.spring.BeanBuilder').newInstance() + def beanBuilderClass = classLoader.loadClass('grails.spring.BeanBuilder') + def beanBuilder = beanBuilderClass.getDeclaredConstructor().newInstance() beanBuilder.beans(beanDefinitions) beanBuilder.registerBeans(registry) } diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializerSpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializerSpec.groovy new file mode 100644 index 0000000000..076f2445bb --- /dev/null +++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializerSpec.groovy @@ -0,0 +1,360 @@ +/* + * 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.bootstrap + +import org.springframework.beans.factory.support.DefaultListableBeanFactory +import org.springframework.beans.factory.support.RootBeanDefinition +import org.springframework.context.support.GenericApplicationContext +import org.springframework.context.support.StaticMessageSource +import org.springframework.core.env.StandardEnvironment +import spock.lang.Specification + +import grails.core.DefaultGrailsApplication +import org.grails.datastore.gorm.events.ConfigurableApplicationContextEventPublisher +import org.grails.datastore.gorm.events.DefaultApplicationEventPublisher +import org.grails.datastore.gorm.services.DefaultTenantService +import org.grails.datastore.gorm.services.DefaultTransactionService + +/** + * Unit coverage for the reusable configuration behaviour in {@link AbstractDatastoreInitializer}, + * exercised through the {@link TestDatastoreInitializer} test double so no real datastore module + * (Hibernate, MongoDB, Neo4j, ...) is required. + */ +class AbstractDatastoreInitializerSpec extends Specification { + + void 'the no-arg constructor uses sensible defaults'() { + when: + def initializer = new TestDatastoreInitializer() + + then: + initializer.packages == [] + initializer.persistentClasses == [] + initializer.configuration instanceof StandardEnvironment + initializer.originalConfiguration == null + initializer.registerApplicationIfNotPresent + } + + void 'a package-name constructor records the given packages'() { + when: + def initializer = new TestDatastoreInitializer('com.example', 'com.other') + + then: + initializer.packages == ['com.example', 'com.other'] + } + + void 'a persistent-class constructor records the given classes'() { + when: + def initializer = new TestDatastoreInitializer(String, Integer) + + then: + initializer.persistentClasses == [String, Integer] + } + + void 'a Map configuration constructor derives a PropertyResolver but retains the original Map'() { + given: + Map config = ['foo.bar': 'baz'] + + when: + def initializer = new TestDatastoreInitializer(config, [String]) + + then: + initializer.originalConfiguration.is(config) + initializer.configuration.getRequiredProperty('foo.bar') == 'baz' + initializer.persistentClasses == [String] + } + + void 'a PropertyResolver configuration constructor keeps the resolver as-is with no original configuration'() { + given: + def resolver = new StandardEnvironment() + + when: + def initializer = new TestDatastoreInitializer(resolver, [String]) + + then: + initializer.configuration.is(resolver) + initializer.originalConfiguration == null + } + + void 'findEventPublisher wraps the registry itself when it is a ConfigurableApplicationContext'() { + given: + def initializer = new TestDatastoreInitializer() + def context = new GenericApplicationContext() + + expect: + initializer.findEventPublisher(context) instanceof ConfigurableApplicationContextEventPublisher + + cleanup: + context.close() + } + + void 'findEventPublisher falls back to the resource loader when the registry is not a ConfigurableApplicationContext'() { + given: + def initializer = new TestDatastoreInitializer() + def context = new GenericApplicationContext() + initializer.setResourceLoader(context) + + expect: + initializer.findEventPublisher(new DefaultListableBeanFactory()) instanceof ConfigurableApplicationContextEventPublisher + + cleanup: + context.close() + } + + void 'findEventPublisher defaults to a DefaultApplicationEventPublisher when neither source is available'() { + given: + def initializer = new TestDatastoreInitializer() + + expect: + initializer.findEventPublisher(new DefaultListableBeanFactory()) instanceof DefaultApplicationEventPublisher + } + + void 'findMessageSource uses the registry itself when it is a MessageSource'() { + given: + def initializer = new TestDatastoreInitializer() + def context = new GenericApplicationContext() + + expect: + initializer.findMessageSource(context).is(context) + + cleanup: + context.close() + } + + void 'findMessageSource falls back to the resource loader when the registry is not a MessageSource'() { + given: + def initializer = new TestDatastoreInitializer() + def context = new GenericApplicationContext() + initializer.setResourceLoader(context) + + expect: + initializer.findMessageSource(new DefaultListableBeanFactory()).is(context) + + cleanup: + context.close() + } + + void 'findMessageSource defaults to a fresh StaticMessageSource when neither source is available'() { + given: + def initializer = new TestDatastoreInitializer() + + expect: + initializer.findMessageSource(new DefaultListableBeanFactory()) instanceof StaticMessageSource + } + + void 'setResourceLoader rebuilds the resource pattern resolver around the given loader'() { + given: + def initializer = new TestDatastoreInitializer() + def loader = new GenericApplicationContext() + + when: + initializer.setResourceLoader(loader) + + then: + initializer.resourcePatternResolver.resourceLoader.is(loader) + + cleanup: + loader.close() + } + + void 'isMappedClass returns true only when the static mapWith property matches the datastore type'() { + given: + def initializer = new TestDatastoreInitializer() + + expect: + initializer.isMappedClass('mongo', MongoEntity) + !initializer.isMappedClass('sql', MongoEntity) + !initializer.isMappedClass('mongo', UnmappedEntity) + } + + void 'collectMappedClasses returns every persistent class when this is not a secondary datastore'() { + given: + def initializer = new TestDatastoreInitializer([MongoEntity, SqlEntity, UnmappedEntity]) + + expect: + initializer.collectMappedClasses('mongo') == [MongoEntity, SqlEntity, UnmappedEntity] + } + + void 'collectMappedClasses filters to only the classes mapped to the given type for a secondary datastore'() { + given: + def initializer = new TestDatastoreInitializer([MongoEntity, SqlEntity, UnmappedEntity]) + initializer.setSecondaryDatastore(true) + + expect: + initializer.collectMappedClasses('mongo') == [MongoEntity] + initializer.collectMappedClasses('sql') == [SqlEntity] + } + + void 'containsRegisteredBean returns true when the registry already contains a bean definition with that name'() { + given: + def registry = new DefaultListableBeanFactory() + registry.registerBeanDefinition('fooBean', new RootBeanDefinition(Object)) + def initializer = new TestDatastoreInitializer() + + expect: + initializer.containsRegisteredBean(new Object(), registry, 'fooBean') + } + + void 'containsRegisteredBean falls back to a springConfig-aware builder when the registry does not know the bean'() { + given: + def registry = new DefaultListableBeanFactory() + def builder = new BeanBuilderStub(springConfig: new SpringConfigStub(beanNames: ['fooBean'] as Set)) + def initializer = new TestDatastoreInitializer() + + expect: + initializer.containsRegisteredBean(builder, registry, 'fooBean') + !initializer.containsRegisteredBean(builder, registry, 'otherBean') + } + + void 'containsRegisteredBean returns false when neither the registry nor the builder know the bean'() { + given: + def registry = new DefaultListableBeanFactory() + def initializer = new TestDatastoreInitializer() + + expect: + !initializer.containsRegisteredBean(new Object(), registry, 'fooBean') + } + + void 'getCommonConfiguration returns a no-op closure by default'() { + given: + def initializer = new TestDatastoreInitializer() + + when: + def closure = initializer.getCommonConfiguration(new DefaultListableBeanFactory(), 'foo') + + then: + closure instanceof Closure + closure() == null + } + + void 'loadDataServices discovers the Service implementations declared for this module'() { + given: + def initializer = new TestDatastoreInitializer() + + when: + def services = initializer.loadDataServices() + + then: + services.defaultTransactionService == DefaultTransactionService + services.defaultTenantService == DefaultTenantService + } + + void 'loadDataServices namespaces service names under the secondary datastore type when given'() { + given: + def initializer = new TestDatastoreInitializer() + + when: + def services = initializer.loadDataServices('foo') + + then: + services.fooDefaultTransactionService == DefaultTransactionService + services.fooDefaultTenantService == DefaultTenantService + } + + void 'isGrailsPresent and getGrailsApplicationClass detect grails-core on the classpath'() { + given: + def initializer = new TestDatastoreInitializer() + + expect: + initializer.isGrailsPresent() + initializer.getGrailsApplicationClass() == DefaultGrailsApplication + } + + void 'getGrailsValidatorClass is no longer supported'() { + given: + def initializer = new TestDatastoreInitializer() + + when: + initializer.getGrailsValidatorClass() + + then: + thrown(UnsupportedOperationException) + } + + void 'getAdditionalBeansConfiguration registers a transaction manager, persistence interceptor, aggregator and every data service'() { + given: + def registry = new DefaultListableBeanFactory() + def initializer = new TestDatastoreInitializer() + + when: + def beanDefinitions = initializer.getAdditionalBeansConfiguration(registry, 'foo') + AbstractDatastoreInitializer.GroovyBeanReaderInit.registerBeans(registry, beanDefinitions) + + then: + registry.containsBeanDefinition('fooTransactionManager') + registry.isAlias('transactionManager') + registry.getAliases('fooTransactionManager') as Set == ['transactionManager'] as Set + registry.containsBeanDefinition('fooPersistenceInterceptor') + registry.containsBeanDefinition('fooPersistenceContextInterceptorAggregator') + registry.containsBeanDefinition('defaultTransactionService') + registry.containsBeanDefinition('defaultTenantService') + !registry.containsBeanDefinition('fooOpenSessionInViewInterceptor') + } + + void 'getAdditionalBeansConfiguration does not alias an already-registered transactionManager bean'() { + given: + def registry = new DefaultListableBeanFactory() + registry.registerBeanDefinition('transactionManager', new RootBeanDefinition(Object)) + def initializer = new TestDatastoreInitializer() + + when: + def beanDefinitions = initializer.getAdditionalBeansConfiguration(registry, 'foo') + AbstractDatastoreInitializer.GroovyBeanReaderInit.registerBeans(registry, beanDefinitions) + + then: + registry.containsBeanDefinition('fooTransactionManager') + !registry.isAlias('transactionManager') + registry.getBeanDefinition('transactionManager').beanClassName == Object.name + } + + void 'configure builds a fully refreshed application context containing the declared beans'() { + given: + def initializer = new TestDatastoreInitializer() + initializer.beanDefinitions = { -> "sampleBean"(String, 'hello') } + + when: + def context = initializer.configure() + + then: + context.isActive() + context.getBean('sampleBean', String) == 'hello' + + cleanup: + context.close() + } + + static class MongoEntity { + static mapWith = 'mongo' + } + + static class SqlEntity { + static mapWith = 'sql' + } + + static class UnmappedEntity { + } + + static class SpringConfigStub { + Set<String> beanNames + boolean containsBean(String name) { name in beanNames } + } + + static class BeanBuilderStub { + SpringConfigStub springConfig + } +} diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializerWebApplicationSpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializerWebApplicationSpec.groovy index 0a76590f3a..7d77719c2d 100644 --- a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializerWebApplicationSpec.groovy +++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializerWebApplicationSpec.groovy @@ -57,9 +57,4 @@ class AbstractDatastoreInitializerWebApplicationSpec extends Specification { expect: !isWeb(new DefaultListableBeanFactory()) } - - static class TestDatastoreInitializer extends AbstractDatastoreInitializer { - Closure getBeanDefinitions(BeanDefinitionRegistry beanDefinitionRegistry) { { -> } } - protected Class getPersistenceInterceptorClass() { null } - } } diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/bootstrap/TestDatastoreInitializer.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/bootstrap/TestDatastoreInitializer.groovy new file mode 100644 index 0000000000..a051d2792e --- /dev/null +++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/bootstrap/TestDatastoreInitializer.groovy @@ -0,0 +1,95 @@ +/* + * 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.bootstrap + +import org.springframework.beans.factory.support.BeanDefinitionRegistry +import org.springframework.core.env.PropertyResolver + +import org.grails.datastore.gorm.support.AbstractDatastorePersistenceContextInterceptor +import org.grails.datastore.mapping.core.Datastore + +/** + * Minimal concrete {@link AbstractDatastoreInitializer} used to exercise the abstract + * class's own behaviour in unit tests without depending on a real datastore module + * (Hibernate, MongoDB, Neo4j, ...). + * + * <p>{@link #beanDefinitions} and {@link #persistenceInterceptorClass} default to + * harmless no-op implementations but can be overridden per-test. + */ +class TestDatastoreInitializer extends AbstractDatastoreInitializer { + + Closure beanDefinitions = { -> } + Class<AbstractDatastorePersistenceContextInterceptor> persistenceInterceptorClass = TestPersistenceContextInterceptor + + TestDatastoreInitializer() { + super() + } + + TestDatastoreInitializer(ClassLoader classLoader, String... packages) { + super(classLoader, packages) + } + + TestDatastoreInitializer(String... packages) { + super(packages) + } + + TestDatastoreInitializer(Collection<Class> persistentClasses) { + super(persistentClasses) + } + + TestDatastoreInitializer(Class... persistentClasses) { + super(persistentClasses) + } + + TestDatastoreInitializer(PropertyResolver configuration, Collection<Class> persistentClasses) { + super(configuration, persistentClasses) + } + + TestDatastoreInitializer(PropertyResolver configuration, Class... persistentClasses) { + super(configuration, persistentClasses) + } + + TestDatastoreInitializer(PropertyResolver configuration, String... packages) { + super(configuration, packages) + } + + TestDatastoreInitializer(Map configuration, Collection<Class> persistentClasses) { + super(configuration, persistentClasses) + } + + TestDatastoreInitializer(Map configuration, Class... persistentClasses) { + super(configuration, persistentClasses) + } + + @Override + Closure getBeanDefinitions(BeanDefinitionRegistry beanDefinitionRegistry) { + beanDefinitions + } + + @Override + protected Class<AbstractDatastorePersistenceContextInterceptor> getPersistenceInterceptorClass() { + persistenceInterceptorClass + } + + static class TestPersistenceContextInterceptor extends AbstractDatastorePersistenceContextInterceptor { + TestPersistenceContextInterceptor(Datastore datastore) { + super(datastore) + } + } +}
