From f6a08b99da72c07ab0df4335f44bb6e9eef4e5a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Z=C3=B6ller?= Date: Sun, 28 Dec 2025 22:38:36 +0100 Subject: [PATCH] Adjust AnalyzeClasses annotation to support individual classes as parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Relates to #1195 Signed-off-by: Andreas Zöller --- .../archunit/junit/AnalyzeClasses.java | 15 +++++++++++ .../internal/ArchUnitRunnerInternal.java | 5 ++++ .../archunit/junit/AnalyzeClasses.java | 15 +++++++++++ .../internal/ArchUnitTestDescriptor.java | 5 ++++ .../internal/ArchUnitTestEngineTest.java | 17 ++++++++++++ .../AnalyzeClassesWithClassesProperty.java | 26 +++++++++++++++++++ .../junit/internal/ClassAnalysisRequest.java | 2 ++ .../archunit/junit/internal/ClassCache.java | 8 ++++++ .../junit/internal/ClassCacheTest.java | 9 +++++++ .../junit/internal/TestAnalysisRequest.java | 9 +++++++ docs/userguide/009_JUnit_Support.adoc | 13 +++++++++- 11 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/AnalyzeClassesWithClassesProperty.java diff --git a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java index a93b15be9f..e77a1e3e44 100644 --- a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java +++ b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java @@ -38,6 +38,16 @@ * a rule checking for no accesses to classes assignable to C will not fail, since ArchUnit does not know about the details * of class B, but only simple information like the fully qualified name. For information how to configure the import and * resolution behavior of missing classes, compare {@link ClassFileImporter}. + *

+ * Classes to be analyzed can be specified in different ways: + * + * These options can be combined. If no option is specified, the package of the annotated test class will be imported. * * @see ArchUnitRunner * @see ClassFileImporter @@ -83,4 +93,9 @@ * @return The {@link CacheMode} to use for this test class. */ CacheMode cacheMode() default CacheMode.FOREVER; + + /** + * @return Classes to be used for testing instead of packages + */ + Class[] classes() default {}; } 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 22198ce123..3547958469 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 @@ -209,5 +209,10 @@ public CacheMode getCacheMode() { public boolean scanWholeClasspath() { return analyzeClasses.wholeClasspath(); } + + @Override + public Class[] getClassesToAnalyze() { + return analyzeClasses.classes(); + } } } diff --git a/archunit-junit/junit5/api/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java b/archunit-junit/junit5/api/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java index f96fae9e07..6ee100781a 100644 --- a/archunit-junit/junit5/api/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java +++ b/archunit-junit/junit5/api/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java @@ -41,6 +41,16 @@ * a rule checking for no accesses to classes assignable to C will not fail, since ArchUnit does not know about the details * of class B, but only simple information like the fully qualified name. For information how to configure the import and * resolution behavior of missing classes, compare {@link ClassFileImporter}. + *

