Skip to content

Commit 634d1c0

Browse files
committed
Introduce @EnabledIf support for JUnit Jupiter
This commit picks up where SPR-14614 left off by introducing a new @EnabledIf annotation to serve as a logical companion to @DisabledIf. In addition, this commit extracts common logic from DisabledIfCondition into a new AbstractExpressionEvaluatingCondition base class which the new EnabledIfCondition also extends. An @EnabledOnMac annotation is also included in the Javadoc as well as in the test suite to demonstrate support for custom composed annotations. Issue: SPR-14644
1 parent 1a30252 commit 634d1c0

File tree

9 files changed

+563
-105
lines changed

9 files changed

+563
-105
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/*
2+
* Copyright 2002-2016 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+
* http://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.junit.jupiter;
18+
19+
import java.lang.annotation.Annotation;
20+
import java.lang.reflect.AnnotatedElement;
21+
import java.util.Optional;
22+
import java.util.function.Function;
23+
24+
import org.apache.commons.logging.Log;
25+
import org.apache.commons.logging.LogFactory;
26+
27+
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
28+
import org.junit.jupiter.api.extension.ContainerExecutionCondition;
29+
import org.junit.jupiter.api.extension.ExtensionContext;
30+
import org.junit.jupiter.api.extension.TestExecutionCondition;
31+
32+
import org.springframework.beans.factory.config.BeanExpressionContext;
33+
import org.springframework.beans.factory.config.BeanExpressionResolver;
34+
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
35+
import org.springframework.context.ApplicationContext;
36+
import org.springframework.context.ConfigurableApplicationContext;
37+
import org.springframework.core.annotation.AnnotatedElementUtils;
38+
import org.springframework.util.Assert;
39+
import org.springframework.util.StringUtils;
40+
41+
/**
42+
* Abstract base class for implementations of {@link ContainerExecutionCondition}
43+
* and {@link TestExecutionCondition} that evaluate expressions configured via
44+
* annotations to determine if a container or test is enabled.
45+
*
46+
* <p>Expressions can be any of the following.
47+
*
48+
* <ul>
49+
* <li>Spring Expression Language (SpEL) expression &mdash; for example:
50+
* <pre style="code">#{systemProperties['os.name'].toLowerCase().contains('mac')}</pre>
51+
* <li>Placeholder for a property available in the Spring
52+
* {@link org.springframework.core.env.Environment Environment} &mdash; for example:
53+
* <pre style="code">${smoke.tests.enabled}</pre>
54+
* <li>Text literal &mdash; for example:
55+
* <pre style="code">true</pre>
56+
* </ul>
57+
*
58+
* @author Sam Brannen
59+
* @author Tadaya Tsuyukubo
60+
* @since 5.0
61+
* @see EnabledIf
62+
* @see DisabledIf
63+
*/
64+
abstract class AbstractExpressionEvaluatingCondition implements ContainerExecutionCondition, TestExecutionCondition {
65+
66+
private static final Log logger = LogFactory.getLog(AbstractExpressionEvaluatingCondition.class);
67+
68+
69+
/**
70+
* Evaluate the expression configured via the supplied annotation type on
71+
* the {@link AnnotatedElement} for the supplied {@link ExtensionContext}.
72+
*
73+
* @param annotationType the type of annotation to process
74+
* @param expressionExtractor a function that extracts the expression from
75+
* the annotation
76+
* @param reasonExtractor a function that extracts the reason from the
77+
* annotation
78+
* @param enabledOnTrue indicates whether the returned {@code ConditionEvaluationResult}
79+
* should be {@link ConditionEvaluationResult#enabled enabled} if the expression
80+
* evaluates to {@code true}
81+
* @param context the {@code ExtensionContext}
82+
* @return {@link ConditionEvaluationResult#enabled enabled} if the container
83+
* or test should be enabled; otherwise {@link ConditionEvaluationResult#disabled disabled}
84+
*/
85+
protected <A extends Annotation> ConditionEvaluationResult evaluateAnnotation(Class<A> annotationType,
86+
Function<A, String> expressionExtractor, Function<A, String> reasonExtractor, boolean enabledOnTrue,
87+
ExtensionContext context) {
88+
89+
AnnotatedElement element = context.getElement().get();
90+
Optional<A> annotation = findMergedAnnotation(element, annotationType);
91+
92+
if (!annotation.isPresent()) {
93+
String reason = String.format("%s is enabled since @%s is not present", element,
94+
annotationType.getSimpleName());
95+
if (logger.isDebugEnabled()) {
96+
logger.debug(reason);
97+
}
98+
return ConditionEvaluationResult.enabled(reason);
99+
}
100+
101+
// @formatter:off
102+
String expression = annotation.map(expressionExtractor).map(String::trim).filter(StringUtils::hasLength)
103+
.orElseThrow(() -> new IllegalStateException(String.format(
104+
"The expression in @%s on [%s] must not be blank", annotationType.getSimpleName(), element)));
105+
// @formatter:on
106+
107+
boolean result = evaluateExpression(expression, annotationType, context);
108+
109+
if (result) {
110+
String adjective = (enabledOnTrue ? "enabled" : "disabled");
111+
String reason = annotation.map(reasonExtractor).filter(StringUtils::hasText).orElseGet(
112+
() -> String.format("%s is %s because @%s(\"%s\") evaluated to true", element, adjective,
113+
annotationType.getSimpleName(), expression));
114+
if (logger.isInfoEnabled()) {
115+
logger.info(reason);
116+
}
117+
return (enabledOnTrue ? ConditionEvaluationResult.enabled(reason)
118+
: ConditionEvaluationResult.disabled(reason));
119+
}
120+
else {
121+
String adjective = (enabledOnTrue ? "disabled" : "enabled");
122+
String reason = String.format("%s is %s because @%s(\"%s\") did not evaluate to true",
123+
element, adjective, annotationType.getSimpleName(), expression);
124+
if (logger.isDebugEnabled()) {
125+
logger.debug(reason);
126+
}
127+
return (enabledOnTrue ? ConditionEvaluationResult.disabled(reason)
128+
: ConditionEvaluationResult.enabled(reason));
129+
}
130+
}
131+
132+
private <A extends Annotation> boolean evaluateExpression(String expression, Class<A> annotationType,
133+
ExtensionContext extensionContext) {
134+
135+
ApplicationContext applicationContext = SpringExtension.getApplicationContext(extensionContext);
136+
137+
if (!(applicationContext instanceof ConfigurableApplicationContext)) {
138+
if (logger.isWarnEnabled()) {
139+
String contextType = (applicationContext != null ? applicationContext.getClass().getName() : "null");
140+
logger.warn(String.format("@%s(\"%s\") could not be evaluated on [%s] since the test " +
141+
"ApplicationContext [%s] is not a ConfigurableApplicationContext",
142+
annotationType.getSimpleName(), expression, extensionContext.getElement(), contextType));
143+
}
144+
return false;
145+
}
146+
147+
ConfigurableBeanFactory configurableBeanFactory = ((ConfigurableApplicationContext) applicationContext).getBeanFactory();
148+
BeanExpressionResolver expressionResolver = configurableBeanFactory.getBeanExpressionResolver();
149+
BeanExpressionContext beanExpressionContext = new BeanExpressionContext(configurableBeanFactory, null);
150+
151+
Object result = expressionResolver.evaluate(configurableBeanFactory.resolveEmbeddedValue(expression),
152+
beanExpressionContext);
153+
154+
Assert.state((result instanceof Boolean || result instanceof String),
155+
() -> String.format("@%s(\"%s\") must evaluate to a String or a Boolean, not %s",
156+
annotationType.getSimpleName(), expression, (result != null ? result.getClass().getName() : "null")));
157+
158+
return (result instanceof Boolean && ((Boolean) result).booleanValue())
159+
|| (result instanceof String && Boolean.parseBoolean((String) result));
160+
}
161+
162+
private static <A extends Annotation> Optional<A> findMergedAnnotation(AnnotatedElement element,
163+
Class<A> annotationType) {
164+
return Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(element, annotationType));
165+
}
166+
167+
}

