Skip to content

Commit 4e75e03

Browse files
committed
enable @AnalyzeClasses annotation to be used as meta annotation
so far users are forced to repeat `@AnalyzeClasses` annotation an every test class. This cause additional maintenance overhead when common properties (e.g. package structure) changes. To support the DRY approach, `@AnalzyeClasses` annotation can now be used as meta annotation. Resolves: #182 Signed-off-by: Mathze <270275+mathze@users.noreply.github.com>
1 parent d1332ca commit 4e75e03

File tree

8 files changed

+348
-12
lines changed

8 files changed

+348
-12
lines changed

archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerInternal.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,14 @@ final class ArchUnitRunnerInternal extends ParentRunner<ArchTestExecution> imple
6363
}
6464

6565
private static AnalyzeClasses checkAnnotation(Class<?> testClass) {
66-
AnalyzeClasses analyzeClasses = testClass.getAnnotation(AnalyzeClasses.class);
67-
ArchTestInitializationException.check(analyzeClasses != null,
66+
List<AnalyzeClasses> analyzeClasses = new AnnotationFinder<>(AnalyzeClasses.class).findAnnotationsOn(testClass);
67+
ArchTestInitializationException.check(!analyzeClasses.isEmpty(),
6868
"Class %s must be annotated with @%s",
6969
testClass.getSimpleName(), AnalyzeClasses.class.getSimpleName());
70-
return analyzeClasses;
70+
ArchTestInitializationException.check(analyzeClasses.size() == 1,
71+
"Multiple @%s annotations found on %s! This is not supported at the moment.",
72+
AnalyzeClasses.class.getSimpleName(), testClass.getSimpleName());
73+
return analyzeClasses.get(0);
7174
}
7275

7376
@Override

archunit-junit/junit4/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerTest.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.tngtech.archunit.junit.internal;
22

3+
import java.lang.annotation.Retention;
4+
import java.lang.annotation.RetentionPolicy;
35
import java.util.Set;
46

57
import com.tngtech.archunit.core.domain.JavaClass;
@@ -50,6 +52,8 @@ public class ArchUnitRunnerTest {
5052
private ArchUnitRunnerInternal runner = newRunner(SomeArchTest.class);
5153
@InjectMocks
5254
private ArchUnitRunnerInternal runnerOfMaxTest = newRunner(MaxAnnotatedTest.class);
55+
@InjectMocks
56+
private ArchUnitRunnerInternal runnerOfMetaAnnotatedAnalyzerClasses = newRunner(MetaAnnotatedTest.class);
5357

5458
@Before
5559
public void setUp() {
@@ -96,6 +100,35 @@ public void rejects_missing_analyze_annotation() {
96100
.hasMessageContaining(AnalyzeClasses.class.getSimpleName());
97101
}
98102

103+
@Test
104+
public void runner_creates_correct_analysis_request_for_meta_annotated_class() {
105+
runnerOfMetaAnnotatedAnalyzerClasses.run(new RunNotifier());
106+
107+
verify(cache).getClassesToAnalyzeFor(eq(MetaAnnotatedTest.class), analysisRequestCaptor.capture());
108+
109+
AnalyzeClasses analyzeClasses = MetaAnnotatedTest.class.getAnnotation(MetaAnnotatedTest.MetaAnalyzeCls.class)
110+
.annotationType().getAnnotation(AnalyzeClasses.class);
111+
ClassAnalysisRequest analysisRequest = analysisRequestCaptor.getValue();
112+
assertThat(analysisRequest.getPackageNames()).isEqualTo(analyzeClasses.packages());
113+
assertThat(analysisRequest.getPackageRoots()).isEqualTo(analyzeClasses.packagesOf());
114+
assertThat(analysisRequest.getLocationProviders()).isEqualTo(analyzeClasses.locations());
115+
assertThat(analysisRequest.scanWholeClasspath()).as("scan whole classpath").isTrue();
116+
assertThat(analysisRequest.getImportOptions()).isEqualTo(analyzeClasses.importOptions());
117+
}
118+
119+
@Test
120+
public void rejects_if_multiple_analyze_annotations() {
121+
assertThatThrownBy(
122+
() -> new ArchUnitRunnerInternal(MultipleAnalyzeClzAnnotationsTest.class)
123+
)
124+
.isInstanceOf(ArchTestInitializationException.class)
125+
.hasMessageContaining("Multiple")
126+
.hasMessageContaining(AnalyzeClasses.class.getSimpleName())
127+
.hasMessageContaining("found")
128+
.hasMessageContaining(MultipleAnalyzeClzAnnotationsTest.class.getSimpleName())
129+
.hasMessageContaining("not supported");
130+
}
131+
99132
private ArchUnitRunnerInternal newRunner(Class<?> testClass) {
100133
try {
101134
return new ArchUnitRunnerInternal(testClass);
@@ -160,4 +193,25 @@ public static class MaxAnnotatedTest {
160193
public static void someTest(JavaClasses classes) {
161194
}
162195
}
196+
197+
@MetaAnnotatedTest.MetaAnalyzeCls
198+
public static class MetaAnnotatedTest {
199+
@ArchTest
200+
public static void someTest(JavaClasses classes) {
201+
}
202+
203+
@Retention(RetentionPolicy.RUNTIME)
204+
@AnalyzeClasses(
205+
packages = {"com.forty", "com.two"},
206+
wholeClasspath = true
207+
)
208+
public @interface MetaAnalyzeCls {
209+
}
210+
}
211+
212+
@MetaAnnotatedTest.MetaAnalyzeCls
213+
@AnalyzeClasses
214+
public static class MultipleAnalyzeClzAnnotationsTest {
215+
216+
}
163217
}

archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestDescriptor.java

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.lang.reflect.Field;
2020
import java.lang.reflect.Member;
2121
import java.lang.reflect.Method;
22+
import java.util.List;
2223
import java.util.function.Consumer;
2324
import java.util.function.Supplier;
2425

@@ -39,7 +40,6 @@
3940
import org.slf4j.Logger;
4041
import org.slf4j.LoggerFactory;
4142

42-
import static com.google.common.base.Preconditions.checkArgument;
4343
import static com.tngtech.archunit.junit.internal.DisplayNameResolver.determineDisplayName;
4444
import static com.tngtech.archunit.junit.internal.ReflectionUtils.getAllFields;
4545
import static com.tngtech.archunit.junit.internal.ReflectionUtils.getAllMethods;
@@ -71,7 +71,8 @@ static void resolve(TestDescriptor parent, ElementResolver resolver, ClassCache
7171
}
7272

7373
private static void createTestDescriptor(TestDescriptor parent, ClassCache classCache, Class<?> clazz, ElementResolver childResolver) {
74-
if (clazz.getAnnotation(AnalyzeClasses.class) == null) {
74+
List<AnalyzeClasses> analyzeClasses = new AnnotationFinder<>(AnalyzeClasses.class).findAnnotationsOn(clazz);
75+
if (analyzeClasses.isEmpty()) {
7576
LOG.warn("Class {} is not annotated with @{} and thus cannot run as a top level test. "
7677
+ "This warning can be ignored if {} is only used as part of a rules library included via {}.in({}.class).",
7778
clazz.getName(), AnalyzeClasses.class.getSimpleName(),
@@ -80,6 +81,10 @@ private static void createTestDescriptor(TestDescriptor parent, ClassCache class
8081
return;
8182
}
8283

84+
ArchTestInitializationException.check(analyzeClasses.size() == 1,
85+
"Multiple @%s annotations found on %s! This is not supported at the moment.",
86+
AnalyzeClasses.class.getSimpleName(), clazz.getSimpleName());
87+
8388
ArchUnitTestDescriptor classDescriptor = new ArchUnitTestDescriptor(childResolver, clazz, classCache);
8489
parent.addChild(classDescriptor);
8590
classDescriptor.createChildren(childResolver);
@@ -295,11 +300,14 @@ private static class JUnit5ClassAnalysisRequest implements ClassAnalysisRequest
295300
}
296301

297302
private static AnalyzeClasses checkAnnotation(Class<?> testClass) {
298-
AnalyzeClasses analyzeClasses = testClass.getAnnotation(AnalyzeClasses.class);
299-
checkArgument(analyzeClasses != null,
303+
List<AnalyzeClasses> analyzeClasses = new AnnotationFinder<>(AnalyzeClasses.class).findAnnotationsOn(testClass);
304+
ArchTestInitializationException.check(!analyzeClasses.isEmpty(),
300305
"Class %s must be annotated with @%s",
301306
testClass.getSimpleName(), AnalyzeClasses.class.getSimpleName());
302-
return analyzeClasses;
307+
ArchTestInitializationException.check(analyzeClasses.size() == 1,
308+
"Multiple @%s annotations found on %s! This is not supported at the moment.",
309+
AnalyzeClasses.class.getSimpleName(), testClass.getSimpleName());
310+
return analyzeClasses.get(0);
303311
}
304312

305313
@Override

archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngineTest.java

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import com.tngtech.archunit.junit.internal.testexamples.FullAnalyzeClassesSpec;
2828
import com.tngtech.archunit.junit.internal.testexamples.LibraryWithPrivateTests;
2929
import com.tngtech.archunit.junit.internal.testexamples.SimpleRuleLibrary;
30+
import com.tngtech.archunit.junit.internal.testexamples.TestClassWithMetaAnnotationForAnalyzeClasses;
3031
import com.tngtech.archunit.junit.internal.testexamples.TestClassWithMetaTag;
3132
import com.tngtech.archunit.junit.internal.testexamples.TestClassWithMetaTags;
3233
import com.tngtech.archunit.junit.internal.testexamples.TestClassWithTags;
@@ -53,6 +54,7 @@
5354
import com.tngtech.archunit.junit.internal.testexamples.subtwo.SimpleRules;
5455
import com.tngtech.archunit.junit.internal.testexamples.wrong.WrongRuleMethodNotStatic;
5556
import com.tngtech.archunit.junit.internal.testexamples.wrong.WrongRuleMethodWrongParameters;
57+
import com.tngtech.archunit.junit.internal.testexamples.wrong.WrongTestClassWithMultipleAnalyzeClassesAnnotations;
5658
import com.tngtech.archunit.junit.internal.testutil.LogCaptor;
5759
import com.tngtech.archunit.junit.internal.testutil.SystemPropertiesExtension;
5860
import com.tngtech.archunit.junit.internal.testutil.TestLogExtension;
@@ -169,6 +171,20 @@ void a_single_test_class() {
169171
assertThat(child.getParent().get()).isEqualTo(descriptor);
170172
}
171173

174+
@Test
175+
void a_test_class_with_meta_annotation_for_analyze_classes() {
176+
EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(TestClassWithMetaAnnotationForAnalyzeClasses.class);
177+
178+
TestDescriptor descriptor = testEngine.discover(discoveryRequest, engineId);
179+
180+
TestDescriptor child = getOnlyElement(descriptor.getChildren());
181+
assertThat(child).isInstanceOf(ArchUnitTestDescriptor.class);
182+
assertThat(child.getUniqueId()).isEqualTo(engineId.append(CLASS_SEGMENT_TYPE, TestClassWithMetaAnnotationForAnalyzeClasses.class.getName()));
183+
assertThat(child.getDisplayName()).isEqualTo(TestClassWithMetaAnnotationForAnalyzeClasses.class.getSimpleName());
184+
assertThat(child.getType()).isEqualTo(CONTAINER);
185+
assertThat(child.getParent()).get().isEqualTo(descriptor);
186+
}
187+
172188
@Test
173189
void source_of_a_single_test_class() {
174190
EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(SimpleRuleField.class);
@@ -505,10 +521,10 @@ void mixed_class_methods_and_fields() {
505521
expectedLeafIds.add(simpleRuleFieldTestId(engineId));
506522
expectedLeafIds.add(simpleRuleMethodTestId(engineId));
507523
Stream.concat(
508-
SimpleRules.RULE_FIELD_NAMES.stream().map(fieldName ->
509-
simpleRulesId(engineId).append(FIELD_SEGMENT_TYPE, fieldName)),
510-
SimpleRules.RULE_METHOD_NAMES.stream().map(methodName ->
511-
simpleRulesId(engineId).append(METHOD_SEGMENT_TYPE, methodName)))
524+
SimpleRules.RULE_FIELD_NAMES.stream().map(fieldName ->
525+
simpleRulesId(engineId).append(FIELD_SEGMENT_TYPE, fieldName)),
526+
SimpleRules.RULE_METHOD_NAMES.stream().map(methodName ->
527+
simpleRulesId(engineId).append(METHOD_SEGMENT_TYPE, methodName)))
512528
.forEach(expectedLeafIds::add);
513529

514530
assertThat(getAllLeafUniqueIds(rootDescriptor))
@@ -1074,6 +1090,21 @@ void cache_is_cleared_afterwards() {
10741090
verify(classCache, atLeastOnce()).getClassesToAnalyzeFor(any(Class.class), any(ClassAnalysisRequest.class));
10751091
verifyNoMoreInteractions(classCache);
10761092
}
1093+
1094+
@Test
1095+
void a_class_with_meta_annotation_for_analyze_classes() {
1096+
execute(createEngineId(), TestClassWithMetaAnnotationForAnalyzeClasses.class);
1097+
1098+
verify(classCache).getClassesToAnalyzeFor(eq(TestClassWithMetaAnnotationForAnalyzeClasses.class), classAnalysisRequestCaptor.capture());
1099+
ClassAnalysisRequest request = classAnalysisRequestCaptor.getValue();
1100+
AnalyzeClasses expected = TestClassWithMetaAnnotationForAnalyzeClasses.class.getAnnotation(TestClassWithMetaAnnotationForAnalyzeClasses.MetaAnalyzeCls.class)
1101+
.annotationType().getAnnotation(AnalyzeClasses.class);
1102+
assertThat(request.getPackageNames()).isEqualTo(expected.packages());
1103+
assertThat(request.getPackageRoots()).isEqualTo(expected.packagesOf());
1104+
assertThat(request.getLocationProviders()).isEqualTo(expected.locations());
1105+
assertThat(request.scanWholeClasspath()).as("scan whole classpath").isTrue();
1106+
assertThat(request.getImportOptions()).isEqualTo(expected.importOptions());
1107+
}
10771108
}
10781109

10791110
@Nested
@@ -1089,6 +1120,19 @@ void rule_method_with_wrong_parameters() {
10891120
.hasMessageContaining(WrongRuleMethodWrongParameters.WRONG_PARAMETERS_METHOD_NAME)
10901121
.hasMessageContaining("must have exactly one parameter of type " + JavaClasses.class.getName());
10911122
}
1123+
1124+
@Test
1125+
void a_test_class_with_multiple_analyze_classes_annotations() {
1126+
EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(WrongTestClassWithMultipleAnalyzeClassesAnnotations.class);
1127+
1128+
assertThatThrownBy(() -> testEngine.discover(discoveryRequest, engineId))
1129+
.isInstanceOf(ArchTestInitializationException.class)
1130+
.hasMessageContaining("Multiple")
1131+
.hasMessageContaining(AnalyzeClasses.class.getSimpleName())
1132+
.hasMessageContaining("found")
1133+
.hasMessageContaining(WrongTestClassWithMultipleAnalyzeClassesAnnotations.class.getSimpleName())
1134+
.hasMessageContaining("not supported");
1135+
}
10921136
}
10931137

10941138
@Nested
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.tngtech.archunit.junit.internal.testexamples;
2+
3+
import com.tngtech.archunit.junit.AnalyzeClasses;
4+
import com.tngtech.archunit.junit.ArchTest;
5+
import com.tngtech.archunit.lang.ArchRule;
6+
7+
import java.lang.annotation.Retention;
8+
import java.lang.annotation.Target;
9+
10+
import static java.lang.annotation.ElementType.TYPE;
11+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
12+
13+
@TestClassWithMetaAnnotationForAnalyzeClasses.MetaAnalyzeCls
14+
public class TestClassWithMetaAnnotationForAnalyzeClasses {
15+
16+
@ArchTest
17+
public static final ArchRule rule_in_class_with_meta_analyze_class_annotation = RuleThatFails.on(UnwantedClass.class);
18+
19+
@Retention(RUNTIME)
20+
@Target(TYPE)
21+
@AnalyzeClasses(wholeClasspath = true)
22+
public @interface MetaAnalyzeCls {
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.tngtech.archunit.junit.internal.testexamples.wrong;
2+
3+
import java.lang.annotation.Retention;
4+
import java.lang.annotation.Target;
5+
6+
import com.tngtech.archunit.junit.AnalyzeClasses;
7+
import com.tngtech.archunit.junit.ArchTest;
8+
import com.tngtech.archunit.junit.internal.testexamples.RuleThatFails;
9+
import com.tngtech.archunit.junit.internal.testexamples.UnwantedClass;
10+
import com.tngtech.archunit.lang.ArchRule;
11+
12+
import static java.lang.annotation.ElementType.TYPE;
13+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
14+
15+
@AnalyzeClasses(packages = "dummy")
16+
@WrongTestClassWithMultipleAnalyzeClassesAnnotations.MetaAnalyzeCls
17+
public class WrongTestClassWithMultipleAnalyzeClassesAnnotations {
18+
19+
@ArchTest
20+
public static final ArchRule dummy_rule = RuleThatFails.on(UnwantedClass.class);
21+
22+
@Retention(RUNTIME)
23+
@Target(TYPE)
24+
@AnalyzeClasses(wholeClasspath = true)
25+
public @interface MetaAnalyzeCls {
26+
}
27+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.tngtech.archunit.junit.internal;
2+
3+
import java.lang.annotation.Annotation;
4+
import java.util.HashSet;
5+
import java.util.LinkedList;
6+
import java.util.List;
7+
8+
class AnnotationFinder<T extends Annotation> {
9+
10+
private final Class<T> annotationClass;
11+
12+
public AnnotationFinder(final Class<T> annotationClass) {
13+
this.annotationClass = annotationClass;
14+
}
15+
16+
/**
17+
* Recursively retrieve all {@link T} annotations from a given element.
18+
*
19+
* @param clazz The clazz from which to retrieve the annotation.
20+
* @return List of all found annotation instance or empty list.
21+
*/
22+
public List<T> findAnnotationsOn(final Class<?> clazz) {
23+
return findAnnotations(clazz.getAnnotations(), new HashSet<>());
24+
}
25+
26+
private List<T> findAnnotations(final Annotation[] annotations, final HashSet<Annotation> visited) {
27+
final List<T> result = new LinkedList<>();
28+
for (Annotation annotation : annotations) {
29+
if (visited.contains(annotation)) {
30+
continue;
31+
} else {
32+
visited.add(annotation);
33+
}
34+
if (annotationClass.isInstance(annotation)) {
35+
result.add(annotationClass.cast(annotation));
36+
} else {
37+
result.addAll(findAnnotations(annotation.annotationType().getAnnotations(), visited));
38+
}
39+
}
40+
return result;
41+
}
42+
}

0 commit comments

Comments
 (0)