Skip to content

Commit 9e90a3b

Browse files
mathzecodecholeric
authored andcommitted
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 <[email protected]>
1 parent 05676d2 commit 9e90a3b

File tree

9 files changed

+223
-28
lines changed

9 files changed

+223
-28
lines changed

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

+5-8
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import static com.tngtech.archunit.junit.internal.ArchRuleDeclaration.elementShouldBeIgnored;
4444
import static com.tngtech.archunit.junit.internal.ArchRuleDeclaration.toDeclarations;
4545
import static com.tngtech.archunit.junit.internal.ArchTestExecution.getValue;
46+
import static com.tngtech.archunit.junit.internal.ReflectionUtils.findAnnotation;
4647
import static java.util.Collections.singleton;
4748
import static java.util.Collections.singletonList;
4849
import static java.util.stream.Collectors.toList;
@@ -53,7 +54,7 @@ final class ArchUnitRunnerInternal extends ParentRunner<ArchTestExecution> imple
5354

5455
ArchUnitRunnerInternal(Class<?> testClass) throws InitializationError {
5556
super(testClass);
56-
checkAnnotation(testClass);
57+
checkTestRunnable(testClass);
5758

5859
try {
5960
ArchUnitSystemPropertyTestFilterJunit4.filter(this);
@@ -62,12 +63,8 @@ final class ArchUnitRunnerInternal extends ParentRunner<ArchTestExecution> imple
6263
}
6364
}
6465

