From 253c79e2a0682a1be1eb854cf6b3733e3b19a889 Mon Sep 17 00:00:00 2001 From: Mathze <270275+mathze@users.noreply.github.com> Date: Sun, 5 May 2024 00:33:05 +0200 Subject: [PATCH] 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> --- .../internal/ArchUnitRunnerInternal.java | 25 ++++++++++++- .../junit/internal/ArchUnitRunnerTest.java | 35 +++++++++++++++++++ .../internal/ArchUnitTestDescriptor.java | 27 ++++++++++++-- .../internal/ArchUnitTestEngineTest.java | 30 ++++++++++++++++ ...ssWithMetaAnnotationForAnalyzeClasses.java | 24 +++++++++++++ 5 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestClassWithMetaAnnotationForAnalyzeClasses.java diff --git a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerInternal.java b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerInternal.java index 2fd97c2419..0655bd403a 100644 --- a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerInternal.java +++ b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerInternal.java @@ -15,6 +15,8 @@ */ package com.tngtech.archunit.junit.internal; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; @@ -63,13 +65,34 @@ final class ArchUnitRunnerInternal extends ParentRunner imple } private static AnalyzeClasses checkAnnotation(Class testClass) { - AnalyzeClasses analyzeClasses = testClass.getAnnotation(AnalyzeClasses.class); + AnalyzeClasses analyzeClasses = findAnalyzeClassesAnnotationOnAnnotations(testClass); ArchTestInitializationException.check(analyzeClasses != null, "Class %s must be annotated with @%s", testClass.getSimpleName(), AnalyzeClasses.class.getSimpleName()); return analyzeClasses; } + /** + * Recursively checks any annotation on a given element for the {@link AnalyzeClasses} annotation. + * + * @param annotatedElement The element to check + * + * @return The found annotation instance or {@code null}. + */ + private static AnalyzeClasses findAnalyzeClassesAnnotationOnAnnotations(AnnotatedElement annotatedElement) { + AnalyzeClasses analyzeClasses = annotatedElement.getAnnotation(AnalyzeClasses.class); + if (null != analyzeClasses) { + return analyzeClasses; + } + for(Annotation annotation : annotatedElement.getAnnotations()) { + analyzeClasses = findAnalyzeClassesAnnotationOnAnnotations(annotation.annotationType()); + if (null != analyzeClasses) { + return analyzeClasses; + } + } + return null; + } + @Override public Statement classBlock(RunNotifier notifier) { Statement statement = super.classBlock(notifier); diff --git a/archunit-junit/junit4/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerTest.java b/archunit-junit/junit4/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerTest.java index d1e1d1c13c..fd0c1be4f7 100644 --- a/archunit-junit/junit4/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerTest.java +++ b/archunit-junit/junit4/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerTest.java @@ -1,5 +1,7 @@ package com.tngtech.archunit.junit.internal; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Set; import com.tngtech.archunit.core.domain.JavaClass; @@ -50,6 +52,8 @@ public class ArchUnitRunnerTest { private ArchUnitRunnerInternal runner = newRunner(SomeArchTest.class); @InjectMocks private ArchUnitRunnerInternal runnerOfMaxTest = newRunner(MaxAnnotatedTest.class); + @InjectMocks + private ArchUnitRunnerInternal runnerOfMetaAnnotatedAnalyzerClasses = newRunner(MetaAnnotatedTest.class); @Before public void setUp() { @@ -96,6 +100,22 @@ public void rejects_missing_analyze_annotation() { .hasMessageContaining(AnalyzeClasses.class.getSimpleName()); } + @Test + public void runner_creates_correct_analysis_request_for_meta_annotated_class() { + runnerOfMetaAnnotatedAnalyzerClasses.run(new RunNotifier()); + + verify(cache).getClassesToAnalyzeFor(eq(MetaAnnotatedTest.class), analysisRequestCaptor.capture()); + + AnalyzeClasses analyzeClasses = MetaAnnotatedTest.class.getAnnotation(MetaAnnotatedTest.MetaAnalyzeCls.class) + .annotationType().getAnnotation(AnalyzeClasses.class); + ClassAnalysisRequest analysisRequest = analysisRequestCaptor.getValue(); + assertThat(analysisRequest.getPackageNames()).isEqualTo(analyzeClasses.packages()); + assertThat(analysisRequest.getPackageRoots()).isEqualTo(analyzeClasses.packagesOf()); + assertThat(analysisRequest.getLocationProviders()).isEqualTo(analyzeClasses.locations()); + assertThat(analysisRequest.scanWholeClasspath()).as("scan whole classpath").isTrue(); + assertThat(analysisRequest.getImportOptions()).isEqualTo(analyzeClasses.importOptions()); + } + private ArchUnitRunnerInternal newRunner(Class testClass) { try { return new ArchUnitRunnerInternal(testClass); @@ -160,4 +180,19 @@ public static class MaxAnnotatedTest { public static void someTest(JavaClasses classes) { } } + + @MetaAnnotatedTest.MetaAnalyzeCls + public static class MetaAnnotatedTest { + @ArchTest + public static void someTest(JavaClasses classes) { + } + + @Retention(RetentionPolicy.RUNTIME) + @AnalyzeClasses( + packages = {"com.fourty", "com.two"}, + wholeClasspath = true + ) + public @interface MetaAnalyzeCls { + } + } } diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestDescriptor.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestDescriptor.java index d66a449627..d7e5623954 100644 --- a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestDescriptor.java +++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestDescriptor.java @@ -15,7 +15,9 @@ */ package com.tngtech.archunit.junit.internal; +import java.lang.annotation.Annotation; import java.lang.reflect.AccessibleObject; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.Member; import java.lang.reflect.Method; @@ -71,7 +73,7 @@ static void resolve(TestDescriptor parent, ElementResolver resolver, ClassCache } private static void createTestDescriptor(TestDescriptor parent, ClassCache classCache, Class clazz, ElementResolver childResolver) { - if (clazz.getAnnotation(AnalyzeClasses.class) == null) { + if (findAnalyzeClassesAnnotationOnAnnotations(clazz) == null) { LOG.warn("Class {} is not annotated with @{} and thus cannot run as a top level test. " + "This warning can be ignored if {} is only used as part of a rules library included via {}.in({}.class).", clazz.getName(), AnalyzeClasses.class.getSimpleName(), @@ -85,6 +87,27 @@ private static void createTestDescriptor(TestDescriptor parent, ClassCache class classDescriptor.createChildren(childResolver); } + /** + * Recursively checks any annotation on a given element for the {@link AnalyzeClasses} annotation. + * + * @param annotatedElement The element to check + * + * @return The found annotation instance or {@code null}. + */ + private static AnalyzeClasses findAnalyzeClassesAnnotationOnAnnotations(AnnotatedElement annotatedElement) { + AnalyzeClasses analyzeClasses = annotatedElement.getAnnotation(AnalyzeClasses.class); + if (null != analyzeClasses) { + return analyzeClasses; + } + for(Annotation annotation : annotatedElement.getAnnotations()) { + analyzeClasses = findAnalyzeClassesAnnotationOnAnnotations(annotation.annotationType()); + if (null != analyzeClasses) { + return analyzeClasses; + } + } + return null; + } + @Override public void createChildren(ElementResolver resolver) { Supplier classes = () -> classCache.getClassesToAnalyzeFor(testClass, new JUnit5ClassAnalysisRequest(testClass)); @@ -295,7 +318,7 @@ private static class JUnit5ClassAnalysisRequest implements ClassAnalysisRequest } private static AnalyzeClasses checkAnnotation(Class testClass) { - AnalyzeClasses analyzeClasses = testClass.getAnnotation(AnalyzeClasses.class); + AnalyzeClasses analyzeClasses = findAnalyzeClassesAnnotationOnAnnotations(testClass); checkArgument(analyzeClasses != null, "Class %s must be annotated with @%s", testClass.getSimpleName(), AnalyzeClasses.class.getSimpleName()); diff --git a/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngineTest.java b/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngineTest.java index 1fac69668c..583be9b458 100644 --- a/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngineTest.java +++ b/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngineTest.java @@ -27,6 +27,7 @@ import com.tngtech.archunit.junit.internal.testexamples.FullAnalyzeClassesSpec; import com.tngtech.archunit.junit.internal.testexamples.LibraryWithPrivateTests; import com.tngtech.archunit.junit.internal.testexamples.SimpleRuleLibrary; +import com.tngtech.archunit.junit.internal.testexamples.TestClassWithMetaAnnotationForAnalyzeClasses; import com.tngtech.archunit.junit.internal.testexamples.TestClassWithMetaTag; import com.tngtech.archunit.junit.internal.testexamples.TestClassWithMetaTags; import com.tngtech.archunit.junit.internal.testexamples.TestClassWithTags; @@ -169,6 +170,20 @@ void a_single_test_class() { assertThat(child.getParent().get()).isEqualTo(descriptor); } + @Test + void a_test_class_with_meta_annotation_for_analyze_classes() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(TestClassWithMetaAnnotationForAnalyzeClasses.class); + + TestDescriptor descriptor = testEngine.discover(discoveryRequest, engineId); + + TestDescriptor child = getOnlyElement(descriptor.getChildren()); + assertThat(child).isInstanceOf(ArchUnitTestDescriptor.class); + assertThat(child.getUniqueId()).isEqualTo(engineId.append(CLASS_SEGMENT_TYPE, TestClassWithMetaAnnotationForAnalyzeClasses.class.getName())); + assertThat(child.getDisplayName()).isEqualTo(TestClassWithMetaAnnotationForAnalyzeClasses.class.getSimpleName()); + assertThat(child.getType()).isEqualTo(CONTAINER); + assertThat(child.getParent()).get().isEqualTo(descriptor); + } + @Test void source_of_a_single_test_class() { EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(SimpleRuleField.class); @@ -1074,6 +1089,21 @@ void cache_is_cleared_afterwards() { verify(classCache, atLeastOnce()).getClassesToAnalyzeFor(any(Class.class), any(ClassAnalysisRequest.class)); verifyNoMoreInteractions(classCache); } + + @Test + void a_class_with_meta_annotation_for_analyze_classes() { + execute(createEngineId(), TestClassWithMetaAnnotationForAnalyzeClasses.class); + + verify(classCache).getClassesToAnalyzeFor(eq(TestClassWithMetaAnnotationForAnalyzeClasses.class), classAnalysisRequestCaptor.capture()); + ClassAnalysisRequest request = classAnalysisRequestCaptor.getValue(); + AnalyzeClasses expected = TestClassWithMetaAnnotationForAnalyzeClasses.class.getAnnotation(TestClassWithMetaAnnotationForAnalyzeClasses.MetaAnalyzeCls.class) + .annotationType().getAnnotation(AnalyzeClasses.class); + assertThat(request.getPackageNames()).isEqualTo(expected.packages()); + assertThat(request.getPackageRoots()).isEqualTo(expected.packagesOf()); + assertThat(request.getLocationProviders()).isEqualTo(expected.locations()); + assertThat(request.scanWholeClasspath()).as("scan whole classpath").isTrue(); + assertThat(request.getImportOptions()).isEqualTo(expected.importOptions()); + } } @Nested diff --git a/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestClassWithMetaAnnotationForAnalyzeClasses.java b/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestClassWithMetaAnnotationForAnalyzeClasses.java new file mode 100644 index 0000000000..396bbdb1ff --- /dev/null +++ b/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestClassWithMetaAnnotationForAnalyzeClasses.java @@ -0,0 +1,24 @@ +package com.tngtech.archunit.junit.internal.testexamples; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@TestClassWithMetaAnnotationForAnalyzeClasses.MetaAnalyzeCls +public class TestClassWithMetaAnnotationForAnalyzeClasses { + + @ArchTest + public static final ArchRule rule_in_class_with_meta_analyze_class_annotation = RuleThatFails.on(UnwantedClass.class); + + @Retention(RUNTIME) + @Target(TYPE) + @AnalyzeClasses(wholeClasspath = true) + public @interface MetaAnalyzeCls { + } +}