Skip to content

Commit

Permalink
Retain existing feature name as prefix in test AOT processing
Browse files Browse the repository at this point in the history
Prior to this commit, test AOT processing failed if a feature name for
generated class names was used for more than one ApplicationContext.

For example, when generating code for org.example.MessageService with a
"Management" feature name, the "BeanDefinitions" class was named as
follows (without a uniquely identifying TestContext###_ feature name
prefix).

org/example/MessageService__ManagementBeanDefinitions.java

When another attempt was made to generate code for the MessageService
using the same "Management" feature name, a FileAlreadyExistsException
was thrown denoting that the class/file name was already in use.

To avoid such naming collisions, this commit introduces a
TestContextGenerationContext which provides a custom implementation of
withName(String) that prepends an existing feature name (if present) to
a new feature name, thereby treating any existing feature name as a
prefix to a new, nested feature name.

Consequently, code generation for the above example now results in
unique class/file names like the following (which retain the uniquely
identifying TestContext###_ prefixes).

org/example/MessageService__TestContext002_ManagementBeanDefinitions.java
org/example/MessageService__TestContext003_ManagementBeanDefinitions.java

Closes spring-projectsgh-30861
  • Loading branch information
sbrannen committed Jul 15, 2023
1 parent 8b97d5f commit ae4aaae
Show file tree
Hide file tree
Showing 11 changed files with 272 additions and 11 deletions.
Expand Up @@ -324,8 +324,8 @@ private MergedContextConfiguration buildMergedContextConfiguration(Class<?> test

DefaultGenerationContext createGenerationContext(Class<?> testClass) {
ClassNameGenerator classNameGenerator = new ClassNameGenerator(ClassName.get(testClass));
DefaultGenerationContext generationContext =
new DefaultGenerationContext(classNameGenerator, this.generatedFiles, this.runtimeHints);
TestContextGenerationContext generationContext =
new TestContextGenerationContext(classNameGenerator, this.generatedFiles, this.runtimeHints);
return generationContext.withName(nextTestContextId());
}

Expand Down
@@ -0,0 +1,85 @@
/*
* Copyright 2002-2023 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 org.springframework.aot.generate.ClassNameGenerator;
import org.springframework.aot.generate.DefaultGenerationContext;
import org.springframework.aot.generate.GeneratedClasses;
import org.springframework.aot.generate.GeneratedFiles;
import org.springframework.aot.hint.RuntimeHints;

/**
* Extension of {@link DefaultGenerationContext} with a custom implementation of
* {@link #withName(String)} that is specific to the <em>Spring TestContext Framework</em>.
*
* @author Sam Brannen
* @since 6.0.12
*/
class TestContextGenerationContext extends DefaultGenerationContext {

private final String featureName;


/**
* Create a new {@link TestContextGenerationContext} instance backed by the
* specified {@link ClassNameGenerator}, {@link GeneratedFiles}, and
* {@link RuntimeHints}.
* @param classNameGenerator the naming convention to use for generated class names
* @param generatedFiles the generated files
* @param runtimeHints the runtime hints
*/
TestContextGenerationContext(ClassNameGenerator classNameGenerator, GeneratedFiles generatedFiles,
RuntimeHints runtimeHints) {
super(classNameGenerator, generatedFiles, runtimeHints);
this.featureName = null;
}

/**
* Create a new {@link TestContextGenerationContext} instance backed by the
* specified {@link GeneratedClasses}, {@link GeneratedFiles}, and
* {@link RuntimeHints}.
* @param generatedClasses the generated classes
* @param generatedFiles the generated files
* @param runtimeHints the runtime hints
*/
private TestContextGenerationContext(GeneratedClasses generatedClasses, GeneratedFiles generatedFiles,
RuntimeHints runtimeHints, String featureName) {
super(generatedClasses, generatedFiles, runtimeHints);
this.featureName = featureName;
}


/**
* Create a new {@link TestContextGenerationContext} instance using the specified
* feature name to qualify generated assets for a dedicated round of code generation.
* <p>If <em>this</em> {@code TestContextGenerationContext} has a configured feature
* name, the supplied feature name will be appended to the existing feature name
* in order to avoid naming collisions.
* @param featureName the feature name to use
* @return a specialized {@link TestContextGenerationContext} for the specified
* feature name
*/
@Override
public TestContextGenerationContext withName(String featureName) {
if (this.featureName != null) {
featureName = this.featureName + featureName;
}
GeneratedClasses generatedClasses = getGeneratedClasses().withFeatureNamePrefix(featureName);
return new TestContextGenerationContext(generatedClasses, getGeneratedFiles(), getRuntimeHints(), featureName);
}

}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
Expand Down Expand Up @@ -45,14 +45,22 @@ abstract class AbstractAotTests {
"org/springframework/context/event/EventListenerMethodProcessor__TestContext002_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests__TestContext002_ApplicationContextInitializer.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests__TestContext002_BeanFactoryRegistrations.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests__TestContext002_ManagementApplicationContextInitializer.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests__TestContext002_ManagementBeanFactoryRegistrations.java",
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext002_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/management/ManagementConfiguration__TestContext002_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/management/ManagementMessageService__TestContext002_ManagementBeanDefinitions.java",
// BasicSpringJupiterTests -- not generated b/c already generated for BasicSpringJupiterSharedConfigTests.
// BasicSpringJupiterTests.NestedTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext003_BeanDefinitions.java",
"org/springframework/context/event/EventListenerMethodProcessor__TestContext003_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests_NestedTests__TestContext003_ApplicationContextInitializer.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests_NestedTests__TestContext003_BeanFactoryRegistrations.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests_NestedTests__TestContext003_ManagementApplicationContextInitializer.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests_NestedTests__TestContext003_ManagementBeanFactoryRegistrations.java",
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext003_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/management/ManagementConfiguration__TestContext003_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/management/ManagementMessageService__TestContext003_ManagementBeanDefinitions.java",
// BasicSpringTestNGTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext004_BeanDefinitions.java",
"org/springframework/context/event/EventListenerMethodProcessor__TestContext004_BeanDefinitions.java",
Expand Down
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
Expand Down Expand Up @@ -36,6 +36,7 @@
import org.springframework.aot.AotDetector;
import org.springframework.aot.generate.GeneratedFiles.Kind;
import org.springframework.aot.generate.InMemoryGeneratedFiles;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.test.generate.CompilerFiles;
import org.springframework.core.test.tools.CompileWithForkedClassLoader;
import org.springframework.core.test.tools.TestCompiler;
Expand Down Expand Up @@ -86,7 +87,7 @@ void endToEndTests() {

// AOT BUILD-TIME: PROCESSING
InMemoryGeneratedFiles generatedFiles = new InMemoryGeneratedFiles();
TestContextAotGenerator generator = new TestContextAotGenerator(generatedFiles);
TestContextAotGenerator generator = new TestContextAotGenerator(generatedFiles, new RuntimeHints(), true);
generator.processAheadOfTime(testClasses);

List<String> sourceFiles = generatedFiles.getGeneratedFiles(Kind.SOURCE).keySet().stream().toList();
Expand Down
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
Expand Down Expand Up @@ -366,14 +366,22 @@ record Mapping(MergedContextConfiguration mergedConfig, ClassName className) {
"org/springframework/context/event/EventListenerMethodProcessor__TestContext001_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests__TestContext001_ApplicationContextInitializer.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests__TestContext001_BeanFactoryRegistrations.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests__TestContext001_ManagementApplicationContextInitializer.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests__TestContext001_ManagementBeanFactoryRegistrations.java",
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext001_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/management/ManagementConfiguration__TestContext001_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/management/ManagementMessageService__TestContext001_ManagementBeanDefinitions.java",
// BasicSpringJupiterTests -- not generated b/c already generated for BasicSpringJupiterSharedConfigTests.
// BasicSpringJupiterTests.NestedTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext002_BeanDefinitions.java",
"org/springframework/context/event/EventListenerMethodProcessor__TestContext002_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests_NestedTests__TestContext002_ApplicationContextInitializer.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests_NestedTests__TestContext002_BeanFactoryRegistrations.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests_NestedTests__TestContext002_ManagementApplicationContextInitializer.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests_NestedTests__TestContext002_ManagementBeanFactoryRegistrations.java",
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext002_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/management/ManagementConfiguration__TestContext002_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/management/ManagementMessageService__TestContext002_ManagementBeanDefinitions.java",
// BasicSpringTestNGTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext003_BeanDefinitions.java",
"org/springframework/context/event/EventListenerMethodProcessor__TestContext003_BeanDefinitions.java",
Expand Down
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
Expand All @@ -21,6 +21,7 @@
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.aot.samples.common.MessageService;
import org.springframework.test.context.aot.samples.management.ManagementConfiguration;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

import static org.assertj.core.api.Assertions.assertThat;
Expand All @@ -31,7 +32,7 @@
* @author Sam Brannen
* @since 6.0
*/
@SpringJUnitConfig(BasicTestConfiguration.class)
@SpringJUnitConfig({BasicTestConfiguration.class, ManagementConfiguration.class})
@TestPropertySource(properties = "test.engine = jupiter")
public class BasicSpringJupiterSharedConfigTests {

Expand Down
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
Expand All @@ -26,6 +26,7 @@
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterTests.DummyTestExecutionListener;
import org.springframework.test.context.aot.samples.common.MessageService;
import org.springframework.test.context.aot.samples.management.ManagementConfiguration;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.context.support.AbstractTestExecutionListener;

Expand All @@ -36,7 +37,7 @@
* @author Sam Brannen
* @since 6.0
*/
@SpringJUnitConfig(BasicTestConfiguration.class)
@SpringJUnitConfig({BasicTestConfiguration.class, ManagementConfiguration.class})
@TestExecutionListeners(listeners = DummyTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS)
@TestPropertySource(properties = "test.engine = jupiter")
public class BasicSpringJupiterTests {
Expand Down
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
Expand All @@ -22,6 +22,7 @@
import org.springframework.test.context.aot.samples.common.DefaultMessageService;
import org.springframework.test.context.aot.samples.common.MessageService;
import org.springframework.test.context.aot.samples.common.SpanishMessageService;
import org.springframework.test.context.aot.samples.management.Managed;

/**
* @author Sam Brannen
Expand All @@ -32,12 +33,14 @@ class BasicTestConfiguration {

@Bean
@Profile("default")
@Managed
MessageService defaultMessageService() {
return new DefaultMessageService();
}

@Bean
@Profile("spanish")
@Managed
MessageService spanishMessageService() {
return new SpanishMessageService();
}
Expand Down
@@ -0,0 +1,35 @@
/*
* Copyright 2002-2023 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.management;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Marker annotation for "managed" beans.
*
* @author Sam Brannen
* @since 6.0.12
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Managed {
}
@@ -0,0 +1,83 @@
/*
* Copyright 2002-2023 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.management;

import java.lang.reflect.Executable;

import org.springframework.aot.generate.GenerationContext;
import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor;
import org.springframework.beans.factory.aot.BeanRegistrationCode;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.aot.ApplicationContextAotGenerator;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.annotation.AnnotatedElementUtils;

/**
* Configuration class that mimics Spring Boot's AOT support for child management
* contexts in
* {@code org.springframework.boot.actuate.autoconfigure.web.server.ChildManagementContextInitializer}.
*
* <p>See <a href="https://github.com/spring-projects/spring-framework/issues/30861">gh-30861</a>.
*
* @author Sam Brannen
* @since 6.0.12
*/
@Configuration
public class ManagementConfiguration {

@Bean
static BeanRegistrationAotProcessor beanRegistrationAotProcessor() {
return registeredBean -> {
Executable factoryMethod = registeredBean.resolveConstructorOrFactoryMethod();
// Make AOT contribution for @Managed @Bean methods.
if (AnnotatedElementUtils.hasAnnotation(factoryMethod, Managed.class)) {
return new AotContribution(createManagementContext());
}
return null;
};
}

private static GenericApplicationContext createManagementContext() {
GenericApplicationContext managementContext = new GenericApplicationContext();
managementContext.registerBean(ManagementMessageService.class);
return managementContext;
}


/**
* Mimics Spring Boot's AOT support for child management contexts in
* {@code org.springframework.boot.actuate.autoconfigure.web.server.ChildManagementContextInitializer.AotContribution}.
*/
private static class AotContribution implements BeanRegistrationAotContribution {

private final GenericApplicationContext managementContext;

AotContribution(GenericApplicationContext managementContext) {
this.managementContext = managementContext;
}

@Override
public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) {
GenerationContext managementGenerationContext = generationContext.withName("Management");
new ApplicationContextAotGenerator().processAheadOfTime(this.managementContext, managementGenerationContext);
}

}

}

0 comments on commit ae4aaae

Please sign in to comment.