Skip to content

Commit

Permalink
Introduce AotTestAttributes mechanism in the TestContext framework
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sbrannen committed Sep 12, 2022
1 parent d3822a2 commit b0ee513
Show file tree
Hide file tree
Showing 8 changed files with 502 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* 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 org.springframework.aot.AotDetector;
import org.springframework.lang.Nullable;

/**
* Holder for metadata specific to ahead-of-time (AOT) support in the <em>Spring
* TestContext Framework</em>.
*
* <p>AOT test attributes are supported in two modes of operation: build-time
* and run-time. At build time, test components can {@linkplain #setAttribute contribute}
* attributes during the AOT processing phase. At run time, test components can
* {@linkplain #getString(String) retrieve} attributes that were contributed at
* build time. If {@link AotDetector#useGeneratedArtifacts()} returns {@code true},
* run-time mode applies.
*
* <p>For example, if a test component computes something at build time that
* cannot be computed at run time, the result of the build-time computation can
* be stored as an AOT attribute and retrieved at run time without repeating the
* computation.
*
* <p>An {@link AotContextLoader} would typically contribute an attribute in
* {@link AotContextLoader#loadContextForAotProcessing loadContextForAotProcessing()};
* whereas, an {@link AotTestExecutionListener} would typically contribute an attribute
* in {@link AotTestExecutionListener#processAheadOfTime processAheadOfTime()}.
* Any other test component &mdash; such as a
* {@link org.springframework.test.context.TestContextBootstrapper TestContextBootstrapper}
* &mdash; can choose to contribute an attribute at any point in time. Note that
* contributing an attribute during standard JVM test execution will not have any
* adverse side effect since AOT attributes will be ignored in that scenario. In
* any case, you should use {@link AotDetector#useGeneratedArtifacts()} to determine
* if invocations of {@link #setAttribute(String, String)} and
* {@link #removeAttribute(String)} are permitted.
*
* @author Sam Brannen
* @since 6.0
*/
public interface AotTestAttributes {

/**
* Get the current instance of {@code AotTestAttributes} to use.
* <p>See the class-level {@link AotTestAttributes Javadoc} for details on
* the two supported modes.
*/
static AotTestAttributes getInstance() {
return new DefaultAotTestAttributes(AotTestAttributesFactory.getAttributes());
}


/**
* Set a {@code String} attribute for later retrieval during AOT run-time execution.
* <p>In general, users should take care to prevent overlaps with other
* metadata attributes by using fully-qualified names, perhaps using a
* class or package name as a prefix.
* @param name the unique attribute name
* @param value the associated attribute value
* @throws UnsupportedOperationException if invoked during
* {@linkplain AotDetector#useGeneratedArtifacts() AOT run-time execution}
* @throws IllegalArgumentException if the provided value is {@code null} or
* if an attempt is made to override an existing attribute
* @see #setAttribute(String, boolean)
* @see #removeAttribute(String)
* @see AotDetector#useGeneratedArtifacts()
*/
void setAttribute(String name, String value);

/**
* Set a {@code boolean} attribute for later retrieval during AOT run-time execution.
* <p>In general, users should take care to prevent overlaps with other
* metadata attributes by using fully-qualified names, perhaps using a
* class or package name as a prefix.
* @param name the unique attribute name
* @param value the associated attribute value
* @throws UnsupportedOperationException if invoked during
* {@linkplain AotDetector#useGeneratedArtifacts() AOT run-time execution}
* @throws IllegalArgumentException if an attempt is made to override an
* existing attribute
* @see #setAttribute(String, String)
* @see #removeAttribute(String)
* @see Boolean#toString(boolean)
* @see AotDetector#useGeneratedArtifacts()
*/
default void setAttribute(String name, boolean value) {
setAttribute(name, Boolean.toString(value));
}

/**
* Remove the attribute stored under the provided name.
* @param name the unique attribute name
* @throws UnsupportedOperationException if invoked during
* {@linkplain AotDetector#useGeneratedArtifacts() AOT run-time execution}
* @see AotDetector#useGeneratedArtifacts()
* @see #setAttribute(String, String)
*/
void removeAttribute(String name);

/**
* Retrieve the attribute value for the given name as a {@link String}.
* @param name the unique attribute name
* @return the associated attribute value, or {@code null} if not found
* @see #getBoolean(String)
* @see #setAttribute(String, String)
*/
@Nullable
String getString(String name);

/**
* Retrieve the attribute value for the given name as a {@code boolean}.
* @param name the unique attribute name
* @return {@code true} if the attribute is set to "true" (ignoring case),
* {@code} false otherwise
* @see #getString(String)
* @see #setAttribute(String, String)
* @see Boolean#parseBoolean(String)
*/
default boolean getBoolean(String name) {
return Boolean.parseBoolean(getString(name));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* 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.Map;

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.core.log.LogMessage;
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;

/**
* Internal code generator for {@link AotTestAttributes}.
*
* @author Sam Brannen
* @since 6.0
*/
class AotTestAttributesCodeGenerator {

private static final Log logger = LogFactory.getLog(AotTestAttributesCodeGenerator.class);

// Map<String, String>
private static final TypeName MAP_TYPE = ParameterizedTypeName.get(Map.class, String.class, String.class);

private static final String GENERATED_SUFFIX = "Generated";

static final String GENERATED_ATTRIBUTES_CLASS_NAME = AotTestAttributes.class.getName() + "__" + GENERATED_SUFFIX;

static final String GENERATED_ATTRIBUTES_METHOD_NAME = "getAttributes";


private final Map<String, String> attributes;

private final GeneratedClass generatedClass;


AotTestAttributesCodeGenerator(Map<String, String> attributes, GeneratedClasses generatedClasses) {
this.attributes = attributes;
this.generatedClass = generatedClasses.addForFeature(GENERATED_SUFFIX, this::generateType);
}


GeneratedClass getGeneratedClass() {
return this.generatedClass;
}

private void generateType(TypeSpec.Builder type) {
logger.debug(LogMessage.format("Generating AOT test attributes in %s",
this.generatedClass.getName().reflectionName()));
type.addJavadoc("Generated map for {@link $T}.", AotTestAttributes.class);
type.addModifiers(Modifier.PUBLIC);
type.addMethod(generateMethod());
}

private MethodSpec generateMethod() {
MethodSpec.Builder method = MethodSpec.methodBuilder(GENERATED_ATTRIBUTES_METHOD_NAME);
method.addModifiers(Modifier.PUBLIC, Modifier.STATIC);
method.returns(MAP_TYPE);
method.addCode(generateCode());
return method.build();
}

private CodeBlock generateCode() {
CodeBlock.Builder code = CodeBlock.builder();
code.addStatement("$T map = new $T<>()", MAP_TYPE, HashMap.class);
this.attributes.forEach((key, value) -> {
logger.trace(LogMessage.format("Storing AOT test attribute: %s = %s", key, value));
code.addStatement("map.put($S, $S)", key, value);
});
code.addStatement("return map");
return code.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* 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.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.aot.AotDetector;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;

/**
* Factory for {@link AotTestAttributes}.
*
* @author Sam Brannen
* @since 6.0
*/
final class AotTestAttributesFactory {

@Nullable
private static volatile Map<String, String> attributes;


private AotTestAttributesFactory() {
}

/**
* Get the underlying attributes map.
* <p>If the map is not already loaded, this method loads the map from the
* generated class when running in {@linkplain AotDetector#useGeneratedArtifacts()
* AOT execution mode} and otherwise creates a new map for storing attributes
* during the AOT processing phase.
*/
static Map<String, String> getAttributes() {
Map<String, String> attrs = attributes;
if (attrs == null) {
synchronized (AotTestAttributesFactory.class) {
attrs = attributes;
if (attrs == null) {
attrs = (AotDetector.useGeneratedArtifacts() ? loadAttributesMap() : new ConcurrentHashMap<>());
attributes = attrs;
}
}
}
return attrs;
}

/**
* Reset AOT test attributes.
* <p>Only for internal use.
*/
static void reset() {
synchronized (AotTestAttributesFactory.class) {
attributes = null;
}
}

@SuppressWarnings({ "rawtypes", "unchecked" })
private static Map<String, String> loadAttributesMap() {
String className = AotTestAttributesCodeGenerator.GENERATED_ATTRIBUTES_CLASS_NAME;
String methodName = AotTestAttributesCodeGenerator.GENERATED_ATTRIBUTES_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()));
Map<String, String> attributes = (Map<String, String>) ReflectionUtils.invokeMethod(method, null);
return Collections.unmodifiableMap(attributes);
}
catch (IllegalStateException ex) {
throw ex;
}
catch (Exception ex) {
throw new IllegalStateException("Failed to invoke %s() method on %s".formatted(methodName, className), ex);
}
}

}

0 comments on commit b0ee513

Please sign in to comment.