Skip to content

Commit

Permalink
enable @AnalyzeClasses annotation to be used as meta annotation
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
mathze committed May 4, 2024
1 parent 8e9744b commit 253c79e
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -63,13 +65,34 @@ final class ArchUnitRunnerInternal extends ParentRunner<ArchTestExecution> 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(),
Expand All @@ -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<JavaClasses> classes = () -> classCache.getClassesToAnalyzeFor(testClass, new JUnit5ClassAnalysisRequest(testClass));
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
}
}

0 comments on commit 253c79e

Please sign in to comment.