Browse Source
TestContextAotGenerator now uses AotTestMappingsCodeGenerator to generate a AotTestMappings__Generated.java class which is loaded in AotTestMappings via reflection in order to retrieve access to ApplicationContextIntializers generated during AOT processing. Furthermore, the processAheadOfTimeAndGenerateAotTestMappings() method in TestContextAotGeneratorTests now performs a rather extensive test including: - emulating TestClassScanner to find test classes - processing all test classes and generating ApplicationContextIntializers - generating mappings for AotTestMappings - compiling all generated code - loading AotTestMappings - using AotTestMappings to instantiate the generated ApplicationContextIntializer - using the AotContextLoader API to load the AOT-optimized ApplicationContext - asserting the behavior of the loaded ApplicationContext See gh-28205 Closes gh-28204pull/28958/head
6 changed files with 315 additions and 46 deletions
@ -0,0 +1,113 @@
@@ -0,0 +1,113 @@
|
||||
/* |
||||
* Copyright 2002-2022 the original author or authors. |
||||
* |
||||
* Licensed 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.springframework.test.context.aot; |
||||
|
||||
import java.lang.reflect.Method; |
||||
import java.util.Map; |
||||
import java.util.function.Supplier; |
||||
|
||||
import org.springframework.context.ApplicationContextInitializer; |
||||
import org.springframework.context.ConfigurableApplicationContext; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.ClassUtils; |
||||
import org.springframework.util.ReflectionUtils; |
||||
|
||||
/** |
||||
* {@code AotTestMappings} provides mappings from test classes to AOT-optimized |
||||
* context initializers. |
||||
* |
||||
* <p>If a test class is not {@linkplain #isSupportedTestClass(Class) supported} in |
||||
* AOT mode, {@link #getContextInitializer(Class)} will return {@code null}. |
||||
* |
||||
* <p>Reflectively accesses {@link #GENERATED_MAPPINGS_CLASS_NAME} generated by |
||||
* the {@link TestContextAotGenerator} to retrieve the mappings generated during |
||||
* AOT processing. |
||||
* |
||||
* @author Sam Brannen |
||||
* @author Stephane Nicoll |
||||
* @since 6.0 |
||||
*/ |
||||
public class AotTestMappings { |
||||
|
||||
// TODO Add support in ClassNameGenerator for supplying a predefined class name.
|
||||
// There is a similar issue in Spring Boot where code relies on a generated name.
|
||||
// Ideally we would generate a class named: org.springframework.test.context.aot.GeneratedAotTestMappings
|
||||
static final String GENERATED_MAPPINGS_CLASS_NAME = AotTestMappings.class.getName() + "__Generated"; |
||||
|
||||
static final String GENERATED_MAPPINGS_METHOD_NAME = "getContextInitializers"; |
||||
|
||||
private final Map<String, Supplier<ApplicationContextInitializer<ConfigurableApplicationContext>>> contextInitializers; |
||||
|
||||
|
||||
public AotTestMappings() { |
||||
this(GENERATED_MAPPINGS_CLASS_NAME); |
||||
} |
||||
|
||||
AotTestMappings(String initializerClassName) { |
||||
this(loadContextInitializersMap(initializerClassName)); |
||||
} |
||||
|
||||
AotTestMappings(Map<String, Supplier<ApplicationContextInitializer<ConfigurableApplicationContext>>> contextInitializers) { |
||||
this.contextInitializers = contextInitializers; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Determine if the specified test class has an AOT-optimized application context |
||||
* initializer. |
||||
* <p>If this method returns {@code true}, {@link #getContextInitializer(Class)} |
||||
* should not return {@code null}. |
||||
*/ |
||||
public boolean isSupportedTestClass(Class<?> testClass) { |
||||
return this.contextInitializers.containsKey(testClass.getName()); |
||||
} |
||||
|
||||
/** |
||||
* Get the AOT {@link ApplicationContextInitializer} for the specified test class. |
||||
* @return the AOT context initializer, or {@code null} if there is no AOT context |
||||
* initializer for the specified test class
|
||||
* @see #isSupportedTestClass(Class) |
||||
*/ |
||||
public ApplicationContextInitializer<ConfigurableApplicationContext> getContextInitializer(Class<?> testClass) { |
||||
Supplier<ApplicationContextInitializer<ConfigurableApplicationContext>> supplier = |
||||
this.contextInitializers.get(testClass.getName()); |
||||
return (supplier != null ? supplier.get() : null); |
||||
} |
||||
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" }) |
||||
private static Map<String, Supplier<ApplicationContextInitializer<ConfigurableApplicationContext>>> |
||||
loadContextInitializersMap(String className) { |
||||
|
||||
String methodName = GENERATED_MAPPINGS_METHOD_NAME; |
||||
|
||||
try { |
||||
Class<?> clazz = ClassUtils.forName(className, null); |
||||
Method method = ReflectionUtils.findMethod(clazz, methodName); |
||||
Assert.state(method != null, () -> "No %s() method found in %s".formatted(methodName, clazz.getName())); |
||||
return (Map<String, Supplier<ApplicationContextInitializer<ConfigurableApplicationContext>>>) |
||||
ReflectionUtils.invokeMethod(method, null); |
||||
} |
||||
catch (IllegalStateException ex) { |
||||
throw ex; |
||||
} |
||||
catch (Exception ex) { |
||||
throw new IllegalStateException("Failed to invoke %s() method in %s".formatted(methodName, className), ex); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,113 @@
@@ -0,0 +1,113 @@
|
||||
/* |
||||
* Copyright 2002-2022 the original author or authors. |
||||
* |
||||
* Licensed 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.springframework.test.context.aot; |
||||
|
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.function.Supplier; |
||||
|
||||
import javax.lang.model.element.Modifier; |
||||
|
||||
import org.apache.commons.logging.Log; |
||||
import org.apache.commons.logging.LogFactory; |
||||
|
||||
import org.springframework.aot.generate.GeneratedClass; |
||||
import org.springframework.aot.generate.GeneratedClasses; |
||||
import org.springframework.context.ApplicationContextInitializer; |
||||
import org.springframework.context.ConfigurableApplicationContext; |
||||
import org.springframework.core.log.LogMessage; |
||||
import org.springframework.javapoet.ClassName; |
||||
import org.springframework.javapoet.CodeBlock; |
||||
import org.springframework.javapoet.MethodSpec; |
||||
import org.springframework.javapoet.ParameterizedTypeName; |
||||
import org.springframework.javapoet.TypeName; |
||||
import org.springframework.javapoet.TypeSpec; |
||||
import org.springframework.javapoet.WildcardTypeName; |
||||
import org.springframework.util.MultiValueMap; |
||||
|
||||
/** |
||||
* Internal code generator for mappings used by {@link AotTestMappings}. |
||||
* |
||||
* @author Sam Brannen |
||||
* @since 6.0 |
||||
*/ |
||||
class AotTestMappingsCodeGenerator { |
||||
|
||||
private static final Log logger = LogFactory.getLog(AotTestMappingsCodeGenerator.class); |
||||
|
||||
private static final ParameterizedTypeName CONTEXT_INITIALIZER = ParameterizedTypeName.get( |
||||
ClassName.get(ApplicationContextInitializer.class), |
||||
WildcardTypeName.subtypeOf(ConfigurableApplicationContext.class)); |
||||
|
||||
private static final ParameterizedTypeName CONTEXT_INITIALIZER_SUPPLIER = ParameterizedTypeName |
||||
.get(ClassName.get(Supplier.class), CONTEXT_INITIALIZER); |
||||
|
||||
// Map<String, Supplier<ApplicationContextInitializer<? extends ConfigurableApplicationContext>>>
|
||||
private static final TypeName CONTEXT_SUPPLIER_MAP = ParameterizedTypeName |
||||
.get(ClassName.get(Map.class), ClassName.get(String.class), CONTEXT_INITIALIZER_SUPPLIER); |
||||
|
||||
|
||||
private final MultiValueMap<ClassName, Class<?>> initializerClassMappings; |
||||
|
||||
private final GeneratedClass generatedClass; |
||||
|
||||
|
||||
AotTestMappingsCodeGenerator(MultiValueMap<ClassName, Class<?>> initializerClassMappings, |
||||
GeneratedClasses generatedClasses) { |
||||
|
||||
this.initializerClassMappings = initializerClassMappings; |
||||
this.generatedClass = generatedClasses.addForFeature("Generated", this::generateType); |
||||
} |
||||
|
||||
|
||||
GeneratedClass getGeneratedClass() { |
||||
return this.generatedClass; |
||||
} |
||||
|
||||
private void generateType(TypeSpec.Builder type) { |
||||
logger.debug(LogMessage.format("Generating AOT test mappings in %s", |
||||
this.generatedClass.getName().reflectionName())); |
||||
type.addJavadoc("Generated mappings for {@link $T}.", AotTestMappings.class); |
||||
type.addModifiers(Modifier.PUBLIC); |
||||
type.addMethod(generateMappingMethod()); |
||||
} |
||||
|
||||
private MethodSpec generateMappingMethod() { |
||||
MethodSpec.Builder method = MethodSpec.methodBuilder(AotTestMappings.GENERATED_MAPPINGS_METHOD_NAME); |
||||
method.addModifiers(Modifier.PUBLIC, Modifier.STATIC); |
||||
method.returns(CONTEXT_SUPPLIER_MAP); |
||||
method.addCode(generateMappingCode()); |
||||
return method.build(); |
||||
} |
||||
|
||||
private CodeBlock generateMappingCode() { |
||||
CodeBlock.Builder code = CodeBlock.builder(); |
||||
code.addStatement("$T map = new $T<>()", CONTEXT_SUPPLIER_MAP, HashMap.class); |
||||
this.initializerClassMappings.forEach((className, testClasses) -> { |
||||
List<String> testClassNames = testClasses.stream().map(Class::getName).toList(); |
||||
logger.debug(LogMessage.format( |
||||
"Generating mapping from AOT context initializer [%s] to test classes %s", |
||||
className.reflectionName(), testClassNames)); |
||||
testClassNames.forEach(testClassName -> |
||||
code.addStatement("map.put($S, () -> new $T())", testClassName, className)); |
||||
}); |
||||
code.addStatement("return map"); |
||||
return code.build(); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue