Browse Source

Allow test classes to provide runtime hints via declarative mechanisms

Prior to this commit, it was possible to register hints for individual
test classes programmatically via the
org.springframework.test.context.aot.TestRuntimeHintsRegistrar SPI;
however, that requires that a custom TestRuntimeHintsRegistrar be
registered via "META-INF/spring/aot.factories". In addition,
implementing a TestRuntimeHintsRegistrar is more cumbersome than using
the core mechanisms such as @Reflective, @ImportRuntimeHints, and
@RegisterReflectionForBinding.

This commit address this by introducing support for @Reflective and
@ImportRuntimeHints on test classes. @RegisterReflectionForBinding
support is available automatically since it is an extension of the
@Reflective mechanism.

Closes gh-29455
pull/29492/head
Sam Brannen 3 years ago
parent
commit
ae3bec5d57
  1. 46
      spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java
  2. 69
      spring-test/src/test/java/org/springframework/test/context/aot/DeclarativeRuntimeHintsTests.java
  3. 76
      spring-test/src/test/java/org/springframework/test/context/aot/samples/hints/DeclarativeRuntimeHintsSpringJupiterTests.java

46
spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java

@ -16,7 +16,10 @@ @@ -16,7 +16,10 @@
package org.springframework.test.context.aot;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
@ -30,12 +33,19 @@ import org.springframework.aot.generate.GeneratedClasses; @@ -30,12 +33,19 @@ import org.springframework.aot.generate.GeneratedClasses;
import org.springframework.aot.generate.GeneratedFiles;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.TypeReference;
import org.springframework.aot.hint.annotation.ReflectiveRuntimeHintsRegistrar;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.aot.AotServices;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.context.aot.ApplicationContextAotGenerator;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.core.log.LogMessage;
import org.springframework.javapoet.ClassName;
import org.springframework.test.context.BootstrapUtils;
@ -117,16 +127,31 @@ public class TestContextAotGenerator { @@ -117,16 +127,31 @@ public class TestContextAotGenerator {
try {
resetAotFactories();
Set<Class<? extends RuntimeHintsRegistrar>> coreRuntimeHintsRegistrarClasses = new LinkedHashSet<>();
ReflectiveRuntimeHintsRegistrar reflectiveRuntimeHintsRegistrar = new ReflectiveRuntimeHintsRegistrar();
MultiValueMap<MergedContextConfiguration, Class<?>> mergedConfigMappings = new LinkedMultiValueMap<>();
ClassLoader classLoader = getClass().getClassLoader();
testClasses.forEach(testClass -> {
MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass);
mergedConfigMappings.add(mergedConfig, testClass);
collectRuntimeHintsRegistrarClasses(testClass, coreRuntimeHintsRegistrarClasses);
reflectiveRuntimeHintsRegistrar.registerRuntimeHints(this.runtimeHints, testClass);
this.testRuntimeHintsRegistrars.forEach(registrar ->
registrar.registerHints(this.runtimeHints, testClass, classLoader));
});
MultiValueMap<ClassName, Class<?>> initializerClassMappings = processAheadOfTime(mergedConfigMappings);
coreRuntimeHintsRegistrarClasses.stream()
.map(BeanUtils::instantiateClass)
.forEach(registrar -> {
if (logger.isTraceEnabled()) {
logger.trace("Processing RuntimeHints contribution from test class [%s]"
.formatted(registrar.getClass().getCanonicalName()));
}
registrar.registerHints(this.runtimeHints, classLoader);
});
MultiValueMap<ClassName, Class<?>> initializerClassMappings = processAheadOfTime(mergedConfigMappings);
generateAotTestContextInitializerMappings(initializerClassMappings);
generateAotTestAttributeMappings();
}
@ -135,6 +160,25 @@ public class TestContextAotGenerator { @@ -135,6 +160,25 @@ public class TestContextAotGenerator {
}
}
/**
* Collect all {@link RuntimeHintsRegistrar} classes declared via
* {@link ImportRuntimeHints @ImportRuntimeHints} on the supplied test class
* and add them to the supplied {@link Set}.
* @param testClass the test class on which to search for {@code @ImportRuntimeHints}
* @param coreRuntimeHintsRegistrarClasses the set of registrar classes
*/
private void collectRuntimeHintsRegistrarClasses(
Class<?> testClass, Set<Class<? extends RuntimeHintsRegistrar>> coreRuntimeHintsRegistrarClasses) {
MergedAnnotations.from(testClass, SearchStrategy.TYPE_HIERARCHY)
.stream(ImportRuntimeHints.class)
.filter(MergedAnnotation::isPresent)
.map(MergedAnnotation::synthesize)
.map(ImportRuntimeHints::value)
.flatMap(Arrays::stream)
.forEach(coreRuntimeHintsRegistrarClasses::add);
}
private void resetAotFactories() {
AotTestAttributesFactory.reset();
AotTestContextInitializersFactory.reset();

69
spring-test/src/test/java/org/springframework/test/context/aot/DeclarativeRuntimeHintsTests.java

@ -0,0 +1,69 @@ @@ -0,0 +1,69 @@
/*
* 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.stream.Stream;
import org.junit.jupiter.api.Test;
import org.springframework.aot.generate.InMemoryGeneratedFiles;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.test.context.aot.samples.hints.DeclarativeRuntimeHintsSpringJupiterTests;
import org.springframework.test.context.aot.samples.hints.DeclarativeRuntimeHintsSpringJupiterTests.SampleClassWithGetter;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.reflection;
import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.resource;
/**
* Tests for declarative support for registering run-time hints for tests, tested
* via the {@link TestContextAotGenerator}
*
* @author Sam Brannen
* @since 6.0
*/
class DeclarativeRuntimeHintsTests extends AbstractAotTests {
private final RuntimeHints runtimeHints = new RuntimeHints();
private final TestContextAotGenerator generator =
new TestContextAotGenerator(new InMemoryGeneratedFiles(), this.runtimeHints);
@Test
void declarativeRuntimeHints() {
Class<?> testClass = DeclarativeRuntimeHintsSpringJupiterTests.class;
this.generator.processAheadOfTime(Stream.of(testClass));
// @Reflective
assertReflectionRegistered(testClass);
// @@RegisterReflectionForBinding
assertReflectionRegistered(SampleClassWithGetter.class);
assertReflectionRegistered(String.class);
assertThat(reflection().onMethod(SampleClassWithGetter.class, "getName")).accepts(this.runtimeHints);
// @ImportRuntimeHints
assertThat(resource().forResource("org/example/config/enigma.txt")).accepts(this.runtimeHints);
}
private void assertReflectionRegistered(Class<?> type) {
assertThat(reflection().onType(type)).as("Reflection hint for %s", type).accepts(this.runtimeHints);
}
}

76
spring-test/src/test/java/org/springframework/test/context/aot/samples/hints/DeclarativeRuntimeHintsSpringJupiterTests.java

@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
/*
* 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.samples.hints;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.annotation.Reflective;
import org.springframework.aot.hint.annotation.RegisterReflectionForBinding;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.test.context.aot.samples.hints.DeclarativeRuntimeHintsSpringJupiterTests.DemoHints;
import org.springframework.test.context.aot.samples.hints.DeclarativeRuntimeHintsSpringJupiterTests.SampleClassWithGetter;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Sam Brannen
* @since 6.0
*/
@SpringJUnitConfig
@Reflective
@RegisterReflectionForBinding(SampleClassWithGetter.class)
@ImportRuntimeHints(DemoHints.class)
public class DeclarativeRuntimeHintsSpringJupiterTests {
@Test
void test(@Autowired String foo) {
assertThat(foo).isEqualTo("bar");
}
@Configuration(proxyBeanMethods = false)
static class Config {
@Bean
String foo() {
return "bar";
}
}
static class DemoHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.resources().registerPattern("org/example/config/*.txt");
}
}
public static class SampleClassWithGetter {
public String getName() {
return null;
}
}
}
Loading…
Cancel
Save