spring-test/src/main/java/org/springframework/test/context/junit/jupiter/DisabledIf.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
* @author Tadaya Tsuyukubo
5555
* @since 5.0
5656
* @see SpringExtension
57+
* @see EnabledIf
5758
* @see org.junit.jupiter.api.Disabled
5859
*/
5960
@Target({ ElementType.TYPE, ElementType.METHOD })

spring-test/src/main/java/org/springframework/test/context/junit/jupiter/DisabledIfCondition.java

Lines changed: 9 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -16,48 +16,31 @@
1616

1717
package org.springframework.test.context.junit.jupiter;
1818

19-
import java.lang.annotation.Annotation;
20-
import java.lang.reflect.AnnotatedElement;
21-
import java.util.Optional;
22-
23-
import org.apache.commons.logging.Log;
24-
import org.apache.commons.logging.LogFactory;
25-
2619
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
2720
import org.junit.jupiter.api.extension.ContainerExecutionCondition;
2821
import org.junit.jupiter.api.extension.ContainerExtensionContext;
2922
import org.junit.jupiter.api.extension.ExtensionContext;
3023
import org.junit.jupiter.api.extension.TestExecutionCondition;
3124
import org.junit.jupiter.api.extension.TestExtensionContext;
3225

33-
import org.springframework.beans.factory.config.BeanExpressionContext;
34-
import org.springframework.beans.factory.config.BeanExpressionResolver;
35-
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
36-
import org.springframework.context.ApplicationContext;
37-
import org.springframework.context.ConfigurableApplicationContext;
38-
import org.springframework.core.annotation.AnnotatedElementUtils;
39-
import org.springframework.util.Assert;
40-
import org.springframework.util.StringUtils;
41-
4226
/**
4327
* {@code DisabledIfCondition} is a composite {@link ContainerExecutionCondition}
4428
* and {@link TestExecutionCondition} that supports the {@link DisabledIf @DisabledIf}
4529
* annotation when using the <em>Spring TestContext Framework</em> in conjunction
4630
* with JUnit 5's <em>Jupiter</em> programming model.
4731
*
48-
* <p>Any attempt to use {@code DisabledIfCondition} without the presence of
49-
* {@link DisabledIf @DisabledIf} will result in an {@link IllegalStateException}.
32+
* <p>Any attempt to use the {@code DisabledIfCondition} without the presence of
33+
* {@link DisabledIf @DisabledIf} will result in an <em>enabled</em>
34+
* {@link ConditionEvaluationResult}.
5035
*
5136
* @author Sam Brannen
5237
* @author Tadaya Tsuyukubo
5338
* @since 5.0
54-
* @see org.springframework.test.context.junit.jupiter.DisabledIf
55-
* @see org.springframework.test.context.junit.jupiter.SpringExtension
39+
* @see DisabledIf
40+
* @see EnabledIf
41+
* @see SpringExtension
5642
*/
57-
public class DisabledIfCondition implements ContainerExecutionCondition, TestExecutionCondition {
58-
59-
private static final Log logger = LogFactory.getLog(DisabledIfCondition.class);
60-
43+
public class DisabledIfCondition extends AbstractExpressionEvaluatingCondition {
6144

6245
/**
6346
* Containers are disabled if {@code @DisabledIf} is present on the test class
@@ -77,65 +60,8 @@ public ConditionEvaluationResult evaluate(TestExtensionContext context) {
7760
return evaluateDisabledIf(context);
7861
}
7962

80-
private ConditionEvaluationResult evaluateDisabledIf(ExtensionContext extensionContext) {
81-
AnnotatedElement element = extensionContext.getElement().get();
82-
Optional<DisabledIf> disabledIf = findMergedAnnotation(element, DisabledIf.class);
83-
Assert.state(disabledIf.isPresent(), () -> "@DisabledIf must be present on " + element);
84-
85-
// @formatter:off
86-
String expression = disabledIf.map(DisabledIf::expression).map(String::trim).filter(StringUtils::hasLength)
87-
.orElseThrow(() -> new IllegalStateException(String.format(
88-
"The expression in @DisabledIf on [%s] must not be blank", element)));
89-
// @formatter:on
90-
91-
if (isDisabled(expression, extensionContext)) {
92-
String reason = disabledIf.map(DisabledIf::reason).filter(StringUtils::hasText).orElseGet(
93-
() -> String.format("%s is disabled because @DisabledIf(\"%s\") evaluated to true", element,
94-
expression));
95-
logger.info(reason);
96-
return ConditionEvaluationResult.disabled(reason);
97-
}
98-
else {
99-
String reason = String.format("%s is enabled because @DisabledIf(\"%s\") did not evaluate to true",
100-
element, expression);
101-
logger.debug(reason);
102-
return ConditionEvaluationResult.enabled(reason);
103-
}
104-
}
105-
106-
private boolean isDisabled(String expression, ExtensionContext extensionContext) {
107-
ApplicationContext applicationContext = SpringExtension.getApplicationContext(extensionContext);
108-
109-
if (!(applicationContext instanceof ConfigurableApplicationContext)) {
110-
if (logger.isWarnEnabled()) {
111-
String contextType = (applicationContext != null ? applicationContext.getClass().getName() : "null");
112-
logger.warn(String.format("@DisabledIf(\"%s\") could not be evaluated on [%s] since the test " +
113-
"ApplicationContext [%s] is not a ConfigurableApplicationContext",
114-
expression, extensionContext.getElement(), contextType));
115-
}
116-
return false;
117-
}
118-
119-
ConfigurableBeanFactory configurableBeanFactory = ((ConfigurableApplicationContext) applicationContext).getBeanFactory();
120-
BeanExpressionResolver expressionResolver = configurableBeanFactory.getBeanExpressionResolver();
121-
BeanExpressionContext beanExpressionContext = new BeanExpressionContext(configurableBeanFactory, null);
122-
123-
Object result = expressionResolver.evaluate(configurableBeanFactory.resolveEmbeddedValue(expression),
124-
beanExpressionContext);
125-
126-
Assert.state((result instanceof Boolean || result instanceof String), () ->
127-
String.format("@DisabledIf(\"%s\") must evaluate to a String or a Boolean, not %s", expression,
128-
(result != null ? result.getClass().getName() : "null")));
129-
130-
boolean disabled = (result instanceof Boolean && ((Boolean) result).booleanValue()) ||
131-
(result instanceof String && Boolean.parseBoolean((String) result));
132-
133-
return disabled;
134-
}
135-
136-
private static <A extends Annotation> Optional<A> findMergedAnnotation(AnnotatedElement element,
137-
Class<A> annotationType) {
138-
return Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(element, annotationType));
63+
private ConditionEvaluationResult evaluateDisabledIf(ExtensionContext context) {
64+
return evaluateAnnotation(DisabledIf.class, DisabledIf::expression, DisabledIf::reason, false, context);
13965
}
14066

14167
}

0 commit comments

Comments
 (0)