Skip to content

Commit f5db8bd

Browse files
committed
Retain existing feature name as prefix in test AOT processing
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 gh-30861
1 parent 317c6fb commit f5db8bd

File tree

11 files changed

+272
-11
lines changed

11 files changed

+272
-11
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -324,8 +324,8 @@ private MergedContextConfiguration buildMergedContextConfiguration(Class<?> test
324324

325325
DefaultGenerationContext createGenerationContext(Class<?> testClass) {
326326
ClassNameGenerator classNameGenerator = new ClassNameGenerator(ClassName.get(testClass));
327-
DefaultGenerationContext generationContext =
328-
new DefaultGenerationContext(classNameGenerator, this.generatedFiles, this.runtimeHints);
327+
TestContextGenerationContext generationContext =
328+
new TestContextGenerationContext(classNameGenerator, this.generatedFiles, this.runtimeHints);
329329
return generationContext.withName(nextTestContextId());
330330
}
331331

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.aot;
18+
19+
import org.springframework.aot.generate.ClassNameGenerator;
20+
import org.springframework.aot.generate.DefaultGenerationContext;
21+
import org.springframework.aot.generate.GeneratedClasses;
22+
import org.springframework.aot.generate.GeneratedFiles;
23+
import org.springframework.aot.hint.RuntimeHints;
24+
25+
/**
26+
* Extension of {@link DefaultGenerationContext} with a custom implementation of
27+
* {@link #withName(String)} that is specific to the <em>Spring TestContext Framework</em>.
28+
*
29+
* @author Sam Brannen
30+
* @since 6.0.12
31+
*/
32+
class TestContextGenerationContext extends DefaultGenerationContext {
33+
34+
private final String featureName;
35+
36+
37+
/**
38+
* Create a new {@link TestContextGenerationContext} instance backed by the
39+
* specified {@link ClassNameGenerator}, {@link GeneratedFiles}, and
40+
* {@link RuntimeHints}.
41+
* @param classNameGenerator the naming convention to use for generated class names
42+
* @param generatedFiles the generated files
43+
* @param runtimeHints the runtime hints
44+
*/
45+
TestContextGenerationContext(ClassNameGenerator classNameGenerator, GeneratedFiles generatedFiles,
46+
RuntimeHints runtimeHints) {
47+
super(classNameGenerator, generatedFiles, runtimeHints);
48+
this.featureName = null;
49+
}
50+
51+
/**
52+
* Create a new {@link TestContextGenerationContext} instance backed by the
53+
* specified {@link GeneratedClasses}, {@link GeneratedFiles}, and
54+
* {@link RuntimeHints}.
55+
* @param generatedClasses the generated classes
56+
* @param generatedFiles the generated files
57+
* @param runtimeHints the runtime hints
58+
*/
59+
private TestContextGenerationContext(GeneratedClasses generatedClasses, GeneratedFiles generatedFiles,
60+
RuntimeHints runtimeHints, String featureName) {
61+
super(generatedClasses, generatedFiles, runtimeHints);
62+
this.featureName = featureName;
63+
}
64+
65+
66+
/**
67+
* Create a new {@link TestContextGenerationContext} instance using the specified
68+
* feature name to qualify generated assets for a dedicated round of code generation.
69+
* <p>If <em>this</em> {@code TestContextGenerationContext} has a configured feature
70+
* name, the supplied feature name will be appended to the existing feature name
71+
* in order to avoid naming collisions.
72+
* @param featureName the feature name to use
73+
* @return a specialized {@link TestContextGenerationContext} for the specified
74+
* feature name
75+
*/
76+
@Override
77+
public TestContextGenerationContext withName(String featureName) {
78+
if (this.featureName != null) {
79+
featureName = this.featureName + featureName;
80+
}
81+
GeneratedClasses generatedClasses = getGeneratedClasses().withFeatureNamePrefix(featureName);
82+
return new TestContextGenerationContext(generatedClasses, getGeneratedFiles(), getRuntimeHints(), featureName);
83+
}
84+
85+
}

spring-test/src/test/java/org/springframework/test/context/aot/AbstractAotTests.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -45,14 +45,22 @@ abstract class AbstractAotTests {
4545
"org/springframework/context/event/EventListenerMethodProcessor__TestContext002_BeanDefinitions.java",
4646
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests__TestContext002_ApplicationContextInitializer.java",
4747
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests__TestContext002_BeanFactoryRegistrations.java",
48+
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests__TestContext002_ManagementApplicationContextInitializer.java",
49+
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests__TestContext002_ManagementBeanFactoryRegistrations.java",
4850
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext002_BeanDefinitions.java",
51+
"org/springframework/test/context/aot/samples/management/ManagementConfiguration__TestContext002_BeanDefinitions.java",
52+
"org/springframework/test/context/aot/samples/management/ManagementMessageService__TestContext002_ManagementBeanDefinitions.java",
4953
// BasicSpringJupiterTests -- not generated b/c already generated for BasicSpringJupiterSharedConfigTests.
5054
// BasicSpringJupiterTests.NestedTests
5155
"org/springframework/context/event/DefaultEventListenerFactory__TestContext003_BeanDefinitions.java",
5256
"org/springframework/context/event/EventListenerMethodProcessor__TestContext003_BeanDefinitions.java",
5357
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests_NestedTests__TestContext003_ApplicationContextInitializer.java",
5458
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests_NestedTests__TestContext003_BeanFactoryRegistrations.java",
59+
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests_NestedTests__TestContext003_ManagementApplicationContextInitializer.java",
60+
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests_NestedTests__TestContext003_ManagementBeanFactoryRegistrations.java",
5561
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext003_BeanDefinitions.java",
62+
"org/springframework/test/context/aot/samples/management/ManagementConfiguration__TestContext003_BeanDefinitions.java",
63+
"org/springframework/test/context/aot/samples/management/ManagementMessageService__TestContext003_ManagementBeanDefinitions.java",
5664
// BasicSpringTestNGTests
5765
"org/springframework/context/event/DefaultEventListenerFactory__TestContext004_BeanDefinitions.java",
5866
"org/springframework/context/event/EventListenerMethodProcessor__TestContext004_BeanDefinitions.java",

spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -36,6 +36,7 @@
3636
import org.springframework.aot.AotDetector;
3737
import org.springframework.aot.generate.GeneratedFiles.Kind;
3838
import org.springframework.aot.generate.InMemoryGeneratedFiles;
39+
import org.springframework.aot.hint.RuntimeHints;
3940
import org.springframework.aot.test.generate.CompilerFiles;
4041
import org.springframework.core.test.tools.CompileWithForkedClassLoader;
4142
import org.springframework.core.test.tools.TestCompiler;
@@ -86,7 +87,7 @@ void endToEndTests() {
8687

8788
// AOT BUILD-TIME: PROCESSING
8889
InMemoryGeneratedFiles generatedFiles = new InMemoryGeneratedFiles();
89-
TestContextAotGenerator generator = new TestContextAotGenerator(generatedFiles);
90+
TestContextAotGenerator generator = new TestContextAotGenerator(generatedFiles, new RuntimeHints(), true);
9091
generator.processAheadOfTime(testClasses);
9192

9293
List<String> sourceFiles = generatedFiles.getGeneratedFiles(Kind.SOURCE).keySet().stream().toList();

spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorTests.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -366,14 +366,22 @@ record Mapping(MergedContextConfiguration mergedConfig, ClassName className) {
366366
"org/springframework/context/event/EventListenerMethodProcessor__TestContext001_BeanDefinitions.java",
367367
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests__TestContext001_ApplicationContextInitializer.java",
368368
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests__TestContext001_BeanFactoryRegistrations.java",
369+
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests__TestContext001_ManagementApplicationContextInitializer.java",
370+
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests__TestContext001_ManagementBeanFactoryRegistrations.java",
369371
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext001_BeanDefinitions.java",
372+
"org/springframework/test/context/aot/samples/management/ManagementConfiguration__TestContext001_BeanDefinitions.java",
373+
"org/springframework/test/context/aot/samples/management/ManagementMessageService__TestContext001_ManagementBeanDefinitions.java",
370374
// BasicSpringJupiterTests -- not generated b/c already generated for BasicSpringJupiterSharedConfigTests.
371375
// BasicSpringJupiterTests.NestedTests
372376
"org/springframework/context/event/DefaultEventListenerFactory__TestContext002_BeanDefinitions.java",
373377
"org/springframework/context/event/EventListenerMethodProcessor__TestContext002_BeanDefinitions.java",
374378
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests_NestedTests__TestContext002_ApplicationContextInitializer.java",
375379
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests_NestedTests__TestContext002_BeanFactoryRegistrations.java",
380+
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests_NestedTests__TestContext002_ManagementApplicationContextInitializer.java",
381+
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests_NestedTests__TestContext002_ManagementBeanFactoryRegistrations.java",
376382
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext002_BeanDefinitions.java",
383+
"org/springframework/test/context/aot/samples/management/ManagementConfiguration__TestContext002_BeanDefinitions.java",
384+
"org/springframework/test/context/aot/samples/management/ManagementMessageService__TestContext002_ManagementBeanDefinitions.java",
377385
// BasicSpringTestNGTests
378386
"org/springframework/context/event/DefaultEventListenerFactory__TestContext003_BeanDefinitions.java",
379387
"org/springframework/context/event/EventListenerMethodProcessor__TestContext003_BeanDefinitions.java",

spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@
2121
import org.springframework.context.ApplicationContext;
2222
import org.springframework.test.context.TestPropertySource;
2323
import org.springframework.test.context.aot.samples.common.MessageService;
24+
import org.springframework.test.context.aot.samples.management.ManagementConfiguration;
2425
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
2526

2627
import static org.assertj.core.api.Assertions.assertThat;
@@ -31,7 +32,7 @@
3132
* @author Sam Brannen
3233
* @since 6.0
3334
*/
34-
@SpringJUnitConfig(BasicTestConfiguration.class)
35+
@SpringJUnitConfig({BasicTestConfiguration.class, ManagementConfiguration.class})
3536
@TestPropertySource(properties = "test.engine = jupiter")
3637
public class BasicSpringJupiterSharedConfigTests {
3738

spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,6 +26,7 @@
2626
import org.springframework.test.context.TestPropertySource;
2727
import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterTests.DummyTestExecutionListener;
2828
import org.springframework.test.context.aot.samples.common.MessageService;
29+
import org.springframework.test.context.aot.samples.management.ManagementConfiguration;
2930
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
3031
import org.springframework.test.context.support.AbstractTestExecutionListener;
3132

@@ -36,7 +37,7 @@
3637
* @author Sam Brannen
3738
* @since 6.0
3839
*/
39-
@SpringJUnitConfig(BasicTestConfiguration.class)
40+
@SpringJUnitConfig({BasicTestConfiguration.class, ManagementConfiguration.class})
4041
@TestExecutionListeners(listeners = DummyTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS)
4142
@TestPropertySource(properties = "test.engine = jupiter")
4243
public class BasicSpringJupiterTests {

spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicTestConfiguration.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,6 +22,7 @@
2222
import org.springframework.test.context.aot.samples.common.DefaultMessageService;
2323
import org.springframework.test.context.aot.samples.common.MessageService;
2424
import org.springframework.test.context.aot.samples.common.SpanishMessageService;
25+
import org.springframework.test.context.aot.samples.management.Managed;
2526

2627
/**
2728
* @author Sam Brannen
@@ -32,12 +33,14 @@ class BasicTestConfiguration {
3233

3334
@Bean
3435
@Profile("default")
36+
@Managed
3537
MessageService defaultMessageService() {
3638
return new DefaultMessageService();
3739
}
3840

3941
@Bean
4042
@Profile("spanish")
43+
@Managed
4144
MessageService spanishMessageService() {
4245
return new SpanishMessageService();
4346
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.aot.samples.management;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
/**
26+
* Marker annotation for "managed" beans.
27+
*
28+
* @author Sam Brannen
29+
* @since 6.0.12
30+
*/
31+
@Documented
32+
@Retention(RetentionPolicy.RUNTIME)
33+
@Target(ElementType.METHOD)
34+
public @interface Managed {
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.aot.samples.management;
18+
19+
import java.lang.reflect.Executable;
20+
21+
import org.springframework.aot.generate.GenerationContext;
22+
import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
23+
import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor;
24+
import org.springframework.beans.factory.aot.BeanRegistrationCode;
25+
import org.springframework.context.annotation.Bean;
26+
import org.springframework.context.annotation.Configuration;
27+
import org.springframework.context.aot.ApplicationContextAotGenerator;
28+
import org.springframework.context.support.GenericApplicationContext;
29+
import org.springframework.core.annotation.AnnotatedElementUtils;
30+
31+
/**
32+
* Configuration class that mimics Spring Boot's AOT support for child management
33+
* contexts in
34+
* {@code org.springframework.boot.actuate.autoconfigure.web.server.ChildManagementContextInitializer}.
35+
*
36+
* <p>See <a href="https://github.com/spring-projects/spring-framework/issues/30861">gh-30861</a>.
37+
*
38+
* @author Sam Brannen
39+
* @since 6.0.12
40+
*/
41+
@Configuration
42+
public class ManagementConfiguration {
43+
44+
@Bean
45+
static BeanRegistrationAotProcessor beanRegistrationAotProcessor() {
46+
return registeredBean -> {
47+
Executable factoryMethod = registeredBean.resolveConstructorOrFactoryMethod();
48+
// Make AOT contribution for @Managed @Bean methods.
49+
if (AnnotatedElementUtils.hasAnnotation(factoryMethod, Managed.class)) {
50+
return new AotContribution(createManagementContext());
51+
}
52+
return null;
53+
};
54+
}
55+
56+
private static GenericApplicationContext createManagementContext() {
57+
GenericApplicationContext managementContext = new GenericApplicationContext();
58+
managementContext.registerBean(ManagementMessageService.class);
59+
return managementContext;
60+
}
61+
62+
63+
/**
64+
* Mimics Spring Boot's AOT support for child management contexts in
65+
* {@code org.springframework.boot.actuate.autoconfigure.web.server.ChildManagementContextInitializer.AotContribution}.
66+
*/
67+
private static class AotContribution implements BeanRegistrationAotContribution {
68+
69+
private final GenericApplicationContext managementContext;
70+
71+
AotContribution(GenericApplicationContext managementContext) {
72+
this.managementContext = managementContext;
73+
}
74+
75+
@Override
76+
public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) {
77+
GenerationContext managementGenerationContext = generationContext.withName("Management");
78+
new ApplicationContextAotGenerator().processAheadOfTime(this.managementContext, managementGenerationContext);
79+
}
80+
81+
}
82+
83+
}

0 commit comments

Comments
 (0)