65-
private static AnalyzeClasses checkAnnotation(Class<?> testClass) {
66-
AnalyzeClasses analyzeClasses = testClass.getAnnotation(AnalyzeClasses.class);
67-
ArchTestInitializationException.check(analyzeClasses != null,
68-
"Class %s must be annotated with @%s",
69-
testClass.getSimpleName(), AnalyzeClasses.class.getSimpleName());
70-
return analyzeClasses;
66+
private static void checkTestRunnable(Class<?> testClass) {
67+
findAnnotation(testClass, AnalyzeClasses.class);
7168
}
7269

7370
@Override
@@ -180,7 +177,7 @@ private static class JUnit4ClassAnalysisRequest implements ClassAnalysisRequest
180177
private final AnalyzeClasses analyzeClasses;
181178

182179
JUnit4ClassAnalysisRequest(Class<?> testClass) {
183-
analyzeClasses = checkAnnotation(testClass);
180+
analyzeClasses = findAnnotation(testClass, AnalyzeClasses.class);
184181
}
185182

186183
@Override

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

+38-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.tngtech.archunit.junit.internal;
22

3+
import java.lang.annotation.Retention;
34
import java.util.Set;
45

56
import com.tngtech.archunit.core.domain.JavaClass;
@@ -28,6 +29,7 @@
2829

2930
import static com.tngtech.archunit.core.domain.TestUtils.importClasses;
3031
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
32+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
3133
import static org.assertj.core.api.Assertions.assertThat;
3234
import static org.assertj.core.api.Assertions.assertThatThrownBy;
3335
import static org.mockito.ArgumentMatchers.any;
@@ -49,7 +51,9 @@ public class ArchUnitRunnerTest {
4951
@InjectMocks
5052
private ArchUnitRunnerInternal runner = newRunner(SomeArchTest.class);
5153
@InjectMocks
52-
private ArchUnitRunnerInternal runnerOfMaxTest = newRunner(MaxAnnotatedTest.class);
54+
private ArchUnitRunnerInternal runnerOfMaxAnnotatedTest = newRunner(MaxAnnotatedTest.class);
55+
@InjectMocks
56+
private ArchUnitRunnerInternal runnerOfMetaAnnotatedTest = newRunner(MetaAnnotatedTest.class);
5357

5458
@Before
5559
public void setUp() {
@@ -58,7 +62,7 @@ public void setUp() {
5862

5963
@Test
6064
public void runner_creates_correct_analysis_request() {
61-
runnerOfMaxTest.run(new RunNotifier());
65+
runnerOfMaxAnnotatedTest.run(new RunNotifier());
6266

6367
verify(cache).getClassesToAnalyzeFor(eq(MaxAnnotatedTest.class), analysisRequestCaptor.capture());
6468

@@ -92,10 +96,26 @@ public void rejects_missing_analyze_annotation() {
9296
)
9397
.isInstanceOf(ArchTestInitializationException.class)
9498
.hasMessageContaining(Object.class.getSimpleName())
95-
.hasMessageContaining("must be annotated")
99+
.hasMessageContaining("is not (meta-)annotated")
96100
.hasMessageContaining(AnalyzeClasses.class.getSimpleName());
97101
}
98102

103+
@Test
104+
public void runner_creates_correct_analysis_request_for_meta_annotated_class() {
105+
runnerOfMetaAnnotatedTest.run(new RunNotifier());
106+
107+
verify(cache).getClassesToAnalyzeFor(eq(MetaAnnotatedTest.class), analysisRequestCaptor.capture());
108+
109+
AnalyzeClasses analyzeClasses = MetaAnnotatedTest.class.getAnnotation(MetaAnnotatedTest.MetaAnalyzeClasses.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+
99119
private ArchUnitRunnerInternal newRunner(Class<?> testClass) {
100120
try {
101121
return new ArchUnitRunnerInternal(testClass);
@@ -160,4 +180,19 @@ public static class MaxAnnotatedTest {
160180
public static void someTest(JavaClasses classes) {
161181
}
162182
}
183+
184+
@MetaAnnotatedTest.MetaAnalyzeClasses
185+
public static class MetaAnnotatedTest {
186+
@ArchTest
187+
public static void someTest(JavaClasses classes) {
188+
}
189+
190+
@Retention(RUNTIME)
191+
@AnalyzeClasses(
192+
packages = {"com.foo", "com.bar"},
193+
wholeClasspath = true
194+
)
195+
public @interface MetaAnalyzeClasses {
196+
}
197+
}
163198
}

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

+5-12
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,13 @@
3939
import org.slf4j.Logger;
4040
import org.slf4j.LoggerFactory;
4141

42-
import static com.google.common.base.Preconditions.checkArgument;
4342
import static com.tngtech.archunit.junit.internal.DisplayNameResolver.determineDisplayName;
43+
import static com.tngtech.archunit.junit.internal.ReflectionUtils.findAnnotation;
4444
import static com.tngtech.archunit.junit.internal.ReflectionUtils.getAllFields;
4545
import static com.tngtech.archunit.junit.internal.ReflectionUtils.getAllMethods;
4646
import static com.tngtech.archunit.junit.internal.ReflectionUtils.getValueOrThrowException;
4747
import static com.tngtech.archunit.junit.internal.ReflectionUtils.invokeMethod;
48+
import static com.tngtech.archunit.junit.internal.ReflectionUtils.tryFindAnnotation;
4849
import static com.tngtech.archunit.junit.internal.ReflectionUtils.withAnnotation;
4950

5051
class ArchUnitTestDescriptor extends AbstractArchUnitTestDescriptor implements CreatesChildren {
@@ -71,8 +72,8 @@ static void resolve(TestDescriptor parent, ElementResolver resolver, ClassCache
7172
}
7273

7374
private static void createTestDescriptor(TestDescriptor parent, ClassCache classCache, Class<?> clazz, ElementResolver childResolver) {
74-
if (clazz.getAnnotation(AnalyzeClasses.class) == null) {
75-
LOG.warn("Class {} is not annotated with @{} and thus cannot run as a top level test. "
75+
if (!tryFindAnnotation(clazz, AnalyzeClasses.class).isPresent()) {
76+
LOG.warn("Class {} is not (meta-)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(),
7879
clazz.getSimpleName(), ArchTests.class.getSimpleName(), clazz.getSimpleName());
@@ -291,15 +292,7 @@ private static class JUnit5ClassAnalysisRequest implements ClassAnalysisRequest
291292
private final AnalyzeClasses analyzeClasses;
292293

293294
JUnit5ClassAnalysisRequest(Class<?> testClass) {
294-
analyzeClasses = checkAnnotation(testClass);
295-
}
296-
297-
private static AnalyzeClasses checkAnnotation(Class<?> testClass) {
298-
AnalyzeClasses analyzeClasses = testClass.getAnnotation(AnalyzeClasses.class);
299-
checkArgument(analyzeClasses != null,
300-
"Class %s must be annotated with @%s",
301-
testClass.getSimpleName(), AnalyzeClasses.class.getSimpleName());
302-
return analyzeClasses;
295+
analyzeClasses = findAnnotation(testClass, AnalyzeClasses.class);
303296
}
304297

305298
@Override

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

+34-4
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;
@@ -169,6 +170,20 @@ void a_single_test_class() {
169170
assertThat(child.getParent().get()).isEqualTo(descriptor);
170171
}
171172

173+
@Test
174+
void a_test_class_with_meta_annotated_analyze_classes() {
175+
EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(TestClassWithMetaAnnotationForAnalyzeClasses.class);
176+
177+
TestDescriptor descriptor = testEngine.discover(discoveryRequest, engineId);
178+
179+
TestDescriptor child = getOnlyElement(descriptor.getChildren());
180+
assertThat(child).isInstanceOf(ArchUnitTestDescriptor.class);
181+
assertThat(child.getUniqueId()).isEqualTo(engineId.append(CLASS_SEGMENT_TYPE, TestClassWithMetaAnnotationForAnalyzeClasses.class.getName()));
182+
assertThat(child.getDisplayName()).isEqualTo(TestClassWithMetaAnnotationForAnalyzeClasses.class.getSimpleName());
183+
assertThat(child.getType()).isEqualTo(CONTAINER);
184+
assertThat(child.getParent()).get().isEqualTo(descriptor);
185+
}
186+
172187
@Test
173188
void source_of_a_single_test_class() {
174189
EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(SimpleRuleField.class);
@@ -505,10 +520,10 @@ void mixed_class_methods_and_fields() {
505520
expectedLeafIds.add(simpleRuleFieldTestId(engineId));
506521
expectedLeafIds.add(simpleRuleMethodTestId(engineId));
507522
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)))
523+
SimpleRules.RULE_FIELD_NAMES.stream().map(fieldName ->
524+
simpleRulesId(engineId).append(FIELD_SEGMENT_TYPE, fieldName)),
525+
SimpleRules.RULE_METHOD_NAMES.stream().map(methodName ->
526+
simpleRulesId(engineId).append(METHOD_SEGMENT_TYPE, methodName)))
512527
.forEach(expectedLeafIds::add);
513528

514529
assertThat(getAllLeafUniqueIds(rootDescriptor))
@@ -1074,6 +1089,21 @@ void cache_is_cleared_afterwards() {
10741089
verify(classCache, atLeastOnce()).getClassesToAnalyzeFor(any(Class.class), any(ClassAnalysisRequest.class));
10751090
verifyNoMoreInteractions(classCache);
10761091
}
1092+
1093+
@Test
1094+
void a_class_with_analyze_classes_as_meta_annotation() {
1095+
execute(createEngineId(), TestClassWithMetaAnnotationForAnalyzeClasses.class);
1096+
1097+
verify(classCache).getClassesToAnalyzeFor(eq(TestClassWithMetaAnnotationForAnalyzeClasses.class), classAnalysisRequestCaptor.capture());
1098+
ClassAnalysisRequest request = classAnalysisRequestCaptor.getValue();
1099+
AnalyzeClasses expected = TestClassWithMetaAnnotationForAnalyzeClasses.class.getAnnotation(TestClassWithMetaAnnotationForAnalyzeClasses.MetaAnalyzeClasses.class)
1100+
.annotationType().getAnnotation(AnalyzeClasses.class);
1101+
assertThat(request.getPackageNames()).isEqualTo(expected.packages());
1102+
assertThat(request.getPackageRoots()).isEqualTo(expected.packagesOf());
1103+
assertThat(request.getLocationProviders()).isEqualTo(expected.locations());
1104+
assertThat(request.scanWholeClasspath()).as("scan whole classpath").isTrue();
1105+
assertThat(request.getImportOptions()).isEqualTo(expected.importOptions());
1106+
}
10771107
}
10781108

10791109
@Nested
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.tngtech.archunit.junit.internal.testexamples;
2+
3+
import java.lang.annotation.Retention;
4+
5+
import com.tngtech.archunit.junit.AnalyzeClasses;
6+
import com.tngtech.archunit.junit.ArchTest;
7+
import com.tngtech.archunit.lang.ArchRule;
8+
9+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
10+
11+
@TestClassWithMetaAnnotationForAnalyzeClasses.MetaAnalyzeClasses
12+
public class TestClassWithMetaAnnotationForAnalyzeClasses {
13+
14+
@ArchTest
15+
public static final ArchRule rule_in_class_with_meta_analyze_class_annotation = RuleThatFails.on(UnwantedClass.class);
16+
17+
@Retention(RUNTIME)
18+
@AnalyzeClasses(wholeClasspath = true)
19+
public @interface MetaAnalyzeClasses {
20+
}
21+
}

archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ArchTestInitializationException.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.tngtech.archunit.junit.internal;
22

33
class ArchTestInitializationException extends RuntimeException {
4-
private ArchTestInitializationException(String message, Object... args) {
4+
ArchTestInitializationException(String message, Object... args) {
55
super(String.format(message, args));
66
}
77

archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ReflectionUtils.java

+39
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import java.lang.reflect.Modifier;
2424
import java.util.Collection;
2525
import java.util.Collections;
26+
import java.util.HashSet;
27+
import java.util.Optional;
2628
import java.util.Set;
2729
import java.util.function.Function;
2830
import java.util.function.Predicate;
@@ -124,4 +126,41 @@ private static <T extends Throwable> void rethrowUnchecked(Throwable throwable)
124126
static Predicate<AnnotatedElement> withAnnotation(Class<? extends Annotation> annotationType) {
125127
return input -> input.isAnnotationPresent(annotationType);
126128
}
129+
130+
/**
131+
* Same as {@link #tryFindAnnotation(Class, Class)}, but throws an exception if no annotation can be found.
132+
*/
133+
static <T extends Annotation> T findAnnotation(final Class<?> clazz, Class<T> annotationType) {
134+
return tryFindAnnotation(clazz, annotationType).orElseThrow(() ->
135+
new ArchTestInitializationException("Class %s is not (meta-)annotated with @%s", clazz.getName(), annotationType.getSimpleName()));
136+
}
137+
138+
/**
139+
* Recursively searches for an annotation of type {@link T} on the given {@code clazz}.
140+
* Returns the first matching annotation that is found.
141+
* Any further matching annotation possibly present within the meta-annotation hierarchy will be ignored.
142+
* If no matching annotation can be found {@link Optional#empty()} will be returned.
143+
*
144+
* @param clazz The {@link Class} from which to retrieve the annotation
145+
* @return The first found annotation instance reachable in the meta-annotation hierarchy or {@link Optional#empty()} if none can be found
146+
*/
147+
static <T extends Annotation> Optional<T> tryFindAnnotation(final Class<?> clazz, Class<T> annotationType) {
148+
return tryFindAnnotation(clazz.getAnnotations(), annotationType, new HashSet<>());
149+
}
150+
151+
private static <T extends Annotation> Optional<T> tryFindAnnotation(final Annotation[] annotations, Class<T> annotationType, Set<Annotation> visited) {
152+
for (Annotation annotation : annotations) {
153+
if (!visited.add(annotation)) {
154+
continue;
155+
}
156+
157+
Optional<T> result = annotationType.isInstance(annotation)
158+
? Optional.of(annotationType.cast(annotation))
159+
: tryFindAnnotation(annotation.annotationType().getAnnotations(), annotationType, visited);
160+
if (result.isPresent()) {
161+
return result;
162+
}
163+
}
164+
return Optional.empty();
165+
}
127166
}

0 commit comments

Comments
 (0)