Skip to content

Commit b0ee513

Browse files
committed
Introduce AotTestAttributes mechanism in the TestContext framework
For certain use cases it is beneficial to be able to compute something during AOT build-time processing and then retrieve the result of that computation during AOT run-time execution, without having to deal with code generation on your own. To support such use cases, this commit introduces an AotTestAttributes mechanism in the Spring TestContext Framework with the following feature set. - conceptually similar to org.springframework.core.AttributeAccessor in the sense that attributes are generic metadata - allows an AOT-aware test component to contribute a key-value pair during the AOT build-time processing phase, where the key is a String and the value is a String - provides convenience methods for storing and retrieving attributes as boolean values - generates the necessary source code during the AOT build-time processing phase in the TestContext framework to create a persistent map of the attributes - provides a mechanism for accessing the stored attributes during AOT run-time execution Closes gh-29100
1 parent d3822a2 commit b0ee513

File tree

8 files changed

+502
-21
lines changed

8 files changed

+502
-21
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Copyright 2002-2022 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.AotDetector;
20+
import org.springframework.lang.Nullable;
21+
22+
/**
23+
* Holder for metadata specific to ahead-of-time (AOT) support in the <em>Spring
24+
* TestContext Framework</em>.
25+
*
26+
* <p>AOT test attributes are supported in two modes of operation: build-time
27+
* and run-time. At build time, test components can {@linkplain #setAttribute contribute}
28+
* attributes during the AOT processing phase. At run time, test components can
29+
* {@linkplain #getString(String) retrieve} attributes that were contributed at
30+
* build time. If {@link AotDetector#useGeneratedArtifacts()} returns {@code true},
31+
* run-time mode applies.
32+
*
33+
* <p>For example, if a test component computes something at build time that
34+
* cannot be computed at run time, the result of the build-time computation can
35+
* be stored as an AOT attribute and retrieved at run time without repeating the
36+
* computation.
37+
*
38+
* <p>An {@link AotContextLoader} would typically contribute an attribute in
39+
* {@link AotContextLoader#loadContextForAotProcessing loadContextForAotProcessing()};
40+
* whereas, an {@link AotTestExecutionListener} would typically contribute an attribute
41+
* in {@link AotTestExecutionListener#processAheadOfTime processAheadOfTime()}.
42+
* Any other test component &mdash; such as a
43+
* {@link org.springframework.test.context.TestContextBootstrapper TestContextBootstrapper}
44+
* &mdash; can choose to contribute an attribute at any point in time. Note that
45+
* contributing an attribute during standard JVM test execution will not have any
46+
* adverse side effect since AOT attributes will be ignored in that scenario. In
47+
* any case, you should use {@link AotDetector#useGeneratedArtifacts()} to determine
48+
* if invocations of {@link #setAttribute(String, String)} and
49+
* {@link #removeAttribute(String)} are permitted.
50+
*
51+
* @author Sam Brannen
52+
* @since 6.0
53+
*/
54+
public interface AotTestAttributes {
55+
56+
/**
57+
* Get the current instance of {@code AotTestAttributes} to use.
58+
* <p>See the class-level {@link AotTestAttributes Javadoc} for details on
59+
* the two supported modes.
60+
*/
61+
static AotTestAttributes getInstance() {
62+
return new DefaultAotTestAttributes(AotTestAttributesFactory.getAttributes());
63+
}
64+
65+
66+
/**
67+
* Set a {@code String} attribute for later retrieval during AOT run-time execution.
68+
* <p>In general, users should take care to prevent overlaps with other
69+
* metadata attributes by using fully-qualified names, perhaps using a
70+
* class or package name as a prefix.
71+
* @param name the unique attribute name
72+
* @param value the associated attribute value
73+
* @throws UnsupportedOperationException if invoked during
74+
* {@linkplain AotDetector#useGeneratedArtifacts() AOT run-time execution}
75+
* @throws IllegalArgumentException if the provided value is {@code null} or
76+
* if an attempt is made to override an existing attribute
77+
* @see #setAttribute(String, boolean)
78+
* @see #removeAttribute(String)
79+
* @see AotDetector#useGeneratedArtifacts()
80+
*/
81+
void setAttribute(String name, String value);
82+
83+
/**
84+
* Set a {@code boolean} attribute for later retrieval during AOT run-time execution.
85+
* <p>In general, users should take care to prevent overlaps with other
86+
* metadata attributes by using fully-qualified names, perhaps using a
87+
* class or package name as a prefix.
88+
* @param name the unique attribute name
89+
* @param value the associated attribute value
90+
* @throws UnsupportedOperationException if invoked during
91+
* {@linkplain AotDetector#useGeneratedArtifacts() AOT run-time execution}
92+
* @throws IllegalArgumentException if an attempt is made to override an
93+
* existing attribute
94+
* @see #setAttribute(String, String)
95+
* @see #removeAttribute(String)
96+
* @see Boolean#toString(boolean)
97+
* @see AotDetector#useGeneratedArtifacts()
98+
*/
99+
default void setAttribute(String name, boolean value) {
100+
setAttribute(name, Boolean.toString(value));
101+
}
102+
103+
/**
104+
* Remove the attribute stored under the provided name.
105+
* @param name the unique attribute name
106+
* @throws UnsupportedOperationException if invoked during
107+
* {@linkplain AotDetector#useGeneratedArtifacts() AOT run-time execution}
108+
* @see AotDetector#useGeneratedArtifacts()
109+
* @see #setAttribute(String, String)
110+
*/
111+
void removeAttribute(String name);
112+
113+
/**
114+
* Retrieve the attribute value for the given name as a {@link String}.
115+
* @param name the unique attribute name
116+
* @return the associated attribute value, or {@code null} if not found
117+
* @see #getBoolean(String)
118+
* @see #setAttribute(String, String)
119+
*/
120+
@Nullable
121+
String getString(String name);
122+
123+
/**
124+
* Retrieve the attribute value for the given name as a {@code boolean}.
125+
* @param name the unique attribute name
126+
* @return {@code true} if the attribute is set to "true" (ignoring case),
127+
* {@code} false otherwise
128+
* @see #getString(String)
129+
* @see #setAttribute(String, String)
130+
* @see Boolean#parseBoolean(String)
131+
*/
132+
default boolean getBoolean(String name) {
133+
return Boolean.parseBoolean(getString(name));
134+
}
135+
136+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright 2002-2022 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 java.util.HashMap;
20+
import java.util.Map;
21+
22+
import javax.lang.model.element.Modifier;
23+
24+
import org.apache.commons.logging.Log;
25+
import org.apache.commons.logging.LogFactory;
26+
27+
import org.springframework.aot.generate.GeneratedClass;
28+
import org.springframework.aot.generate.GeneratedClasses;
29+
import org.springframework.core.log.LogMessage;
30+
import org.springframework.javapoet.CodeBlock;
31+
import org.springframework.javapoet.MethodSpec;
32+
import org.springframework.javapoet.ParameterizedTypeName;
33+
import org.springframework.javapoet.TypeName;
34+
import org.springframework.javapoet.TypeSpec;
35+
36+
/**
37+
* Internal code generator for {@link AotTestAttributes}.
38+
*
39+
* @author Sam Brannen
40+
* @since 6.0
41+
*/
42+
class AotTestAttributesCodeGenerator {
43+
44+
private static final Log logger = LogFactory.getLog(AotTestAttributesCodeGenerator.class);
45+
46+
// Map<String, String>
47+
private static final TypeName MAP_TYPE = ParameterizedTypeName.get(Map.class, String.class, String.class);
48+
49+
private static final String GENERATED_SUFFIX = "Generated";
50+
51+
static final String GENERATED_ATTRIBUTES_CLASS_NAME = AotTestAttributes.class.getName() + "__" + GENERATED_SUFFIX;
52+
53+
static final String GENERATED_ATTRIBUTES_METHOD_NAME = "getAttributes";
54+
55+
56+
private final Map<String, String> attributes;
57+
58+
private final GeneratedClass generatedClass;
59+
60+
61+
AotTestAttributesCodeGenerator(Map<String, String> attributes, GeneratedClasses generatedClasses) {
62+
this.attributes = attributes;
63+
this.generatedClass = generatedClasses.addForFeature(GENERATED_SUFFIX, this::generateType);
64+
}
65+
66+
67+
GeneratedClass getGeneratedClass() {
68+
return this.generatedClass;
69+
}
70+
71+
private void generateType(TypeSpec.Builder type) {
72+
logger.debug(LogMessage.format("Generating AOT test attributes in %s",
73+
this.generatedClass.getName().reflectionName()));
74+
type.addJavadoc("Generated map for {@link $T}.", AotTestAttributes.class);
75+
type.addModifiers(Modifier.PUBLIC);
76+
type.addMethod(generateMethod());
77+
}
78+
79+
private MethodSpec generateMethod() {
80+
MethodSpec.Builder method = MethodSpec.methodBuilder(GENERATED_ATTRIBUTES_METHOD_NAME);
81+
method.addModifiers(Modifier.PUBLIC, Modifier.STATIC);
82+
method.returns(MAP_TYPE);
83+
method.addCode(generateCode());
84+
return method.build();
85+
}
86+
87+
private CodeBlock generateCode() {
88+
CodeBlock.Builder code = CodeBlock.builder();
89+
code.addStatement("$T map = new $T<>()", MAP_TYPE, HashMap.class);
90+
this.attributes.forEach((key, value) -> {
91+
logger.trace(LogMessage.format("Storing AOT test attribute: %s = %s", key, value));
92+
code.addStatement("map.put($S, $S)", key, value);
93+
});
94+
code.addStatement("return map");
95+
return code.build();
96+
}
97+
98+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright 2002-2022 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 java.lang.reflect.Method;
20+
import java.util.Collections;
21+
import java.util.Map;
22+
import java.util.concurrent.ConcurrentHashMap;
23+
24+
import org.springframework.aot.AotDetector;
25+
import org.springframework.lang.Nullable;
26+
import org.springframework.util.Assert;
27+
import org.springframework.util.ClassUtils;
28+
import org.springframework.util.ReflectionUtils;
29+
30+
/**
31+
* Factory for {@link AotTestAttributes}.
32+
*
33+
* @author Sam Brannen
34+
* @since 6.0
35+
*/
36+
final class AotTestAttributesFactory {
37+
38+
@Nullable
39+
private static volatile Map<String, String> attributes;
40+
41+
42+
private AotTestAttributesFactory() {
43+
}
44+
45+
/**
46+
* Get the underlying attributes map.
47+
* <p>If the map is not already loaded, this method loads the map from the
48+
* generated class when running in {@linkplain AotDetector#useGeneratedArtifacts()
49+
* AOT execution mode} and otherwise creates a new map for storing attributes
50+
* during the AOT processing phase.
51+
*/
52+
static Map<String, String> getAttributes() {
53+
Map<String, String> attrs = attributes;
54+
if (attrs == null) {
55+
synchronized (AotTestAttributesFactory.class) {
56+
attrs = attributes;
57+
if (attrs == null) {
58+
attrs = (AotDetector.useGeneratedArtifacts() ? loadAttributesMap() : new ConcurrentHashMap<>());
59+
attributes = attrs;
60+
}
61+
}
62+
}
63+
return attrs;
64+
}
65+
66+
/**
67+
* Reset AOT test attributes.
68+
* <p>Only for internal use.
69+
*/
70+
static void reset() {
71+
synchronized (AotTestAttributesFactory.class) {
72+
attributes = null;
73+
}
74+
}
75+
76+
@SuppressWarnings({ "rawtypes", "unchecked" })
77+
private static Map<String, String> loadAttributesMap() {
78+
String className = AotTestAttributesCodeGenerator.GENERATED_ATTRIBUTES_CLASS_NAME;
79+
String methodName = AotTestAttributesCodeGenerator.GENERATED_ATTRIBUTES_METHOD_NAME;
80+
try {
81+
Class<?> clazz = ClassUtils.forName(className, null);
82+
Method method = ReflectionUtils.findMethod(clazz, methodName);
83+
Assert.state(method != null, () -> "No %s() method found in %s".formatted(methodName, clazz.getName()));
84+
Map<String, String> attributes = (Map<String, String>) ReflectionUtils.invokeMethod(method, null);
85+
return Collections.unmodifiableMap(attributes);
86+
}
87+
catch (IllegalStateException ex) {
88+
throw ex;
89+
}
90+
catch (Exception ex) {
91+
throw new IllegalStateException("Failed to invoke %s() method on %s".formatted(methodName, className), ex);
92+
}
93+
}
94+
95+
}

0 commit comments

Comments
 (0)