Skip to content

Commit 3615fc7

Browse files
m-schlattmichaelmpkorstanje
authored
[Spring] Support @CucumberContextConfiguration as a meta-annotation (#2630)
Using @CucumberContextConfiguration as a meta-annotation caused a CucumberBackendException because SpringFactory only detected the raw use of the class. The method hasCucumberContextConfiguration now does recognize the use of @CucumberContextConfiguration as meta-annotation or with inheritance. Co-authored-by: michael <michael.schlatt@gmail.com> Co-authored-by: M.P. Korstanje <rien.korstanje@gmail.com>
1 parent aa05551 commit 3615fc7

File tree

9 files changed

+123
-51
lines changed

9 files changed

+123
-51
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
### Fixed
1515
- [Core] Don't swallow parse errors on the CLI ([2632](https://github.com/cucumber/cucumber-jvm/issues/2632) M.P. Korstanje)
1616

17+
### Added
18+
- [Spring] Support @CucumberContextConfiguration as a meta-annotation ([2491](https://github.com/cucumber/cucumber-jvm/issues/2491) Michael Schlatt)
19+
1720
### Changed
1821
- [Core] Update dependency io.cucumber:gherkin to v24.1
1922
- [Core] Delegate encoding and BOM handling to gherkin ([2624](https://github.com/cucumber/cucumber-jvm/issues/2624) M.P. Korstanje)

cucumber-spring/src/main/java/io/cucumber/spring/SpringBackend.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import io.cucumber.core.resource.ClasspathScanner;
88
import io.cucumber.core.resource.ClasspathSupport;
99

10+
import java.lang.reflect.Modifier;
1011
import java.net.URI;
1112
import java.util.Collection;
1213
import java.util.List;
@@ -31,7 +32,8 @@ public void loadGlue(Glue glue, List<URI> gluePaths) {
3132
.map(ClasspathSupport::packageName)
3233
.map(classFinder::scanForClassesInPackage)
3334
.flatMap(Collection::stream)
34-
.filter((Class<?> clazz) -> clazz.getAnnotation(CucumberContextConfiguration.class) != null)
35+
.filter(SpringFactory::hasCucumberContextConfiguration)
36+
.filter(this::checkIfOfClassTypeAndNotAbstract)
3537
.distinct()
3638
.forEach(container::addClass);
3739
}
@@ -51,4 +53,7 @@ public Snippet getSnippet() {
5153
return null;
5254
}
5355

56+
private boolean checkIfOfClassTypeAndNotAbstract(Class<?> clazz) {
57+
return !clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers());
58+
}
5459
}

cucumber-spring/src/main/java/io/cucumber/spring/SpringFactory.java

Lines changed: 12 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import io.cucumber.core.resource.ClasspathSupport;
66
import org.apiguardian.api.API;
77
import org.springframework.beans.BeansException;
8+
import org.springframework.core.annotation.AnnotatedElementUtils;
89
import org.springframework.stereotype.Component;
910
import org.springframework.test.annotation.DirtiesContext;
1011
import org.springframework.test.context.BootstrapWith;
@@ -13,13 +14,8 @@
1314
import org.springframework.test.context.TestContextManager;
1415
import org.springframework.test.context.web.WebAppConfiguration;
1516

16-
import java.lang.annotation.Annotation;
17-
import java.util.ArrayDeque;
1817
import java.util.Collection;
19-
import java.util.Collections;
20-
import java.util.Deque;
2118
import java.util.HashSet;
22-
import java.util.Set;
2319

2420
/**
2521
* Spring based implementation of ObjectFactory.
@@ -69,27 +65,25 @@ public boolean addClass(final Class<?> stepClass) {
6965
}
7066

7167
private static void checkNoComponentAnnotations(Class<?> type) {
72-
for (Annotation annotation : type.getAnnotations()) {
73-
if (hasComponentAnnotation(annotation)) {
74-
throw new CucumberBackendException(String.format("" +
75-
"Glue class %1$s was annotated with @%2$s; marking it as a candidate for auto-detection by " +
76-
"Spring. Glue classes are detected and registered by Cucumber. Auto-detection of glue classes by "
77-
+
78-
"spring may lead to duplicate bean definitions. Please remove the @%2$s annotation",
79-
type.getName(),
80-
annotation.annotationType().getSimpleName()));
81-
}
68+
if (AnnotatedElementUtils.isAnnotated(type, Component.class)) {
69+
throw new CucumberBackendException(String.format("" +
70+
"Glue class %1$s was (meta-)annotated with @Component; marking it as a candidate for auto-detection by "
71+
+
72+
"Spring. Glue classes are detected and registered by Cucumber. Auto-detection of glue classes by "
73+
+
74+
"spring may lead to duplicate bean definitions. Please remove the @Component (meta-)annotation",
75+
type.getName()));
8276
}
8377
}
8478

85-
private static boolean hasCucumberContextConfiguration(Class<?> stepClass) {
86-
return stepClass.getAnnotation(CucumberContextConfiguration.class) != null;
79+
static boolean hasCucumberContextConfiguration(Class<?> stepClass) {
80+
return AnnotatedElementUtils.isAnnotated(stepClass, CucumberContextConfiguration.class);
8781
}
8882

8983
private void checkOnlyOneClassHasCucumberContextConfiguration(Class<?> stepClass) {
9084
if (withCucumberContextConfiguration != null) {
9185
throw new CucumberBackendException(String.format("" +
92-
"Glue class %1$s and %2$s are both annotated with @CucumberContextConfiguration.\n" +
86+
"Glue class %1$s and %2$s are both (meta-)annotated with @CucumberContextConfiguration.\n" +
9387
"Please ensure only one class configures the spring context\n" +
9488
"\n" +
9589
"By default Cucumber scans the entire classpath for context configuration.\n" +
@@ -100,32 +94,6 @@ private void checkOnlyOneClassHasCucumberContextConfiguration(Class<?> stepClass
10094
}
10195
}
10296

103-
private static boolean hasComponentAnnotation(Annotation annotation) {
104-
return hasAnnotation(annotation, Collections.singleton(Component.class));
105-
}
106-
107-
private static boolean hasAnnotation(Annotation annotation, Collection<Class<? extends Annotation>> desired) {
108-
Set<Class<? extends Annotation>> seen = new HashSet<>();
109-
Deque<Class<? extends Annotation>> toCheck = new ArrayDeque<>();
110-
toCheck.add(annotation.annotationType());
111-
112-
while (!toCheck.isEmpty()) {
113-
Class<? extends Annotation> annotationType = toCheck.pop();
114-
if (desired.contains(annotationType)) {
115-
return true;
116-
}
117-
118-
seen.add(annotationType);
119-
for (Annotation annotationTypesAnnotations : annotationType.getAnnotations()) {
120-
if (!seen.contains(annotationTypesAnnotations.annotationType())) {
121-
toCheck.add(annotationTypesAnnotations.annotationType());
122-
}
123-
}
124-
125-
}
126-
return false;
127-
}
128-
12997
@Override
13098
public void start() {
13199
if (withCucumberContextConfiguration == null) {

cucumber-spring/src/test/java/io/cucumber/spring/SpringBackendTest.java

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33
import io.cucumber.core.backend.Glue;
44
import io.cucumber.core.backend.ObjectFactory;
5-
import io.cucumber.core.backend.StepDefinition;
65
import io.cucumber.spring.annotationconfig.AnnotationContextConfiguration;
6+
import io.cucumber.spring.cucumbercontextconfigannotation.AbstractWithComponentAnnotation;
7+
import io.cucumber.spring.cucumbercontextconfigannotation.AnnotatedInterface;
8+
import io.cucumber.spring.cucumbercontextconfigannotation.WithMetaAnnotation;
79
import org.junit.jupiter.api.BeforeEach;
810
import org.junit.jupiter.api.Test;
911
import org.junit.jupiter.api.extension.ExtendWith;
10-
import org.mockito.ArgumentCaptor;
11-
import org.mockito.Captor;
1212
import org.mockito.Mock;
1313
import org.mockito.junit.jupiter.MockitoExtension;
1414

@@ -52,4 +52,28 @@ void finds_annotaiton_context_configuration_once_by_classpath_url() {
5252
verify(factory, times(1)).addClass(AnnotationContextConfiguration.class);
5353
}
5454

55+
@Test
56+
void ignoresAbstractClassWithCucumberContextConfiguration() {
57+
backend.loadGlue(glue, singletonList(
58+
URI.create("classpath:io/cucumber/spring/cucumbercontextconfigannotation")));
59+
backend.buildWorld();
60+
verify(factory, times(0)).addClass(AbstractWithComponentAnnotation.class);
61+
}
62+
63+
@Test
64+
void ignoresInterfaceWithCucumberContextConfiguration() {
65+
backend.loadGlue(glue, singletonList(
66+
URI.create("classpath:io/cucumber/spring/cucumbercontextconfigannotation")));
67+
backend.buildWorld();
68+
verify(factory, times(0)).addClass(AnnotatedInterface.class);
69+
}
70+
71+
@Test
72+
void considersClassWithCucumberContextConfigurationMetaAnnotation() {
73+
backend.loadGlue(glue, singletonList(
74+
URI.create("classpath:io/cucumber/spring/cucumbercontextconfigannotation")));
75+
backend.buildWorld();
76+
verify(factory, times(1)).addClass(WithMetaAnnotation.class);
77+
}
78+
5579
}

cucumber-spring/src/test/java/io/cucumber/spring/SpringFactoryTest.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import io.cucumber.spring.componentannotation.WithControllerAnnotation;
1414
import io.cucumber.spring.contextconfig.BellyStepDefinitions;
1515
import io.cucumber.spring.contexthierarchyconfig.WithContextHierarchyAnnotation;
16+
import io.cucumber.spring.cucumbercontextconfigannotation.WithInheritedAnnotation;
17+
import io.cucumber.spring.cucumbercontextconfigannotation.WithMetaAnnotation;
1618
import io.cucumber.spring.dirtiescontextconfig.DirtiesContextBellyStepDefinitions;
1719
import io.cucumber.spring.metaconfig.dirties.DirtiesContextBellyMetaStepDefinitions;
1820
import io.cucumber.spring.metaconfig.general.BellyMetaStepDefinitions;
@@ -261,7 +263,7 @@ void shouldFailIfMultipleClassesWithSpringAnnotationsAreFound() {
261263
Executable testMethod = () -> factory.addClass(BellyStepDefinitions.class);
262264
CucumberBackendException actualThrown = assertThrows(CucumberBackendException.class, testMethod);
263265
assertThat(actualThrown.getMessage(), startsWith(
264-
"Glue class class io.cucumber.spring.contextconfig.BellyStepDefinitions and class io.cucumber.spring.SpringFactoryTest$WithSpringAnnotations are both annotated with @CucumberContextConfiguration.\n"
266+
"Glue class class io.cucumber.spring.contextconfig.BellyStepDefinitions and class io.cucumber.spring.SpringFactoryTest$WithSpringAnnotations are both (meta-)annotated with @CucumberContextConfiguration.\n"
265267
+
266268
"Please ensure only one class configures the spring context"));
267269
}
@@ -273,7 +275,7 @@ void shouldFailIfClassWithSpringComponentAnnotationsIsFound() {
273275
Executable testMethod = () -> factory.addClass(WithComponentAnnotation.class);
274276
CucumberBackendException actualThrown = assertThrows(CucumberBackendException.class, testMethod);
275277
assertThat(actualThrown.getMessage(), is(equalTo(
276-
"Glue class io.cucumber.spring.componentannotation.WithComponentAnnotation was annotated with @Component; marking it as a candidate for auto-detection by Spring. Glue classes are detected and registered by Cucumber. Auto-detection of glue classes by spring may lead to duplicate bean definitions. Please remove the @Component annotation")));
278+
"Glue class io.cucumber.spring.componentannotation.WithComponentAnnotation was (meta-)annotated with @Component; marking it as a candidate for auto-detection by Spring. Glue classes are detected and registered by Cucumber. Auto-detection of glue classes by spring may lead to duplicate bean definitions. Please remove the @Component (meta-)annotation")));
277279
}
278280

279281
@Test
@@ -283,7 +285,7 @@ void shouldFailIfClassWithAnnotationAnnotatedWithSpringComponentAnnotationsIsFou
283285
Executable testMethod = () -> factory.addClass(WithControllerAnnotation.class);
284286
CucumberBackendException actualThrown = assertThrows(CucumberBackendException.class, testMethod);
285287
assertThat(actualThrown.getMessage(), is(equalTo(
286-
"Glue class io.cucumber.spring.componentannotation.WithControllerAnnotation was annotated with @Controller; marking it as a candidate for auto-detection by Spring. Glue classes are detected and registered by Cucumber. Auto-detection of glue classes by spring may lead to duplicate bean definitions. Please remove the @Controller annotation")));
288+
"Glue class io.cucumber.spring.componentannotation.WithControllerAnnotation was (meta-)annotated with @Component; marking it as a candidate for auto-detection by Spring. Glue classes are detected and registered by Cucumber. Auto-detection of glue classes by spring may lead to duplicate bean definitions. Please remove the @Component (meta-)annotation")));
287289
}
288290

289291
@Test
@@ -359,6 +361,22 @@ void shouldBeStoppableWhenFacedWithFailedApplicationContext(Class<?> contextConf
359361
assertDoesNotThrow(factory::stop);
360362
}
361363

364+
@Test
365+
void shouldNotFailWithCucumberContextConfigurationMetaAnnotation() {
366+
final ObjectFactory factory = new SpringFactory();
367+
factory.addClass(WithMetaAnnotation.class);
368+
369+
assertDoesNotThrow(factory::start);
370+
}
371+
372+
@Test
373+
void shouldNotFailWithCucumberContextConfigurationInheritedAnnotation() {
374+
final ObjectFactory factory = new SpringFactory();
375+
factory.addClass(WithInheritedAnnotation.class);
376+
377+
assertDoesNotThrow(factory::start);
378+
}
379+
362380
@CucumberContextConfiguration
363381
@ContextConfiguration("classpath:cucumber.xml")
364382
public static class WithSpringAnnotations {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package io.cucumber.spring.cucumbercontextconfigannotation;
2+
3+
import io.cucumber.spring.CucumberContextConfiguration;
4+
import org.springframework.test.context.ContextConfiguration;
5+
6+
@CucumberContextConfiguration
7+
@ContextConfiguration("classpath:cucumber.xml")
8+
public abstract class AbstractWithComponentAnnotation {
9+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package io.cucumber.spring.cucumbercontextconfigannotation;
2+
3+
import io.cucumber.spring.CucumberContextConfiguration;
4+
5+
@CucumberContextConfiguration
6+
public interface AnnotatedInterface {
7+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.cucumber.spring.cucumbercontextconfigannotation;
2+
3+
import io.cucumber.spring.CucumberContextConfiguration;
4+
import org.springframework.test.context.ContextConfiguration;
5+
6+
import java.lang.annotation.*;
7+
8+
public class WithInheritedAnnotation extends ParentClass {
9+
}
10+
11+
@InheritableCumberContextConfiguration
12+
class ParentClass {
13+
}
14+
15+
@Target(ElementType.TYPE)
16+
@Retention(RetentionPolicy.RUNTIME)
17+
@CucumberContextConfiguration
18+
@ContextConfiguration("classpath:cucumber.xml")
19+
@Inherited
20+
@interface InheritableCumberContextConfiguration {
21+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package io.cucumber.spring.cucumbercontextconfigannotation;
2+
3+
import io.cucumber.spring.CucumberContextConfiguration;
4+
import org.springframework.test.context.ContextConfiguration;
5+
6+
import java.lang.annotation.*;
7+
8+
@MyTestAnnotation
9+
public class WithMetaAnnotation {
10+
}
11+
12+
@Target(ElementType.TYPE)
13+
@Retention(RetentionPolicy.RUNTIME)
14+
@CucumberContextConfiguration
15+
@ContextConfiguration("classpath:cucumber.xml")
16+
@interface MyTestAnnotation {
17+
}

0 commit comments

Comments
 (0)