+ * Classes to be analyzed can be specified in different ways: + * + * These options can be combined. If no option is specified, the package of the annotated test class will be imported. * * @see ClassFileImporter */ @@ -87,4 +97,9 @@ * @return The {@link CacheMode} to use for this test class. */ CacheMode cacheMode() default CacheMode.FOREVER; + + /** + * @return Classes to be used for testing instead of packages + */ + Class[] classes() default {}; } 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 b8f83d3174..abed0941d2 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 @@ -324,6 +324,11 @@ public CacheMode getCacheMode() { public boolean scanWholeClasspath() { return analyzeClasses.wholeClasspath(); } + + @Override + public Class[] getClassesToAnalyze() { + return analyzeClasses.classes(); + } } private static class TestMember { 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 449c2f3269..c108c69ece 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 @@ -20,6 +20,7 @@ import com.tngtech.archunit.junit.engine_api.FieldSelector; import com.tngtech.archunit.junit.engine_api.FieldSource; import com.tngtech.archunit.junit.internal.ArchUnitTestEngine.SharedCache; +import com.tngtech.archunit.junit.internal.testexamples.AnalyzeClassesWithClassesProperty; import com.tngtech.archunit.junit.internal.testexamples.ClassWithPrivateTests; import com.tngtech.archunit.junit.internal.testexamples.ComplexMetaTags; import com.tngtech.archunit.junit.internal.testexamples.ComplexRuleLibrary; @@ -1079,6 +1080,7 @@ void passes_AnalyzeClasses_to_cache() { assertThat(request.getLocationProviders()).isEqualTo(expected.locations()); assertThat(request.scanWholeClasspath()).as("scan whole classpath").isTrue(); assertThat(request.getImportOptions()).isEqualTo(expected.importOptions()); + assertThat(request.getClassesToAnalyze()).isEmpty(); } @Test @@ -1104,6 +1106,21 @@ void a_class_with_analyze_classes_as_meta_annotation() { assertThat(request.scanWholeClasspath()).as("scan whole classpath").isTrue(); assertThat(request.getImportOptions()).isEqualTo(expected.importOptions()); } + + @Test + void passes_AnalyzeClasses_with_new_classes_property_to_cache() { + execute(createEngineId(), AnalyzeClassesWithClassesProperty.class); + + verify(classCache).getClassesToAnalyzeFor(eq(AnalyzeClassesWithClassesProperty.class), classAnalysisRequestCaptor.capture()); + ClassAnalysisRequest request = classAnalysisRequestCaptor.getValue(); + AnalyzeClasses expected = AnalyzeClassesWithClassesProperty.class.getAnnotation(AnalyzeClasses.class); + assertThat(request.getClassesToAnalyze()).isEqualTo(expected.classes()); + assertThat(request.getImportOptions()).isEqualTo(expected.importOptions()); + assertThat(request.getPackageNames()).isEmpty(); + assertThat(request.getPackageRoots()).isEmpty(); + assertThat(request.getLocationProviders()).isEmpty(); + assertThat(request.scanWholeClasspath()).as("scan whole classpath").isFalse(); + } } @Nested diff --git a/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/AnalyzeClassesWithClassesProperty.java b/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/AnalyzeClassesWithClassesProperty.java new file mode 100644 index 0000000000..52b4413ebf --- /dev/null +++ b/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/AnalyzeClassesWithClassesProperty.java @@ -0,0 +1,26 @@ +package com.tngtech.archunit.junit.internal.testexamples; + +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.importer.ImportOption; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchCondition; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.lang.ConditionEvents; +import com.tngtech.archunit.library.testclasses.coveringallclasses.first.First; +import com.tngtech.archunit.library.testclasses.coveringallclasses.second.Second; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; + +@AnalyzeClasses( + classes = {First.class, Second.class}, + importOptions = {ImportOption.DoNotIncludeTests.class, ImportOption.DoNotIncludeJars.class} +) +public class AnalyzeClassesWithClassesProperty { + @ArchTest + public static final ArchRule irrelevant = classes().should(new ArchCondition("exist") { + @Override + public void check(JavaClass item, ConditionEvents events) { + } + }); +} diff --git a/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ClassAnalysisRequest.java b/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ClassAnalysisRequest.java index 52675d7f22..bbd650d876 100644 --- a/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ClassAnalysisRequest.java +++ b/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ClassAnalysisRequest.java @@ -19,4 +19,6 @@ interface ClassAnalysisRequest { CacheMode getCacheMode(); boolean scanWholeClasspath(); + + Class[] getClassesToAnalyze(); } diff --git a/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ClassCache.java b/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ClassCache.java index 44e9535f83..5102f114a9 100644 --- a/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ClassCache.java +++ b/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ClassCache.java @@ -183,6 +183,7 @@ private Specific(ClassAnalysisRequest classAnalysisRequest, Class testClass) declaredLocations = ImmutableSet.builder() .addAll(getLocationsOfPackages(classAnalysisRequest)) .addAll(getLocationsOfProviders(classAnalysisRequest, testClass)) + .addAll(getLocationsOfClasses(classAnalysisRequest)) .addAll(classAnalysisRequest.scanWholeClasspath() ? Locations.inClassPath() : emptySet()) .build(); } @@ -201,6 +202,12 @@ private Set getLocationsOfProviders(ClassAnalysisRequest classAnalysis .collect(toSet()); } + private Set getLocationsOfClasses(ClassAnalysisRequest classAnalysisRequest) { + return stream(classAnalysisRequest.getClassesToAnalyze()) + .flatMap(clazz -> Locations.ofClass(clazz).stream()) + .collect(toSet()); + } + private LocationProvider tryCreate(Class providerClass) { try { return newInstanceOf(providerClass); @@ -240,6 +247,7 @@ private static boolean noSpecificLocationRequested(ClassAnalysisRequest classAna return classAnalysisRequest.getPackageNames().length == 0 && classAnalysisRequest.getPackageRoots().length == 0 && classAnalysisRequest.getLocationProviders().length == 0 + && classAnalysisRequest.getClassesToAnalyze().length == 0 && !classAnalysisRequest.scanWholeClasspath(); } } diff --git a/archunit-junit/src/test/java/com/tngtech/archunit/junit/internal/ClassCacheTest.java b/archunit-junit/src/test/java/com/tngtech/archunit/junit/internal/ClassCacheTest.java index eb1cb5b6e5..2ec7c2dd00 100644 --- a/archunit-junit/src/test/java/com/tngtech/archunit/junit/internal/ClassCacheTest.java +++ b/archunit-junit/src/test/java/com/tngtech/archunit/junit/internal/ClassCacheTest.java @@ -108,6 +108,15 @@ public void gets_all_classes_relative_to_class() { assertThat(classes.contain(getClass())).as("root class is contained itself").isTrue(); } + @Test + public void gets_all_classes_specified() { + JavaClasses classes = cache.getClassesToAnalyzeFor(TestClass.class, new TestAnalysisRequest() + .withClassesToAnalyze(getClass())); + + assertThat(classes).hasSize(1); + assertThat(classes.contain(getClass())).as("root class is contained itself").isTrue(); + } + @Test public void get_all_classes_by_LocationProvider() { JavaClasses classes = cache.getClassesToAnalyzeFor(TestClass.class, new TestAnalysisRequest() diff --git a/archunit-junit/src/test/java/com/tngtech/archunit/junit/internal/TestAnalysisRequest.java b/archunit-junit/src/test/java/com/tngtech/archunit/junit/internal/TestAnalysisRequest.java index b395c989c1..2b5618d7e4 100644 --- a/archunit-junit/src/test/java/com/tngtech/archunit/junit/internal/TestAnalysisRequest.java +++ b/archunit-junit/src/test/java/com/tngtech/archunit/junit/internal/TestAnalysisRequest.java @@ -12,6 +12,7 @@ class TestAnalysisRequest implements ClassAnalysisRequest { private boolean wholeClasspath = false; private Class[] importOptions = new Class[0]; private CacheMode cacheMode = CacheMode.FOREVER; + private Class[] classesToAnalyze = new Class[0]; @Override public String[] getPackageNames() { @@ -33,6 +34,9 @@ public boolean scanWholeClasspath() { return wholeClasspath; } + @Override + public Class[] getClassesToAnalyze() { return classesToAnalyze; } + @Override public Class[] getImportOptions() { return importOptions; @@ -74,4 +78,9 @@ TestAnalysisRequest withCacheMode(CacheMode cacheMode) { this.cacheMode = cacheMode; return this; } + + TestAnalysisRequest withClassesToAnalyze(Class... classesToAnalyze) { + this.classesToAnalyze = classesToAnalyze; + return this; + } } diff --git a/docs/userguide/009_JUnit_Support.adoc b/docs/userguide/009_JUnit_Support.adoc index 89b7cfd0e0..dd7be23336 100644 --- a/docs/userguide/009_JUnit_Support.adoc +++ b/docs/userguide/009_JUnit_Support.adoc @@ -89,7 +89,18 @@ packages these classes reside in will be imported: @AnalyzeClasses(packagesOf = {SubOneConfiguration.class, SubTwoConfiguration.class}) ---- -As a third option, locations can be specified freely by implementing a `LocationProvider`: +As an alternative to specifying packages, you can directly specify individual classes to be analyzed: + +[source,java,options="nowrap"] +---- +@AnalyzeClasses(classes = {String.class, Integer.class}) +---- + +This allows for more fine-grained control over which classes are imported for testing. +You can combine the `classes` property with other properties like `packages`, `packagesOf`, etc. +In this case, all specified classes and packages will be imported for analysis. + +As another option, locations can be specified freely by implementing a `LocationProvider`: [source,java,options="nowrap"] ----