org.springframework.test.context.junit.jupiter.AbstractExpressionEvaluatingCondition Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of spring-test Show documentation
Show all versions of spring-test Show documentation
Spring TestContext Framework
/*
* 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.junit.jupiter;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.util.Optional;
import java.util.function.Function;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
import org.junit.jupiter.api.extension.ExecutionCondition;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.springframework.beans.factory.config.BeanExpressionContext;
import org.springframework.beans.factory.config.BeanExpressionResolver;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
import org.springframework.test.context.TestContextAnnotationUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Abstract base class for implementations of {@link ExecutionCondition} that
* evaluate expressions configured via annotations to determine if a container
* or test is enabled.
*
* Expressions can be any of the following.
*
*
* - Spring Expression Language (SpEL) expression — for example:
*
#{systemProperties['os.name'].toLowerCase().contains('mac')}
* - Placeholder for a property available in the Spring
* {@link org.springframework.core.env.Environment Environment} — for example:
*
${smoke.tests.enabled}
* - Text literal — for example:
*
true
*
*
* @author Sam Brannen
* @author Tadaya Tsuyukubo
* @since 5.0
* @see EnabledIf
* @see DisabledIf
*/
abstract class AbstractExpressionEvaluatingCondition implements ExecutionCondition {
private static final Log logger = LogFactory.getLog(AbstractExpressionEvaluatingCondition.class);
/**
* Evaluate the expression configured via the supplied annotation type on
* the {@link AnnotatedElement} for the supplied {@link ExtensionContext}.
* @param annotationType the type of annotation to process
* @param expressionExtractor a function that extracts the expression from
* the annotation
* @param reasonExtractor a function that extracts the reason from the
* annotation
* @param loadContextExtractor a function that extracts the {@code loadContext}
* flag from the annotation
* @param enabledOnTrue indicates whether the returned {@code ConditionEvaluationResult}
* should be {@link ConditionEvaluationResult#enabled(String) enabled} if the expression
* evaluates to {@code true}
* @param context the {@code ExtensionContext}
* @return {@link ConditionEvaluationResult#enabled(String) enabled} if the container
* or test should be enabled; otherwise {@link ConditionEvaluationResult#disabled(String) disabled}
*/
protected ConditionEvaluationResult evaluateAnnotation(Class annotationType,
Function expressionExtractor, Function reasonExtractor,
Function loadContextExtractor, boolean enabledOnTrue, ExtensionContext context) {
Assert.state(context.getElement().isPresent(), "No AnnotatedElement");
AnnotatedElement element = context.getElement().get();
Optional annotation = findMergedAnnotation(element, annotationType);
if (annotation.isEmpty()) {
String reason = String.format("%s is enabled since @%s is not present", element,
annotationType.getSimpleName());
if (logger.isDebugEnabled()) {
logger.debug(reason);
}
return ConditionEvaluationResult.enabled(reason);
}
String expression = annotation.map(expressionExtractor).map(String::trim).filter(StringUtils::hasLength)
.orElseThrow(() -> new IllegalStateException(String.format(
"The expression in @%s on [%s] must not be blank", annotationType.getSimpleName(), element)));
boolean loadContext = loadContextExtractor.apply(annotation.get());
boolean evaluatedToTrue = evaluateExpression(expression, loadContext, annotationType, context);
ConditionEvaluationResult result;
if (evaluatedToTrue) {
String adjective = (enabledOnTrue ? "enabled" : "disabled");
String reason = annotation.map(reasonExtractor).filter(StringUtils::hasText).orElseGet(
() -> String.format("%s is %s because @%s(\"%s\") evaluated to true", element, adjective,
annotationType.getSimpleName(), expression));
if (logger.isInfoEnabled()) {
logger.info(reason);
}
result = (enabledOnTrue ? ConditionEvaluationResult.enabled(reason)
: ConditionEvaluationResult.disabled(reason));
}
else {
String adjective = (enabledOnTrue ? "disabled" : "enabled");
String reason = String.format("%s is %s because @%s(\"%s\") did not evaluate to true",
element, adjective, annotationType.getSimpleName(), expression);
if (logger.isDebugEnabled()) {
logger.debug(reason);
}
result = (enabledOnTrue ? ConditionEvaluationResult.disabled(reason) :
ConditionEvaluationResult.enabled(reason));
}
// If we eagerly loaded the ApplicationContext to evaluate SpEL expressions
// and the test class ends up being disabled, we have to check if the
// user asked for the ApplicationContext to be closed via @DirtiesContext,
// since the DirtiesContextTestExecutionListener will never be invoked for
// a disabled test class.
// See https://github.com/spring-projects/spring-framework/issues/26694
if (loadContext && result.isDisabled() && element instanceof Class testClass) {
DirtiesContext dirtiesContext = TestContextAnnotationUtils.findMergedAnnotation(testClass, DirtiesContext.class);
if (dirtiesContext != null) {
HierarchyMode hierarchyMode = dirtiesContext.hierarchyMode();
SpringExtension.getTestContextManager(context).getTestContext().markApplicationContextDirty(hierarchyMode);
}
}
return result;
}
private boolean evaluateExpression(String expression, boolean loadContext,
Class annotationType, ExtensionContext context) {
Assert.state(context.getElement().isPresent(), "No AnnotatedElement");
AnnotatedElement element = context.getElement().get();
GenericApplicationContext gac = null;
ApplicationContext applicationContext;
if (loadContext) {
applicationContext = SpringExtension.getApplicationContext(context);
}
else {
gac = new GenericApplicationContext();
gac.refresh();
applicationContext = gac;
}
if (!(applicationContext instanceof ConfigurableApplicationContext cac)) {
if (logger.isWarnEnabled()) {
String contextType = applicationContext.getClass().getName();
logger.warn(String.format("@%s(\"%s\") could not be evaluated on [%s] since the test " +
"ApplicationContext [%s] is not a ConfigurableApplicationContext",
annotationType.getSimpleName(), expression, element, contextType));
}
return false;
}
ConfigurableBeanFactory configurableBeanFactory = cac.getBeanFactory();
BeanExpressionResolver expressionResolver = configurableBeanFactory.getBeanExpressionResolver();
Assert.state(expressionResolver != null, "No BeanExpressionResolver");
BeanExpressionContext beanExpressionContext = new BeanExpressionContext(configurableBeanFactory, null);
Object result = expressionResolver.evaluate(
configurableBeanFactory.resolveEmbeddedValue(expression), beanExpressionContext);
if (gac != null) {
gac.close();
}
if (result instanceof Boolean b) {
return b;
}
else if (result instanceof String str) {
str = str.trim().toLowerCase();
if ("true".equals(str)) {
return true;
}
Assert.state("false".equals(str),
() -> String.format("@%s(\"%s\") on %s must evaluate to \"true\" or \"false\", not \"%s\"",
annotationType.getSimpleName(), expression, element, result));
return false;
}
else {
String message = String.format("@%s(\"%s\") on %s must evaluate to a String or a Boolean, not %s",
annotationType.getSimpleName(), expression, element,
(result != null ? result.getClass().getName() : "null"));
throw new IllegalStateException(message);
}
}
private static Optional findMergedAnnotation(
AnnotatedElement element, Class annotationType) {
return Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(element, annotationType));
}
}