Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enable @AnalyzeClasses annotation to be used as meta annotation #1300

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,14 @@ final class ArchUnitRunnerInternal extends ParentRunner<ArchTestExecution> imple
}

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

@Override
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,35 @@ 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());
}

@Test
public void rejects_if_multiple_analyze_annotations() {
assertThatThrownBy(
() -> new ArchUnitRunnerInternal(MultipleAnalyzeClzAnnotationsTest.class)
)
.isInstanceOf(ArchTestInitializationException.class)
.hasMessageContaining("Multiple")
.hasMessageContaining(AnalyzeClasses.class.getSimpleName())
.hasMessageContaining("found")
.hasMessageContaining(MultipleAnalyzeClzAnnotationsTest.class.getSimpleName())
.hasMessageContaining("not supported");
}

private ArchUnitRunnerInternal newRunner(Class<?> testClass) {
try {
return new ArchUnitRunnerInternal(testClass);
Expand Down Expand Up @@ -160,4 +193,25 @@ 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.forty", "com.two"},
wholeClasspath = true
)
public @interface MetaAnalyzeCls {
}
}

@MetaAnnotatedTest.MetaAnalyzeCls
@AnalyzeClasses
public static class MultipleAnalyzeClzAnnotationsTest {

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;

Expand All @@ -39,7 +40,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.google.common.base.Preconditions.checkArgument;
import static com.tngtech.archunit.junit.internal.DisplayNameResolver.determineDisplayName;
import static com.tngtech.archunit.junit.internal.ReflectionUtils.getAllFields;
import static com.tngtech.archunit.junit.internal.ReflectionUtils.getAllMethods;
Expand Down Expand Up @@ -71,7 +71,8 @@ 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) {
List<AnalyzeClasses> analyzeClasses = new AnnotationFinder<>(AnalyzeClasses.class).findAnnotationsOn(clazz);
if (analyzeClasses.isEmpty()) {
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 @@ -80,6 +81,10 @@ private static void createTestDescriptor(TestDescriptor parent, ClassCache class
return;
}

ArchTestInitializationException.check(analyzeClasses.size() == 1,
"Multiple @%s annotations found on %s! This is not supported at the moment.",
AnalyzeClasses.class.getSimpleName(), clazz.getSimpleName());

ArchUnitTestDescriptor classDescriptor = new ArchUnitTestDescriptor(childResolver, clazz, classCache);
parent.addChild(classDescriptor);
classDescriptor.createChildren(childResolver);
Expand Down Expand Up @@ -295,11 +300,14 @@ private static class JUnit5ClassAnalysisRequest implements ClassAnalysisRequest
}

private static AnalyzeClasses checkAnnotation(Class<?> testClass) {
AnalyzeClasses analyzeClasses = testClass.getAnnotation(AnalyzeClasses.class);
checkArgument(analyzeClasses != null,
List<AnalyzeClasses> analyzeClasses = new AnnotationFinder<>(AnalyzeClasses.class).findAnnotationsOn(testClass);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how to reach this from test.

ArchTestInitializationException.check(!analyzeClasses.isEmpty(),
"Class %s must be annotated with @%s",
testClass.getSimpleName(), AnalyzeClasses.class.getSimpleName());
return analyzeClasses;
ArchTestInitializationException.check(analyzeClasses.size() == 1,
"Multiple @%s annotations found on %s! This is not supported at the moment.",
AnalyzeClasses.class.getSimpleName(), testClass.getSimpleName());
return analyzeClasses.get(0);
}

@Override
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 All @@ -53,6 +54,7 @@
import com.tngtech.archunit.junit.internal.testexamples.subtwo.SimpleRules;
import com.tngtech.archunit.junit.internal.testexamples.wrong.WrongRuleMethodNotStatic;
import com.tngtech.archunit.junit.internal.testexamples.wrong.WrongRuleMethodWrongParameters;
import com.tngtech.archunit.junit.internal.testexamples.wrong.WrongTestClassWithMultipleAnalyzeClassesAnnotations;
import com.tngtech.archunit.junit.internal.testutil.LogCaptor;
import com.tngtech.archunit.junit.internal.testutil.SystemPropertiesExtension;
import com.tngtech.archunit.junit.internal.testutil.TestLogExtension;
Expand Down Expand Up @@ -169,6 +171,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 @@ -505,10 +521,10 @@ void mixed_class_methods_and_fields() {
expectedLeafIds.add(simpleRuleFieldTestId(engineId));
expectedLeafIds.add(simpleRuleMethodTestId(engineId));
Stream.concat(
SimpleRules.RULE_FIELD_NAMES.stream().map(fieldName ->
simpleRulesId(engineId).append(FIELD_SEGMENT_TYPE, fieldName)),
SimpleRules.RULE_METHOD_NAMES.stream().map(methodName ->
simpleRulesId(engineId).append(METHOD_SEGMENT_TYPE, methodName)))
SimpleRules.RULE_FIELD_NAMES.stream().map(fieldName ->
simpleRulesId(engineId).append(FIELD_SEGMENT_TYPE, fieldName)),
SimpleRules.RULE_METHOD_NAMES.stream().map(methodName ->
simpleRulesId(engineId).append(METHOD_SEGMENT_TYPE, methodName)))
.forEach(expectedLeafIds::add);

assertThat(getAllLeafUniqueIds(rootDescriptor))
Expand Down Expand Up @@ -1074,6 +1090,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 All @@ -1089,6 +1120,19 @@ void rule_method_with_wrong_parameters() {
.hasMessageContaining(WrongRuleMethodWrongParameters.WRONG_PARAMETERS_METHOD_NAME)
.hasMessageContaining("must have exactly one parameter of type " + JavaClasses.class.getName());
}

@Test
void a_test_class_with_multiple_analyze_classes_annotations() {
EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(WrongTestClassWithMultipleAnalyzeClassesAnnotations.class);

assertThatThrownBy(() -> testEngine.discover(discoveryRequest, engineId))
.isInstanceOf(ArchTestInitializationException.class)
.hasMessageContaining("Multiple")
.hasMessageContaining(AnalyzeClasses.class.getSimpleName())
.hasMessageContaining("found")
.hasMessageContaining(WrongTestClassWithMultipleAnalyzeClassesAnnotations.class.getSimpleName())
.hasMessageContaining("not supported");
}
}

@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 {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.tngtech.archunit.junit.internal.testexamples.wrong;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.junit.internal.testexamples.RuleThatFails;
import com.tngtech.archunit.junit.internal.testexamples.UnwantedClass;
import com.tngtech.archunit.lang.ArchRule;

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@AnalyzeClasses(packages = "dummy")
@WrongTestClassWithMultipleAnalyzeClassesAnnotations.MetaAnalyzeCls
public class WrongTestClassWithMultipleAnalyzeClassesAnnotations {

@ArchTest
public static final ArchRule dummy_rule = RuleThatFails.on(UnwantedClass.class);

@Retention(RUNTIME)
@Target(TYPE)
@AnalyzeClasses(wholeClasspath = true)
public @interface MetaAnalyzeCls {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.tngtech.archunit.junit.internal;

import java.lang.annotation.Annotation;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;

class AnnotationFinder<T extends Annotation> {

private final Class<T> annotationClass;

public AnnotationFinder(final Class<T> annotationClass) {
this.annotationClass = annotationClass;
}

/**
* Recursively retrieve all {@link T} annotations from a given element.
*
* @param clazz The clazz from which to retrieve the annotation.
* @return List of all found annotation instance or empty list.
*/
public List<T> findAnnotationsOn(final Class<?> clazz) {
return findAnnotations(clazz.getAnnotations(), new HashSet<>());
}

private List<T> findAnnotations(final Annotation[] annotations, final HashSet<Annotation> visited) {
final List<T> result = new LinkedList<>();
for (Annotation annotation : annotations) {
if (visited.contains(annotation)) {
continue;
} else {
visited.add(annotation);
}
if (annotationClass.isInstance(annotation)) {
result.add(annotationClass.cast(annotation));
} else {
result.addAll(findAnnotations(annotation.annotationType().getAnnotations(), visited));
}
}
return result;
}
}
Loading